Skip to content

Commit 8e7a595

Browse files
committed
feat(math): add plane algebraic curve operations (#1877)
Add plane_algebraic_curves domain with 3 operations: affine curve check, projective closure via homogenization, and affine chart extraction via dehomogenization. Uses SymPy for exact polynomial computation. Closes #1877
1 parent 2e54684 commit 8e7a595

10 files changed

Lines changed: 348 additions & 3 deletions

File tree

src/jacobian/catalog/builtins.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
from jacobian.math.additive_combinatorics._tools import (
1313
TOOLS as ADDITIVE_COMBINATORICS_TOOLS,
1414
)
15+
from jacobian.math.plane_algebraic_curves._admission import (
16+
ADMISSIONS as PLANE_ALGEBRAIC_CURVES_ADMISSIONS,
17+
)
18+
from jacobian.math.plane_algebraic_curves._tools import (
19+
TOOLS as PLANE_ALGEBRAIC_CURVES_TOOLS,
20+
)
1521
from jacobian.math.algebraic_combinatorics._admission import (
1622
ADMISSIONS as ALGEBRAIC_COMBINATORICS_ADMISSIONS,
1723
)
@@ -339,6 +345,7 @@
339345
*ELECTRICAL_NETWORKS_TOOLS,
340346
*REGULAR_LANGUAGES_TOOLS,
341347
*ALGEBRAIC_COMBINATORICS_TOOLS,
348+
*PLANE_ALGEBRAIC_CURVES_TOOLS,
342349
*REAL_ALGEBRA_TOOLS,
343350
*FINITE_METRIC_SPACES_TOOLS,
344351
*PETRI_NET_TOOLS,
@@ -351,6 +358,7 @@
351358
_RAW_ADMISSIONS: tuple[OperationAdmission, ...] = (
352359
*ADDITIVE_COMBINATORICS_ADMISSIONS,
353360
*ALGEBRAIC_COMBINATORICS_ADMISSIONS,
361+
*PLANE_ALGEBRAIC_CURVES_ADMISSIONS,
354362
*ANALYSIS_ADMISSIONS,
355363
*ARITHMETIC_ADMISSIONS,
356364
*ARITHMETIC_COUNTING_ADMISSIONS,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Plane algebraic curve 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+
"algebraic_geometry.affine_plane_curve.check",
10+
AdmissionDecision.KEEP,
11+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
12+
),
13+
OperationAdmission(
14+
"algebraic_geometry.plane_curve.projective_closure.compute",
15+
AdmissionDecision.KEEP,
16+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
17+
),
18+
OperationAdmission(
19+
"algebraic_geometry.projective_curve.affine_chart.compute",
20+
AdmissionDecision.KEEP,
21+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
22+
),
23+
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Typed wire contracts for plane algebraic curve operations."""
2+
3+
from __future__ import annotations
4+
5+
from pydantic import Field
6+
7+
from jacobian._models import StrictModel
8+
9+
MAX_VARS = 3
10+
MAX_COEFF = 4096
11+
12+
13+
class AffineCurveRequest(StrictModel):
14+
"""An affine plane curve f(x, y) = 0."""
15+
16+
variables: tuple[str, ...] = Field(min_length=2, max_length=2)
17+
polynomial: str = Field(min_length=1, max_length=MAX_COEFF)
18+
19+
20+
class ProjectiveClosureRequest(StrictModel):
21+
"""Compute the projective closure of an affine curve."""
22+
23+
variables: tuple[str, ...] = Field(min_length=2, max_length=2)
24+
polynomial: str = Field(min_length=1, max_length=MAX_COEFF)
25+
26+
27+
class AffineChartRequest(StrictModel):
28+
"""Extract an affine chart from a projective curve."""
29+
30+
variables: tuple[str, ...] = Field(min_length=3, max_length=3)
31+
polynomial: str = Field(min_length=1, max_length=MAX_COEFF)
32+
chart_variable: str = Field(min_length=1, max_length=64)
33+
34+
35+
# Results
36+
37+
38+
class AffineCurveResult(StrictModel):
39+
is_valid: bool
40+
degree: int = Field(ge=0)
41+
method: str = "SYmpy_CURVE_CHECK"
42+
43+
44+
class ProjectiveClosureResult(StrictModel):
45+
polynomial: str
46+
variables: tuple[str, ...]
47+
method: str = "HOMOGENIZATION"
48+
49+
50+
class AffineChartResult(StrictModel):
51+
polynomial: str
52+
variables: tuple[str, ...]
53+
method: str = "DEhomOGENIZATION"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Domain functions for plane algebraic curve operations."""
2+
3+
from __future__ import annotations
4+
5+
import sympy
6+
7+
from jacobian.math.plane_algebraic_curves._models import (
8+
AffineChartRequest,
9+
AffineChartResult,
10+
AffineCurveRequest,
11+
AffineCurveResult,
12+
ProjectiveClosureRequest,
13+
ProjectiveClosureResult,
14+
)
15+
16+
17+
def compute_affine_curve_check(request: AffineCurveRequest) -> AffineCurveResult:
18+
"""Check that a polynomial defines a valid affine plane curve."""
19+
var_symbols = sympy.symbols(request.variables)
20+
var_map = dict(zip(request.variables, var_symbols, strict=True))
21+
poly = sympy.sympify(request.polynomial, locals=var_map)
22+
degree = int(sympy.total_degree(poly))
23+
is_valid = poly != 0
24+
return AffineCurveResult(
25+
is_valid=is_valid,
26+
degree=degree,
27+
)
28+
29+
30+
def compute_projective_closure(
31+
request: ProjectiveClosureRequest,
32+
) -> ProjectiveClosureResult:
33+
"""Compute the projective closure by homogenizing with a new variable."""
34+
var_symbols = list(sympy.symbols(request.variables))
35+
var_map = dict(zip(request.variables, var_symbols, strict=True))
36+
poly = sympy.sympify(request.polynomial, locals=var_map)
37+
z = sympy.Symbol("z")
38+
terms = sympy.Poly(poly, *var_symbols)
39+
degree = terms.total_degree()
40+
new_terms = []
41+
for monom, coeff in terms.as_dict().items():
42+
total_deg = sum(monom)
43+
factor = z ** (degree - total_deg)
44+
term = coeff
45+
for i, exp in enumerate(monom):
46+
term *= var_symbols[i] ** exp
47+
new_terms.append(term * factor)
48+
homogenized = sympy.expand(sum(new_terms))
49+
return ProjectiveClosureResult(
50+
polynomial=str(homogenized),
51+
variables=(*request.variables, "z"),
52+
)
53+
54+
55+
def compute_affine_chart(request: AffineChartRequest) -> AffineChartResult:
56+
"""Extract an affine chart by dehomogenizing at the chart variable."""
57+
var_symbols = list(sympy.symbols(request.variables))
58+
var_map = dict(zip(request.variables, var_symbols, strict=True))
59+
chart_var = sympy.Symbol(request.chart_variable)
60+
61+
if request.chart_variable not in request.variables:
62+
raise ValueError("chart_variable must be one of the projective variables")
63+
64+
poly = sympy.sympify(request.polynomial, locals=var_map)
65+
idx = request.variables.index(request.chart_variable)
66+
other_vars = [v for i, v in enumerate(request.variables) if i != idx]
67+
68+
dehomogenized = poly.subs(chart_var, 1)
69+
dehomogenized = sympy.expand(dehomogenized)
70+
71+
return AffineChartResult(
72+
polynomial=str(dehomogenized),
73+
variables=tuple(other_vars),
74+
)
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""Plane algebraic curve 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.plane_algebraic_curves._models import (
10+
AffineChartRequest,
11+
AffineChartResult,
12+
AffineCurveRequest,
13+
AffineCurveResult,
14+
ProjectiveClosureRequest,
15+
ProjectiveClosureResult,
16+
)
17+
from jacobian.math.plane_algebraic_curves._operations import (
18+
compute_affine_chart,
19+
compute_affine_curve_check,
20+
compute_projective_closure,
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+
"algebraic_geometry.affine_plane_curve.check",
51+
"Check an affine plane curve",
52+
"Check that a polynomial defines a valid affine plane curve f(x,y)=0 "
53+
"and return its degree.",
54+
AffineCurveRequest,
55+
AffineCurveResult,
56+
compute_affine_curve_check,
57+
"algebraic-geometry",
58+
"affine-curve",
59+
"exact",
60+
examples=(
61+
example(
62+
"circle",
63+
"Check the unit circle x^2 + y^2 - 1 = 0.",
64+
{
65+
"variables": ["x", "y"],
66+
"polynomial": "x**2 + y**2 - 1",
67+
},
68+
),
69+
),
70+
),
71+
_op(
72+
"algebraic_geometry.plane_curve.projective_closure.compute",
73+
"Compute the projective closure of an affine curve",
74+
"Homogenize an affine plane curve to obtain its projective closure.",
75+
ProjectiveClosureRequest,
76+
ProjectiveClosureResult,
77+
compute_projective_closure,
78+
"algebraic-geometry",
79+
"projective-closure",
80+
"exact",
81+
examples=(
82+
example(
83+
"circle_closure",
84+
"Projective closure of x^2 + y^2 - 1.",
85+
{
86+
"variables": ["x", "y"],
87+
"polynomial": "x**2 + y**2 - 1",
88+
},
89+
),
90+
),
91+
),
92+
_op(
93+
"algebraic_geometry.projective_curve.affine_chart.compute",
94+
"Extract an affine chart from a projective curve",
95+
"Dehomogenize a projective curve at the given chart variable by "
96+
"setting that variable to 1.",
97+
AffineChartRequest,
98+
AffineChartResult,
99+
compute_affine_chart,
100+
"algebraic-geometry",
101+
"affine-chart",
102+
"exact",
103+
examples=(
104+
example(
105+
"chart_z",
106+
"Extract the z=1 chart of x^2 + y^2 - z^2.",
107+
{
108+
"variables": ["x", "y", "z"],
109+
"polynomial": "x**2 + y**2 - z**2",
110+
"chart_variable": "z",
111+
},
112+
),
113+
),
114+
),
115+
)
116+
117+
__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": "plane_algebraic_curves",
4+
"operations": {
5+
"algebraic_geometry.affine_plane_curve.check": {
6+
"input_schema": "sha256:3da4e24fb42e02ef27a2b86823396546c65f4eda6ae2181ff2296e73d6ead0d6",
7+
"output_schema": "sha256:ebbe1cbf85ae3ac33a346cfd5457d80a87ec979f9849a5e340d3f79f06e762b9"
8+
},
9+
"algebraic_geometry.plane_curve.projective_closure.compute": {
10+
"input_schema": "sha256:38f21b9ec38af2258ddecd2a8273562dbb725463e5df19eea5c5b889de6227e6",
11+
"output_schema": "sha256:b18e7f7351c526592ca42bed59a186f182beedc167dcacbdbcfc11216ed775a3"
12+
},
13+
"algebraic_geometry.projective_curve.affine_chart.compute": {
14+
"input_schema": "sha256:dbc389f87c55719512a31608d7108a0047cad7e78ddf8b049c3ace374a036a72",
15+
"output_schema": "sha256:476deb3413dde8691cfec3fc9c965af5feac7278e8d6ebc471b5f32f6fb7fec9"
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/plane_algebraic_curves/__init__.py

Whitespace-only changes.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Tests for plane algebraic curve operations."""
2+
3+
from jacobian.math.plane_algebraic_curves._models import (
4+
AffineChartRequest,
5+
AffineCurveRequest,
6+
ProjectiveClosureRequest,
7+
)
8+
from jacobian.math.plane_algebraic_curves._operations import (
9+
compute_affine_chart,
10+
compute_affine_curve_check,
11+
compute_projective_closure,
12+
)
13+
from jacobian.math.plane_algebraic_curves._tools import TOOLS
14+
15+
16+
def test_catalog_contains_only_audited_operations() -> None:
17+
assert {tool.operation_id for tool in TOOLS} == {
18+
"algebraic_geometry.affine_plane_curve.check",
19+
"algebraic_geometry.plane_curve.projective_closure.compute",
20+
"algebraic_geometry.projective_curve.affine_chart.compute",
21+
}
22+
23+
24+
def test_affine_curve_check_circle() -> None:
25+
request = AffineCurveRequest(
26+
variables=("x", "y"), polynomial="x**2 + y**2 - 1"
27+
)
28+
result = compute_affine_curve_check(request)
29+
assert result.is_valid is True
30+
assert result.degree == 2
31+
32+
33+
def test_projective_closure_circle() -> None:
34+
request = ProjectiveClosureRequest(
35+
variables=("x", "y"), polynomial="x**2 + y**2 - 1"
36+
)
37+
result = compute_projective_closure(request)
38+
assert "z" in result.polynomial
39+
40+
41+
def test_affine_chart_circle() -> None:
42+
request = AffineChartRequest(
43+
variables=("x", "y", "z"),
44+
polynomial="x**2 + y**2 - z**2",
45+
chart_variable="z",
46+
)
47+
result = compute_affine_chart(request)
48+
assert result.polynomial == "x**2 + y**2 - 1"
49+
assert result.variables == ("x", "y")

0 commit comments

Comments
 (0)