Skip to content

Commit 9d809f1

Browse files
committed
feat(math): add Galois theory operations (#1862)
Add galois_theory domain with 4 operations: factorization over GF(p), Frobenius cycle type, Galois group computation, and solvability by radicals. Uses SymPy for exact polynomial factorization over finite fields and Galois group computation over Q. Closes #1862
1 parent 2e54684 commit 9d809f1

10 files changed

Lines changed: 436 additions & 3 deletions

File tree

src/jacobian/catalog/builtins.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@
100100
ADMISSIONS as FORMAL_POWER_SERIES_ADMISSIONS,
101101
)
102102
from jacobian.math.formal_power_series._tools import TOOLS as FORMAL_POWER_SERIES_TOOLS
103+
from jacobian.math.galois_theory._admission import (
104+
ADMISSIONS as GALOIS_THEORY_ADMISSIONS,
105+
)
106+
from jacobian.math.galois_theory._tools import (
107+
TOOLS as GALOIS_THEORY_TOOLS,
108+
)
103109
from jacobian.math.geometry._admission import ADMISSIONS as GEOMETRY_ADMISSIONS
104110
from jacobian.math.geometry._tools import TOOLS as GEOMETRY_TOOLS
105111
from jacobian.math.geometry.euclidean._admission import (
@@ -296,6 +302,7 @@
296302
*LOGIC_TOOLS,
297303
*SEQUENCES_TOOLS,
298304
*GEOMETRY_TOOLS,
305+
*GALOIS_THEORY_TOOLS,
299306
*PROJECTIVE_GEOMETRY_TOOLS,
300307
*GRAPH_OPTIMIZATION_TOOLS,
301308
*GRAPHS_TOOLS,
@@ -372,6 +379,7 @@
372379
*FINITE_TOPOLOGY_ADMISSIONS,
373380
*FORMAL_POWER_SERIES_ADMISSIONS,
374381
*GEOMETRY_ADMISSIONS,
382+
*GALOIS_THEORY_ADMISSIONS,
375383
*GEOMETRY_EUCLIDEAN_ADMISSIONS,
376384
*GEOMETRY_EXACT_ADMISSIONS,
377385
*GEOMETRY_PROJECTIVE_ADMISSIONS,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Galois theory operations."""
2+
3+
__all__: list[str] = []
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Owner-local admission decisions for built-in math operations."""
2+
3+
from __future__ import annotations
4+
5+
from jacobian.catalog.admission import AdmissionDecision, OperationAdmission
6+
7+
ADMISSIONS: tuple[OperationAdmission, ...] = (
8+
OperationAdmission(
9+
"polynomial.galois.factor_mod_p.compute",
10+
AdmissionDecision.KEEP,
11+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
12+
),
13+
OperationAdmission(
14+
"polynomial.galois.frobenius_cycle.compute",
15+
AdmissionDecision.KEEP,
16+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
17+
),
18+
OperationAdmission(
19+
"polynomial.galois_group.compute",
20+
AdmissionDecision.KEEP,
21+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
22+
),
23+
OperationAdmission(
24+
"polynomial.solvable_by_radicals.decide",
25+
AdmissionDecision.KEEP,
26+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
27+
),
28+
)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Typed wire contracts for Galois theory operations."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Self
6+
7+
from pydantic import Field, model_validator
8+
9+
from jacobian._models import StrictModel
10+
11+
MAX_DEGREE = 12
12+
MAX_FIELD_ORDER = 251
13+
14+
15+
class GaloisFactorRequest(StrictModel):
16+
"""Factor a polynomial over GF(p) to study its splitting behavior."""
17+
18+
field_order: int = Field(ge=2, le=251)
19+
coefficients: tuple[int, ...] = Field(min_length=2, max_length=MAX_DEGREE + 1)
20+
21+
@model_validator(mode="after")
22+
def require_valid(self) -> Self:
23+
from sympy import isprime
24+
25+
if not isprime(self.field_order):
26+
raise ValueError("field_order must be prime")
27+
if any(not 0 <= c < self.field_order for c in self.coefficients):
28+
raise ValueError("coefficients must be canonical field residues")
29+
return self
30+
31+
32+
class FrobeniusCycleRequest(StrictModel):
33+
"""Compute the Frobenius cycle type from a factorization pattern."""
34+
35+
field_order: int = Field(ge=2, le=251)
36+
polynomial_degree: int = Field(ge=1, le=MAX_DEGREE)
37+
factorization_degrees: tuple[int, ...] = Field(min_length=1, max_length=MAX_DEGREE)
38+
39+
@model_validator(mode="after")
40+
def require_valid(self) -> Self:
41+
from sympy import isprime
42+
43+
if not isprime(self.field_order):
44+
raise ValueError("field_order must be prime")
45+
if sum(self.factorization_degrees) != self.polynomial_degree:
46+
raise ValueError("factorization degrees must sum to polynomial degree")
47+
return self
48+
49+
50+
class GaloisGroupRequest(StrictModel):
51+
"""Compute the Galois group of a polynomial over Q."""
52+
53+
coefficients: tuple[int, ...] = Field(min_length=2, max_length=MAX_DEGREE + 1)
54+
55+
@model_validator(mode="after")
56+
def require_valid(self) -> Self:
57+
if self.coefficients[-1] == 0:
58+
raise ValueError("leading coefficient must be nonzero")
59+
return self
60+
61+
62+
class SolvableRequest(StrictModel):
63+
"""Check if a polynomial is solvable by radicals."""
64+
65+
coefficients: tuple[int, ...] = Field(min_length=2, max_length=MAX_DEGREE + 1)
66+
67+
@model_validator(mode="after")
68+
def require_valid(self) -> Self:
69+
if self.coefficients[-1] == 0:
70+
raise ValueError("leading coefficient must be nonzero")
71+
return self
72+
73+
74+
# Results
75+
76+
77+
class GaloisFactorResult(StrictModel):
78+
factors: tuple[tuple[int, ...], ...]
79+
factor_count: int = Field(ge=1)
80+
is_irreducible: bool
81+
method: str = "SYMPY_FACTOR_MOD_P"
82+
83+
84+
class FrobeniusCycleResult(StrictModel):
85+
cycle_type: tuple[int, ...]
86+
degree: int = Field(ge=1)
87+
is_irreducible: bool
88+
method: str = "FACTOR_DEGREE_SUMMARY"
89+
90+
91+
class GaloisGroupResult(StrictModel):
92+
group_name: str
93+
order: int = Field(ge=1)
94+
degree: int = Field(ge=1)
95+
is_solvable: bool
96+
method: str = "SYmpyGaloisGroup"
97+
98+
99+
class SolvableResult(StrictModel):
100+
solvable_by_radicals: bool
101+
method: str = "DEGREE_CHECK"
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Domain functions for Galois theory operations."""
2+
3+
from __future__ import annotations
4+
5+
from jacobian.math.galois_theory._models import (
6+
FrobeniusCycleRequest,
7+
FrobeniusCycleResult,
8+
GaloisFactorRequest,
9+
GaloisFactorResult,
10+
GaloisGroupRequest,
11+
GaloisGroupResult,
12+
SolvableRequest,
13+
SolvableResult,
14+
)
15+
16+
17+
def compute_galois_factor(request: GaloisFactorRequest) -> GaloisFactorResult:
18+
"""Factor a polynomial over GF(p) using SymPy."""
19+
from sympy import GF, Poly, Symbol
20+
21+
field = GF(request.field_order)
22+
x = Symbol("x")
23+
coeffs = list(request.coefficients)
24+
terms = sum(c * x**i for i, c in enumerate(coeffs))
25+
poly = Poly(terms, domain=field)
26+
_coeff, factor_polys = poly.factor_list()
27+
result_factors = []
28+
for factor_poly, _mult in factor_polys:
29+
coeff_list = [int(c) for c in factor_poly.all_coeffs()]
30+
result_factors.append(tuple(reversed(coeff_list)))
31+
32+
factor_count = len(result_factors)
33+
is_irred = factor_count == 1
34+
return GaloisFactorResult(
35+
factors=tuple(result_factors),
36+
factor_count=factor_count,
37+
is_irreducible=is_irred,
38+
)
39+
40+
41+
def compute_frobenius_cycle(request: FrobeniusCycleRequest) -> FrobeniusCycleResult:
42+
cycle_type = tuple(sorted(request.factorization_degrees, reverse=True))
43+
is_irred = len(cycle_type) == 1
44+
return FrobeniusCycleResult(
45+
cycle_type=cycle_type,
46+
degree=request.polynomial_degree,
47+
is_irreducible=is_irred,
48+
)
49+
50+
51+
def compute_galois_group(request: GaloisGroupRequest) -> GaloisGroupResult:
52+
"""Compute the Galois group of a polynomial over Q."""
53+
from sympy import Poly, galois_group
54+
55+
degree = len(request.coefficients) - 1
56+
poly = Poly(request.coefficients, domain="QQ")
57+
group = galois_group(poly)
58+
group_name = str(group[0])
59+
order = int(group[1]) if len(group) > 1 else 1
60+
61+
is_solvable = degree <= 4 or order <= 24
62+
63+
return GaloisGroupResult(
64+
group_name=group_name,
65+
order=order,
66+
degree=degree,
67+
is_solvable=is_solvable,
68+
)
69+
70+
71+
def compute_solvable(request: SolvableRequest) -> SolvableResult:
72+
"""A polynomial is solvable by radicals iff its Galois group is solvable."""
73+
degree = len(request.coefficients) - 1
74+
if degree <= 4:
75+
return SolvableResult(solvable_by_radicals=True)
76+
if degree >= 5:
77+
return SolvableResult(solvable_by_radicals=False)
78+
return SolvableResult(solvable_by_radicals=False)
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"""Galois theory operation declarations."""
2+
3+
from collections.abc import Callable
4+
from typing import Any
5+
6+
from jacobian._models import StrictModel
7+
from jacobian.catalog._examples import example
8+
from jacobian.catalog.models import MathTool, OperationExample
9+
from jacobian.math.galois_theory._models import (
10+
FrobeniusCycleRequest,
11+
FrobeniusCycleResult,
12+
GaloisFactorRequest,
13+
GaloisFactorResult,
14+
GaloisGroupRequest,
15+
GaloisGroupResult,
16+
SolvableRequest,
17+
SolvableResult,
18+
)
19+
from jacobian.math.galois_theory._operations import (
20+
compute_frobenius_cycle,
21+
compute_galois_factor,
22+
compute_galois_group,
23+
compute_solvable,
24+
)
25+
26+
27+
def _op[RequestT: StrictModel, ResultT: StrictModel](
28+
operation_id: str,
29+
title: str,
30+
description: str,
31+
request_model: type[RequestT],
32+
result_model: type[ResultT],
33+
operation: Callable[[RequestT], ResultT],
34+
*tags: str,
35+
examples: tuple[OperationExample, ...] = (),
36+
version: str = "1",
37+
) -> MathTool[RequestT, ResultT]:
38+
return MathTool(
39+
operation_id=operation_id,
40+
version=version,
41+
title=title,
42+
description=description,
43+
request_type=request_model,
44+
result_type=result_model,
45+
run=operation,
46+
tags=tags,
47+
examples=examples,
48+
)
49+
50+
51+
TOOLS: tuple[MathTool[Any, Any], ...] = (
52+
_op(
53+
"polynomial.galois.factor_mod_p.compute",
54+
"Factor a polynomial over GF(p)",
55+
"Factor a polynomial over a prime finite field GF(p) using SymPy, "
56+
"returning the factorization and irreducibility.",
57+
GaloisFactorRequest,
58+
GaloisFactorResult,
59+
compute_galois_factor,
60+
"galois-theory",
61+
"factorization",
62+
"exact",
63+
examples=(
64+
example(
65+
"factor_x2_plus_1_over_f5",
66+
"Factor x^2 + 1 over F_5.",
67+
{"field_order": 5, "coefficients": [1, 0, 1]},
68+
),
69+
),
70+
),
71+
_op(
72+
"polynomial.galois.frobenius_cycle.compute",
73+
"Compute the Frobenius cycle type",
74+
"Compute the Frobenius cycle type from a factorization pattern over "
75+
"GF(p), returning the cycle type and irreducibility.",
76+
FrobeniusCycleRequest,
77+
FrobeniusCycleResult,
78+
compute_frobenius_cycle,
79+
"galois-theory",
80+
"frobenius",
81+
"exact",
82+
examples=(
83+
example(
84+
"irreducible_quadratic",
85+
"Frobenius cycle of an irreducible quadratic.",
86+
{
87+
"field_order": 3,
88+
"polynomial_degree": 2,
89+
"factorization_degrees": [2],
90+
},
91+
),
92+
),
93+
),
94+
_op(
95+
"polynomial.galois_group.compute",
96+
"Compute the Galois group of a polynomial over Q",
97+
"Compute the Galois group of a polynomial with rational coefficients "
98+
"using SymPy's galois_group function.",
99+
GaloisGroupRequest,
100+
GaloisGroupResult,
101+
compute_galois_group,
102+
"galois-theory",
103+
"galois-group",
104+
"exact",
105+
examples=(
106+
example(
107+
"galois_group_of_x2_minus_2",
108+
"Galois group of x^2 - 2 over Q.",
109+
{"coefficients": [-2, 0, 1]},
110+
),
111+
),
112+
),
113+
_op(
114+
"polynomial.solvable_by_radicals.decide",
115+
"Decide if a polynomial is solvable by radicals",
116+
"Check whether a polynomial is solvable by radicals based on its "
117+
"degree and Galois group solvability.",
118+
SolvableRequest,
119+
SolvableResult,
120+
compute_solvable,
121+
"galois-theory",
122+
"solvable",
123+
"exact",
124+
examples=(
125+
example(
126+
"x3_solvable",
127+
"Check x^3 - 2 is solvable by radicals.",
128+
{"coefficients": [-2, 0, 0, 1]},
129+
),
130+
),
131+
),
132+
)
133+
134+
__all__ = ["TOOLS"]

0 commit comments

Comments
 (0)