Skip to content

docs(showpieces): The Nebula — one solve, every path, every step - #363

Draft
ofloveandhate wants to merge 6 commits into
bertiniteam:developfrom
ofloveandhate:feature/nebula-showpiece
Draft

docs(showpieces): The Nebula — one solve, every path, every step#363
ofloveandhate wants to merge 6 commits into
bertiniteam:developfrom
ofloveandhate:feature/nebula-showpiece

Conversation

@ofloveandhate

Copy link
Copy Markdown
Contributor

A fourth showpiece: one solve, every path, every step, exposed onto a single additive buffer
like a long exposure. Where the Loom draws a few strands and Basins (#352) photographs parameter
space, the Nebula photographs solution space in transit.

Draft: the frames and the prose are settled, but see Open questions below.

The idea

The exposure is meant literally: brightness = time spent. Between each pair of tracked samples
the tracker spent some path time |Δt|; that much light is deposited along the segment joining
them, spread evenly over its length. A path that crawls burns bright; a path racing to infinity
leaves a faint comet streak — the same light, smeared further.

The part that makes it honest: a segment's light does not depend on how many samples the tracker
chose to leave on it. Weighting by step count draws a picture of the stepper; weighting by path
time
draws the path. Refine the tolerances until the step count doubles and the image is
unchanged.

Colour runs with log|t| — cool at the start system, white-hot into the endgame — so the frame
reads as a flow without arrows. (Colouring by arg ℓ(x) is a trap worth naming: position already
is (Re ℓ, Im ℓ), so arg ℓ is just the screen's polar angle, a pinwheel carrying nothing.)

The frames

system paths solutions time
hero Kuramoto, 7 oscillators 4096 124 equilibria 28s
teaching Noonburg-style, 6 neurons 729 717 2.4s

Kuramoto is the model of synchronisation — fireflies flashing together, a grid locking to one
frequency — algebraized the way its equilibria are actually counted by continuation. Two
constraints there are load-bearing, not decoration: the phase-rotation symmetry makes the solution
set positive-dimensional, so the last oscillator is gauge-fixed at θ=0; and the frequencies
must sum to zero or no equilibrium exists (summing the equations annihilates the antisymmetric
coupling term), so the last frequency is derived, never drawn.

The whole piece is ~31s of tracking. The subject search was the expensive part, not the render.

Measured, not assumed

Everything load-bearing here was measured, and three assumptions died:

  • Power-series endgame, not Cauchy. Cauchy samples in circles around t=0, so |t| is
    multi-sheeted and a path can teleport back to the endgame boundary — which the dwell weighting
    would draw as the brightest artifact in the frame. Measured over 5880 cyclic-5/6/7 paths,
    power-series gives |t| monotone on every path, zero teleports. It is also ~1500× cheaper
    here: under Cauchy, samples/path explode superlinearly (259 → 970 → 11861 for n=5,6,7) and
    cyclic-7 becomes a ~29-hour render; under power-series they stay flat (202 → 462 → 335) and
    cyclic-7 is 68 seconds. Same wall the Flight Recorder hit in 0cbc756.
  • Serial, not threaded. A Python observer re-acquires the GIL every step, so 12 threads convoy:
    cyclic-6 measured 8.95s serial vs 66.21s threaded (7.4× slower). Threading only wins with no
    observer attached (6.79s → 2.43s). threaded_solve_test.py guarantees observers fire safely
    under threads — not that they are fast.
  • cyclic-n is the wrong subject — and not for the reason the design note feared. It is not too
    symmetric, it is a fuzzy ball: every path starts near |x|=1 and ends near |x|=1, so the
    whole solve lives in one small disc. Not a tone-map failure — the buffer's peak is only 5× its
    99.5th percentile, so the core is a genuinely broad dense region.

The criterion that emerged: what makes a nebula is flow structure plus a workable
solution-to-path ratio
. cyclic-7 (921/5040) is a blob because nothing travels; 4-site
phosphorylation (9/4096) is a starburst because everything diverges.

Verification

  • python nebula.py --selftest asserts the exposure conserves energy exactly (rel err 0.00e+00) on synthetic segments including sub-pixel ones, and reports the real-data border loss
    (0.001%).
  • refresh_doc_artifacts.py --plots --only nebula regenerates both frames byte-identically to
    the committed PNGs — the real check that seed + num_threads=1 is deterministic.
  • tools/py_doclint.py passes both gates (correctness; undocumented ratchet at baseline 14).
  • Sphinx builds clean, no warnings, both images resolve.
  • Renderer A/B: the same cached track rendered through my numpy bincount splat and through
    datashader's numba AA lines gives visually identical images — an independent cross-validation of
    the splat. numpy ships: same picture, 2× faster (0.79s vs 1.71s), no dependency.

Notable implementation details

  • Streaming meta-observer subclasses SolutionPathCollector and drains .series as it fills, so
    memory is O(one path) rather than O(solve).
  • Energy is spread as E/m over m = max(1, ceil(L/step)) subsamples, not as E/L: identical
    for long segments, but no 1/L → ∞ singularity when a segment lands inside one pixel. The pixel
    is the sensor; motion below it is not resolvable.
  • Segments are clipped to the frame before subdividing, or one diverger allocates millions of
    subsamples.
  • Windows are literal constants (Basins' convention): suggest_window() proposes them from the
    data and prints them; they get pasted in. Auto-windowing would make a committed PNG depend on
    run-to-run data.
  • The generator carries a system registry (cyclic-n, Noonburg, Katsura, Kuramoto, multisite
    phosphorylation, Stewart-Gough, dense random), a --scout contact sheet that renders every
    candidate projection from a single solve, and the --selftest.

Open questions for review

  • The docs page names Homotopy Basins in plain text, not a :doc: link, because docs(showpieces): Homotopy Basins #352 is
    unmerged and a cross-reference would break this build. It wants to become a link when docs(showpieces): Homotopy Basins #352 lands
    — the Loom/Nebula/Basins triangle is the page's closing move.
  • Precision. Both frames are double. Double is lossy on some systems (cyclic-7 finds 921 of
    the known 924 cyclic 7-roots), though neither shipped frame depends on that. Worth deciding
    whether the hero should be AMP.
  • The teaching frame is deliberately not the noon-n benchmark. It is a sibling:
    x_i(1 + Σ_{j≠i} x_j²) - 1, whereas noon-n is x_i Σ_{j≠i} x_j² - c x_i + 1 with c = 11/10.
    x → -x carries it onto noon at c = -1, so their solution sets are mirror images — but their
    paths differ regardless, since a total-degree homotopy runs from a fixed start system and
    reflecting the target does not reflect the journey. Both real noon variants render worse
    (lopsided, clipped). Both are in the registry, each named for what it is. Flagging in case the
    sibling should be swapped for the benchmark on principle.
  • No ADR. This is a doc artifact, not a library decision. Happy to add one if the
    track-once/render-many split or the power-series choice reads as arbitrary.

Not in this PR

Alt-Burmester 9-0 was explored as a subject and is not feasible with a Bézout-style start
system
: it is the (1,8) case, a 24×24 system with 8652 solutions, but the total-degree start is
1,099,511,627,776 paths (b2's own num_start_points(); 453 years at the measured 0.013 s/path) and
the best mhom grouping found is still ~1.2e9. That gap is exactly what regeneration exists for.
A showpiece for when the regeneration cascade lands.

🤖 Generated with Claude Code

Track every total-degree path of one system, keep every accepted tracker step, project C^n to a
plane, and expose it all onto an additive buffer.  Brightness IS dwell: each segment deposits the
path-time |dt| the tracker spent crossing it, spread over its rasterised footprint, so the picture
is of the PATH and not of the stepper.

Engine notes, all measured rather than assumed:

* Power-series endgame, not Cauchy.  Measured over 5880 cyclic-5/6/7 paths: |t| monotone on EVERY
  path, zero teleports.  Cauchy would also be ~1500x dearer -- its samples/path explode
  superlinearly (259 -> 970 -> 11861 for n=5,6,7; cyclic-7 becomes a ~29h render), while
  power-series stays flat (202 -> 462 -> 335) and cyclic-7 is 68s.
* Serial, not threaded.  A Python observer re-acquires the GIL every step, so 12 threads convoy:
  measured cyclic-6 at 8.95s serial vs 66.21s threaded (7.4x SLOWER).  Threading is only a win
  with no observer attached (6.79s -> 2.43s).  num_threads=1 also matches flight_recorder.
* Streaming meta-observer subclasses SolutionPathCollector and drains .series as it fills, so
  memory is O(one path) rather than O(solve).
* Energy is spread as E/m over m = max(1, ceil(L/step)) subsamples, NOT as E/L: identical for long
  segments, but no 1/L -> inf singularity when a segment lands inside one pixel.  Segments are
  clipped to the frame BEFORE subdividing, or a diverger allocates millions of subsamples.
* --selftest asserts the exposure conserves energy exactly (synthetic, incl. sub-pixel segments)
  and reports the real-data border loss (0.001%).
…andom

cyclic-n turns out to be the wrong subject, and not for the reason the design note feared: it is
not too symmetric, it is a fuzzy BALL.  Every cyclic-n path starts near |x|=1 and ends near |x|=1,
so the whole solve lives in one small disc and any linear projection is a blob with spikes.  Not a
tone-map failure -- measured, the peak is only 5x the 99.5th percentile, so the core is a genuinely
broad dense region, not blown highlights.

So the registry grows subjects whose paths actually travel.  The Kuramoto model is the find: how a
crowd of oscillators pulls itself into synchrony (fireflies, power grids), algebraized the way its
equilibria have actually been counted by homotopy continuation.  4096 paths, 124 equilibria, 37s.

Two constraints there are load-bearing, not decoration:
* the phase-rotation symmetry makes the solution set positive-dimensional, so the last oscillator
  is gauge-fixed at theta=0 -- without it a zero-dim solve is meaningless;
* the frequencies must sum to zero or NO equilibrium exists (summing the equations kills the
  antisymmetric coupling term), so the last frequency is derived, never drawn.

It also brings its own projection: the order parameter r e^{i psi} = (1/N) sum e^{i theta_j} is
*exactly* a complex-linear functional in the (s, c) coordinates, so for once the composition knob
is not a choice -- the frame IS synchronisation space.
Two more subjects, both verified against something other than my own arithmetic.

mass_action_system derives steady-state polynomials from a reaction list rather than transcribing
them: mass action is a fixed rule, so a network determines its own system.  Conservation laws are
not decoration -- a reaction network's stoichiometric matrix is rank-deficient, so unless one ODE
per conserved quantity is REPLACED by its conservation law the system is singular and a zero-dim
solve is meaningless.  phosphorylation_network(n) builds the multisite phosphorylation ladder, CRN
theory's standard multistationarity benchmark.  Measured: n=3 -> 512 paths / 7 steady states,
n=4 -> 4096 paths / 9.  The 2n+1 pattern corroborates the derivation.

stewart_gough_system: forward kinematics of a six-legged platform -- given six leg lengths, where
is the platform? -- the problem this library's own authors made their name on.  The leg lengths are
computed exactly from a CHOSEN pose, so a known answer is planted in the system; measured, the
solve recovers it to 1.9e-13, which is the real test that the formulation is right.  The chosen
quaternion is the Pythagorean quadruple (1,2,2,4)/5, because an exact rational point on the unit
sphere cannot be had by rounding.

Double precision reports 78 finite solutions where the classic count is 40 poses.  The quaternion
double cover (R(q) = R(-q)) would explain 80, with 2 lost to double precision as cyclic-7 loses 3
of 924.  That is a HYPOTHESIS: the pairing test timed out at 10 min under AMP and is unverified.

Neither is the hero.  Both render as starbursts, not nebulae -- phos4 finds 9 steady states among
4096 paths, so the frame is overwhelmingly divergence, and its 99%-energy window is hw=378 (the
divergers travel far and drag the frame out), collapsing the structure to a dot.
…g-6 teaching

Both frames chosen by silviana off the prototype contact sheets, and both project onto coord0 --
the first coordinate's own complex plane.  Picked by looking, not by argument; the Kuramoto order
parameter is the projection with the better STORY (it is exactly a linear functional, so the frame
would literally be synchronisation space) but it renders as a diffuse cocoon and lost on looks.

  teaching: noonburg-6, 729 paths / 717 solutions / 2.4s.  Few enough paths that individual
            strands stay legible, so the frame teaches what the piece is -- each filament is one
            tracked path, brightening as it slows into its solution.  Reads as wings.
  hero:     Kuramoto-7, 4096 paths / 124 equilibria / 28s.  Dense enough to be a cloud: two
            blazing knots where bundles converge, filaments streaming between them.

Windows are literal constants, per Basins' convention: suggest_window() proposes them from tracked
data and prints them, and they get pasted in.  Auto-windowing at render time would make a committed
PNG depend on run-to-run data and churn the diff on every regeneration.

The whole piece is ~31s of tracking, which is a tenth of the budget -- because the subject search
was the expensive part, not the render.
Docs page, toctree line, refresh_doc_artifacts manifest entry, CHANGELOG [3.5.0] block.

Verified: py_doclint passes both gates (correctness + undocumented ratchet at baseline 14); sphinx
builds with no warnings and both images resolve; `refresh_doc_artifacts.py --plots --only nebula`
regenerates both frames BYTE-IDENTICALLY to the committed ones, which is the real check that the
seed + num_threads=1 recipe is deterministic.

The page does not :doc:-link Homotopy Basins even though the prose wants the Loom/Nebula/Basins
triangle: Basins is unmerged (PR bertiniteam#352), so a cross-reference would break this build.  It is named in
plain text and can become a link when bertiniteam#352 lands.
…ame it correctly

Caught by looking up sources instead of trusting memory.  The teaching frame was described, in the
generator and on the docs page, as "Noonburg's neural network".  It is not.  The noon-n benchmark
is

    x_i * (sum_{j != i} x_j^2) - c*x_i + 1 = 0,   c = 11/10

whereas the frame is x_i*(1 + sum_{j != i} x_j^2) - 1 = 0 -- same total degree, different system,
and a misattribution of the picture.

So both now exist and say what they are.  noon_system(n, c=11/10) is the actual benchmark;
noonburg_like_system(n) is the frame, documented as a SIBLING of that family, not a member: in
noon's form it has the x_i coefficient of c = -1 but the opposite constant, and x -> -x carries it
onto noon at c = -1, so their solution sets are mirror images.

Their PATHS differ regardless, which is the part that matters to a picture of paths: a total-degree
homotopy runs from a FIXED start system to the target, so reflecting the target does not reflect
the journey.  Measured: this system leaves 108k samples where noon at c=-1 leaves 25k, and they
render quite differently (both were rendered and compared).  Both real noon variants make worse
pictures -- sparser, lopsided, clipped -- so the frame keeps its system and loses its wrong name.

Frames re-render BYTE-IDENTICALLY: the equations never changed, only what we call them.
@ofloveandhate

Copy link
Copy Markdown
Contributor Author

This is, obviously, Claude-written, with me steering. I won't accept this til I rewrite it in my own style. It was fun to brainstorm with the AI, to find good systems to visualize in this way.

@ofloveandhate

Copy link
Copy Markdown
Contributor Author

See #360

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant