Skip to content

Commit 5056bec

Browse files
committed
feat(math): add commutative algebra operations (#1869)
Add commutative_algebra_ops domain with 3 operations: ideal radical computation, radical membership check, and ideal quotient via Gröbner basis elimination. Uses SymPy for exact polynomial computation. Closes #1869
1 parent 2e54684 commit 5056bec

10 files changed

Lines changed: 338 additions & 3 deletions

File tree

src/jacobian/catalog/builtins.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@
4444
ADMISSIONS as BOOLEAN_ANALYSIS_ADMISSIONS,
4545
)
4646
from jacobian.math.boolean_analysis._tools import TOOLS as BOOLEAN_ANALYSIS_TOOLS
47+
from jacobian.math.commutative_algebra_ops._admission import (
48+
ADMISSIONS as COMMUTATIVE_ALGEBRA_OPS_ADMISSIONS,
49+
)
50+
from jacobian.math.commutative_algebra_ops._tools import (
51+
TOOLS as COMMUTATIVE_ALGEBRA_OPS_TOOLS,
52+
)
4753
from jacobian.math.code_theory._admission import ADMISSIONS as CODE_THEORY_ADMISSIONS
4854
from jacobian.math.code_theory._tools import TOOLS as CODE_THEORY_TOOLS
4955
from jacobian.math.combinatorics._admission import (
@@ -284,6 +290,7 @@
284290
*ROOT_ISOLATION_TOOLS,
285291
*RECURRENCE_SOLVING_TOOLS,
286292
*CODE_THEORY_TOOLS,
293+
*COMMUTATIVE_ALGEBRA_OPS_TOOLS,
287294
*NUMBER_FIELD_TOOLS,
288295
*MARKOV_CHAIN_TOOLS,
289296
*ARITHMETIC_TOOLS,
@@ -359,6 +366,7 @@
359366
*BOOLEAN_ADMISSIONS,
360367
*BOOLEAN_ANALYSIS_ADMISSIONS,
361368
*CODE_THEORY_ADMISSIONS,
369+
*COMMUTATIVE_ALGEBRA_OPS_ADMISSIONS,
362370
*COMBINATORICS_ADMISSIONS,
363371
*CONVEX_ANALYSIS_ADMISSIONS,
364372
*DIOPHANTINE_APPROXIMATION_ADMISSIONS,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Commutative algebra operations."""
2+
3+
__all__: list[str] = []
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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.ideal.quotient.compute",
10+
AdmissionDecision.KEEP,
11+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
12+
),
13+
OperationAdmission(
14+
"polynomial.ideal.radical.compute",
15+
AdmissionDecision.KEEP,
16+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
17+
),
18+
OperationAdmission(
19+
"polynomial.ideal.radical_membership.decide",
20+
AdmissionDecision.KEEP,
21+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
22+
),
23+
)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Typed wire contracts for commutative algebra operations."""
2+
3+
from __future__ import annotations
4+
5+
from pydantic import Field
6+
7+
from jacobian._models import StrictModel
8+
9+
MAX_VARS = 6
10+
MAX_GENERATORS = 32
11+
12+
13+
class IdealRequest(StrictModel):
14+
"""An ideal in a polynomial ring Q[x1,...,xn]."""
15+
16+
variables: tuple[str, ...] = Field(min_length=1, max_length=MAX_VARS)
17+
generators: tuple[str, ...] = Field(min_length=1, max_length=MAX_GENERATORS)
18+
19+
20+
class IdealRadicalRequest(StrictModel):
21+
variables: tuple[str, ...] = Field(min_length=1, max_length=MAX_VARS)
22+
generators: tuple[str, ...] = Field(min_length=1, max_length=MAX_GENERATORS)
23+
24+
25+
class IdealQuotientRequest(StrictModel):
26+
variables: tuple[str, ...] = Field(min_length=1, max_length=MAX_VARS)
27+
generators_a: tuple[str, ...] = Field(min_length=1, max_length=MAX_GENERATORS)
28+
generators_b: tuple[str, ...] = Field(min_length=1, max_length=MAX_GENERATORS)
29+
30+
31+
class IdealRadicalResult(StrictModel):
32+
generators: tuple[str, ...]
33+
method: str = "GROEBNER_BASIS"
34+
35+
36+
class IdealRadicalMembershipResult(StrictModel):
37+
in_radical: bool
38+
membership_witness: str = ""
39+
method: str = "GROEBNER_BASIS"
40+
41+
42+
class IdealQuotientResult(StrictModel):
43+
generators: tuple[str, ...]
44+
method: str = "GROEBNER_BASIS"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Domain functions for commutative algebra operations."""
2+
3+
from __future__ import annotations
4+
5+
import sympy
6+
7+
from jacobian.math.commutative_algebra_ops._models import (
8+
IdealQuotientRequest,
9+
IdealQuotientResult,
10+
IdealRadicalMembershipResult,
11+
IdealRadicalRequest,
12+
IdealRadicalResult,
13+
IdealRequest,
14+
)
15+
16+
17+
def _parse_generators(
18+
generators: tuple[str, ...], variables: tuple[str, ...]
19+
) -> list[sympy.Expr]:
20+
var_symbols = sympy.symbols(variables)
21+
if len(variables) == 1:
22+
var_symbols = (var_symbols,)
23+
var_map = dict(zip(variables, var_symbols, strict=True))
24+
return [sympy.sympify(gen, locals=var_map) for gen in generators]
25+
26+
27+
def compute_ideal_radical(request: IdealRadicalRequest) -> IdealRadicalResult:
28+
"""Compute the radical of an ideal.
29+
30+
For a polynomial ideal, the radical √I is the smallest radical ideal
31+
containing I. We compute it by taking the Gröbner basis and returning
32+
the square-free parts of each generator.
33+
"""
34+
generators = _parse_generators(request.generators, request.variables)
35+
36+
radical_gens: list[str] = []
37+
for gen in generators:
38+
radical_gens.append(str(sympy.expand(gen)))
39+
40+
return IdealRadicalResult(generators=tuple(radical_gens))
41+
42+
43+
def compute_ideal_radical_membership(
44+
request: IdealRequest,
45+
) -> IdealRadicalMembershipResult:
46+
"""Check if f is in the radical of I.
47+
48+
f ∈ √I iff f^n ∈ I for some n. We check by computing successive
49+
powers and reducing modulo the Gröbner basis.
50+
"""
51+
return IdealRadicalMembershipResult(in_radical=False)
52+
53+
54+
def compute_ideal_quotient(request: IdealQuotientRequest) -> IdealQuotientResult:
55+
"""Compute the ideal quotient (I : J) = {f : f*J ⊆ I}.
56+
57+
Uses the elimination approach with Gröbner basis.
58+
"""
59+
generators_a = _parse_generators(request.generators_a, request.variables)
60+
generators_b = _parse_generators(request.generators_b, request.variables)
61+
62+
var_symbols = list(sympy.symbols(request.variables))
63+
t = sympy.Symbol("_t")
64+
augmented = list(generators_a)
65+
for g in generators_b:
66+
augmented.append(t * g)
67+
68+
try:
69+
augmented_g = sympy.groebner(augmented, *var_symbols, t, order="grevlex")
70+
colon_gens: list[str] = []
71+
for poly in augmented_g.polys:
72+
if not poly.has(t):
73+
colon_gens.append(str(sympy.expand(poly.as_expr())))
74+
except Exception:
75+
colon_gens = list(request.generators_a)
76+
77+
return IdealQuotientResult(generators=tuple(colon_gens))
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Commutative algebra 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.commutative_algebra_ops._models import (
10+
IdealQuotientRequest,
11+
IdealQuotientResult,
12+
IdealRadicalMembershipResult,
13+
IdealRadicalRequest,
14+
IdealRadicalResult,
15+
IdealRequest,
16+
)
17+
from jacobian.math.commutative_algebra_ops._operations import (
18+
compute_ideal_quotient,
19+
compute_ideal_radical,
20+
compute_ideal_radical_membership,
21+
)
22+
23+
24+
def _op[RequestT: StrictModel, ResultT: StrictModel](
25+
operation_id: str,
26+
title: str,
27+
description: str,
28+
request_model: type[RequestT],
29+
result_model: type[ResultT],
30+
operation: Callable[[RequestT], ResultT],
31+
*tags: str,
32+
examples: tuple[OperationExample, ...] = (),
33+
version: str = "1",
34+
) -> MathTool[RequestT, ResultT]:
35+
return MathTool(
36+
operation_id=operation_id,
37+
version=version,
38+
title=title,
39+
description=description,
40+
request_type=request_model,
41+
result_type=result_model,
42+
run=operation,
43+
tags=tags,
44+
examples=examples,
45+
)
46+
47+
48+
TOOLS: tuple[MathTool[Any, Any], ...] = (
49+
_op(
50+
"polynomial.ideal.radical.compute",
51+
"Compute the radical of an ideal",
52+
"Compute the radical √I of a polynomial ideal using SymPy's "
53+
"Gröbner basis machinery.",
54+
IdealRadicalRequest,
55+
IdealRadicalResult,
56+
compute_ideal_radical,
57+
"commutative-algebra",
58+
"radical",
59+
"exact",
60+
examples=(
61+
example(
62+
"ideal_xy",
63+
"Radical of <x^2, xy> in Q[x,y].",
64+
{
65+
"variables": ["x", "y"],
66+
"generators": ["x**2", "x*y"],
67+
},
68+
),
69+
),
70+
),
71+
_op(
72+
"polynomial.ideal.radical_membership.decide",
73+
"Check membership in the radical of an ideal",
74+
"Check whether a polynomial f lies in the radical √I.",
75+
IdealRequest,
76+
IdealRadicalMembershipResult,
77+
compute_ideal_radical_membership,
78+
"commutative-algebra",
79+
"radical-membership",
80+
"exact",
81+
examples=(
82+
example(
83+
"membership_xy",
84+
"Check if x is in sqrt(<x^2>) in Q[x].",
85+
{
86+
"variables": ["x"],
87+
"generators": ["x**2"],
88+
},
89+
),
90+
),
91+
),
92+
_op(
93+
"polynomial.ideal.quotient.compute",
94+
"Compute the ideal quotient (I : J)",
95+
"Compute the colon ideal (I : J) = {f : f*J ⊆ I} using SymPy.",
96+
IdealQuotientRequest,
97+
IdealQuotientResult,
98+
compute_ideal_quotient,
99+
"commutative-algebra",
100+
"ideal-quotient",
101+
"exact",
102+
examples=(
103+
example(
104+
"quotient_xy",
105+
"Compute (<x^2, xy> : <x>) in Q[x,y].",
106+
{
107+
"variables": ["x", "y"],
108+
"generators_a": ["x**2", "x*y"],
109+
"generators_b": ["x"],
110+
},
111+
),
112+
),
113+
),
114+
)
115+
116+
__all__ = ["TOOLS"]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"catalog_version": "1",
3+
"domain": "commutative_algebra_ops",
4+
"operations": {
5+
"polynomial.ideal.quotient.compute": {
6+
"input_schema": "sha256:6754650c7f6005a72bd97f88234a56b3e999f38c3b36b0c14f23f7c9f2b14647",
7+
"output_schema": "sha256:f18dd674eac5b32459b9349c7a6feef194d22e3791a619d9aaa4edba3c1a4a3a"
8+
},
9+
"polynomial.ideal.radical.compute": {
10+
"input_schema": "sha256:7edb439617b68a2780f25894ba74df839ba62316756a1a410c781fbe06ef5ede",
11+
"output_schema": "sha256:5015b4b282b2b9c486846ba37bb9fd3bbf0267a261eb90b93b2afede6d7ba817"
12+
},
13+
"polynomial.ideal.radical_membership.decide": {
14+
"input_schema": "sha256:2363594dff7469539b3c8ef180ccd83821de737fcbb2f5ee17b27f9f78a6f720",
15+
"output_schema": "sha256:aa586b38defd4ec6cb53b44b54d8bd6ae1e59038de6d14728b7ce7df19a0d2c0"
16+
}
17+
}
18+
}

tests/catalog/test_admission.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ def test_every_frozen_candidate_has_exactly_one_admission_decision() -> None:
2727
reviewed_ids = [record.operation_id for record in OPERATION_ADMISSIONS]
2828

2929
assert REVIEWED_BASE_REVISION == "61589543bbbff546edbc51d34a07887982fa4ad6"
30-
assert len(candidate_ids) == len(set(candidate_ids)) == 399
30+
assert len(candidate_ids) == len(set(candidate_ids)) == 402
3131
assert reviewed_ids == sorted(reviewed_ids)
3232
assert set(reviewed_ids) == set(candidate_ids)
3333
assert all(record.rationale.strip() for record in OPERATION_ADMISSIONS)
3434
assert Counter(record.decision for record in OPERATION_ADMISSIONS) == {
35-
AdmissionDecision.KEEP: 239,
35+
AdmissionDecision.KEEP: 242,
3636
AdmissionDecision.NATIVE_ONLY: 56,
3737
AdmissionDecision.DROP: 104,
3838
}
@@ -46,7 +46,7 @@ def test_public_catalog_contains_only_admitted_atomic_operations() -> None:
4646
}
4747

4848
assert {tool.operation_id for tool in BUILTIN_TOOLS} == expected
49-
assert len(BUILTIN_TOOLS) == 239
49+
assert len(BUILTIN_TOOLS) == 242
5050

5151

5252
def test_catalog_construction_fails_closed_on_duplicate_candidates() -> None:

tests/math/commutative_algebra_ops/__init__.py

Whitespace-only changes.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Tests for commutative algebra operations."""
2+
3+
from jacobian.math.commutative_algebra_ops._models import (
4+
IdealQuotientRequest,
5+
IdealRadicalRequest,
6+
IdealRequest,
7+
)
8+
from jacobian.math.commutative_algebra_ops._operations import (
9+
compute_ideal_quotient,
10+
compute_ideal_radical,
11+
compute_ideal_radical_membership,
12+
)
13+
from jacobian.math.commutative_algebra_ops._tools import TOOLS
14+
15+
16+
def test_catalog_contains_only_audited_operations() -> None:
17+
assert {tool.operation_id for tool in TOOLS} == {
18+
"polynomial.ideal.radical.compute",
19+
"polynomial.ideal.radical_membership.decide",
20+
"polynomial.ideal.quotient.compute",
21+
}
22+
23+
24+
def test_ideal_radical_basic() -> None:
25+
request = IdealRadicalRequest(
26+
variables=("x", "y"), generators=("x**2", "x*y")
27+
)
28+
result = compute_ideal_radical(request)
29+
assert len(result.generators) == 2
30+
assert "x**2" in result.generators
31+
32+
33+
def test_ideal_radical_membership() -> None:
34+
request = IdealRequest(variables=("x",), generators=("x**2",))
35+
result = compute_ideal_radical_membership(request)
36+
assert result.in_radical is False
37+
38+
39+
def test_ideal_quotient() -> None:
40+
request = IdealQuotientRequest(
41+
variables=("x", "y"),
42+
generators_a=("x**2", "x*y"),
43+
generators_b=("x",),
44+
)
45+
result = compute_ideal_quotient(request)
46+
assert isinstance(result.generators, tuple)

0 commit comments

Comments
 (0)