Skip to content

Commit f2c6fed

Browse files
committed
feat(math): add projective coordinate operations (#1892)
Add projective_coords_ops domain with 3 operations: rational projective point construction, standard affine chart dehomogenization, and chart transition maps. Uses exact rational arithmetic via CanonicalRational. Closes #1892
1 parent 2e54684 commit f2c6fed

10 files changed

Lines changed: 408 additions & 3 deletions

File tree

src/jacobian/catalog/builtins.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,12 @@
236236
from jacobian.math.polynomials.real_algebra._tools import TOOLS as REAL_ALGEBRA_TOOLS
237237
from jacobian.math.posets._admission import ADMISSIONS as POSETS_ADMISSIONS
238238
from jacobian.math.posets._tools import TOOLS as POSETS_TOOLS
239+
from jacobian.math.projective_coords_ops._admission import (
240+
ADMISSIONS as PROJECTIVE_COORDS_OPS_ADMISSIONS,
241+
)
242+
from jacobian.math.projective_coords_ops._tools import (
243+
TOOLS as PROJECTIVE_COORDS_OPS_TOOLS,
244+
)
239245
from jacobian.math.probability._admission import ADMISSIONS as PROBABILITY_ADMISSIONS
240246
from jacobian.math.probability._tools import TOOLS as PROBABILITY_TOOLS
241247
from jacobian.math.recurrence_solving._admission import (
@@ -312,6 +318,7 @@
312318
*MULTIVARIATE_POLYNOMIAL_TOOLS,
313319
*ANALYSIS_TOOLS,
314320
*PROBABILITY_TOOLS,
321+
*PROJECTIVE_COORDS_OPS_TOOLS,
315322
*OPTIMIZATION_TOOLS,
316323
*TOPOLOGY_TOOLS,
317324
*POSETS_TOOLS,
@@ -411,6 +418,7 @@
411418
*POLYNOMIALS_REAL_ALGEBRA_ADMISSIONS,
412419
*POSETS_ADMISSIONS,
413420
*PROBABILITY_ADMISSIONS,
421+
*PROJECTIVE_COORDS_OPS_ADMISSIONS,
414422
*RECURRENCE_SOLVING_ADMISSIONS,
415423
*REGULAR_LANGUAGES_ADMISSIONS,
416424
*ROOT_ISOLATION_ADMISSIONS,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Projective coordinate 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+
"projective.rational_point.construct",
10+
AdmissionDecision.KEEP,
11+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
12+
),
13+
OperationAdmission(
14+
"projective.standard_chart.compute",
15+
AdmissionDecision.KEEP,
16+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
17+
),
18+
OperationAdmission(
19+
"projective.chart_transition.compute",
20+
AdmissionDecision.KEEP,
21+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
22+
),
23+
)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Typed wire contracts for projective coordinate 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._exact import CanonicalRational
10+
from jacobian._models import StrictModel
11+
12+
MAX_DIM = 16
13+
14+
15+
class RationalProjectivePoint(StrictModel):
16+
"""A projective point [x0 : x1 : ... : xn] over the rationals."""
17+
18+
coordinates: tuple[CanonicalRational, ...] = Field(min_length=1, max_length=MAX_DIM + 1)
19+
20+
@model_validator(mode="after")
21+
def require_not_all_zero(self) -> Self:
22+
if all(c.as_fraction() == 0 for c in self.coordinates):
23+
raise ValueError("projective point must have at least one nonzero coordinate")
24+
return self
25+
26+
27+
class RationalPointConstructRequest(StrictModel):
28+
coordinates: tuple[CanonicalRational, ...] = Field(min_length=1, max_length=MAX_DIM + 1)
29+
30+
@model_validator(mode="after")
31+
def require_valid(self) -> Self:
32+
if all(c.as_fraction() == 0 for c in self.coordinates):
33+
raise ValueError("projective point must have at least one nonzero coordinate")
34+
return self
35+
36+
37+
class StandardChartRequest(StrictModel):
38+
point: RationalProjectivePoint
39+
chart_index: int = Field(ge=0)
40+
41+
@model_validator(mode="after")
42+
def require_valid(self) -> Self:
43+
if self.chart_index >= len(self.point.coordinates):
44+
raise ValueError("chart_index out of range")
45+
if self.point.coordinates[self.chart_index].as_fraction() == 0:
46+
raise ValueError("chart coordinate must be nonzero")
47+
return self
48+
49+
50+
class ChartTransitionRequest(StrictModel):
51+
point: RationalProjectivePoint
52+
chart_i: int = Field(ge=0)
53+
chart_j: int = Field(ge=0)
54+
55+
@model_validator(mode="after")
56+
def require_valid(self) -> Self:
57+
n = len(self.point.coordinates)
58+
if self.chart_i >= n or self.chart_j >= n:
59+
raise ValueError("chart index out of range")
60+
if self.point.coordinates[self.chart_i].as_fraction() == 0:
61+
raise ValueError("chart_i coordinate must be nonzero")
62+
if self.point.coordinates[self.chart_j].as_fraction() == 0:
63+
raise ValueError("chart_j coordinate must be nonzero")
64+
return self
65+
66+
67+
# Results
68+
69+
70+
class RationalPointConstructResult(StrictModel):
71+
canonical: tuple[CanonicalRational, ...]
72+
scale: CanonicalRational
73+
projective_dimension: int = Field(ge=0)
74+
method: str = "FIRST_NONZERO_SCALE"
75+
76+
77+
class StandardChartResult(StrictModel):
78+
affine_point: tuple[CanonicalRational, ...]
79+
chart_index: int = Field(ge=0)
80+
method: str = "DEhomOGENIZATION"
81+
82+
83+
class ChartTransitionResult(StrictModel):
84+
transition: tuple[CanonicalRational, ...]
85+
chart_i: int = Field(ge=0)
86+
chart_j: int = Field(ge=0)
87+
method: str = "CHART_TRANSITION"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Domain functions for projective coordinate operations."""
2+
3+
from __future__ import annotations
4+
5+
from fractions import Fraction
6+
7+
from jacobian._exact import CanonicalRational
8+
from jacobian.canonical import format_canonical_integer
9+
from jacobian.math.projective_coords_ops._models import (
10+
ChartTransitionRequest,
11+
ChartTransitionResult,
12+
RationalPointConstructRequest,
13+
RationalPointConstructResult,
14+
StandardChartRequest,
15+
StandardChartResult,
16+
)
17+
18+
19+
def _rational(frac: Fraction) -> CanonicalRational:
20+
return CanonicalRational(
21+
num=format_canonical_integer(frac.numerator),
22+
den=format_canonical_integer(frac.denominator),
23+
)
24+
25+
26+
def compute_rational_point_construct(
27+
request: RationalPointConstructRequest,
28+
) -> RationalPointConstructResult:
29+
"""Canonicalize by scaling so first nonzero coordinate is 1."""
30+
coords = request.coordinates
31+
for _i, c in enumerate(coords):
32+
if c.as_fraction() != 0:
33+
scale = c
34+
inv = Fraction(1, 1) / c.as_fraction()
35+
canonical = tuple(_rational(v.as_fraction() * inv) for v in coords)
36+
return RationalPointConstructResult(
37+
canonical=canonical,
38+
scale=scale,
39+
projective_dimension=len(coords) - 1,
40+
)
41+
raise ValueError("all coordinates are zero")
42+
43+
44+
def compute_standard_chart(request: StandardChartRequest) -> StandardChartResult:
45+
"""Dehomogenize at the given chart index (divide by that coordinate)."""
46+
coords = request.point.coordinates
47+
chart = request.chart_index
48+
inv = Fraction(1, 1) / coords[chart].as_fraction()
49+
affine = tuple(
50+
_rational(coords[i].as_fraction() * inv)
51+
for i in range(len(coords))
52+
if i != chart
53+
)
54+
return StandardChartResult(
55+
affine_point=affine,
56+
chart_index=chart,
57+
)
58+
59+
60+
def compute_chart_transition(request: ChartTransitionRequest) -> ChartTransitionResult:
61+
"""Compute the transition map from chart_i to chart_j coordinates."""
62+
coords = request.point.coordinates
63+
xi = coords[request.chart_i].as_fraction()
64+
xj = coords[request.chart_j].as_fraction()
65+
ratios = tuple(
66+
_rational(coords[i].as_fraction() * xi / xj)
67+
for i in range(len(coords))
68+
if i != request.chart_i and i != request.chart_j
69+
)
70+
return ChartTransitionResult(
71+
transition=ratios,
72+
chart_i=request.chart_i,
73+
chart_j=request.chart_j,
74+
)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""Projective coordinate 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.projective_coords_ops._models import (
10+
ChartTransitionRequest,
11+
ChartTransitionResult,
12+
RationalPointConstructRequest,
13+
RationalPointConstructResult,
14+
StandardChartRequest,
15+
StandardChartResult,
16+
)
17+
from jacobian.math.projective_coords_ops._operations import (
18+
compute_chart_transition,
19+
compute_rational_point_construct,
20+
compute_standard_chart,
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+
"projective.rational_point.construct",
51+
"Construct a canonical rational projective point",
52+
"Canonicalize a rational projective point by scaling so the first "
53+
"nonzero coordinate is 1.",
54+
RationalPointConstructRequest,
55+
RationalPointConstructResult,
56+
compute_rational_point_construct,
57+
"projective",
58+
"rational",
59+
"exact",
60+
examples=(
61+
example(
62+
"p1_point",
63+
"Construct [2 : 4] in P^1(Q).",
64+
{
65+
"coordinates": [
66+
{"num": "2", "den": "1"},
67+
{"num": "4", "den": "1"},
68+
],
69+
},
70+
),
71+
),
72+
),
73+
_op(
74+
"projective.standard_chart.compute",
75+
"Dehomogenize at a standard affine chart",
76+
"Dehomogenize a projective point at the given chart index by "
77+
"dividing all coordinates by that coordinate.",
78+
StandardChartRequest,
79+
StandardChartResult,
80+
compute_standard_chart,
81+
"projective",
82+
"affine-chart",
83+
"exact",
84+
examples=(
85+
example(
86+
"chart_0",
87+
"Dehomogenize [1 : 2 : 3] at chart 0.",
88+
{
89+
"point": {
90+
"coordinates": [
91+
{"num": "1", "den": "1"},
92+
{"num": "2", "den": "1"},
93+
{"num": "3", "den": "1"},
94+
],
95+
},
96+
"chart_index": 0,
97+
},
98+
),
99+
),
100+
),
101+
_op(
102+
"projective.chart_transition.compute",
103+
"Compute the transition map between two charts",
104+
"Compute the transition map from chart_i to chart_j for a projective "
105+
"point.",
106+
ChartTransitionRequest,
107+
ChartTransitionResult,
108+
compute_chart_transition,
109+
"projective",
110+
"chart-transition",
111+
"exact",
112+
examples=(
113+
example(
114+
"transition_0_to_1",
115+
"Transition from chart 0 to chart 1 for [1 : 2 : 3].",
116+
{
117+
"point": {
118+
"coordinates": [
119+
{"num": "1", "den": "1"},
120+
{"num": "2", "den": "1"},
121+
{"num": "3", "den": "1"},
122+
],
123+
},
124+
"chart_i": 0,
125+
"chart_j": 1,
126+
},
127+
),
128+
),
129+
),
130+
)
131+
132+
__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": "projective_coords_ops",
4+
"operations": {
5+
"projective.chart_transition.compute": {
6+
"input_schema": "sha256:a1a8e861998d910997d4a3376f946d480263433af632f3e18f21f8856aa2727b",
7+
"output_schema": "sha256:ca16172cf4fbc56c67bd31b7bf90a052497391af16884de7474fba510d0317f6"
8+
},
9+
"projective.rational_point.construct": {
10+
"input_schema": "sha256:252dbcae7c0d2a3e1c911bf5f6a8d3c56763d0f673c77c4c57b149361118ffa0",
11+
"output_schema": "sha256:e28e31ddf8672685284de4b9f30a0ce47563432d4f3233b6791348b82fcc9e07"
12+
},
13+
"projective.standard_chart.compute": {
14+
"input_schema": "sha256:28e244772a727fbd402bef2db9a035c214f8ae7e208fe7add3ae33c2ccb12122",
15+
"output_schema": "sha256:a4b39996105b18c5f12e46df95eb7a5098db17adbaf53597b65ee0c004ea0db2"
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/projective_coords_ops/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)