process-geometry 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. aeg_shakespeare/__init__.py +52 -0
  2. aeg_shakespeare/_legacy_api.py +299 -0
  3. aeg_shakespeare/analysis/__init__.py +12 -0
  4. aeg_shakespeare/analysis/abelian.py +69 -0
  5. aeg_shakespeare/analysis/algebraic.py +11 -0
  6. aeg_shakespeare/analysis/am.py +19 -0
  7. aeg_shakespeare/analysis/connection.py +74 -0
  8. aeg_shakespeare/analysis/decomposition.py +37 -0
  9. aeg_shakespeare/analysis/module.py +5 -0
  10. aeg_shakespeare/central.py +20 -0
  11. aeg_shakespeare/constraints.py +121 -0
  12. aeg_shakespeare/construction.py +303 -0
  13. aeg_shakespeare/core.py +59 -0
  14. aeg_shakespeare/cost.py +51 -0
  15. aeg_shakespeare/discovery/__init__.py +53 -0
  16. aeg_shakespeare/discovery/coefficient_extension.py +75 -0
  17. aeg_shakespeare/discovery/polynomial.py +342 -0
  18. aeg_shakespeare/discovery/selection.py +146 -0
  19. aeg_shakespeare/discovery/structured.py +201 -0
  20. aeg_shakespeare/families.py +31 -0
  21. aeg_shakespeare/frame.py +5 -0
  22. aeg_shakespeare/function_theory/__init__.py +100 -0
  23. aeg_shakespeare/function_theory/abel_jacobi.py +246 -0
  24. aeg_shakespeare/function_theory/abelian.py +129 -0
  25. aeg_shakespeare/function_theory/algebraic.py +105 -0
  26. aeg_shakespeare/function_theory/am.py +266 -0
  27. aeg_shakespeare/function_theory/intersection.py +321 -0
  28. aeg_shakespeare/function_theory/module.py +118 -0
  29. aeg_shakespeare/function_theory/period_matrix.py +155 -0
  30. aeg_shakespeare/function_theory/periods.py +216 -0
  31. aeg_shakespeare/function_theory/real_branch_cycles.py +286 -0
  32. aeg_shakespeare/function_theory/weierstrass.py +136 -0
  33. aeg_shakespeare/grammar.py +225 -0
  34. aeg_shakespeare/history_geometry.py +276 -0
  35. aeg_shakespeare/linear.py +70 -0
  36. aeg_shakespeare/presentation/__init__.py +23 -0
  37. aeg_shakespeare/presentation/budget.py +27 -0
  38. aeg_shakespeare/presentation/canonicalization.py +143 -0
  39. aeg_shakespeare/presentation/constraints.py +5 -0
  40. aeg_shakespeare/presentation/construction.py +19 -0
  41. aeg_shakespeare/presentation/grammar.py +15 -0
  42. aeg_shakespeare/presentation/history.py +45 -0
  43. aeg_shakespeare/presentation/morphism.py +66 -0
  44. aeg_shakespeare/presentation/relations.py +31 -0
  45. aeg_shakespeare/presentation/search.py +31 -0
  46. aeg_shakespeare/process/__init__.py +16 -0
  47. aeg_shakespeare/process/finite/__init__.py +43 -0
  48. aeg_shakespeare/process/finite/cocycle.py +166 -0
  49. aeg_shakespeare/process/finite/families.py +318 -0
  50. aeg_shakespeare/process/history.py +53 -0
  51. aeg_shakespeare/process/local/__init__.py +7 -0
  52. aeg_shakespeare/process/local/direction.py +88 -0
  53. aeg_shakespeare/process/local/frame.py +73 -0
  54. aeg_shakespeare/process/local/system.py +43 -0
  55. aeg_shakespeare/relations.py +374 -0
  56. aeg_shakespeare/rewrite.py +157 -0
  57. aeg_shakespeare/search.py +286 -0
  58. aeg_shakespeare/signature.py +155 -0
  59. process_geometry-0.0.3.dist-info/METADATA +305 -0
  60. process_geometry-0.0.3.dist-info/RECORD +63 -0
  61. process_geometry-0.0.3.dist-info/WHEEL +5 -0
  62. process_geometry-0.0.3.dist-info/licenses/LICENSE +24 -0
  63. process_geometry-0.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,266 @@
1
+ """Function theory generated by Addition (A) and Multiplication (M).
2
+
3
+ Mathematical lineage
4
+ --------------------
5
+ Ordinary calculus is usually taught after the additive coordinate line has
6
+ already been chosen: translation is invisible background structure, and
7
+ multiplication appears later as a nonlinear operation on numbers or functions.
8
+ The AEG/Shakespeare reconstruction asks what changes if Addition and
9
+ Multiplication themselves are treated as primitive process directions before a
10
+ function theory is chosen.
11
+
12
+ At finite scale the two processes are
13
+
14
+ T_t: (a, v) -> (a + t, v),
15
+ S_s: (a, v) -> (exp(s) * a, v + s),
16
+
17
+ and their history is noncommutative:
18
+
19
+ S_s T_t = T_(exp(s) t) S_s.
20
+
21
+ The infinitesimal shadow is therefore not two arbitrary differential operators
22
+ but the arithmetic frame
23
+
24
+ A = d/da,
25
+ M = d/dv + a d/da,
26
+
27
+ with ``[A, M] = A``. Function families are then judged by how simply they
28
+ transform under these process histories. The power-weight lattice is one such
29
+ family; process inversion exposes resonance loci, and those loci force
30
+ logarithmic or Jordan-like extensions rather than assuming ``log`` in advance.
31
+
32
+ Boundary
33
+ --------
34
+ A/M is the first concrete Shakespeare function theory, not the universal one.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ from dataclasses import dataclass
40
+
41
+ import sympy as sp
42
+
43
+ from ..process.local import ProcessFrame
44
+
45
+
46
+ def affine_am_frame(a: sp.Symbol, v: sp.Symbol) -> ProcessFrame:
47
+ """Return the canonical Addition/Multiplication process frame."""
48
+ return ProcessFrame(
49
+ assignments=(a, v),
50
+ generators={
51
+ "A": {a: sp.S.One, v: sp.S.Zero},
52
+ "M": {a: a, v: sp.S.One},
53
+ },
54
+ )
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class AMState:
59
+ a: sp.Expr
60
+ v: sp.Expr
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class AMPowerWeight:
65
+ a: sp.Symbol
66
+ v: sp.Symbol
67
+ nu: sp.Expr
68
+ weight: sp.Expr
69
+
70
+ @property
71
+ def expression(self) -> sp.Expr:
72
+ return self.a ** self.nu * sp.exp((self.weight - self.nu) * self.v)
73
+
74
+ def product(self, other: "AMPowerWeight") -> "AMPowerWeight":
75
+ if self.a != other.a or self.v != other.v:
76
+ raise ValueError("power-weight elements must use the same A/M coordinates")
77
+ return AMPowerWeight(
78
+ self.a,
79
+ self.v,
80
+ sp.simplify(self.nu + other.nu),
81
+ sp.simplify(self.weight + other.weight),
82
+ )
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class AMPrimitive:
87
+ generator: str
88
+ integrand: AMPowerWeight
89
+ expression: sp.Expr
90
+ resonant: bool
91
+ exceptional_factor: sp.Expr
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class AMPathFlow:
96
+ start: sp.Expr
97
+ end: sp.Expr
98
+ beta_integral: sp.Expr
99
+ history_term: sp.Expr
100
+ a_end: sp.Expr
101
+ v_end: sp.Expr
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class AMFunctionTheory:
106
+ """Concrete function calculus generated by Addition and Multiplication."""
107
+
108
+ a: sp.Symbol
109
+ v: sp.Symbol
110
+
111
+ @property
112
+ def frame(self) -> ProcessFrame:
113
+ return affine_am_frame(self.a, self.v)
114
+
115
+ def A(self, expr: sp.Expr) -> sp.Expr:
116
+ return self.frame.apply("A", expr)
117
+
118
+ def M(self, expr: sp.Expr) -> sp.Expr:
119
+ return self.frame.apply("M", expr)
120
+
121
+ def commutator(self, expr: sp.Expr) -> sp.Expr:
122
+ return self.frame.commutator("A", "M", expr)
123
+
124
+ def add(self, state: AMState, amount: sp.Expr) -> AMState:
125
+ amount = sp.sympify(amount)
126
+ return AMState(sp.expand(state.a + amount), sp.sympify(state.v))
127
+
128
+ def multiply(self, state: AMState, log_scale: sp.Expr) -> AMState:
129
+ log_scale = sp.sympify(log_scale)
130
+ return AMState(
131
+ sp.expand(sp.exp(log_scale) * state.a),
132
+ sp.expand(state.v + log_scale),
133
+ )
134
+
135
+ def finite_relation_residual(
136
+ self,
137
+ state: AMState,
138
+ *,
139
+ amount: sp.Expr,
140
+ log_scale: sp.Expr,
141
+ ) -> tuple[sp.Expr, sp.Expr]:
142
+ amount = sp.sympify(amount)
143
+ log_scale = sp.sympify(log_scale)
144
+ left = self.multiply(self.add(state, amount), log_scale)
145
+ right = self.add(
146
+ self.multiply(state, log_scale),
147
+ sp.exp(log_scale) * amount,
148
+ )
149
+ return (
150
+ sp.simplify(left.a - right.a),
151
+ sp.simplify(left.v - right.v),
152
+ )
153
+
154
+ def power_weight(self, nu: sp.Expr, weight: sp.Expr) -> AMPowerWeight:
155
+ return AMPowerWeight(self.a, self.v, sp.sympify(nu), sp.sympify(weight))
156
+
157
+ def multiply_power_weights(
158
+ self,
159
+ left: AMPowerWeight,
160
+ right: AMPowerWeight,
161
+ ) -> AMPowerWeight:
162
+ return left.product(right)
163
+
164
+ def A_primitive(self, element: AMPowerWeight) -> AMPrimitive:
165
+ if element.a != self.a or element.v != self.v:
166
+ raise ValueError("power-weight element belongs to another A/M frame")
167
+ exceptional = sp.simplify(element.nu + 1)
168
+ if exceptional == 0:
169
+ primitive = sp.exp((element.weight + 1) * self.v) * sp.log(self.a)
170
+ return AMPrimitive(
171
+ generator="A",
172
+ integrand=element,
173
+ expression=primitive,
174
+ resonant=True,
175
+ exceptional_factor=exceptional,
176
+ )
177
+ next_element = self.power_weight(element.nu + 1, element.weight + 1)
178
+ return AMPrimitive(
179
+ generator="A",
180
+ integrand=element,
181
+ expression=sp.simplify(next_element.expression / exceptional),
182
+ resonant=False,
183
+ exceptional_factor=exceptional,
184
+ )
185
+
186
+ def M_primitive(self, element: AMPowerWeight) -> AMPrimitive:
187
+ if element.a != self.a or element.v != self.v:
188
+ raise ValueError("power-weight element belongs to another A/M frame")
189
+ exceptional = sp.simplify(element.weight)
190
+ if exceptional == 0:
191
+ return AMPrimitive(
192
+ generator="M",
193
+ integrand=element,
194
+ expression=sp.expand(self.v * element.expression),
195
+ resonant=True,
196
+ exceptional_factor=exceptional,
197
+ )
198
+ return AMPrimitive(
199
+ generator="M",
200
+ integrand=element,
201
+ expression=sp.simplify(element.expression / exceptional),
202
+ resonant=False,
203
+ exceptional_factor=exceptional,
204
+ )
205
+
206
+ def primitive_residual(self, primitive: AMPrimitive) -> sp.Expr:
207
+ if primitive.generator == "A":
208
+ image = self.A(primitive.expression)
209
+ elif primitive.generator == "M":
210
+ image = self.M(primitive.expression)
211
+ else:
212
+ raise ValueError(f"unknown A/M primitive generator: {primitive.generator!r}")
213
+ return sp.simplify(sp.expand(image - primitive.integrand.expression))
214
+
215
+ def shifted_M(self, expr: sp.Expr, shift: sp.Expr) -> sp.Expr:
216
+ return sp.expand(self.M(expr) - sp.sympify(shift) * sp.sympify(expr))
217
+
218
+ def pbw_residual(self, expr: sp.Expr, *, m: int, n: int) -> sp.Expr:
219
+ if m < 0 or n < 0:
220
+ raise ValueError("m and n must be non-negative")
221
+ left = self.frame.iterate("A", expr, m)
222
+ left = self.frame.iterate("M", left, n)
223
+ right = sp.sympify(expr)
224
+ for _ in range(n):
225
+ right = self.shifted_M(right, m)
226
+ right = self.frame.iterate("A", right, m)
227
+ return sp.simplify(sp.expand(left - right))
228
+
229
+ def path_flow(
230
+ self,
231
+ alpha: sp.Expr,
232
+ beta: sp.Expr,
233
+ time: sp.Symbol,
234
+ *,
235
+ start: sp.Expr,
236
+ end: sp.Expr,
237
+ a0: sp.Expr,
238
+ v0: sp.Expr,
239
+ ) -> AMPathFlow:
240
+ alpha = sp.sympify(alpha)
241
+ beta = sp.sympify(beta)
242
+ start = sp.sympify(start)
243
+ end = sp.sympify(end)
244
+ a0 = sp.sympify(a0)
245
+ v0 = sp.sympify(v0)
246
+
247
+ tau = sp.Dummy("tau")
248
+ sigma = sp.Dummy("sigma")
249
+ beta_sigma = beta.subs(time, sigma)
250
+ beta_integral = sp.integrate(beta_sigma, (sigma, start, end))
251
+ beta_to_tau = sp.integrate(beta_sigma, (sigma, start, tau))
252
+ alpha_tau = alpha.subs(time, tau)
253
+ history_term = sp.integrate(
254
+ sp.exp(beta_integral - beta_to_tau) * alpha_tau,
255
+ (tau, start, end),
256
+ )
257
+ a_end = sp.simplify(sp.exp(beta_integral) * a0 + history_term)
258
+ v_end = sp.simplify(v0 + beta_integral)
259
+ return AMPathFlow(
260
+ start=start,
261
+ end=end,
262
+ beta_integral=sp.simplify(beta_integral),
263
+ history_term=sp.simplify(history_term),
264
+ a_end=a_end,
265
+ v_end=v_end,
266
+ )
@@ -0,0 +1,321 @@
1
+ """Sampled intersection pairing for lifted process cycles.
2
+
3
+ Primitive question
4
+ ------------------
5
+ A period matrix is only a candidate Riemann period presentation until the cycles
6
+ carrying its periods have the right intersection structure. On a genus-g
7
+ oriented surface a canonical basis satisfies
8
+
9
+ a_i . a_j = 0,
10
+ b_i . b_j = 0,
11
+ a_i . b_j = delta_ij.
12
+
13
+ How much of that structure can be measured directly from the sampled lifted
14
+ histories already used by Shakespeare's period engine?
15
+
16
+ Classical shadow
17
+ ----------------
18
+ The algebraic intersection number of transverse oriented curves is the signed
19
+ sum of their intersections. On a hyperelliptic double cover, two projected
20
+ base paths may cross at the same x-value while their lifts lie on opposite
21
+ sheets, in which case there is *no* intersection on the Riemann surface. A
22
+ symplectic homology basis has intersection matrix
23
+
24
+ J = [[0, I], [-I, 0]].
25
+
26
+ See Farkas--Kra and Forster in ``docs/REFERENCES.md``.
27
+
28
+ Shakespeare reconstruction
29
+ ---------------------------
30
+ The implementation works with the actual lifted histories rather than only the
31
+ projected curves. It first finds transverse intersections of polygonal base
32
+ segments. At each projected crossing it interpolates the two continued y
33
+ values and asks whether the histories occupy the same sheet. Only same-sheet
34
+ crossings contribute their orientation sign to the sampled intersection number.
35
+
36
+ This makes a subtle point executable: *projected crossing is not surface
37
+ intersection*. Sheet history is part of the topology.
38
+
39
+ Executable contract
40
+ -------------------
41
+ ``lifted_path_intersections`` reports every resolved projected crossing together
42
+ with its orientation and sheet relation. ``sampled_intersection_number`` sums
43
+ same-sheet signs and refuses unresolved sheet comparisons. For an
44
+ ``AbelianCycleSystem``, ``sampled_intersection_form`` builds the full 2g-by-2g
45
+ matrix in A-then-B order and compares it with the canonical symplectic form.
46
+
47
+ ``sampled_riemann_profile`` combines that sampled topological evidence with the
48
+ symmetry/positive-imaginary-part checks of an ``AbelianPeriodMatrix``.
49
+
50
+ Boundary
51
+ --------
52
+ This remains a sampled numerical topology engine, not a certified algebraic
53
+ intersection algorithm. Paths must be polygonally resolved, transverse, and
54
+ away from branch points. Tangencies, shared segments, endpoint crossings, and
55
+ near-degenerate sheet comparisons are rejected or ignored rather than assigned
56
+ an intersection number. Exact homology classes and deformation invariance are
57
+ future layers.
58
+ """
59
+
60
+ from __future__ import annotations
61
+
62
+ from dataclasses import dataclass
63
+ from typing import Literal
64
+
65
+ import sympy as sp
66
+
67
+ from .period_matrix import AbelianCycleSystem, AbelianPeriodMatrix
68
+ from .periods import LiftedSquareRootPath
69
+
70
+ SheetRelation = Literal["same", "opposite", "unresolved"]
71
+
72
+
73
+ def _cross(left: complex, right: complex) -> float:
74
+ return float((left.conjugate() * right).imag)
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class LiftedCycleIntersection:
79
+ """One transverse crossing of two projected segments with sheet metadata."""
80
+
81
+ base_point: complex
82
+ left_segment: int
83
+ right_segment: int
84
+ orientation: int
85
+ sheet_relation: SheetRelation
86
+ relative_same_sheet_error: float
87
+ relative_opposite_sheet_error: float
88
+
89
+ @property
90
+ def contributes_on_surface(self) -> bool:
91
+ return self.sheet_relation == "same"
92
+
93
+
94
+ def _proper_segment_intersection(
95
+ p: complex,
96
+ p_next: complex,
97
+ q: complex,
98
+ q_next: complex,
99
+ *,
100
+ geometry_tolerance: float,
101
+ ) -> tuple[float, float, complex, int] | None:
102
+ r = p_next - p
103
+ s = q_next - q
104
+ denominator = _cross(r, s)
105
+ if abs(denominator) <= geometry_tolerance:
106
+ return None
107
+
108
+ delta = q - p
109
+ t = _cross(delta, s) / denominator
110
+ u = _cross(delta, r) / denominator
111
+ margin = geometry_tolerance
112
+ if not (margin < t < 1.0 - margin and margin < u < 1.0 - margin):
113
+ return None
114
+
115
+ point = p + t * r
116
+ orientation = 1 if denominator > 0 else -1
117
+ return t, u, point, orientation
118
+
119
+
120
+ def lifted_path_intersections(
121
+ left: LiftedSquareRootPath,
122
+ right: LiftedSquareRootPath,
123
+ *,
124
+ sheet_tolerance: float = 1e-3,
125
+ geometry_tolerance: float = 1e-12,
126
+ dedup_tolerance: float = 1e-8,
127
+ ) -> tuple[LiftedCycleIntersection, ...]:
128
+ """Measure transverse projected crossings and resolve their sheet relation."""
129
+
130
+ if left.curve != right.curve:
131
+ raise ValueError("lifted paths must belong to the same curve")
132
+ if sheet_tolerance <= 0 or geometry_tolerance <= 0 or dedup_tolerance <= 0:
133
+ raise ValueError("intersection tolerances must be positive")
134
+
135
+ intersections: list[LiftedCycleIntersection] = []
136
+ for i in range(len(left.x_values) - 1):
137
+ p = left.x_values[i]
138
+ p_next = left.x_values[i + 1]
139
+ left_min_x, left_max_x = sorted((p.real, p_next.real))
140
+ left_min_y, left_max_y = sorted((p.imag, p_next.imag))
141
+
142
+ for j in range(len(right.x_values) - 1):
143
+ q = right.x_values[j]
144
+ q_next = right.x_values[j + 1]
145
+ right_min_x, right_max_x = sorted((q.real, q_next.real))
146
+ right_min_y, right_max_y = sorted((q.imag, q_next.imag))
147
+
148
+ if left_max_x < right_min_x or right_max_x < left_min_x:
149
+ continue
150
+ if left_max_y < right_min_y or right_max_y < left_min_y:
151
+ continue
152
+
153
+ crossing = _proper_segment_intersection(
154
+ p,
155
+ p_next,
156
+ q,
157
+ q_next,
158
+ geometry_tolerance=geometry_tolerance,
159
+ )
160
+ if crossing is None:
161
+ continue
162
+ t, u, point, orientation = crossing
163
+
164
+ if any(abs(point - old.base_point) <= dedup_tolerance for old in intersections):
165
+ continue
166
+
167
+ left_y = left.y_values[i] + t * (left.y_values[i + 1] - left.y_values[i])
168
+ right_y = right.y_values[j] + u * (right.y_values[j + 1] - right.y_values[j])
169
+ scale = max(1.0, abs(left_y), abs(right_y))
170
+ same_error = abs(left_y - right_y) / scale
171
+ opposite_error = abs(left_y + right_y) / scale
172
+
173
+ if same_error <= sheet_tolerance and same_error < opposite_error:
174
+ sheet_relation: SheetRelation = "same"
175
+ elif opposite_error <= sheet_tolerance and opposite_error < same_error:
176
+ sheet_relation = "opposite"
177
+ else:
178
+ sheet_relation = "unresolved"
179
+
180
+ intersections.append(
181
+ LiftedCycleIntersection(
182
+ base_point=point,
183
+ left_segment=i,
184
+ right_segment=j,
185
+ orientation=orientation,
186
+ sheet_relation=sheet_relation,
187
+ relative_same_sheet_error=float(same_error),
188
+ relative_opposite_sheet_error=float(opposite_error),
189
+ )
190
+ )
191
+
192
+ return tuple(intersections)
193
+
194
+
195
+ def sampled_intersection_number(
196
+ left: LiftedSquareRootPath,
197
+ right: LiftedSquareRootPath,
198
+ *,
199
+ sheet_tolerance: float = 1e-3,
200
+ ) -> int:
201
+ """Signed same-sheet intersection count for two sampled lifted cycles."""
202
+
203
+ crossings = lifted_path_intersections(
204
+ left,
205
+ right,
206
+ sheet_tolerance=sheet_tolerance,
207
+ )
208
+ unresolved = [crossing for crossing in crossings if crossing.sheet_relation == "unresolved"]
209
+ if unresolved:
210
+ raise ValueError("sampled intersections contain unresolved sheet comparisons")
211
+ return sum(
212
+ crossing.orientation
213
+ for crossing in crossings
214
+ if crossing.contributes_on_surface
215
+ )
216
+
217
+
218
+ def canonical_symplectic_form(genus: int) -> tuple[tuple[int, ...], ...]:
219
+ """Return J=[[0,I],[-I,0]] in A-then-B cycle order."""
220
+
221
+ if genus < 1:
222
+ raise ValueError("genus must be positive")
223
+ size = 2 * genus
224
+ rows = [[0 for _ in range(size)] for _ in range(size)]
225
+ for index in range(genus):
226
+ rows[index][genus + index] = 1
227
+ rows[genus + index][index] = -1
228
+ return tuple(tuple(row) for row in rows)
229
+
230
+
231
+ @dataclass(frozen=True)
232
+ class SampledIntersectionForm:
233
+ """Numerically measured intersection form for one A/B cycle system."""
234
+
235
+ cycles: AbelianCycleSystem
236
+ matrix: tuple[tuple[int, ...], ...]
237
+
238
+ @property
239
+ def genus(self) -> int:
240
+ return self.cycles.curve.generic_genus or 0
241
+
242
+ @property
243
+ def is_skew_symmetric(self) -> bool:
244
+ return all(
245
+ self.matrix[i][j] == -self.matrix[j][i]
246
+ for i in range(len(self.matrix))
247
+ for j in range(len(self.matrix))
248
+ )
249
+
250
+ @property
251
+ def determinant(self) -> int:
252
+ return int(sp.Matrix(self.matrix).det())
253
+
254
+ @property
255
+ def is_unimodular(self) -> bool:
256
+ return abs(self.determinant) == 1
257
+
258
+ @property
259
+ def canonical_residual(self) -> int:
260
+ expected = canonical_symplectic_form(self.genus)
261
+ return max(
262
+ abs(self.matrix[i][j] - expected[i][j])
263
+ for i in range(len(self.matrix))
264
+ for j in range(len(self.matrix))
265
+ )
266
+
267
+ @property
268
+ def is_canonical_symplectic(self) -> bool:
269
+ return (
270
+ self.is_skew_symmetric
271
+ and self.is_unimodular
272
+ and self.canonical_residual == 0
273
+ )
274
+
275
+
276
+ def sampled_intersection_form(
277
+ cycles: AbelianCycleSystem,
278
+ *,
279
+ sheet_tolerance: float = 1e-3,
280
+ ) -> SampledIntersectionForm:
281
+ """Measure the 2g-by-2g sampled intersection matrix in A-then-B order."""
282
+
283
+ ordered = cycles.a_cycles + cycles.b_cycles
284
+ size = len(ordered)
285
+ matrix = [[0 for _ in range(size)] for _ in range(size)]
286
+ for i in range(size):
287
+ for j in range(i + 1, size):
288
+ value = sampled_intersection_number(
289
+ ordered[i],
290
+ ordered[j],
291
+ sheet_tolerance=sheet_tolerance,
292
+ )
293
+ matrix[i][j] = value
294
+ matrix[j][i] = -value
295
+ return SampledIntersectionForm(cycles, tuple(tuple(row) for row in matrix))
296
+
297
+
298
+ @dataclass(frozen=True)
299
+ class SampledRiemannProfile:
300
+ """Combined sampled topology + period-matrix consistency profile."""
301
+
302
+ periods: AbelianPeriodMatrix
303
+ intersections: SampledIntersectionForm
304
+
305
+ def __post_init__(self) -> None:
306
+ if self.periods.cycles != self.intersections.cycles:
307
+ raise ValueError("period and intersection data must use the same cycle system")
308
+
309
+ @property
310
+ def passes(self) -> bool:
311
+ return (
312
+ self.intersections.is_canonical_symplectic
313
+ and self.periods.riemann_shape_passes()
314
+ )
315
+
316
+
317
+ def sampled_riemann_profile(
318
+ periods: AbelianPeriodMatrix,
319
+ intersections: SampledIntersectionForm,
320
+ ) -> SampledRiemannProfile:
321
+ return SampledRiemannProfile(periods=periods, intersections=intersections)
@@ -0,0 +1,118 @@
1
+ """Finite function modules for process-based function theory.
2
+
3
+ A function module is represented by a finite basis together with explicit action
4
+ rules for named process generators. The action table is primary; matrices are
5
+ not required by the public object. A symbolic ``ProcessFrame`` can be attached
6
+ as a verifier so that a proposed low-complexity function language carries an
7
+ exact certificate.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Mapping
14
+
15
+ import sympy as sp
16
+
17
+ from ..process.local import ProcessFrame
18
+
19
+ Coordinates = tuple[sp.Expr, ...]
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class ProcessFunctionModule:
24
+ """A finite basis closed under declared process-generator actions."""
25
+
26
+ basis: tuple[sp.Expr, ...]
27
+ actions: Mapping[str, tuple[Coordinates, ...]]
28
+
29
+ def __post_init__(self) -> None:
30
+ basis = tuple(sp.sympify(item) for item in self.basis)
31
+ if not basis:
32
+ raise ValueError("function module requires a non-empty basis")
33
+ object.__setattr__(self, "basis", basis)
34
+
35
+ dimension = len(basis)
36
+ if not self.actions:
37
+ raise ValueError("function module requires at least one process action")
38
+ for generator, columns in self.actions.items():
39
+ if not generator:
40
+ raise ValueError("generator names must be non-empty")
41
+ if len(columns) != dimension:
42
+ raise ValueError(
43
+ f"generator {generator!r} must provide one action column per basis item"
44
+ )
45
+ for coordinates in columns:
46
+ if len(coordinates) != dimension:
47
+ raise ValueError("action coordinate vectors must match module dimension")
48
+
49
+ @property
50
+ def dimension(self) -> int:
51
+ return len(self.basis)
52
+
53
+ @property
54
+ def generators(self) -> tuple[str, ...]:
55
+ return tuple(self.actions)
56
+
57
+ def action_expression(self, generator: str, basis_index: int) -> sp.Expr:
58
+ if generator not in self.actions:
59
+ raise KeyError(f"unknown module generator: {generator!r}")
60
+ coordinates = self.actions[generator][basis_index]
61
+ return sp.expand(
62
+ sum(
63
+ sp.sympify(coefficient) * basis_item
64
+ for coefficient, basis_item in zip(coordinates, self.basis)
65
+ )
66
+ )
67
+
68
+ def verification_residuals(self, frame: ProcessFrame) -> Mapping[str, tuple[sp.Expr, ...]]:
69
+ result: dict[str, tuple[sp.Expr, ...]] = {}
70
+ for generator in self.generators:
71
+ if generator not in frame.generators:
72
+ raise KeyError(f"frame does not define module generator {generator!r}")
73
+ residuals = []
74
+ for index, basis_item in enumerate(self.basis):
75
+ declared = self.action_expression(generator, index)
76
+ actual = frame.apply(generator, basis_item)
77
+ residuals.append(sp.simplify(sp.expand(actual - declared)))
78
+ result[generator] = tuple(residuals)
79
+ return result
80
+
81
+ def verify(self, frame: ProcessFrame) -> bool:
82
+ return all(
83
+ residual == 0
84
+ for residuals in self.verification_residuals(frame).values()
85
+ for residual in residuals
86
+ )
87
+
88
+
89
+ def polynomial_am_module(
90
+ a: sp.Symbol,
91
+ degree: int,
92
+ ) -> ProcessFunctionModule:
93
+ """Return the finite polynomial A/M module ``span(1,a,...,a**degree)``."""
94
+
95
+ if degree < 0:
96
+ raise ValueError("degree must be non-negative")
97
+ basis = tuple(a**k for k in range(degree + 1))
98
+ dimension = degree + 1
99
+
100
+ addition_columns: list[Coordinates] = []
101
+ multiplication_columns: list[Coordinates] = []
102
+ for k in range(dimension):
103
+ addition = [sp.S.Zero] * dimension
104
+ if k > 0:
105
+ addition[k - 1] = sp.Integer(k)
106
+ addition_columns.append(tuple(addition))
107
+
108
+ multiplication = [sp.S.Zero] * dimension
109
+ multiplication[k] = sp.Integer(k)
110
+ multiplication_columns.append(tuple(multiplication))
111
+
112
+ return ProcessFunctionModule(
113
+ basis=basis,
114
+ actions={
115
+ "A": tuple(addition_columns),
116
+ "M": tuple(multiplication_columns),
117
+ },
118
+ )