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,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
+ )