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.
- aeg_shakespeare/__init__.py +52 -0
- aeg_shakespeare/_legacy_api.py +299 -0
- aeg_shakespeare/analysis/__init__.py +12 -0
- aeg_shakespeare/analysis/abelian.py +69 -0
- aeg_shakespeare/analysis/algebraic.py +11 -0
- aeg_shakespeare/analysis/am.py +19 -0
- aeg_shakespeare/analysis/connection.py +74 -0
- aeg_shakespeare/analysis/decomposition.py +37 -0
- aeg_shakespeare/analysis/module.py +5 -0
- aeg_shakespeare/central.py +20 -0
- aeg_shakespeare/constraints.py +121 -0
- aeg_shakespeare/construction.py +303 -0
- aeg_shakespeare/core.py +59 -0
- aeg_shakespeare/cost.py +51 -0
- aeg_shakespeare/discovery/__init__.py +53 -0
- aeg_shakespeare/discovery/coefficient_extension.py +75 -0
- aeg_shakespeare/discovery/polynomial.py +342 -0
- aeg_shakespeare/discovery/selection.py +146 -0
- aeg_shakespeare/discovery/structured.py +201 -0
- aeg_shakespeare/families.py +31 -0
- aeg_shakespeare/frame.py +5 -0
- aeg_shakespeare/function_theory/__init__.py +100 -0
- aeg_shakespeare/function_theory/abel_jacobi.py +246 -0
- aeg_shakespeare/function_theory/abelian.py +129 -0
- aeg_shakespeare/function_theory/algebraic.py +105 -0
- aeg_shakespeare/function_theory/am.py +266 -0
- aeg_shakespeare/function_theory/intersection.py +321 -0
- aeg_shakespeare/function_theory/module.py +118 -0
- aeg_shakespeare/function_theory/period_matrix.py +155 -0
- aeg_shakespeare/function_theory/periods.py +216 -0
- aeg_shakespeare/function_theory/real_branch_cycles.py +286 -0
- aeg_shakespeare/function_theory/weierstrass.py +136 -0
- aeg_shakespeare/grammar.py +225 -0
- aeg_shakespeare/history_geometry.py +276 -0
- aeg_shakespeare/linear.py +70 -0
- aeg_shakespeare/presentation/__init__.py +23 -0
- aeg_shakespeare/presentation/budget.py +27 -0
- aeg_shakespeare/presentation/canonicalization.py +143 -0
- aeg_shakespeare/presentation/constraints.py +5 -0
- aeg_shakespeare/presentation/construction.py +19 -0
- aeg_shakespeare/presentation/grammar.py +15 -0
- aeg_shakespeare/presentation/history.py +45 -0
- aeg_shakespeare/presentation/morphism.py +66 -0
- aeg_shakespeare/presentation/relations.py +31 -0
- aeg_shakespeare/presentation/search.py +31 -0
- aeg_shakespeare/process/__init__.py +16 -0
- aeg_shakespeare/process/finite/__init__.py +43 -0
- aeg_shakespeare/process/finite/cocycle.py +166 -0
- aeg_shakespeare/process/finite/families.py +318 -0
- aeg_shakespeare/process/history.py +53 -0
- aeg_shakespeare/process/local/__init__.py +7 -0
- aeg_shakespeare/process/local/direction.py +88 -0
- aeg_shakespeare/process/local/frame.py +73 -0
- aeg_shakespeare/process/local/system.py +43 -0
- aeg_shakespeare/relations.py +374 -0
- aeg_shakespeare/rewrite.py +157 -0
- aeg_shakespeare/search.py +286 -0
- aeg_shakespeare/signature.py +155 -0
- process_geometry-0.0.3.dist-info/METADATA +305 -0
- process_geometry-0.0.3.dist-info/RECORD +63 -0
- process_geometry-0.0.3.dist-info/WHEEL +5 -0
- process_geometry-0.0.3.dist-info/licenses/LICENSE +24 -0
- 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
|
+
)
|