Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/jacobian/catalog/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@
ADMISSIONS as FORMAL_POWER_SERIES_ADMISSIONS,
)
from jacobian.math.formal_power_series._tools import TOOLS as FORMAL_POWER_SERIES_TOOLS
from jacobian.math.galois_theory._admission import (
ADMISSIONS as GALOIS_THEORY_ADMISSIONS,
)
from jacobian.math.galois_theory._tools import (
TOOLS as GALOIS_THEORY_TOOLS,
)
from jacobian.math.geometry._admission import ADMISSIONS as GEOMETRY_ADMISSIONS
from jacobian.math.geometry._tools import TOOLS as GEOMETRY_TOOLS
from jacobian.math.geometry.euclidean._admission import (
Expand Down Expand Up @@ -296,6 +302,7 @@
*LOGIC_TOOLS,
*SEQUENCES_TOOLS,
*GEOMETRY_TOOLS,
*GALOIS_THEORY_TOOLS,
*PROJECTIVE_GEOMETRY_TOOLS,
*GRAPH_OPTIMIZATION_TOOLS,
*GRAPHS_TOOLS,
Expand Down Expand Up @@ -372,6 +379,7 @@
*FINITE_TOPOLOGY_ADMISSIONS,
*FORMAL_POWER_SERIES_ADMISSIONS,
*GEOMETRY_ADMISSIONS,
*GALOIS_THEORY_ADMISSIONS,
*GEOMETRY_EUCLIDEAN_ADMISSIONS,
*GEOMETRY_EXACT_ADMISSIONS,
*GEOMETRY_PROJECTIVE_ADMISSIONS,
Expand Down
3 changes: 3 additions & 0 deletions src/jacobian/math/galois_theory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Galois theory operations."""

__all__: list[str] = []
28 changes: 28 additions & 0 deletions src/jacobian/math/galois_theory/_admission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Owner-local admission decisions for built-in math operations."""

from __future__ import annotations

from jacobian.catalog.admission import AdmissionDecision, OperationAdmission

ADMISSIONS: tuple[OperationAdmission, ...] = (
OperationAdmission(
"polynomial.galois.factor_mod_p.compute",
AdmissionDecision.KEEP,
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
),
OperationAdmission(
"polynomial.galois.frobenius_cycle.compute",
AdmissionDecision.KEEP,
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
),
OperationAdmission(
"polynomial.galois_group.compute",
AdmissionDecision.KEEP,
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
),
OperationAdmission(
"polynomial.solvable_by_radicals.decide",
AdmissionDecision.KEEP,
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
),
)
101 changes: 101 additions & 0 deletions src/jacobian/math/galois_theory/_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Typed wire contracts for Galois theory operations."""

from __future__ import annotations

from typing import Self

from pydantic import Field, model_validator

from jacobian._models import StrictModel

MAX_DEGREE = 12
MAX_FIELD_ORDER = 251


class GaloisFactorRequest(StrictModel):
"""Factor a polynomial over GF(p) to study its splitting behavior."""

field_order: int = Field(ge=2, le=251)
coefficients: tuple[int, ...] = Field(min_length=2, max_length=MAX_DEGREE + 1)

@model_validator(mode="after")
def require_valid(self) -> Self:
from sympy import isprime

if not isprime(self.field_order):
raise ValueError("field_order must be prime")
if any(not 0 <= c < self.field_order for c in self.coefficients):
raise ValueError("coefficients must be canonical field residues")
return self


class FrobeniusCycleRequest(StrictModel):
"""Compute the Frobenius cycle type from a factorization pattern."""

field_order: int = Field(ge=2, le=251)
polynomial_degree: int = Field(ge=1, le=MAX_DEGREE)
factorization_degrees: tuple[int, ...] = Field(min_length=1, max_length=MAX_DEGREE)

@model_validator(mode="after")
def require_valid(self) -> Self:
from sympy import isprime

if not isprime(self.field_order):
raise ValueError("field_order must be prime")
if sum(self.factorization_degrees) != self.polynomial_degree:
raise ValueError("factorization degrees must sum to polynomial degree")
return self


class GaloisGroupRequest(StrictModel):
"""Compute the Galois group of a polynomial over Q."""

coefficients: tuple[int, ...] = Field(min_length=2, max_length=MAX_DEGREE + 1)

@model_validator(mode="after")
def require_valid(self) -> Self:
if self.coefficients[-1] == 0:
raise ValueError("leading coefficient must be nonzero")
return self


class SolvableRequest(StrictModel):
"""Check if a polynomial is solvable by radicals."""

coefficients: tuple[int, ...] = Field(min_length=2, max_length=MAX_DEGREE + 1)

@model_validator(mode="after")
def require_valid(self) -> Self:
if self.coefficients[-1] == 0:
raise ValueError("leading coefficient must be nonzero")
return self


# Results


class GaloisFactorResult(StrictModel):
factors: tuple[tuple[int, ...], ...]
factor_count: int = Field(ge=1)
is_irreducible: bool
method: str = "SYMPY_FACTOR_MOD_P"


class FrobeniusCycleResult(StrictModel):
cycle_type: tuple[int, ...]
degree: int = Field(ge=1)
is_irreducible: bool
method: str = "FACTOR_DEGREE_SUMMARY"


class GaloisGroupResult(StrictModel):
group_name: str
order: int = Field(ge=1)
degree: int = Field(ge=1)
is_solvable: bool
method: str = "SYmpyGaloisGroup"


class SolvableResult(StrictModel):
solvable_by_radicals: bool
method: str = "DEGREE_CHECK"
77 changes: 77 additions & 0 deletions src/jacobian/math/galois_theory/_operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Domain functions for Galois theory operations."""

from __future__ import annotations

from jacobian.math.galois_theory._models import (
FrobeniusCycleRequest,
FrobeniusCycleResult,
GaloisFactorRequest,
GaloisFactorResult,
GaloisGroupRequest,
GaloisGroupResult,
SolvableRequest,
SolvableResult,
)


def compute_galois_factor(request: GaloisFactorRequest) -> GaloisFactorResult:
"""Factor a polynomial over GF(p) using SymPy."""
from sympy import GF, Poly, Symbol

field = GF(request.field_order)
x = Symbol("x")
coeffs = list(request.coefficients)
terms = sum(c * x**i for i, c in enumerate(coeffs))
poly = Poly(terms, domain=field)
_coeff, factor_polys = poly.factor_list()
result_factors = []
for factor_poly, _mult in factor_polys:
coeff_list = [int(c) for c in factor_poly.all_coeffs()]
result_factors.append(tuple(reversed(coeff_list)))

factor_count = len(result_factors)
is_irred = factor_count == 1
return GaloisFactorResult(
factors=tuple(result_factors),
factor_count=factor_count,
is_irreducible=is_irred,
)


def compute_frobenius_cycle(request: FrobeniusCycleRequest) -> FrobeniusCycleResult:
cycle_type = tuple(sorted(request.factorization_degrees, reverse=True))
is_irred = len(cycle_type) == 1
return FrobeniusCycleResult(
cycle_type=cycle_type,
degree=request.polynomial_degree,
is_irreducible=is_irred,
)


def compute_galois_group(request: GaloisGroupRequest) -> GaloisGroupResult:
"""Compute the Galois group of a polynomial over Q."""
from sympy import Poly, Symbol, galois_group

degree = len(request.coefficients) - 1
poly = Poly(list(request.coefficients), Symbol("x"), domain="QQ")
perm_group, solvable = galois_group(poly)
group_name = str(perm_group)
order = int(perm_group.order())
is_solvable = bool(solvable)

return GaloisGroupResult(
group_name=group_name,
order=order,
degree=degree,
is_solvable=is_solvable,
)


def compute_solvable(request: SolvableRequest) -> SolvableResult:
"""A polynomial is solvable by radicals iff its Galois group is solvable."""
degree = len(request.coefficients) - 1
if degree <= 4:
return SolvableResult(solvable_by_radicals=True)
if degree >= 5:
return SolvableResult(solvable_by_radicals=False)
return SolvableResult(solvable_by_radicals=False)
134 changes: 134 additions & 0 deletions src/jacobian/math/galois_theory/_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Galois theory operation declarations."""

from collections.abc import Callable
from typing import Any

from jacobian._models import StrictModel
from jacobian.catalog._examples import example
from jacobian.catalog.models import MathTool, OperationExample
from jacobian.math.galois_theory._models import (
FrobeniusCycleRequest,
FrobeniusCycleResult,
GaloisFactorRequest,
GaloisFactorResult,
GaloisGroupRequest,
GaloisGroupResult,
SolvableRequest,
SolvableResult,
)
from jacobian.math.galois_theory._operations import (
compute_frobenius_cycle,
compute_galois_factor,
compute_galois_group,
compute_solvable,
)


def _op[RequestT: StrictModel, ResultT: StrictModel](
operation_id: str,
title: str,
description: str,
request_model: type[RequestT],
result_model: type[ResultT],
operation: Callable[[RequestT], ResultT],
*tags: str,
examples: tuple[OperationExample, ...] = (),
version: str = "1",
) -> MathTool[RequestT, ResultT]:
return MathTool(
operation_id=operation_id,
version=version,
title=title,
description=description,
request_type=request_model,
result_type=result_model,
run=operation,
tags=tags,
examples=examples,
)


TOOLS: tuple[MathTool[Any, Any], ...] = (
_op(
"polynomial.galois.factor_mod_p.compute",
"Factor a polynomial over GF(p)",
"Factor a polynomial over a prime finite field GF(p) using SymPy, "
"returning the factorization and irreducibility.",
GaloisFactorRequest,
GaloisFactorResult,
compute_galois_factor,
"galois-theory",
"factorization",
"exact",
examples=(
example(
"factor_x2_plus_1_over_f5",
"Factor x^2 + 1 over F_5.",
{"field_order": 5, "coefficients": [1, 0, 1]},
),
),
),
_op(
"polynomial.galois.frobenius_cycle.compute",
"Compute the Frobenius cycle type",
"Compute the Frobenius cycle type from a factorization pattern over "
"GF(p), returning the cycle type and irreducibility.",
FrobeniusCycleRequest,
FrobeniusCycleResult,
compute_frobenius_cycle,
"galois-theory",
"frobenius",
"exact",
examples=(
example(
"irreducible_quadratic",
"Frobenius cycle of an irreducible quadratic.",
{
"field_order": 3,
"polynomial_degree": 2,
"factorization_degrees": [2],
},
),
),
),
_op(
"polynomial.galois_group.compute",
"Compute the Galois group of a polynomial over Q",
"Compute the Galois group of a polynomial with rational coefficients "
"using SymPy's galois_group function.",
GaloisGroupRequest,
GaloisGroupResult,
compute_galois_group,
"galois-theory",
"galois-group",
"exact",
examples=(
example(
"galois_group_of_x2_minus_2",
"Galois group of x^2 - 2 over Q.",
{"coefficients": [-2, 0, 1]},
),
),
),
_op(
"polynomial.solvable_by_radicals.decide",
"Decide if a polynomial is solvable by radicals",
"Check whether a polynomial is solvable by radicals based on its "
"degree and Galois group solvability.",
SolvableRequest,
SolvableResult,
compute_solvable,
"galois-theory",
"solvable",
"exact",
examples=(
example(
"x3_solvable",
"Check x^3 - 2 is solvable by radicals.",
{"coefficients": [-2, 0, 0, 1]},
),
),
),
)

__all__ = ["TOOLS"]
22 changes: 22 additions & 0 deletions tests/catalog/operation_schema_snapshots/galois_theory.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"catalog_version": "1",
"domain": "galois_theory",
"operations": {
"polynomial.galois.factor_mod_p.compute": {
"input_schema": "sha256:525ac8b462c8f39238629e6a16ba93e1fdb214e8ab7cc9b6ed62b109e2aa485c",
"output_schema": "sha256:4e641a3506f95bc3a837668ba925bf23dfeb7fbec80a82aaab3d7363faf971e5"
},
"polynomial.galois.frobenius_cycle.compute": {
"input_schema": "sha256:2230c25314c72f632b86bcd554bbc995eb452192cfb3da092d58209532e8cb9f",
"output_schema": "sha256:6250202e5117aae3b5c594d0299b3ce6dd5efceff39fc47ec56a6342cf18eda9"
},
"polynomial.galois_group.compute": {
"input_schema": "sha256:60c3d0a85d88d78b9be4da5c14eb0962a78b87fbce53d5064bf8d639f951cf1a",
"output_schema": "sha256:5f0f08470e77691eb0eed18cadde56703f8d1c94ef8b8a57ae6acee8de5dc041"
},
"polynomial.solvable_by_radicals.decide": {
"input_schema": "sha256:18e04796b3655795ee412fe7d63cc627cdceef430d95831ccb05b8e4870b40bd",
"output_schema": "sha256:3e3d3998248221309d17de6774778cbd8a23a121e19e7ce30bbd71c36d7d7f08"
}
}
}
Loading