docs(showpieces): The Nebula — one solve, every path, every step - #363
Draft
ofloveandhate wants to merge 6 commits into
Draft
docs(showpieces): The Nebula — one solve, every path, every step#363ofloveandhate wants to merge 6 commits into
ofloveandhate wants to merge 6 commits into
Conversation
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.
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. |
Contributor
Author
|
See #360 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 joiningthem, 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 framereads as a flow without arrows. (Colouring by
arg ℓ(x)is a trap worth naming: position alreadyis
(Re ℓ, Im ℓ), soarg ℓis just the screen's polar angle, a pinwheel carrying nothing.)The frames
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:
t=0, so|t|ismulti-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× cheaperhere: 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.
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.pyguarantees observers fire safelyunder threads — not that they are fast.
symmetric, it is a fuzzy ball: every path starts near
|x|=1and ends near|x|=1, so thewhole 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 --selftestasserts 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 nebularegenerates both frames byte-identically tothe committed PNGs — the real check that seed +
num_threads=1is deterministic.tools/py_doclint.pypasses both gates (correctness; undocumented ratchet at baseline 14).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
SolutionPathCollectorand drains.seriesas it fills, somemory is O(one path) rather than O(solve).
E/moverm = max(1, ceil(L/step))subsamples, not asE/L: identicalfor long segments, but no
1/L → ∞singularity when a segment lands inside one pixel. The pixelis the sensor; motion below it is not resolvable.
subsamples.
suggest_window()proposes them from thedata and prints them; they get pasted in. Auto-windowing would make a committed PNG depend on
run-to-run data.
phosphorylation, Stewart-Gough, dense random), a
--scoutcontact sheet that renders everycandidate projection from a single solve, and the
--selftest.Open questions for review
:doc:link, because docs(showpieces): Homotopy Basins #352 isunmerged 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.
the known 924 cyclic 7-roots), though neither shipped frame depends on that. Worth deciding
whether the hero should be AMP.
x_i(1 + Σ_{j≠i} x_j²) - 1, whereas noon-n isx_i Σ_{j≠i} x_j² - c x_i + 1withc = 11/10.x → -xcarries it onto noon atc = -1, so their solution sets are mirror images — but theirpaths 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.
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) andthe 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