Skip to content

Commit 424f168

Browse files
ofloveandhateclaude
andcommitted
docs(showpieces): make the Loom engine exact — no floats, no _const
Following up the de-smell: _const still modelled "how to sneak a float past coefficient's refusal" and built a complex constant wastefully as re + im*I. Remove it entirely by never having a float: the loop parameters are exact rationals (fractions.Fraction) and the phase is a rational multiple of pi (phi_over_pi -> phi_over_pi * bertini.Pi). A single _c_of_theta builds c = center + radius*(cos theta + i sin theta) for both the homotopy and the start configuration (DRY). branch_values is gone too -- the branch modulus is exactly degree-1. The only remaining float/complex is value transport for numpy plotting (extracting roots, axis ticks), which is correct. Same braid, exact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fc2ffb4 commit 424f168

1 file changed

Lines changed: 34 additions & 42 deletions

File tree

python/docs/source/showpieces/monodromy_loom/monodromy_loom.py

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
import math
3838
import os
39+
from fractions import Fraction
3940

4041
import numpy as np
4142

@@ -58,58 +59,49 @@
5859

5960
# --- the engine: bake the whole parameter loop into one homotopy --------------------------------
6061
#
61-
# Node arithmetic promotes ints, exposes the exact constants bertini.Pi and bertini.I, and honours
62-
# ** -- so the tree is written almost verbatim: x**degree, 2*bertini.Pi, bertini.I. The one thing
63-
# the library will not do implicitly is turn a Python float/complex into a coefficient (a 16-digit
64-
# literal would silently cap the arbitrary-precision tree), so a float parameter goes through its
65-
# exact decimal string via _const.
66-
67-
def _const(z):
68-
"""An exact constant node from a Python number, via coefficient's exact-string path: a real
69-
number -> its decimal; a complex -> re + im * bertini.I."""
70-
z = complex(z)
71-
node = bertini.coefficient(repr(z.real))
72-
return node if z.imag == 0 else node + bertini.coefficient(repr(z.imag)) * bertini.I
73-
74-
def loom_homotopy(degree, center, radius, phi):
75-
"""H(x, t) = x^d - d*x - c(t), c(t) = center + radius*exp(i*(theta + phi)), theta = 2pi(1-t).
76-
77-
At t=1 theta=0 (the start configuration); at t=0 theta=2pi (the loop has closed). Tracking a
78-
root of the start configuration from t=1 to t=0 carries it once around the loop.
62+
# Every constant here is EXACT and nothing reaches for a float: the loop parameters are rationals
63+
# (fractions.Fraction) and the phase is a rational multiple of pi. Node arithmetic promotes ints,
64+
# exposes the exact constants bertini.Pi and bertini.I, honours **, and bertini.coefficient accepts
65+
# exact values -- so the homotopy is written verbatim, with no coercion to defeat.
66+
67+
def _c_of_theta(theta, center, radius):
68+
"""The loop's moving coefficient c = center + radius * (cos theta + i sin theta), as a node.
69+
``center`` / ``radius`` are exact (int or fractions.Fraction); ``theta`` is a node."""
70+
return (bertini.coefficient(center)
71+
+ bertini.coefficient(radius) * (bertini.cos(theta) + bertini.I * bertini.sin(theta)))
72+
73+
def loom_homotopy(degree, center, radius, phi_over_pi):
74+
"""H(x, t) = x^d - d*x - c(t), c(t) = center + radius*exp(i*(theta + phi)),
75+
theta = 2pi(1-t), phi = phi_over_pi * pi.
76+
77+
At t=1 theta=phi (the start configuration); at t=0 the loop has closed. Tracking a root of the
78+
start configuration from t=1 to t=0 carries it once around the loop.
7979
"""
8080
x = bertini.Variable('x')
8181
t = bertini.Variable('t')
82-
theta = 2 * bertini.Pi * (1 - t) + _const(phi)
83-
c_t = _const(center) + _const(radius) * (bertini.cos(theta) + bertini.I * bertini.sin(theta))
82+
theta = 2 * bertini.Pi * (1 - t) + bertini.coefficient(phi_over_pi) * bertini.Pi
8483
sys = bertini.System()
85-
sys.add_function(x**degree - degree * x - c_t)
84+
sys.add_function(x**degree - degree * x - _c_of_theta(theta, center, radius))
8685
sys.add_path_variable(t)
8786
sys.add_variable_group(bertini.VariableGroup([x]))
8887
return sys
8988

90-
def start_configuration(degree, center, radius, phi):
91-
"""The roots of the start configuration f(x) = x^d - d*x - c0, c0 = c(theta=0)."""
92-
c0 = center + radius * complex(math.cos(phi), math.sin(phi))
89+
def start_configuration(degree, center, radius, phi_over_pi):
90+
"""The roots of the start configuration f(x) = x^d - d*x - c(t=1), where theta = phi at t=1."""
9391
x = bertini.Variable('x')
92+
theta_start = bertini.coefficient(phi_over_pi) * bertini.Pi
9493
sys = bertini.System()
9594
sys.add_variable_group(bertini.VariableGroup([x]))
96-
sys.add_function(x**degree - degree * x - _const(c0))
95+
sys.add_function(x**degree - degree * x - _c_of_theta(theta_start, center, radius))
9796
solver = bertini.nag_algorithm.ZeroDimSolver(sys, mptype='adaptive')
9897
solver.solve()
9998
return [complex(s[0]) for s in solver.all_solutions()]
10099

101-
def branch_values(degree):
102-
"""The (d-1) branch values of x^d - d*x - c: c = x^d - d*x at each critical point (the
103-
(d-1)-th roots of unity, where d*x^{d-1} - d = 0). All share one modulus."""
104-
crit = [complex(math.cos(2 * math.pi * k / (degree - 1)),
105-
math.sin(2 * math.pi * k / (degree - 1))) for k in range(degree - 1)]
106-
return [z**degree - degree * z for z in crit]
107-
108-
def track_loop(degree, center, radius, phi, tol=1e-10):
100+
def track_loop(degree, center, radius, phi_over_pi, tol=1e-10):
109101
"""Track every strand once around the loop. Returns a list of per-strand dicts with the loop
110102
angle ``theta``, the complex position ``x``, and the tracker diagnostics along the path."""
111-
H = loom_homotopy(degree, center, radius, phi)
112-
roots = start_configuration(degree, center, radius, phi)
103+
H = loom_homotopy(degree, center, radius, phi_over_pi)
104+
roots = start_configuration(degree, center, radius, phi_over_pi)
113105

114106
tracker = bertini.AMPTracker(H)
115107
tracker.setup(bertini.tracking.Predictor.RK4, tol, 1e6,
@@ -224,20 +216,20 @@ def stress(ls):
224216
def teaching_frame(out):
225217
"""x^3 - 3x - c: a loop around ONE branch point (c = +2) -> a single transposition."""
226218
bertini.random.set_random_seed(1) # deterministic start-root ordering -> stable render
227-
# branch points at c = +/-2; put the loop centre right of +2 so it encircles +2, excludes -2,
228-
# and its closest approach (the pinch) lands at theta = pi, mid-loop.
229-
radius, graze = 1.5, 0.02
230-
center = 2.0 + (radius - graze)
231-
strands, _ = track_loop(3, complex(center, 0.0), radius, phi=0.0)
219+
# x^d - d*x has branch points at c = +/-(d-1); for d=3 that is +/-2. Centre the loop right of
220+
# +2 so it encircles +2, excludes -2, and its closest approach (the pinch) is at theta = pi.
221+
graze, radius = Fraction(1, 50), Fraction(3, 2) # 0.02, 1.5
222+
center = (3 - 1) + (radius - graze) # 2 + (radius - graze)
223+
strands, _ = track_loop(3, center, radius, phi_over_pi=0)
232224
render(strands, out,
233225
'The Monodromy Loom — teaching case: $x^3 - 3x - c$',
234226
'loop one branch point → two roots SWAP (a transposition); the third rides straight')
235227

236228
def showpiece_frame(out):
237229
"""x^5 - 5x - c: a loop around ALL FOUR branch points -> a full 5-cycle, four pinches."""
238230
bertini.random.set_random_seed(1) # deterministic start-root ordering -> stable render
239-
radius = abs(branch_values(5)[0]) + 0.15 # circle |c| = R just outside the branch orbit
240-
strands, _ = track_loop(5, 0.0 + 0.0j, radius, phi=math.pi / 4) # phi keeps pinches off the seam
231+
radius = (5 - 1) + Fraction(3, 20) # circle |c| = R just outside the branch orbit |c| = 4
232+
strands, _ = track_loop(5, 0, radius, phi_over_pi=Fraction(1, 4)) # phi = pi/4 keeps pinches off the seam
241233
render(strands, out,
242234
'The Monodromy Loom — showpiece: $x^5 - 5x - c$',
243235
'loop encircles four branch points → a full 5-cycle; four pinches where the tracker sweats')

0 commit comments

Comments
 (0)