Skip to content

Commit 5412655

Browse files
authored
feat: add incidence structure operations (#1732) (#2114)
* feat(math): add integral binary quadratic forms domain with check, evaluate, reduce, equivalence, and class enumeration Implement the primitive positive-definite integral binary quadratic form domain (issue #1830) with five exact operations: - number_theory.binary_quadratic_form.check: validate coefficients as a primitive positive-definite form with negative discriminant D ≡ 0 or 1 (mod 4), returning the discriminant and symmetric Gram matrix - number_theory.binary_quadratic_form.evaluate: exact evaluation Q(x,y) = a*x^2 + b*x*y + c*y^2 with primitive-pair status - number_theory.binary_quadratic_form.reduce: Gauss reduction to canonical reduced form with SL_2(Z) witness matrix and step ledger - number_theory.binary_quadratic_form.proper_equivalence.decide: decide proper equivalence by comparing canonical reduced representatives, returning the exact SL_2(Z) transformation witness - number_theory.binary_quadratic_form.reduced_classes.compute: enumerate all reduced primitive positive-definite classes of a given discriminant, returning the complete class set and class number h(D) Each result model re-runs the native kernel to verify exactness (fail-closed binding). All 22 known-answer, boundary, and adversarial tests pass. * Add request/result models for incidence structure operations Add Pydantic models for containment profiles, intersections, dual, complement, restriction, derived/residual, Levi graph, and Gram matrix operations. These complement the existing incidence matrix and degree profile models. * Add incidence structure operations and register tools Implement pure computation functions for containment profiles (t-subset codegrees), block intersections, dual, complement, restriction, derived/residual, Levi graph, and Gram matrix. Register all 10 MathTool declarations and admission decisions in the catalog. * Add tests for incidence structure operations Add tests covering all 10 operations: matrix, degree profile, containment profiles (t=1,2 on triangle and Fano plane), intersections, dual (including double-dual), complement (with size identity), restriction, derived/residual, Levi graph (with label collision), and Gram matrices (point and block axes). All 26 tests pass. * fix(math): lint and review fixes * style: ruff format * fix: mypy and boundary validation
1 parent caf0438 commit 5412655

5 files changed

Lines changed: 998 additions & 1 deletion

File tree

src/jacobian/math/incidence_structures/_admission.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,46 @@
1818
AdmissionDecision.KEEP,
1919
"per-point and per-block degree profiles with total incidence count",
2020
),
21+
OperationAdmission(
22+
"incidence.containment_profiles.compute",
23+
AdmissionDecision.KEEP,
24+
"t-subset containment multiplicity profiles with histogram",
25+
),
26+
OperationAdmission(
27+
"incidence.intersections.compute",
28+
AdmissionDecision.KEEP,
29+
"block pairwise intersection subsets and size histogram",
30+
),
31+
OperationAdmission(
32+
"incidence.dual.compute",
33+
AdmissionDecision.KEEP,
34+
"dual incidence structure swapping points and block IDs",
35+
),
36+
OperationAdmission(
37+
"incidence.complement.compute",
38+
AdmissionDecision.KEEP,
39+
"block complement incidence structure preserving block IDs",
40+
),
41+
OperationAdmission(
42+
"incidence.restriction.compute",
43+
AdmissionDecision.KEEP,
44+
"point/block restriction preserving block IDs",
45+
),
46+
OperationAdmission(
47+
"incidence.derived_residual.compute",
48+
AdmissionDecision.KEEP,
49+
"derived and residual incidence structures at a point",
50+
),
51+
OperationAdmission(
52+
"incidence.levi_graph.compute",
53+
AdmissionDecision.KEEP,
54+
"labelled bipartite Levi incidence graph",
55+
),
56+
OperationAdmission(
57+
"incidence.gram.compute",
58+
AdmissionDecision.KEEP,
59+
"Gram concordance matrix N N^T or N^T N",
60+
),
2161
)
2262

2363
REGISTRATION = OperationRegistration(TOOLS, ADMISSIONS)

src/jacobian/math/incidence_structures/_models.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010

1111
MAX_POINTS = 100
1212
MAX_BLOCKS = 100
13+
MAX_T = 10
14+
MAX_SUBSETS = 5_000
15+
MAX_PAIRS = 5_000
16+
MAX_MATRIX_CELLS = 10_000
17+
MAX_GRAPH_EDGES = 5_000
18+
MAX_LABEL_BYTES = 1_024
19+
MAX_RESULT_BYTES = 1_000_000
1320

1421

1522
class IncidenceStructure(StrictModel):
@@ -55,3 +62,152 @@ class DegreeProfileResult(StrictModel):
5562
point_degrees: tuple[tuple[str, int], ...]
5663
block_degrees: tuple[tuple[str, int], ...]
5764
total_incidences: int
65+
66+
67+
# ---------------------------------------------------------------------------
68+
# 3. Containment profiles (t-subset codegree profiles)
69+
# ---------------------------------------------------------------------------
70+
71+
72+
class ContainmentProfileRequest(StrictModel):
73+
incidence: IncidenceStructure
74+
t: int = Field(ge=1, le=MAX_T)
75+
76+
77+
class ContainmentProfileResult(StrictModel):
78+
t: int
79+
subset_profile: tuple[tuple[tuple[str, ...], int], ...]
80+
histogram: tuple[tuple[int, int], ...]
81+
min_multiplicity: int
82+
max_multiplicity: int
83+
is_constant: bool
84+
constant_lambda: int | None = None
85+
86+
87+
# ---------------------------------------------------------------------------
88+
# 4. Block intersection profiles
89+
# ---------------------------------------------------------------------------
90+
91+
92+
class IntersectionsRequest(StrictModel):
93+
incidence: IncidenceStructure
94+
95+
96+
class IntersectionsResult(StrictModel):
97+
pairwise: tuple[tuple[str, str, tuple[str, ...], int], ...]
98+
histogram: tuple[tuple[int, int], ...]
99+
100+
101+
# ---------------------------------------------------------------------------
102+
# 5. Dual incidence structure
103+
# ---------------------------------------------------------------------------
104+
105+
106+
class DualRequest(StrictModel):
107+
incidence: IncidenceStructure
108+
109+
110+
class DualResult(StrictModel):
111+
points: tuple[str, ...]
112+
block_ids: tuple[str, ...]
113+
blocks: tuple[tuple[str, ...], ...]
114+
point_map: tuple[tuple[str, str], ...]
115+
block_map: tuple[tuple[str, str], ...]
116+
117+
118+
# ---------------------------------------------------------------------------
119+
# 6. Complement incidence structure
120+
# ---------------------------------------------------------------------------
121+
122+
123+
class ComplementRequest(StrictModel):
124+
incidence: IncidenceStructure
125+
126+
127+
class ComplementResult(StrictModel):
128+
points: tuple[str, ...]
129+
block_ids: tuple[str, ...]
130+
blocks: tuple[tuple[str, ...], ...]
131+
correspondence: tuple[tuple[str, tuple[str, ...], tuple[str, ...]], ...]
132+
133+
134+
# ---------------------------------------------------------------------------
135+
# 7. Restriction (point/block deletion and restriction)
136+
# ---------------------------------------------------------------------------
137+
138+
139+
class RestrictionRequest(StrictModel):
140+
incidence: IncidenceStructure
141+
points: tuple[str, ...] = Field(default_factory=tuple)
142+
block_ids: tuple[str, ...] = Field(default_factory=tuple)
143+
144+
145+
class RestrictionResult(StrictModel):
146+
points: tuple[str, ...]
147+
block_ids: tuple[str, ...]
148+
blocks: tuple[tuple[str, ...], ...]
149+
150+
151+
# ---------------------------------------------------------------------------
152+
# 8. Derived and residual incidence structures
153+
# ---------------------------------------------------------------------------
154+
155+
156+
class DerivedResidualRequest(StrictModel):
157+
incidence: IncidenceStructure
158+
point: str
159+
kind: str = Field(default="derived")
160+
161+
@model_validator(mode="after")
162+
def require_valid_kind(self) -> Self:
163+
if self.kind not in ("derived", "residual"):
164+
raise ValueError("kind must be 'derived' or 'residual'")
165+
if self.point not in self.incidence.points:
166+
raise ValueError("anchor point must be a declared point")
167+
return self
168+
169+
170+
class DerivedResidualResult(StrictModel):
171+
kind: str
172+
anchor_point: str
173+
points: tuple[str, ...]
174+
block_ids: tuple[str, ...]
175+
blocks: tuple[tuple[str, ...], ...]
176+
source_blocks: tuple[str, ...]
177+
178+
179+
# ---------------------------------------------------------------------------
180+
# 9. Levi graph (bipartite incidence graph)
181+
# ---------------------------------------------------------------------------
182+
183+
184+
class LeviGraphRequest(StrictModel):
185+
incidence: IncidenceStructure
186+
187+
188+
class LeviGraphResult(StrictModel):
189+
left_vertices: tuple[str, ...]
190+
right_vertices: tuple[str, ...]
191+
edges: tuple[tuple[str, str], ...]
192+
193+
194+
# ---------------------------------------------------------------------------
195+
# 10. Gram / concordance matrix
196+
# ---------------------------------------------------------------------------
197+
198+
199+
class GramRequest(StrictModel):
200+
incidence: IncidenceStructure
201+
axis: str = Field(default="point")
202+
203+
@model_validator(mode="after")
204+
def require_valid_axis(self) -> Self:
205+
if self.axis not in ("point", "block"):
206+
raise ValueError("axis must be 'point' or 'block'")
207+
return self
208+
209+
210+
class GramResult(StrictModel):
211+
axis: str
212+
labels: tuple[str, ...]
213+
matrix: tuple[tuple[int, ...], ...]

0 commit comments

Comments
 (0)