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,155 @@
|
|
|
1
|
+
"""Cycle systems and candidate normalized period matrices.
|
|
2
|
+
|
|
3
|
+
Primitive question
|
|
4
|
+
------------------
|
|
5
|
+
A single period is not yet the global object used by Abelian function theory.
|
|
6
|
+
For a genus-g curve one needs g independent holomorphic differentials and 2g
|
|
7
|
+
closed cycles. Their integrals form two g-by-g blocks conventionally written
|
|
8
|
+
A and B. When the cycles are a symplectic homology basis and A is invertible,
|
|
9
|
+
the normalized period matrix is
|
|
10
|
+
|
|
11
|
+
tau = A^{-1} B.
|
|
12
|
+
|
|
13
|
+
Classical shadow
|
|
14
|
+
----------------
|
|
15
|
+
For a compact Riemann surface, a normalized period matrix associated with a
|
|
16
|
+
symplectic homology basis is symmetric and has positive-definite imaginary
|
|
17
|
+
part. These are the Riemann bilinear constraints underlying the Jacobian.
|
|
18
|
+
See Farkas--Kra, Forster, and Mumford in ``docs/REFERENCES.md``.
|
|
19
|
+
|
|
20
|
+
Shakespeare reconstruction
|
|
21
|
+
---------------------------
|
|
22
|
+
This module deliberately starts one step earlier. The caller supplies *closed
|
|
23
|
+
lifted histories* already produced by the branch-continuation layer. We first
|
|
24
|
+
measure every canonical differential on every supplied cycle, then normalize
|
|
25
|
+
the resulting blocks. Only afterwards do we ask whether the numerical matrix
|
|
26
|
+
has the symmetry/positivity shape expected of genuine Riemann period data.
|
|
27
|
+
|
|
28
|
+
Crucially, the current engine does not compute intersection numbers. Passing
|
|
29
|
+
the matrix-shape checks is therefore evidence that a supplied cycle system is
|
|
30
|
+
consistent with a symplectic period presentation, not a proof that the cycles
|
|
31
|
+
form a canonical homology basis.
|
|
32
|
+
|
|
33
|
+
Boundary
|
|
34
|
+
--------
|
|
35
|
+
No automatic homology basis, intersection pairing, certified quadrature error,
|
|
36
|
+
or Jacobian construction is implemented here. The positivity test uses the
|
|
37
|
+
Sylvester criterion on the symmetrized numerical imaginary part.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
|
|
44
|
+
import sympy as sp
|
|
45
|
+
|
|
46
|
+
from .abelian import holomorphic_differential_basis
|
|
47
|
+
from .algebraic import HyperellipticProfile
|
|
48
|
+
from .periods import LiftedSquareRootPath, integrate_lifted_differential
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class AbelianCycleSystem:
|
|
53
|
+
"""Caller-supplied A/B closed lifted cycles for a genus-g quotient."""
|
|
54
|
+
|
|
55
|
+
curve: HyperellipticProfile
|
|
56
|
+
a_cycles: tuple[LiftedSquareRootPath, ...]
|
|
57
|
+
b_cycles: tuple[LiftedSquareRootPath, ...]
|
|
58
|
+
|
|
59
|
+
def __post_init__(self) -> None:
|
|
60
|
+
genus = self.curve.generic_genus
|
|
61
|
+
if genus is None or genus < 1:
|
|
62
|
+
raise ValueError("cycle system requires a generically smooth positive-genus curve")
|
|
63
|
+
if len(self.a_cycles) != genus or len(self.b_cycles) != genus:
|
|
64
|
+
raise ValueError("cycle system requires exactly g A-cycles and g B-cycles")
|
|
65
|
+
for cycle in self.a_cycles + self.b_cycles:
|
|
66
|
+
if cycle.curve != self.curve:
|
|
67
|
+
raise ValueError("all cycles must belong to the declared curve")
|
|
68
|
+
if not cycle.lifted_closed:
|
|
69
|
+
raise ValueError("period cycles must be closed on the lifted surface")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class AbelianPeriodMatrix:
|
|
74
|
+
"""Measured A/B period blocks and normalized candidate ``tau=A^{-1}B``."""
|
|
75
|
+
|
|
76
|
+
cycles: AbelianCycleSystem
|
|
77
|
+
a_periods: tuple[tuple[complex, ...], ...]
|
|
78
|
+
b_periods: tuple[tuple[complex, ...], ...]
|
|
79
|
+
tau: tuple[tuple[complex, ...], ...]
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def genus(self) -> int:
|
|
83
|
+
return len(self.tau)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def symmetry_residual(self) -> float:
|
|
87
|
+
return max(
|
|
88
|
+
abs(self.tau[i][j] - self.tau[j][i])
|
|
89
|
+
for i in range(self.genus)
|
|
90
|
+
for j in range(self.genus)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def imaginary_part(self) -> tuple[tuple[float, ...], ...]:
|
|
95
|
+
return tuple(
|
|
96
|
+
tuple(float(self.tau[i][j].imag) for j in range(self.genus))
|
|
97
|
+
for i in range(self.genus)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def imaginary_part_positive_definite(self, *, tolerance: float = 1e-10) -> bool:
|
|
101
|
+
"""Check positive-definiteness of the symmetrized Im(tau) numerically."""
|
|
102
|
+
|
|
103
|
+
imag = self.imaginary_part
|
|
104
|
+
symmetric = [
|
|
105
|
+
[0.5 * (imag[i][j] + imag[j][i]) for j in range(self.genus)]
|
|
106
|
+
for i in range(self.genus)
|
|
107
|
+
]
|
|
108
|
+
for size in range(1, self.genus + 1):
|
|
109
|
+
determinant = complex(sp.N(sp.Matrix([row[:size] for row in symmetric[:size]]).det(), 30))
|
|
110
|
+
if abs(determinant.imag) > tolerance or determinant.real <= tolerance:
|
|
111
|
+
return False
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
def riemann_shape_passes(self, *, tolerance: float = 1e-8) -> bool:
|
|
115
|
+
"""Check the numerical symmetry/positivity shape required of period data.
|
|
116
|
+
|
|
117
|
+
This is not a homology-intersection certificate. It tests necessary
|
|
118
|
+
matrix properties assuming the caller's cycles are intended as a
|
|
119
|
+
symplectic basis.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
self.symmetry_residual <= tolerance
|
|
124
|
+
and self.imaginary_part_positive_definite(tolerance=tolerance)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def compute_period_matrix(cycles: AbelianCycleSystem) -> AbelianPeriodMatrix:
|
|
129
|
+
"""Integrate the canonical basis over supplied A/B cycles and normalize."""
|
|
130
|
+
|
|
131
|
+
differentials = holomorphic_differential_basis(cycles.curve)
|
|
132
|
+
a_rows = tuple(
|
|
133
|
+
tuple(integrate_lifted_differential(cycle, differential) for cycle in cycles.a_cycles)
|
|
134
|
+
for differential in differentials
|
|
135
|
+
)
|
|
136
|
+
b_rows = tuple(
|
|
137
|
+
tuple(integrate_lifted_differential(cycle, differential) for cycle in cycles.b_cycles)
|
|
138
|
+
for differential in differentials
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
a_matrix = sp.Matrix(a_rows)
|
|
142
|
+
b_matrix = sp.Matrix(b_rows)
|
|
143
|
+
if abs(complex(sp.N(a_matrix.det(), 30))) <= 1e-14:
|
|
144
|
+
raise ValueError("A-period block is numerically singular")
|
|
145
|
+
tau_matrix = a_matrix.inv() * b_matrix
|
|
146
|
+
tau = tuple(
|
|
147
|
+
tuple(complex(sp.N(tau_matrix[i, j], 30)) for j in range(tau_matrix.cols))
|
|
148
|
+
for i in range(tau_matrix.rows)
|
|
149
|
+
)
|
|
150
|
+
return AbelianPeriodMatrix(
|
|
151
|
+
cycles=cycles,
|
|
152
|
+
a_periods=a_rows,
|
|
153
|
+
b_periods=b_rows,
|
|
154
|
+
tau=tau,
|
|
155
|
+
)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""History lifting, square-root monodromy, and numerical period integration.
|
|
2
|
+
|
|
3
|
+
Primitive question
|
|
4
|
+
------------------
|
|
5
|
+
Once a process quotient has produced a curve ``y^2=P(x)``, how do we keep track
|
|
6
|
+
of *which sheet* a history occupies while the base coordinate ``x`` moves around
|
|
7
|
+
branch points? Returning to the same base point need not return to the same
|
|
8
|
+
lifted state: analytic continuation around a branch point can change ``y`` to
|
|
9
|
+
``-y``. This is the simplest executable model of the distinction
|
|
10
|
+
|
|
11
|
+
state return != history return.
|
|
12
|
+
|
|
13
|
+
Classical shadow
|
|
14
|
+
----------------
|
|
15
|
+
Classically this is monodromy of the two-sheeted square-root covering. On a
|
|
16
|
+
smooth hyperelliptic curve, periods arise by integrating holomorphic
|
|
17
|
+
differentials along closed lifted cycles. In genus one two independent periods
|
|
18
|
+
form the lattice underlying the complex torus; in higher genus the same idea
|
|
19
|
+
feeds a period matrix and Jacobian. See Forster, Farkas--Kra, and the references
|
|
20
|
+
collected in ``docs/REFERENCES.md``.
|
|
21
|
+
|
|
22
|
+
Shakespeare reconstruction
|
|
23
|
+
---------------------------
|
|
24
|
+
The library does not jump directly to a named elliptic or Abelian function.
|
|
25
|
+
Instead it makes the history lift executable:
|
|
26
|
+
|
|
27
|
+
1. a base path in the complex ``x``-plane is sampled;
|
|
28
|
+
2. the square root is continued by choosing, at each step, the sign closest to
|
|
29
|
+
the previously selected value;
|
|
30
|
+
3. the resulting lifted path records whether a closed base loop closes on the
|
|
31
|
+
Riemann surface or changes sheet;
|
|
32
|
+
4. a differential ``x^k dx/y`` can then be integrated along the lifted history.
|
|
33
|
+
|
|
34
|
+
This is deliberately a *bounded numerical continuation engine*. It is useful
|
|
35
|
+
for calibrating topology/period structure, but it is not a certified homology
|
|
36
|
+
solver or a proof of a period matrix.
|
|
37
|
+
|
|
38
|
+
Executable contract
|
|
39
|
+
-------------------
|
|
40
|
+
``lift_square_root_path`` returns every selected sheet value and an explicit
|
|
41
|
+
``sheet_multiplier`` when the base loop closes. ``integrate_lifted_differential``
|
|
42
|
+
uses a complex trapezoidal rule along the supplied samples. ``GenusOneLattice``
|
|
43
|
+
packages two already-computed periods and their ratio without pretending to
|
|
44
|
+
construct the cycles that produced them.
|
|
45
|
+
|
|
46
|
+
Boundary
|
|
47
|
+
--------
|
|
48
|
+
The continuation rule assumes sufficiently fine sampling and no sample at a
|
|
49
|
+
branch point. Numerical integration carries discretization error. Arbitrary
|
|
50
|
+
homology-basis construction, adaptive contour refinement, rigorous error bounds,
|
|
51
|
+
Riemann bilinear relations in genus > 1, and Jacobians remain later work.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
from __future__ import annotations
|
|
55
|
+
|
|
56
|
+
import cmath
|
|
57
|
+
from dataclasses import dataclass
|
|
58
|
+
from typing import Iterable
|
|
59
|
+
|
|
60
|
+
import sympy as sp
|
|
61
|
+
|
|
62
|
+
from .abelian import HyperellipticDifferential
|
|
63
|
+
from .algebraic import HyperellipticProfile
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _numeric_polynomial_coefficients(
|
|
67
|
+
expr: sp.Expr,
|
|
68
|
+
symbol: sp.Symbol,
|
|
69
|
+
*,
|
|
70
|
+
digits: int,
|
|
71
|
+
) -> tuple[complex, ...]:
|
|
72
|
+
expr = sp.sympify(expr)
|
|
73
|
+
remaining = expr.free_symbols - {symbol}
|
|
74
|
+
if remaining:
|
|
75
|
+
raise ValueError(f"numeric continuation requires all parameters fixed: {remaining}")
|
|
76
|
+
try:
|
|
77
|
+
polynomial = sp.Poly(expr, symbol)
|
|
78
|
+
except sp.PolynomialError as exc:
|
|
79
|
+
raise ValueError("numeric continuation requires a polynomial in the curve variable") from exc
|
|
80
|
+
return tuple(complex(sp.N(coefficient, digits)) for coefficient in polynomial.all_coeffs())
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _horner(coefficients: tuple[complex, ...], value: complex) -> complex:
|
|
84
|
+
result = 0j
|
|
85
|
+
for coefficient in coefficients:
|
|
86
|
+
result = result * value + coefficient
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class LiftedSquareRootPath:
|
|
92
|
+
"""A sampled base path together with one continuously chosen square-root lift."""
|
|
93
|
+
|
|
94
|
+
curve: HyperellipticProfile
|
|
95
|
+
x_values: tuple[complex, ...]
|
|
96
|
+
y_values: tuple[complex, ...]
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def base_closed(self) -> bool:
|
|
100
|
+
if len(self.x_values) < 2:
|
|
101
|
+
return False
|
|
102
|
+
scale = max(1.0, abs(self.x_values[0]), abs(self.x_values[-1]))
|
|
103
|
+
return abs(self.x_values[-1] - self.x_values[0]) <= 1e-10 * scale
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def sheet_multiplier(self) -> int | None:
|
|
107
|
+
"""Return +1/-1 for a closed base loop, or ``None`` if not comparable."""
|
|
108
|
+
|
|
109
|
+
if not self.base_closed or not self.y_values:
|
|
110
|
+
return None
|
|
111
|
+
start = self.y_values[0]
|
|
112
|
+
end = self.y_values[-1]
|
|
113
|
+
if abs(start) == 0:
|
|
114
|
+
return None
|
|
115
|
+
same = abs(end - start)
|
|
116
|
+
flipped = abs(end + start)
|
|
117
|
+
return 1 if same <= flipped else -1
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def lifted_closed(self) -> bool:
|
|
121
|
+
return self.sheet_multiplier == 1
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def lift_square_root_path(
|
|
125
|
+
curve: HyperellipticProfile,
|
|
126
|
+
x_values: Iterable[complex],
|
|
127
|
+
*,
|
|
128
|
+
initial_y: complex | None = None,
|
|
129
|
+
digits: int = 50,
|
|
130
|
+
branch_tolerance: float = 1e-24,
|
|
131
|
+
) -> LiftedSquareRootPath:
|
|
132
|
+
"""Continue one branch of ``sqrt(P(x))`` along a sampled complex path.
|
|
133
|
+
|
|
134
|
+
At each sample the principal square root and its negative are compared with
|
|
135
|
+
the previously selected lift. The closer one is chosen. This is the
|
|
136
|
+
discrete analogue of analytic continuation along a path.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
xs = tuple(complex(value) for value in x_values)
|
|
140
|
+
if len(xs) < 2:
|
|
141
|
+
raise ValueError("a lifted path requires at least two base samples")
|
|
142
|
+
|
|
143
|
+
coefficients = _numeric_polynomial_coefficients(
|
|
144
|
+
curve.polynomial,
|
|
145
|
+
curve.x,
|
|
146
|
+
digits=digits,
|
|
147
|
+
)
|
|
148
|
+
ys: list[complex] = []
|
|
149
|
+
previous: complex | None = complex(initial_y) if initial_y is not None else None
|
|
150
|
+
|
|
151
|
+
for x_value in xs:
|
|
152
|
+
p_value = _horner(coefficients, x_value)
|
|
153
|
+
if abs(p_value) <= branch_tolerance:
|
|
154
|
+
raise ValueError("sample path meets or approaches a branch point too closely")
|
|
155
|
+
principal = cmath.sqrt(p_value)
|
|
156
|
+
candidates = (principal, -principal)
|
|
157
|
+
if previous is None:
|
|
158
|
+
selected = principal
|
|
159
|
+
else:
|
|
160
|
+
selected = min(candidates, key=lambda candidate: abs(candidate - previous))
|
|
161
|
+
ys.append(selected)
|
|
162
|
+
previous = selected
|
|
163
|
+
|
|
164
|
+
return LiftedSquareRootPath(curve=curve, x_values=xs, y_values=tuple(ys))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def integrate_lifted_differential(
|
|
168
|
+
path: LiftedSquareRootPath,
|
|
169
|
+
differential: HyperellipticDifferential,
|
|
170
|
+
*,
|
|
171
|
+
digits: int = 50,
|
|
172
|
+
) -> complex:
|
|
173
|
+
"""Numerically integrate ``x^k dx/y`` along an already lifted path."""
|
|
174
|
+
|
|
175
|
+
del digits # kept for API symmetry with the continuation routine
|
|
176
|
+
if differential.curve != path.curve:
|
|
177
|
+
raise ValueError("differential and lifted path must belong to the same curve")
|
|
178
|
+
|
|
179
|
+
values = tuple(
|
|
180
|
+
(x_value ** differential.power) / y_value
|
|
181
|
+
for x_value, y_value in zip(path.x_values, path.y_values, strict=True)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
total = 0j
|
|
185
|
+
for index in range(len(path.x_values) - 1):
|
|
186
|
+
dx = path.x_values[index + 1] - path.x_values[index]
|
|
187
|
+
total += 0.5 * (values[index] + values[index + 1]) * dx
|
|
188
|
+
return total
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(frozen=True)
|
|
192
|
+
class GenusOneLattice:
|
|
193
|
+
"""Two non-collinear periods of a genus-one quotient.
|
|
194
|
+
|
|
195
|
+
The object packages period data supplied by explicit cycle computations or
|
|
196
|
+
exact symmetries. It does not discover a homology basis by itself.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
omega_a: complex
|
|
200
|
+
omega_b: complex
|
|
201
|
+
|
|
202
|
+
def __post_init__(self) -> None:
|
|
203
|
+
if abs(self.omega_a) == 0:
|
|
204
|
+
raise ValueError("first period must be nonzero")
|
|
205
|
+
if abs((self.omega_b / self.omega_a).imag) <= 1e-14:
|
|
206
|
+
raise ValueError("genus-one periods must be non-collinear over R")
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def tau(self) -> complex:
|
|
210
|
+
return self.omega_b / self.omega_a
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def oriented_area(self) -> float:
|
|
214
|
+
"""Signed Euclidean area of the period parallelogram."""
|
|
215
|
+
|
|
216
|
+
return float((self.omega_a.conjugate() * self.omega_b).imag)
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Constructed A/B cycles for real-split hyperelliptic process quotients.
|
|
2
|
+
|
|
3
|
+
Primitive question
|
|
4
|
+
------------------
|
|
5
|
+
The period and intersection layers can measure cycles once those cycles have
|
|
6
|
+
already been supplied. That still leaves a representation choice outside the
|
|
7
|
+
library: *where did the A/B cycles come from?*
|
|
8
|
+
|
|
9
|
+
For a real-split even-degree hyperelliptic quotient
|
|
10
|
+
|
|
11
|
+
y^2 = c prod_j (x-e_j), e_1 < ... < e_(2g+2),
|
|
12
|
+
|
|
13
|
+
there is a classical answer visible directly in the branch-point order. Pair
|
|
14
|
+
adjacent branch points into cuts
|
|
15
|
+
|
|
16
|
+
[e_1,e_2], [e_3,e_4], ..., [e_(2g+1),e_(2g+2)].
|
|
17
|
+
|
|
18
|
+
A standard symplectic presentation may take ``a_i`` around the i-th nonreference
|
|
19
|
+
cut and ``b_i`` around the even branch set beginning at the right end of that
|
|
20
|
+
cut and ending at the left end of the final reference cut. In index notation
|
|
21
|
+
used here,
|
|
22
|
+
|
|
23
|
+
a_i encloses e_(2i), ..., e_(2i+1),
|
|
24
|
+
b_i encloses e_(2i+1), ..., e_(2g),
|
|
25
|
+
|
|
26
|
+
for ``i=0,...,g-1``. The resulting abstract pairing is
|
|
27
|
+
|
|
28
|
+
a_i . a_j = b_i . b_j = 0,
|
|
29
|
+
a_i . b_j = delta_ij.
|
|
30
|
+
|
|
31
|
+
This is the familiar branch-cut construction behind hyperelliptic homology and
|
|
32
|
+
period calculations; see Farkas--Kra and Frauendiener--Klein in
|
|
33
|
+
``docs/REFERENCES.md``.
|
|
34
|
+
|
|
35
|
+
Shakespeare reconstruction
|
|
36
|
+
---------------------------
|
|
37
|
+
The important change is that the cycle basis is no longer an unexplained input.
|
|
38
|
+
The branch-point presentation itself emits:
|
|
39
|
+
|
|
40
|
+
1. the cut pairing;
|
|
41
|
+
2. exact combinatorial A/B cycle specifications;
|
|
42
|
+
3. the target symplectic intersection matrix forced by that construction;
|
|
43
|
+
4. sampled base contours realizing those specifications;
|
|
44
|
+
5. lifted histories obtained by the existing square-root continuation engine.
|
|
45
|
+
|
|
46
|
+
The exact combinatorial certificate and the sampled realization are deliberately
|
|
47
|
+
kept separate. Later code can ask whether numerical lifted intersections agree
|
|
48
|
+
with the pairing promised by construction.
|
|
49
|
+
|
|
50
|
+
Executable contract
|
|
51
|
+
-------------------
|
|
52
|
+
``real_branch_cut_presentation`` validates a supplied ordered list of all
|
|
53
|
+
``2g+2`` real branch points of an even-degree quotient. ``construct_real_branch_cycles``
|
|
54
|
+
turns the canonical interval specifications into nested/transverse ellipses and
|
|
55
|
+
lifts them to the Riemann surface. Under the implementation's fixed convention
|
|
56
|
+
that continuation starts at the rightmost point on the principal square-root
|
|
57
|
+
sheet, nested B-contours alternate their base orientation so that the lifted
|
|
58
|
+
cycles realize ``a_i.b_i=+1``. The function returns both the construction
|
|
59
|
+
metadata and an ``AbelianCycleSystem`` ready for period integration.
|
|
60
|
+
|
|
61
|
+
Boundary
|
|
62
|
+
--------
|
|
63
|
+
This module handles only the real-split, even-degree case with explicitly
|
|
64
|
+
supplied branch points. It does not discover cuts for arbitrary complex branch
|
|
65
|
+
configurations, prove numerical contour deformation invariance, or replace a
|
|
66
|
+
general Tretkoff--Tretkoff style homology algorithm. The ellipse geometry is a
|
|
67
|
+
sampled realization of a classical branch-cut presentation, not the topology
|
|
68
|
+
itself.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
from __future__ import annotations
|
|
72
|
+
|
|
73
|
+
import math
|
|
74
|
+
from dataclasses import dataclass
|
|
75
|
+
from typing import Literal
|
|
76
|
+
|
|
77
|
+
import sympy as sp
|
|
78
|
+
|
|
79
|
+
from .algebraic import HyperellipticProfile
|
|
80
|
+
from .period_matrix import AbelianCycleSystem
|
|
81
|
+
from .periods import LiftedSquareRootPath, lift_square_root_path
|
|
82
|
+
|
|
83
|
+
CycleFamily = Literal["A", "B"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class RealBranchCycleSpec:
|
|
88
|
+
"""One cycle specified by the consecutive branch points it encloses."""
|
|
89
|
+
|
|
90
|
+
family: CycleFamily
|
|
91
|
+
index: int
|
|
92
|
+
left_branch_index: int
|
|
93
|
+
right_branch_index: int
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def branch_count(self) -> int:
|
|
97
|
+
return self.right_branch_index - self.left_branch_index + 1
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class RealBranchCutPresentation:
|
|
102
|
+
"""Classical branch-cut presentation for a real-split even-degree curve."""
|
|
103
|
+
|
|
104
|
+
curve: HyperellipticProfile
|
|
105
|
+
branch_points: tuple[float, ...]
|
|
106
|
+
a_specs: tuple[RealBranchCycleSpec, ...]
|
|
107
|
+
b_specs: tuple[RealBranchCycleSpec, ...]
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def genus(self) -> int:
|
|
111
|
+
genus = self.curve.generic_genus
|
|
112
|
+
assert genus is not None
|
|
113
|
+
return genus
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def reference_cut(self) -> tuple[float, float]:
|
|
117
|
+
return self.branch_points[-2], self.branch_points[-1]
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def construction_intersection_form(self) -> tuple[tuple[int, ...], ...]:
|
|
121
|
+
"""Return the exact target ``[[0,I],[-I,0]]`` in A-then-B order."""
|
|
122
|
+
|
|
123
|
+
genus = self.genus
|
|
124
|
+
size = 2 * genus
|
|
125
|
+
matrix = [[0 for _ in range(size)] for _ in range(size)]
|
|
126
|
+
for index in range(genus):
|
|
127
|
+
matrix[index][genus + index] = 1
|
|
128
|
+
matrix[genus + index][index] = -1
|
|
129
|
+
return tuple(tuple(row) for row in matrix)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True)
|
|
133
|
+
class ConstructedRealBranchCycles:
|
|
134
|
+
"""Sampled lifted realization plus its branch-cut construction certificate."""
|
|
135
|
+
|
|
136
|
+
presentation: RealBranchCutPresentation
|
|
137
|
+
a_cycles: tuple[LiftedSquareRootPath, ...]
|
|
138
|
+
b_cycles: tuple[LiftedSquareRootPath, ...]
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def cycle_system(self) -> AbelianCycleSystem:
|
|
142
|
+
return AbelianCycleSystem(
|
|
143
|
+
curve=self.presentation.curve,
|
|
144
|
+
a_cycles=self.a_cycles,
|
|
145
|
+
b_cycles=self.b_cycles,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def construction_intersection_form(self) -> tuple[tuple[int, ...], ...]:
|
|
150
|
+
return self.presentation.construction_intersection_form
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def real_branch_cut_presentation(
|
|
154
|
+
curve: HyperellipticProfile,
|
|
155
|
+
branch_points: tuple[float, ...] | list[float],
|
|
156
|
+
*,
|
|
157
|
+
root_tolerance: float = 1e-10,
|
|
158
|
+
) -> RealBranchCutPresentation:
|
|
159
|
+
"""Validate real branch points and emit the canonical interval cycle specs."""
|
|
160
|
+
|
|
161
|
+
genus = curve.generic_genus
|
|
162
|
+
if genus is None or genus < 1:
|
|
163
|
+
raise ValueError("real branch-cycle presentation requires positive generic genus")
|
|
164
|
+
if curve.degree != 2 * genus + 2:
|
|
165
|
+
raise ValueError("real branch-cycle presentation currently requires even degree 2g+2")
|
|
166
|
+
|
|
167
|
+
points = tuple(float(value) for value in branch_points)
|
|
168
|
+
expected = 2 * genus + 2
|
|
169
|
+
if len(points) != expected:
|
|
170
|
+
raise ValueError(f"expected exactly {expected} real branch points")
|
|
171
|
+
if any(not math.isfinite(value) for value in points):
|
|
172
|
+
raise ValueError("branch points must be finite real numbers")
|
|
173
|
+
if any(points[index] >= points[index + 1] for index in range(len(points) - 1)):
|
|
174
|
+
raise ValueError("branch points must be strictly increasing")
|
|
175
|
+
if root_tolerance <= 0:
|
|
176
|
+
raise ValueError("root_tolerance must be positive")
|
|
177
|
+
|
|
178
|
+
for point in points:
|
|
179
|
+
value = complex(sp.N(curve.polynomial.subs(curve.x, point), 40))
|
|
180
|
+
scale = max(1.0, abs(point) ** curve.degree)
|
|
181
|
+
if abs(value) > root_tolerance * scale:
|
|
182
|
+
raise ValueError(f"supplied branch point {point} is not a root of the curve polynomial")
|
|
183
|
+
|
|
184
|
+
a_specs = tuple(
|
|
185
|
+
RealBranchCycleSpec("A", index, 2 * index, 2 * index + 1)
|
|
186
|
+
for index in range(genus)
|
|
187
|
+
)
|
|
188
|
+
# The final pair [e_(2g), e_(2g+1)] is the reference cut. b_i surrounds
|
|
189
|
+
# the even branch set from the right endpoint of a_i to the left endpoint
|
|
190
|
+
# of that reference cut. Nested B-contours then intersect only their dual A.
|
|
191
|
+
b_specs = tuple(
|
|
192
|
+
RealBranchCycleSpec("B", index, 2 * index + 1, 2 * genus)
|
|
193
|
+
for index in range(genus)
|
|
194
|
+
)
|
|
195
|
+
if any(spec.branch_count % 2 for spec in a_specs + b_specs):
|
|
196
|
+
raise AssertionError("constructed hyperelliptic cycles must enclose even branch sets")
|
|
197
|
+
|
|
198
|
+
return RealBranchCutPresentation(
|
|
199
|
+
curve=curve,
|
|
200
|
+
branch_points=points,
|
|
201
|
+
a_specs=a_specs,
|
|
202
|
+
b_specs=b_specs,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _ellipse_for_spec(
|
|
207
|
+
presentation: RealBranchCutPresentation,
|
|
208
|
+
spec: RealBranchCycleSpec,
|
|
209
|
+
*,
|
|
210
|
+
samples: int,
|
|
211
|
+
padding_fraction: float,
|
|
212
|
+
height_fraction: float,
|
|
213
|
+
) -> tuple[complex, ...]:
|
|
214
|
+
points = presentation.branch_points
|
|
215
|
+
left_index = spec.left_branch_index
|
|
216
|
+
right_index = spec.right_branch_index
|
|
217
|
+
left = points[left_index]
|
|
218
|
+
right = points[right_index]
|
|
219
|
+
span = right - left
|
|
220
|
+
|
|
221
|
+
left_clearance = (
|
|
222
|
+
left - points[left_index - 1]
|
|
223
|
+
if left_index > 0
|
|
224
|
+
else points[1] - points[0]
|
|
225
|
+
)
|
|
226
|
+
right_clearance = (
|
|
227
|
+
points[right_index + 1] - right
|
|
228
|
+
if right_index + 1 < len(points)
|
|
229
|
+
else points[-1] - points[-2]
|
|
230
|
+
)
|
|
231
|
+
padding = padding_fraction * min(left_clearance, right_clearance)
|
|
232
|
+
center = 0.5 * (left + right)
|
|
233
|
+
horizontal_radius = 0.5 * span + padding
|
|
234
|
+
vertical_radius = height_fraction * horizontal_radius
|
|
235
|
+
|
|
236
|
+
contour = tuple(
|
|
237
|
+
center
|
|
238
|
+
+ horizontal_radius * math.cos(2.0 * math.pi * step / samples)
|
|
239
|
+
+ 1j * vertical_radius * math.sin(2.0 * math.pi * step / samples)
|
|
240
|
+
for step in range(samples + 1)
|
|
241
|
+
)
|
|
242
|
+
# All contours start at their rightmost point. With the principal-square-root
|
|
243
|
+
# initial sheet used by lift_square_root_path, successive nested B cycles
|
|
244
|
+
# acquire alternating lifted orientation. Reverse the odd-indexed ones so
|
|
245
|
+
# the realized basis matches the construction convention a_i.b_i=+1.
|
|
246
|
+
if spec.family == "B" and spec.index % 2 == 1:
|
|
247
|
+
contour = tuple(reversed(contour))
|
|
248
|
+
return contour
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def construct_real_branch_cycles(
|
|
252
|
+
presentation: RealBranchCutPresentation,
|
|
253
|
+
*,
|
|
254
|
+
samples: int = 1024,
|
|
255
|
+
padding_fraction: float = 0.18,
|
|
256
|
+
height_fraction: float = 0.35,
|
|
257
|
+
) -> ConstructedRealBranchCycles:
|
|
258
|
+
"""Materialize and lift the A/B contours emitted by a real branch presentation."""
|
|
259
|
+
|
|
260
|
+
if samples < 64:
|
|
261
|
+
raise ValueError("cycle construction requires at least 64 contour samples")
|
|
262
|
+
if not 0 < padding_fraction < 0.5:
|
|
263
|
+
raise ValueError("padding_fraction must lie between 0 and 0.5")
|
|
264
|
+
if height_fraction <= 0:
|
|
265
|
+
raise ValueError("height_fraction must be positive")
|
|
266
|
+
|
|
267
|
+
def lift(spec: RealBranchCycleSpec) -> LiftedSquareRootPath:
|
|
268
|
+
base_path = _ellipse_for_spec(
|
|
269
|
+
presentation,
|
|
270
|
+
spec,
|
|
271
|
+
samples=samples,
|
|
272
|
+
padding_fraction=padding_fraction,
|
|
273
|
+
height_fraction=height_fraction,
|
|
274
|
+
)
|
|
275
|
+
path = lift_square_root_path(presentation.curve, base_path)
|
|
276
|
+
if not path.lifted_closed:
|
|
277
|
+
raise ValueError(
|
|
278
|
+
f"constructed {spec.family}{spec.index + 1} contour did not close on the lifted surface"
|
|
279
|
+
)
|
|
280
|
+
return path
|
|
281
|
+
|
|
282
|
+
return ConstructedRealBranchCycles(
|
|
283
|
+
presentation=presentation,
|
|
284
|
+
a_cycles=tuple(lift(spec) for spec in presentation.a_specs),
|
|
285
|
+
b_cycles=tuple(lift(spec) for spec in presentation.b_specs),
|
|
286
|
+
)
|