Repository navigation
Conversation
Where netsalt's two above-threshold solvers disagree on the buffon, and why.
The figures cost hours of solver time, so they and the (small) arrays they were
drawn from are checked in rather than regenerated on demand.
Three figures, each with its plotting script and the probe that produced its
data:
li_salt_vs_linear.png the L-I curves. Full SALT lases 6 modes at 1.0769x
and 5 from 1.0846x; linear keeps all 6 and grows
the sixth from 4.67 to 14.16.
what_linear_misses.png the gap split into "wrong mode count" and
"over-saturation". Giving linear the SAME five
modes moves the total-output gap from +16.8 % to
+16.4 % at 1.1616x -- the count is worth 0.4 of
16.8 points. The rest is 1/(1+u) expanded to 1-u,
which over-estimates depletion with an O(u^2)
error.
extinction_mechanism.png why the mode is lost. Backgrounds built with
saturated vs threshold profiles and pulled vs
threshold k put the candidate at +4.3482e-05,
-7.3351e-05 and -7.3341e-05: the last two agree to
four figures, so frequency pulling does nothing and
the whole swing is profile deformation. The
survivor of the near-degenerate pair deforms by
400 % of its own peak and the pair's overlap
collapses from 0.68 to 0.29 -- they segregate, and
the winner takes the gain.
New scripts: sweep_pump_ladder.py (the ladder that produced the L-I data, with
linear recorded for all original slots so a dropped mode's linear curve does not
vanish with it), plot_pump_ladder.py, plot_linear_gap.py,
plot_extinction_mechanism.py. The existing probes now write their arrays to
out/ and figures to figures/, both already ignored, so running them leaves the
tree clean.
.gitignore gains a narrow exception: figures elsewhere are build artifacts that
run.sh regenerates, but these are the record of a measurement and are meant to
outlive the run that made them. The committed extinction data is reduced --
pumped samples only, thinned 4x, float32 -- 399 KiB against the probe's 8.9 MB.
The README also records what is still open: whether an extinguished mode
re-ignites once the survivors' profiles deform further (the mechanism predicts
not, which is the falsifiable part), that the degeneracy is a triplet rather
than a pair, whether the pattern repeats as modes 7-12 turn on, and the
possible bistability of the two five-mode states.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
§3 of the results README argued that the extinction would not reverse:
deformation grows with pump, deformation is what killed the mode, so it should
get more dark. The measurement says otherwise, and the wrong prediction is kept
in the text because it was the falsifiable part.
Reading the alpha of the extinguished mode's branch off the sweep that admits
and drops by net gain (probe_mode_reignition.py re-screens every candidate at
every pump, dropped ones included): +6.06e-05 at 1.0846x, decaying smoothly
through +2.62e-07 at 1.1462x, to -2.59e-06 at 1.1538x and -5.98e-06 at 1.1615x.
From 1.10x it falls two orders of magnitude and crosses zero near 1.150x. The
hole does not deepen over the loser indefinitely -- past a point the winner
reshapes away from it.
Below 1.09x the same alpha swings either side of zero several times, so this is
not one extinction and one return; the cluster goes in and out repeatedly. The
mode count follows: ~6 at 1.085x, dipping to 5, climbing to 9 by 1.1385x.
Four things this does NOT establish, recorded with the figure rather than
glossed:
* which cluster member re-ignites. From 1.1000x the probes started at
k = 10.680054 and 10.679976 return identical alpha to five significant
figures -- both root finds land on the same root.
* the crossing location beyond one 0.77 % pump step. alpha at 1.1462x is
2.6e-07, ~100x the noise floor measured on converged incumbents (~1e-9), so
the sign is solid and the position is not.
* agreement with §1's ladder. This sweep admits from the bottom, so its active
set differs; at 1.0846x it finds k = 10.68009 with net gain where the ladder
found it dark. Different background, not a contradiction -- and it is the
multistability question appearing in the data.
* anything above 1.1615x. The sweep was interrupted there, of a 1.30x target,
and never wrote its final intensities. A 10-mode solve at 1.1538x refused to
converge twice, 6692 s and 7126 s, against ~1000 s at M = 9; that is the
cost wall and it is now an open question in its own right.
The figure uses a symlog axis: the 1.0846x point sits at -3.5e-04 while the
crossing happens at 2.6e-07, so on a linear axis the subject of the figure is a
flat line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
The coarse sweep (0.77 % steps) put the co-lasing count at 6 -> 5 -> 9 and an extinguished mode's alpha crossing zero near 1.150x. Steps that size cannot say whether those transitions are sharp, and the count behind them was reconstructed from solve attempts in a log rather than from accepted sets -- the grow step accepts a set only if every amplitude clears the lasing floor AND the residuals pass, so a solve can report converged and still be rejected. Some of the jumps may be that, not physics. This sweep records the accepted set explicitly at every pump, logs the alpha of every candidate it screens, and steps at 0.25 %. It can afford the fine grid because it seeds from the converged five-mode state at 1.0846x (AUDIT.md §13) instead of building the active set up from threshold, which is where the coarse sweep spent most of its time. Per pump it solves the current set -- solve_salt_varying drops a mode it has verified dark and re-solves the remainder, so shrinking needs no extra logic -- then screens every other candidate with net_gain_alpha on the survivors' background and tries admitting those with net gain, keeping each only if the enlarged set holds. It writes after every pump, since a sweep this long outlives several container restarts; the previous one was lost before it could save. What this cannot settle is whether the count depends on which branch the continuation is on. The coarse data already hints that it does: at 1.0846x the sweep found a mode with net gain that the forced ladder found dark, on a different active set. That needs a hysteresis run -- up, then back down from the top state -- which the recorded states make possible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
The coarse sweep's count moved 6 -> 5 -> 9 in steps that looked abrupt, and it
was reconstructed from solve ATTEMPTS in a log rather than accepted sets -- the
grow step accepts a set only if every amplitude clears the lasing floor and the
residuals pass, so a solve can report converged and still be rejected. Some of
those jumps were that.
At 0.25 % steps, with the accepted set recorded directly, the net gain
underneath every transition is linear in pump and crosses zero transversally.
For k = 10.6133 the successive alpha differences over six pumps are -5.68,
-5.66, -5.66, -5.64, -5.63, -5.62 (x1e-6 per step): a straight line to four
figures. The apparent jumps are three effects on top of that smooth physics:
* counting is discrete while alpha is continuous;
* k = 10.6264 crosses at 1.1276x and k = 10.6522 at 1.1286x -- 0.09 % of
threshold apart against a 0.25 % grid -- so both are admitted in one step
and the count appears to move by two. They are not degenerate in threshold,
just closer than the resolution;
* admission lags the crossing by up to a step: k = 10.6133 crosses at 1.1103x,
is past the -1e-6 margin by 1.1121x, and is accepted at 1.1146x, the failed
trial solve showing as that pump costing 348 s against a typical 145 s.
The 6 -> 5 dip did not reproduce: the count is a flat 5 from 1.0846x to 1.1121x
then rises monotonically. This sweep also seeds differently, so step size and
branch both changed -- the dip is not robust, which of the two caused it is not
established, and the README says so.
The re-ignition is confirmed and pinned: alpha crosses zero at 1.1476x, linear
to 1.4 % over the six approaching pumps. But at 1.1521x, past the admission
margin, the ten-mode solve ran 14137 s and failed; the accepted count stayed at
9. That is the same wall the coarse sweep hit twice at 1.1538x, and it is now
clear it is not incidental -- it sits exactly where the re-ignited mode has to
join. The physics says ten modes lase above 1.1476x and the solver cannot
produce that solution, so this fixture cannot be followed past ~1.15x until
M = 10 converges. Promoted to the blocking open question.
The sweep was stopped with 3 of 31 pumps left: each would retry the same
ten-mode solve at ~4 h for no new information.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
Keeps this results branch stacked on an up-to-date base: the M=10 wall recorded in the mode_competition README is fixed upstream, and the numbers here were all produced before that fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
The mode_competition README named M = 10 as the blocking item and the reason the sweeps stop at ~1.15x. AUDIT.md §15 fixes it -- it was a seeding bug, not a cost wall -- so the note now records that, and that every figure and number here was produced before the fix and can be pushed further up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
sweep_fine_transitions.py saves after every pump, so a sweep that takes hours can be looked at long before it ends -- and these sweeps have twice now outlived a container. This plots whatever is on disk: per-mode L-I, total output against the co-lasing count, and the turn-on pumps. The figure says in its title that the sweep may still be going, so a partial one cannot be mistaken for a complete one. Written for the re-run after the warm-start fix (AUDIT.md §15), where the sweep reaches ten co-lasing modes -- past the point the three previous attempts died. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
Two changes to the admit/drop sweep and its plot.
sweep_fine_transitions.py resumes. These sweeps run for hours and have now
outlived four containers; the last one died at 40 of 84 pumps and restarting
from the top would have re-paid all of it. If the output npz already holds
pumps it reloads them, seeds from the last recorded state -- which is a
converged one, exactly what the next pump wants -- and continues from there.
plot_sweep_v2.py now computes the linear competition-matrix solution on the same
pump grid, for EVERY candidate including the ones full SALT never lases, and
draws it dashed against full SALT in all three panels: per-mode L-I, total
output, and the co-lasing count.
That comparison is what the sweep is for, and at 1.2796x it shows the two models
disagreeing qualitatively rather than by an offset. Counts are close -- SALT
5 -> 11, linear 6 -> 10 -- but they do not agree on which modes:
k = 10.6499 SALT a = 10.15, linear never lases it at any pump in range
k = 10.6801 SALT 6.52 against linear 27.20 (-76 %); linear's third largest
mode is SALT's eighth, and linear cannot represent the
extinction and re-ignition it actually undergoes
k = 10.6875 SALT 36.62 against linear 23.01 (+59 %)
k = 10.6522 SALT 2.61 against linear 6.65 (-61 %)
Total output is SALT +13.5 % at the top of the range so far.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
The sweep began at 1.0846x from a converged five-mode state, which is what makes a fine grid affordable but hides the bottom of the curve: the first mode's turn-on, and the pumps where the set grows from one mode to five. A start pump is now a CLI argument, and a start at or below 1.02x seeds instead with the single lowest-threshold mode just above its own threshold and lets the admit/drop logic build the rest from nothing. Starting at exactly 1.0000x does not work, and correctly so: at D0 = D0_thr the first mode sits exactly at threshold, its intensity is zero, and there is no positive-amplitude solution to find. The sweep reports "base solve did not converge" and stops. Start just above it -- 1.005x lases the first mode with a small finite amplitude. The low leg is run as its own continuation into its own npz, which also gives a consistency check worth having: it arrives at 1.0846x from threshold, where the high leg was *seeded* by hand. Agreement there justifies joining the two; disagreement would be the branch dependence these sweeps have hinted at twice, and would have to be reported rather than smoothed over by drawing one curve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
Joins the two continuation legs -- out/fine_low.npz from just above the first lasing threshold with a single mode, and out/fine_v2.npz from the converged five-mode state at 1.0846x upward -- and draws the linear competition matrix against them for every candidate, including ones full SALT never lases. The join is a claim, not a formality: the legs are separate continuations, and this repo has twice found the answer depending on which branch you are on. It holds here. The low leg arrives at 1.0850x from threshold at [2.26, 24.09, 9.75, 9.05, 9.81]; the high leg was seeded by hand at 1.0846x at [2.253, 23.980, 9.699, 8.976, 9.726]. The 0.3-0.9 % differences are what the 0.037 % pump gap predicts, so both are on the same branch and the hand seed is vindicated. The figure marks the join. Starting from the foot also shows what a truncated curve could not. The count is non-monotone exactly where the paper's curves live -- 1, 3, 4, 6, 5, 6, 8, 9, 10, 11 -- and the dip at 1.0800x is the §13 extinction, now reached by continuation from threshold rather than constructed, so it is not an artefact of where the sweep was started. The low leg finds it on its own. At 1.3346x, 68 pumps in, the two models disagree in both directions while their totals nearly agree (SALT 280.7 against linear 248.0, +13.2 %): k = 10.6264 SALT 11.26 against linear 6.86 +64 % k = 10.6875 SALT 41.75 against linear 27.46 +52 % k = 10.6499 SALT 12.89, linear never lases it at any pump in range k = 10.6801 SALT 10.11 against linear 33.33 -70 % k = 10.7074 SALT 0.70 against linear 2.71 -74 % A total-power measurement would make the linear model look nearly right; a spectrally resolved one would not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
The header printed START, the default or CLI start pump, even when the resume path had replaced it with "last recorded pump + one step". A zoom run seeded from an existing npz therefore announced "13 pumps, 1.0846x .. 1.0815x" while correctly running 1.0755x .. 1.0815x. The work was right and the log was not, which is the kind of thing that later gets read back as data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
The small jump just below 1.085x in the L-I curve is not under-resolution. At 0.05 % steps -- ten times finer than the step that produced it -- the dying mode's intensity is still RISING right up to the last pump where a solution exists (3.445 -> 3.47 -> 3.50 at 1.0750x, 1.0755x, 1.0760x), and then the six-mode solve stops converging. A branch that ends while its amplitude is finite and growing is a fold, not a continuous switch-off. Solving both branches at the same pumps: pump six-mode five-mode alpha of the sixth on the five-mode state 1.0700 converges a=3.157 converges -1.35e-04 net gain 1.0730 converges a=3.334 converges +5.13e-05 lossy 1.0760 converges a=3.499 converges +4.94e-05 lossy 1.0765 FAILS (a->2.586) converges +4.91e-05 lossy 1.0780 FAILS (a->2.584) converges +4.81e-05 lossy So over roughly 1.073x-1.076x there are two self-consistent SALT solutions at one pump: one with the mode lasing at a ~ 3.5, one with it genuinely below threshold on that background rather than merely dropped. Below ~1.0715x the five-mode state is not a physical steady state at all -- the sixth mode has net gain on it and must join. An upward sweep therefore holds the six-mode branch to the fold and then drops discontinuously onto the five-mode one, which is exactly the jump, and why finer steps sharpen it rather than smoothing it. What conv=False does NOT prove is that a branch ended; it is also what solver trouble looks like, and this session has been caught by that distinction twice. What supports the fold reading is the combination: the amplitude rising to the last converged pump, the five-mode state being simultaneously stable there, the failure reproducing at two pumps above and none of the three below, and the failed solves drifting to a ~ 2.58, below the 3.50 they had been climbing -- which is what a solver losing a stable branch to its unstable partner looks like near a saddle-node. Proving it outright needs continuation THROUGH the turning point, which this solver does not do. probe_hysteresis.py is the test that does not need that: sweep the pump back down from a five-mode state and see whether it holds five modes below the pump where it jumped off six. Hysteresis is also something the linear competition matrix cannot produce -- its solution is unique by construction, so it has no branches to be trapped on and no history to depend on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…orrection Records §7: the jump just below 1.085x in the L-I curve is a first-order mode switch with a bistable window, not under-resolution. At 0.05 % steps the dying mode's intensity is still RISING (3.445 -> 3.47 -> 3.50) when its branch stops converging, which is a fold rather than a continuous switch-off, and both the five- and six-mode branches converge over ~1.0735x-1.0763x. Hysteresis settles it without needing continuation through the fold: sweeping up the laser holds six modes to ~1.0763x then drops to five; sweeping back down it holds five to ~1.0735x then jumps back to six. A loop ~0.3 % of threshold wide. The linear model cannot produce this at all -- its solution is unique by construction, so there is no second branch to be trapped on and no history dependence. CORRECTION to 4727118, which reported alpha = +5.13e-05 (lossy) at 1.0730x and would have put the window's lower edge too low. That number does not reproduce. Re-solving the five-mode state there by two independent routes -- fresh from the 1.0760x six-mode state, and continued down from the 1.0800x five-mode state -- gives the same state to five digits (2.0419 / 20.7425 / 8.3013 / 6.7951 / 7.2593) and the same alpha = -1.4743e-04, net gain. Three of four determinations agree and the outlier came from one run. The five-mode family does NOT have a second branch there; the lower edge is between 1.0730x and 1.0740x. The fold test is being re-run verbatim to find out whether that row was a one-off or whether the solver has a reproducibility problem, which would matter well beyond this section given how much of this analysis rests on alpha. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
net_gain_alpha reports what the root nearest a given k0 does on a given background. Reading one such call as "this state is stable" is wrong, and I have been doing it. On this buffon the cluster near k = 10.6800 carries several roots within 1e-4. At 1.0730x, two probes started 1.5e-05 apart converge to different ones: k0 = 10.680076 finds a root at 10.679988 with alpha = +5.13e-05 (lossy), while k0 = 10.680091 finds 10.680069 with alpha = -1.47e-04 (net gain). Narrowing the search window from the 0.1 default to 1.4e-04 changes neither -- both roots are inside it -- so this is not a window that wants tightening, it is a spectrum denser than one probe can characterise. Both numbers are correct answers about different modes. This also retires two false leads chased on the way. The solver is not path dependent: the same five-mode solve run cold, after a six-mode solve, and cold again gives identical states and identical alpha (A == B == C), with the cubic resampling cache empty throughout. And the fold test's disputed row is not a non-reproducible one-off: re-running it verbatim reproduces it bit for bit. The correction in cb1a889 said otherwise on both counts and is itself wrong; it will be fixed once this measurement replaces inference. A state is unstable if ANY root has net gain, so stability is the minimum alpha over all candidates. probe_state_stability.py probes every candidate not currently lasing, reports the root each probe actually lands on -- so a probe answering about the wrong mode is visible rather than silent -- and takes the minimum. It is pointed first at the question this undermines: whether the five-mode branch really is stable below the fold at ~1.0763x, i.e. whether the bistable window and hysteresis loop survive a proper stability test. The fold itself does not depend on alpha and is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
Probing EVERY candidate on the five-mode background, rather than one root, confirms the bistable window and gives a better mechanism for its lower edge. 1.0700x unstable root 10.680071 alpha = -1.354e-04 net gain 1.0720x unstable root 10.680070 alpha = -1.434e-04 1.0730x unstable root 10.680069 alpha = -1.474e-04 1.0740x stable both probes find ONE root 10.679989, alpha = +5.07e-05 1.0750x stable alpha = +5.00e-05 1.0760x stable alpha = +4.94e-05 Below 1.0740x two roots sit near 10.6800, one with net gain at ~10.68007 and one lossy at ~10.679988, and probes started 1.5e-05 apart land on either. At and above 1.0740x both probes converge to the same root: the net-gain root has not drifted away, it has merged with its lossy partner and annihilated. A saddle-node in the root structure -- the same bifurcation as the fold, seen from the five-mode side. The alpha sign flip the hysteresis sweep saw between 1.0740x and 1.0730x was therefore real physics, not a probe artefact. So the window stands: five-mode stable above ~1.0735x, six-mode branch existing to ~1.0763x, both stable between, a loop ~0.28 % of threshold wide. Corrects cb1a889, which called the fold test's +5.13e-05 at 1.0730x non-reproducible. It reproduces bit for bit, and the solver is not path dependent (identical five-mode solve cold, after a six-mode solve, and cold again: identical states, identical alpha, resampling cache empty throughout). The probe had answered about the other root of the pair, which genuinely is lossy. Both numbers were right; reading either as a verdict on a STATE was not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…de count The guard rejected a non-converged base solve only when the solve had also kept every mode. The reasoning was that a mode going dark is the ordinary way a set shrinks -- but failure and loss are exactly what happen together, so a failed solve that dropped a mode sailed through and was written to the npz as data. Measured at 1.4646x: a 26001 s solve took M from 11 to 10, rearranged the amplitudes wholesale (first mode 2.94 -> 8.67, second 101.45 -> 45.58) and came out with total output 7 % LOWER than the previous pump for 0.34 % MORE pump. No branch does that. The row is dropped from out/fine_v2.npz; the trustworthy range is 1.005x .. 1.4596x, which is what the published figures already show. This is the same mistake as the one fixed in solve_salt_varying earlier in this branch -- reading a lost mode as evidence about physics instead of checking it -- in a different place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
The extinction in §13 happened because two modes were spatially near-identical at threshold -- 0.68 pump-weighted overlap -- and segregated under saturation, the winner taking the pump-rich region and the loser starving. Asking whether other pairs do the same needs the right criterion, and k spacing is not it: gamma_perp = 0.5 puts even the widest gap among these twelve candidates 220x inside the gain linewidth, so every pair competes. Proximity in k is not what distinguished them. Spacing does say something structural, though. Sorted, the twelve candidates hold exactly one near-degenerate cluster -- a triplet at 10.679331 / 10.679976 / 10.680091 spanning 7.6e-04 -- while every other gap is at least 2.28e-03, twenty times the triplet's narrowest. So whatever the overlaps say, this fixture has no second cluster for the pattern to repeat on. probe_mode_overlaps.py computes the pump-weighted overlap over every pair of threshold modes and ranks them, reporting where the known pair sits and which others come within a factor of two. If none do, the question cannot be settled by pumping this fixture harder; it needs a wider k window with more candidates, which is a concrete experiment rather than an open question. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
Ranking all 66 pairs of threshold modes by pump-weighted overlap answers "does it repeat" and corrects a number in the extinction section. CORRECTION. The extinguishing pair's threshold overlap was given as 0.68. That was computed inconsistently -- the dying mode's field evaluated on a saturated background against the partner's threshold shape. Taken consistently, both from the unsaturated operator, it is 0.9995, and the three members of the cluster at 10.680 overlap at 0.9995-1.0000: they are spatially the same mode. The collapse under saturation is 0.9995 -> 0.29, larger than reported, so the segregation reading is strengthened rather than weakened. THE PATTERN REPEATS. The pair 10.704320 / 10.707383 sits alone in fourth place at 0.9706 with every remaining pair below 0.57, and it is exactly the pair failing at the top of the sweep: 10.7043 is the strongest mode in the laser (a = 101.45 at 1.4596x) while 10.7074 fell 0.44 -> 0.21 between 1.2796x and 1.4596x and is what the eleven-mode solve could not hold. Spacing would have missed it. The second pair is four times wider apart in k than the first and belongs to no near-degenerate cluster; by spacing this fixture has exactly one cluster, so that heuristic predicts one switch where there are two. gamma_perp = 0.5 puts even the widest gap 220x inside the gain linewidth, so proximity in k distinguishes nothing. Overlap does. This reframes the cost wall near 1.46x -- 500 s, 8748 s, 26001 s per pump with eleven modes never converging -- as most likely the same extinction physics rather than a solver defect: there is no eleven-mode solution once 10.7074 has gone. The ten-mode solve testing that is still running. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
A fixed grid is the wrong instrument near a mode switch. The state changes fast there, and a step sized for the smooth stretches overshoots what the continuation can track. Measured at the second switch. Per-pump cost had been flat at 500-700 s for twenty consecutive pumps from 1.3596x to 1.4546x; then a 0.50 % step from 1.4596x cost 8748 s and the next 26001 s without converging. From the same state, a 0.05 % step costs ~580 s and converges normally. The region is not intrinsically expensive -- the step was too large for it. Worse, the 26001 s solve drove k = 10.7074 to zero. The fine crossing shows that mode declining linearly (0.211732, 0.207523, 0.203286, 0.199027 at 0.05 % steps) and still lasing at ~0.18 there, reaching zero near 1.484x. So that solve produced a plausible-looking answer that was simply wrong, which is what the guard added in 26dbbd3 now rejects -- and this change stops the sweep walking into that situation in the first place. So halve the step on a failed solve and retry, growing back by 1.5 towards the requested step once things are smooth; STEP becomes a ceiling rather than a fixed value. A failure that survives shrinking to STEP/32 is not about step size and still stops the sweep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
Shrinking only when a solve fails means paying the whole failure first. Near the second switch a doomed solve still burns its full outer budget before saying so -- about 90 minutes at eleven modes -- and only then does the step halve. The first 0.50 % step after 1.4621x was 30 minutes into exactly that when this landed. The solver already reports how many outer iterations it used, and a solve that needed most of its budget has said the step is too long for the region before it fails. So shrink when a converged solve used more than half its budget, and only grow back after one that used less than a quarter. Failure still halves the step and retries, as before; this just stops the sweep walking into the failure to find out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
§8 predicted from spatial overlap that 10.704320 / 10.707383 would be the next pair to fight. It is, and it ends differently from the first pair. Crossed at 0.05 % and 0.15 % steps, the dying mode's intensity falls linearly: 0.211732, 0.207523, 0.203286, 0.199027, 0.194746, 0.190443 at 1.4596x..1.4621x and 0.177397 at 1.4636x. Slope -8.583 per unit pump, maximum deviation from the line 1.07e-04 -- 0.05 % -- across two different step sizes. Zero at 1.4843x. That is an ordinary second-order switch-off. The first switch was not: there the dying mode's amplitude was still RISING (3.445 -> 3.50) when its branch ceased to exist, with a bistable window and a hysteresis loop 0.28 % wide. So high spatial overlap predicts THAT two modes will fight and not HOW the fight ends -- 0.9995 resolves by a fold, 0.9706 by a continuous extinction. Also records the cost structure as a threshold rather than a gradient: 0.05 % costs ~580 s and 0.15 % costs ~670-720 s, both converging, while 0.50 % costs 8748 s and then 26001 s and fails. Tripling the step costs 15 % more; ten times fails outright. And the failing solve drove this mode to zero where the fine crossing shows it still lasing at ~0.18, which is why the guard rejecting non-converged solves matters. Separates measured from extrapolated: the amplitudes, slope and linearity are measured; 1.4843x is extrapolation the sweep has not yet reached, and the qualitative finding does not depend on reaching it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…weep data plot_full_li.py now takes any number of continuation legs rather than exactly two, and joins out/fine_switch2.npz -- the fine crossing of the second switch -- onto the curve. 101 pumps, 1.0050x .. 1.4666x. Also checks in the three sweeps the published figures are drawn from (fine_low, fine_v2, fine_switch2), so the curve can be redrawn and the per-mode numbers re-derived without repeating days of solver time. At the top of the range the two models now agree on the count (11 each) and on total output to 12 %, while disagreeing per mode by -98 % to +2000 %: k = 10.6499 carries 21.50 in full SALT against 1.01 in linear, and k = 10.7074 is at 0.15 and dying in full SALT against 6.57 and rising in linear. Agreeing on how many modes lase and agreeing on which modes lase are different things, and the first converges while the second diverges. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…stered up front Every result in examples/audit/results/mode_competition/ rests on one graph, and the overlap reading of section 8 was derived from the two switches it predicts -- on that fixture it cannot be wrong. This adds the graph it has not seen. examples/buffon/buffon_competition_b/ is an independent draw at identical parameters (seed 7, make_graph.py regenerates it), rescaled by the shared inner_total_length to the same optical length, so the mode density over the same k window is comparable. Same uniform pump, same twelve candidates, same solver. Its twelve thresholds span 4.66 % against the first graph's 4.7 %. Its overlap structure is not a copy: the highest pair is 0.9693 where the first graph has three at 0.9995-1.0000, and there is no cliff -- 25 pairs sit above 0.54 and rank 1 is 0.04 clear of rank 2. So the first graph's fold needed a spatial degeneracy that is not generic. probe_mode_overlaps.py and sweep_fine_transitions.py now take the fixture as an argument and default to buffon_competition, so the existing reproducers are unchanged; re-running the first fixture reproduces its ranking exactly. The sweep refuses a warm start that belongs to the other graph. README section 10 records the prediction -- which mode dies, that the switch is continuous, that nothing is first-order -- with its falsifiers, before the sweep that tests it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…ook at the sweep Section 9 called the second switch "linear" from seven points over 0.004 in pump. Twelve points over twice the range show it is not: the decline accelerates, and a free-exponent fit of a ~ (D0_c - D0)^p gives p = 0.848, beating p = 1 by 13x in residual and p = 0.5 by 60x. Sub-linear, nowhere near the square root a fold would give, extinction at 1.4808x against the 1.4835x the straight line extrapolated. Section 9 now carries the exponent table and the corrected reading. examples/audit/fit_switch_exponent.py does the fit, and settles which case a trace is in from the sweep rather than from the fit: if the next recorded pump no longer holds the mode, the branch ended there and no exponent is defined, so it prints the end-point and the survivors' jump instead. At the first switch that is a = 3.445 at 1.0750x -- the mode's own maximum -- gone at 1.0800x, with 10.7043 gaining 20 % and 10.6875 gaining 54 % in one step. A first version of the test called any truncated trace a fold, which would have labelled the still-running second switch one. It asks the file now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…4711x The sweep has added five pumps since the figure was made. plot_full_li.py takes --fixture=NAME now, so the same plot can be drawn for the second graph; the default is unchanged and graph A's figure regenerates identically but longer (104 pumps, 1.0050x .. 1.4711x, total +11.9 % over linear at the top). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…ompetition is not the mechanism The second graph's first extinction lands at the second pump on the grid: at 1.0050x two modes lase (10.762463 at 1.6501, 10.787597 at 0.0934) and at 1.0100x the second is gone. It is a real event, not a root-tracking artefact -- an independent solve warm-started from the state before it converges in 17 outer iterations to the same answer, and the dead mode's net gain on the survivor's background is +2.015e-03 at every probe window from 1e-03 to 0.1 with the root moving +3.5e-04. probe_verified_drop.py is that check. The victim is a member of the rank-1 overlap pair, which is the letter of the registered prediction and an accident: its partner's threshold is 1.0352x so the pair never co-lased, and the mode that actually killed it is ranked 61 of 66 by symmetric overlap and 121 of 132 by asymmetric cross-saturation. The two modes are spatially disjoint. probe_cross_saturation.py adds the asymmetric measure, which is the better- motivated one and does no better: ranks 3 and 9 of 132 for the first graph's two switches, but rank 1 is a pair that never switches, and 121 of 132 for the second graph's event. Then the arithmetic that settles it. The mean fractional gain depletion the killer inflicts on the victim is 0.006 %, against the victim's 0.718 % margin above its own threshold and the 0.342 % it saturates itself by. Gain competition is a hundredfold too small, and the linear competition matrix -- which contains nothing else -- keeps the mode lasing to 1.20x. The second graph's SALT-vs-linear disagreement therefore runs opposite to the first graph's. README section 10 records the failed prediction and what is left of section 8: a description of one graph, not a predictor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
… probe is why The second Buffon realisation retires a mode at 1.0100x. It is still lasing, and three independent checks say so. Its own root, continued in pump in 0.08 % steps and warm started in both k and alpha, has net gain from its threshold at 1.0028x past 1.0204x -- alpha monotone from 0 to -1.27e-04, |lambda_1| ~ 1.5e-10 at every point, and alpha = -5.2e-05 at the pump where it was retired. The gain the survivor takes from it is 0.006 % against its 0.718 % margin, and the two modes are spatially disjoint. Re-seeding the same solve from the linear model instead of from the previous pump converges in 16 iterations to a = 0.30403 with residuals 1.0e-08 and 7.0e-07, better converged than the one-mode answer, and within 3 % of the linear model. The mechanism: alpha = 0 is a bad start for this root find -- the trough is ~1e-05 wide in alpha while |lambda_1| at alpha = 0 is already ~1.0 -- and a second genuine root sits 1.1e-03 away in Re k at alpha = +2.0e-03. MINPACK with 30 evaluations converges to the neighbour, and a residual check cannot catch it because the neighbour is a real root. The physical drift is 1.2e-06 per pump step against net_gain_alpha's default k_window of 0.1. probe_phantom_extinction.py is the reproducer. probe_verified_drop.py is the check that nearly missed it, kept with its flaw written down: it varied the window around the k a floored solve returned, which is already off branch, so every width agreed about the wrong root. The graph-B sweep above 1.0100x is stopped and its data void; the prediction registered in the results README is untested rather than failed. The first graph's two switches are not of this kind -- the fold retires a mode at its own maximum with a measured hysteresis loop, the second declines over twelve pumps -- but the re-ignition of section 13 rests on a net_gain_alpha sign change alone and needs the same re-check. AUDIT.md section 16 and results README section 10 record all of it, with the proposed fix listed but not applied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…, and a second seed The probe that decides whether a mode still has gain retired a mode that was still lasing (AUDIT.md section 16). Four changes, all in netsalt/salt_varying.py. net_gain_alpha's k_window has no default any more -- the old 0.1 was a thousand times a lasing root's physical drift -- and net_gain_window(k, other_ks) sizes it from the nearest other known root under an absolute cap of 1e-04. The cap is the part that matters: the root that defeated the old probe is in no candidate list, and the nearest actual candidate is far enough that a fraction of that gap alone leaves it just outside. The probe now also returns +inf when |lambda_1| at its answer is not small, because MINPACK hands back its last iterate whether or not it converged. Every caller reads +inf as undecided rather than as darkness: an undecided candidate is not admitted, an undecided floored mode is not declared dark, and solve_salt_varying reports a lost mode instead of inventing an extinction. Above a candidate's own threshold a single probe has no safe start. alpha = 0 is the right physics and the wrong basin, and not marginally: with the window at 1e-04, diag scaling, eps down to 1e-09, factor 0.01 and a 200-evaluation budget, every setting tried still converged on the root 1.1e-03 away. net_gain_alpha_continued walks up from the threshold where alpha = 0 is exact and then steps onto the saturated background. Measured on the case that broke: one and two steps abstain, three (0.24 % each) agree with a ten-step walk to 4e-10, and the final step onto the survivor's background moves alpha from -5.2241e-05 to -5.1971e-05 -- the competitor costs that mode 0.5 % of its net gain, which matches the 0.006 % gain depletion measured independently. The walk depends only on the pump, not on what the other modes burnt, so NetGainTracker walks each candidate once and extends it as the pump rises: a step or two per pump instead of ~220 at 1.46x threshold. The step count is capped at 400 whatever the excess; past that the steps grow, and losing the root means abstaining, which is the safe answer. Finally, solve_salt_varying retries once before believing a floor, re-seeding each floored mode at its entering amplitude times the survivors' median growth. Once per solve, not once per outer iteration -- without that guard it is `outer` extra complete solves. API: net_gain_alpha requires k_window; net_gain_window, net_gain_alpha_continued and NetGainTracker are public; solve_salt_varying takes optional thresholds= / threshold_ks=, which compute_modal_intensities_varying and examples/audit/sweep_fine_transitions.py now pass. Four audit scripts that relied on the old default pass a sized window instead. Verified so far: ruff clean, and 14 new unit tests over the window arithmetic, the abstention paths and the tracker's caching (stubbed probe -- the line fixture's modes sit at too large an alpha for a real walk to resolve). The full suite and the two end-to-end regressions (graph B's phantom must now survive, graph A's genuine fold must still die) were still running when this was committed; results follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…t pump The first version of the fix made the second graph's case worse, not better: converged=False with both modes at zero. SALT_DEBUG says why at the first outer iteration -- a=[1.7435e+00 1.7436e-09], D0/tgt=1.2214. The second mode is on the weight floor immediately, while the achieved D0 is still 22 % above target and the survivor sits at 1.74 against the 3.32 it ends at. The re-seed therefore re-seeded from the state that had just failed: the growth ratio it needs is 2.01 and at that point it reads 1.06. The retry failed, the corrected extinction test said "not dark", the drop was refused, and the loop ground out its whole budget carrying a mode it cannot satisfy -- the least-squares poisoning that the drop branch exists to avoid. So neither question is asked until the continuation has arrived at the pump it was asked for. Both the extinction test and the re-seed now gate on |D0_now/D0_target - 1| <= _D0_SETTLE_TOL (1e-3), and the re-seed moved after the extinction test, so it runs only for a mode that is genuinely not dark. That also stops the continuation-based test, which is far more expensive than the single probe it replaced, from running on every outer iteration. Verification of this restructuring was still running when this was committed. The specific risk it carries: gating the *drop* on settled is a behaviour change on the first graph's known-good path, and if D0_now cannot settle while a dead mode is in the set, that gate has to apply to the re-seed alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…vors' growth Two errors in the previous attempt, both measured. The settled gate broke the first graph as well as the second. Its "PASS" was superficial: the mode died, but the drop never fired, so the loop ground out its whole budget with the dead mode in the set -- residuals 2.5e-03 .. 2.8e-02 against the 8.5e-07 in 21 iterations the drop normally gives. The chicken and egg is real: with a floored mode in the set D0_now never settles, because the solve degrades instead, so gating on settling means neither the extinction test nor the re-seed ever runs. The gates are gone and both are back where they were. The re-seed formula was wrong too. entering x median growth scales every amplitude by the same factor, which leaves the ratio untouched -- and the ratio is the only thing that needs to change. On buffon_competition_b between 1.0050x and 1.0100x the entering ratio a2/a1 is 0.0566, that formula gives 0.0566 by construction, and the answer is 0.0916. What works is the per-mode near-threshold law, a_mu proportional to D0/D0_thr_mu - 1, extrapolated from the pump the warm start came from: 3.3002 and 0.3057 against answers of 3.31857 and 0.30403, 0.6 % and 0.5 %, well inside the basin the warm start missed. That law is already in the file as the one-point case of _predicted_amplitude, which the pipeline sweep applies to its own seeds, so _reseeded_retry calls it rather than restating it. The rescue needs one new input, D0_entering -- the pump the warm start was a solution at -- and runs first, before the extinction test, since it does not look at the current state and so can fire at the first outer iteration. examples/audit/sweep_fine_transitions.py passes it, its warm start being literally the previous pump's solution. compute_modal_intensities_varying deliberately does not: its seeds are already extrapolated along the same law, and passing it would apply the law twice. Both regressions and the full suite were re-running against this when it was committed; the previous container was reclaimed mid-run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…tive set Same bug as AUDIT.md section 16, a second instance of it, and this one was about to be made worse. _floored_modes_are_dark sized its probe window from the modes in the active set. On buffon_competition at 1.0800x the dying mode's nearest *active* neighbour is 7.25e-04 away, so that gives the 1e-04 cap -- and a probe from its entering k then travels -8.4e-05 and lands 1.6e-05 from a *candidate* 1.15e-04 away, reporting that neighbour's loss as this mode's. The candidate list gives a window of 2.3e-05 and rejects the jump. Only the caller has that list, so solve_salt_varying takes candidate_ks= and compute_modal_intensities_varying and examples/audit/sweep_fine_transitions.py both pass every finite candidate. The diagnostic that found this also killed the fix I was about to write. Graph A was failing because the drop never fired, and the obvious reading was that the continuation abstains near the fold, so a single-probe fallback would rescue it. Measured on that state, the continuation does not abstain -- it answers net gain, alpha = -1.754e-04 at k = 10.680066 -- and it is the single probe that is wrong, landing on the partner at 10.679992. The fallback would have reinstated exactly the bug being fixed. AUDIT.md section 13's remark that the dying mode's "root is pulled towards its near-degenerate partner" was the bug describing itself. Which leaves a finding bigger than the fix. On the five survivors' converged state at 1.0800x -- 2.170 / 22.694 / 9.145 / 8.104 / 8.747, reproducing the published numbers exactly -- the sixth mode has net gain. A mode with net gain has to lase, so that five-mode state is not a SALT solution and the first graph's first switch is suspect in the same way the second graph's was. A seed ladder is running to find whether a six-mode solution exists there; alpha is small, so the mode should lase weakly, and the near-threshold law extrapolating it to 3.764 through a fold is the likely reason the rescue missed it. Verified at this commit: ruff clean, and the full suite green at 228 passed (14:32), functional test included, so the full_salt_newton path is unchanged. Graph B's phantom now survives with a converged solve -- 3.31857 and 0.30403, residuals 1.1e-08 and 7.8e-07. Graph A's case is unresolved and is the subject of the ladder above. The regression harness now requires a converged solve with clean residuals, not merely the expected mode count, which is how it called the previous graph-A run a pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…nd one exp for sin and cos Profiling a three-mode solve on the production buffon put 104 s of 167 in node_solution_varying, essentially all of it in the per-edge propagator rather than in the 486x486 eigensolve. Splitting a secular-matrix rebuild three ways at 8 samples per wavelength (n_steps 408, 217 varying edges): eps sampling 5.06 ms, _magnus_step_matrices 20.28 ms, _ordered_product 31.04 ms, whole rebuild 56.92 ms -- so the ordered product alone was 55 %. _ordered_product now carries the four entries as separate arrays and writes the 2x2 product out instead of calling @ on an (n_edges, 2, 2) stack. At 2x2 the FLOPs are trivial and numpy's matmul gufunc dominates: 76 us per call against the ~5 us the arithmetic needs. 25.38 ms -> 3.82 ms in isolation, 31.04 -> 4.25 ms in place, a 7.3x. The association order is unchanged, so it agrees with the matmul form to 1.3e-15. _exp_traceless_batch gets cos w and sinc w from one complex exponential instead of numpy.sin and numpy.cos. cos and sin of a *complex* argument each decompose into several real transcendentals while exp(1j w) gives both from one: 9.85 ms -> 4.23 ms on the 217 x 408 sample grid, agreeing to 2.2e-15. Rebuild totals: 56.92 -> 29.82 ms at 8 samples per wavelength (1.91x) and 35.37 -> 16.56 ms at the 5 the audit sweeps use (2.14x). That rebuild is ~95 % of every residual evaluation, so solves gain the same factor. Neither change is bit-identical, and the docstrings that claimed bit-identity now claim machine precision with the measured deviation, which is the honest contract: the propagator tests assert allclose at rtol 1e-10..1e-11, five orders clear of 2.6e-15. Two avenues re-measured and rejected, recorded in the docstring so they are not re-explored: caching the laplacian's structural rebuild is worth 1.5 s of 167 despite 1608 calls, and the log-depth pairwise tree is 1.0x/1.03x against matmul and *slower* than the explicit form (5.70 against 3.82 ms), the strided gathers costing what the saved iterations buy. Verified: 44 propagator / varying-laplacian / SALT tests pass, and the graph-B regression reproduces its recorded answer to every printed digit -- 10.762462:3.31857 and 10.787598:0.30403, residuals 1.1e-08 and 7.8e-07, converged in 16 outer iterations, identical to the pre-change run. Still available and not done: per-edge n_steps, since the single global value is sized by the longest edge (19.96) while the mean pumped edge is 11.52, worth an exact 1.73x; and a custom Jacobian exploiting that residual_mu depends on k_nu only through the weak clamp Gamma_nu, worth ~1.25x. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
…, bit-identical) A residual evaluation asks for lambda_1(k_mu) at M different k on one set of saturated profiles, and the sampling of eps on the propagator's abscissae sat inside the transfer-matrix build, so the whole per-edge Python loop ran M times. On the production buffon that loop is 5.11 ms of an 18.25 ms lambda_1 evaluation, and it is independent of n_steps (5.01 ms at 255 against 5.06 at 408) -- pure per-edge overhead across 217 calls, the _halves fast path having already made the point evaluation itself free. The first attempt simply reused the samples across modes and was wrong: a profile returns eps_edge + gamma(k) * D0_eff(x), and gamma moves with k, so the samples are not k-independent. It drove the test fixture's solve to a = 3.5e+12. The k-dependence is only a scalar prefactor on a fixed array, though, so sampling D0_eff once and applying each mode's gamma with one multiply-add is exact. _SaturatedEdgeProfile.d0_at exposes the gain-free part and __call__ is now eps_edge + gain * d0_at(x), so the two cannot drift apart; _profile_samples builds the parts and _samples_at_gain does the combine. Measured on the production buffon at 5 samples per wavelength, residual evaluation: M=3 74.7 -> 57.5 ms, M=6 119.2 -> 100.5 ms, M=11 224.8 -> 183.6 ms, with max relative deviation in lambda_1 of 0.00e+00 at every M -- bit-identical, which is as strong as validation gets: no physics can have changed. Also cached _magnus_sample_points on (length, n_steps, method). The abscissae depend only on an edge's geometry and the scheme, so they are the same at every k, every pump and every mode, yet were rebuilt per edge on each of the ~1600 rebuilds a solve performs -- 0.80 ms of the 5.11 ms loop. The cached arrays are marked read-only since they are now shared. edge_transfer_matrices, construct_laplacian_varying and node_solution_varying take an optional samples=; sample_eps_profiles is the public way to build it for callers whose profiles are arbitrary callables, and _profile_samples returns None for those so they fall back to sampling per k. Verified: 54 propagator / varying-laplacian / SALT / net-gain / oversampling tests pass, and lambda_1 is bit-identical at M = 1, 3, 6 and 11. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
… in one step The fixed solver appeared to condemn the first graph's switch as well as the second graph's phantom: asked for six modes at 1.0800x it returned converged=False with the dying mode at zero, and four seeds spanning 75x all landed on bit-identical non-solutions. The test was wrong, not the physics. It crossed the fold in a single 0.5 % pump step, which section 9's own cost table says is exactly what fails there, while the sweep that discovered the fold used 0.05 %. Walked properly, the six-mode branch holds at 1.0755x (35 it, residuals 3.4e-07 .. 5.3e-07), 1.0760x (32 it, 4.4e-07 .. 7.5e-07) and 1.0780x (22 it, 8.9e-08 .. 5.6e-07), with the dying mode rising into the turning point -- 3.445 -> 3.4724 -> 3.4993 -> 3.6042 -- which is the signature section 13 recorded for this fold. The fold sits in 1.0780x < D0 <= 1.0800x and sections 7-9 stand. AUDIT.md section 18 records this, plus the consequence that a non-converged six-mode solve past a fold is the expected answer rather than a defect, and the lesson that a regression test crossing a fold in one step tests the step size and not the solver. Pinning the fold from 0.2 % to 0.05 % needs three independent probes, each warm started from the banked 1.0780x state and ~7 min after the section 17 speed-ups. That is precision rather than a new conclusion, and it is parked: the container running this work was recycled eight times in a day, each time before a 7-minute solve could complete. The 1.0780x result is checkpointed in examples/buffon/buffon_competition/out/fold_bisect.json. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
This branch has not been deployed
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.
Stacked on #55 — base is
claude/netsalt-code-audit-4snowr, so this diff is only the results record and the scripts that produce it. Merge #55 first and this retargets tomasteron its own.Nothing here changes
netsalt/. It is the kept record of where the two above-threshold solvers disagree on the buffon, so the results survive without re-running hours of solver time.What is in it
examples/audit/results/mode_competition/— a README, three figures, and the (small) arrays they were drawn from.The fixture is
examples/buffon/buffon_competition(added in #55): the buffon over the productionkwindow, uniform pump, 12 candidates whose thresholds all land within 4.7 % of each other — so which modes lase is settled by competition, not threshold ordering. Pump ladder of 12 points from 1.0769× to 1.1616×, 0.77 % steps, warm-started; every pump converged with live-mode residuals ≤ 1e-6.1. The L–I curves
Full SALT lases six modes at 1.0769× and five from 1.0846× on. Linear keeps all six and grows the sixth from 4.67 to 14.16 across the ladder.
2. The lost mode is not the main error
Striking the extinguished mode from linear's candidate set and re-running on the same grid moves the total-output gap from +16.8 % to +16.4 % at 1.1616×. Correcting the mode count is worth 0.4 of 16.8 points.
The rest is the linearised denominator:
1/(1+u)expanded to1 − u, and1/(1+u) > 1 − ufor everyu > 0, so linear always overestimates gain depletion and under-predicts output, with anO(u²)error. Measured +4.9 % → +16.4 % across an 8 % pump range, still climbing at the top.3. Why the mode is lost
net_gain_alphatakes the background as an argument, so it can be built the way either model would — same five amplitudes, same pump:B and C agree to four significant figures, so frequency pulling contributes ~0.01 % — the whole swing is profile deformation. The survivor of the near-degenerate pair deforms by 400 % of its own peak (5× the next mode) and the pair's pump-weighted overlap collapses from 0.68 to 0.29: two modes that are spatially near-identical at threshold segregate under saturation, and the winner takes the gain. A matrix built once from threshold profiles sees only the 0.68.
4. A gap in the existing safeguard
competition_conditioningis 19.3 over all 12 candidates and 2.4 over the pair linear gets wrong, so the ill-conditioning warning stays quiet. The failure is not conditioning ofT; it is thatTis the wrong operator, which no test onTcan detect.Scripts
New:
sweep_pump_ladder.py(the ladder behind the L–I data — records linear for all original slots, so a dropped mode's linear curve does not vanish with it),plot_pump_ladder.py,plot_linear_gap.py,plot_extinction_mechanism.py. Each chdirs to the fixture itself, so they run from anywhere.The existing probes now write arrays to
out/and figures tofigures/, both already ignored, so running them leaves the tree clean..gitignoregains a narrow exception forexamples/audit/results/**: figures elsewhere are build artifacts thatrun.shregenerates, these are the record of a measurement. The committed extinction data is reduced — pumped samples only, thinned 4×, float32 — 399 KiB against the probe's 8.9 MB. The directory is 1.1 MB total.Still open, and recorded as such
probe_mode_reignition.pyrecords every candidate's α at every pump; it was still running when this was written.k = 10.6793, 10.6800, 10.6801all inside 1e-3, the third turning on at 1.0355×.🤖 Generated with Claude Code
https://claude.ai/code/session_015cB7hwiL9WmKk4nH6rULvS
Generated by Claude Code