The PCA section's orientation is arbitrary, and it moves |lam2| by thirteen times run 022's quoted floor
builds_on: and related:The section's orientation is an unrecorded instrument choice
Found by asking what normal form a tool emits, not by a failing test. numpy.linalg.svd returns singular vectors with arbitrary sign — the standard fact that every PCA implementation handles with an explicit convention (sklearn's svd_flip) and that R18c.py does not handle at all. calibrate() returns Vt[:modes] raw.
Why the sign is not free
stream_section() detects a crossing with a directional test:
hit = (prev[:, 0] < 0) & (cur[:, 0] >= 0)
Only upward crossings of mode 0. Negate mode 0 and the same code collects the downward crossings instead. Both are legitimate Poincaré sections of the same flow, they are disjoint, and their first-return maps are different maps — so there is no reason their Ulam spectra should agree, and they do not.
CORRECTION 2026-09-26, same day: the magnitude below is withdrawn
The mechanism stands. The number does not. An independent reproduction at the same stated inputs got 14,573 crossings and a shift of 0.046 / 0.037 / 0.032 — three to four times what is tabulated below — and, decisively, showed the shift moving from 0.037 to 0.022 at one grid from changing v0 alone. At ~14.5k crossings the orientation systematic is not determined to better than about ±0.02.
So the table below is one realisation of a noisy statistic, reported to four decimals as though it were a measurement. That is the error this project exists to catch, committed by the codifier, in a note about instrument error. The underlying reason is not subtle: the KS flow is chaotic, so any difference at 1e-15 in the basis — a different LAPACK build, a different numpy — sends the trajectory somewhere else entirely, and the crossing sequence is not reproducible across machines even in principle. Two runs sample the same attractor; they do not produce the same points.
What survives, and it is the load-bearing part. The mechanism, which is read from the code and needs no measurement: the sign is unpinned, the crossing test is directional, and negating mode 0 collects a different section. The mode-1 invariance, which is structural (permutation similarity), not statistical. The determinism of the fit on one machine. And the qualitative claim that the orientation systematic exceeds run 022's 0.002 floor — which both measurements agree on and which the larger reproduction strengthens, not weakens.
What is withdrawn: the specific figures, and the phrase "five times that floor". What replaces them is a measurement with an error bar, in notes/pca-section-orientation-evidence.md (N-030): at L = 22, N = 64, h = 0.01, calib 1e6, 6×10⁶ steps and 30 realisations, the shift is +0.0261 ± 0.0064 at grid 12 and +0.0257 ± 0.0048 at grid 16, a bootstrap over the realisations, four standard errors from zero. That is the current best estimate of the systematic at L = 22, and the only one that carries an error bar. It says nothing about L = 26, 30 or 34, which have not been measured.
Also corrected: "equal by necessity" for the two crossing counts is too strong. The reproduction got 14,573 and 14,574. A trajectory can start or end mid-cycle, so the counts are equal to ±1 per realisation, not exactly.
---
Withdrawn — the original measurement, at L = 22, N = 128, h = 0.25, 4×10⁶ steps, one realisation
| grid | cells | mode 0 as fitted | mode 0 negated | shift |
|---|---|---|---|---|
| 6 | 36 | 0.5168 | 0.5039 | 0.0129 |
| 8 | 64 | 0.5356 | 0.5268 | 0.0088 |
| 10 | 100 | 0.5537 | 0.5447 | 0.0090 |
Both orientations return 14,326 crossings — equal by necessity, since on a bounded attractor every upward crossing of a coordinate is matched by a downward one.
The exposure is mode 0 only, and that is worth knowing
Negating a section coordinate (mode 1) changes |lam2| by nothing at all — 0.5168 / 0.5356 / 0.5537, identical to four decimals at every grid. That is not luck. cell_index() normalises each axis to its own observed span, so a reflected axis produces the same lattice with its cells relabelled, and a relabelled partition is permutation-similar, which leaves the spectrum alone. The same argument covers the k-means scheme, whose distances are reflection-invariant.
So the fix is one line, on one vector. Pin mode 0's sign by a deterministic rule — the svd_flip convention, making the largest-magnitude entry positive, is the standard one — and record the convention in the run's output. Nothing else in the partition machinery needs touching.
What this does and does not say about the filed runs
The fit is deterministic. Two calls to calibrate() with the same arguments return a bit-identical basis, within a process and across processes. So this is not a source of run-to-run scatter on one machine, and --modes 3 versus --modes 5 is unaffected — both slice Vt[0] out of the same SVD, so they share a section by construction, and run 023's 3-vs-5-mode agreement does not come into question.
What it is, is an unrecorded systematic of about 0.01 on every |lam2| this instrument has ever produced, plus a reproducibility hazard across machines: a different LAPACK build can return the opposite sign for the same input, and this project runs the same scripts on two Macs.
The finding that replaced it: pinning is a break, not insurance
Checked at the arguments run 023 actually used (N = 64, h = 0.01), not the ones above. The svd_flip convention — largest-magnitude entry positive — is satisfied at L = 22, 28 and 34, and violated at L = 26 and L = 30. So the two L values that fail to converge were computed on the opposite section from the three that do, and pinning the sign would change their filed numbers without rerunning anything.
This is not "26 and 30 are wrong". Each L has its own attractor and its own fitted basis; there is no canonical orientation shared across different L, so absent a convention each L simply received one of its two legitimate sections at random. The correct statement is worse in a duller way: run 023 compared |lam2| across five L under an uncontrolled per-L binary choice that moves the answer by more than the effects being compared, and the choice happened to split 26/30 against 22/28/34 — exactly the partition the verdict turns on.
And svd_flip is a fragile convention on this data. The margin between mode 0's largest and second-largest entries is 0.0001 in three of the eight cases measured, because mode 0 is close to sinusoidal and its two peaks are nearly equal. The rule is stable against a LAPACK sign flip, which leaves magnitudes intact, but it is decided at the fourth decimal and a slightly different calibration trajectory could flip it. Pinning with it would manufacture the appearance of determinacy over a coin toss.
The better move is not to pin at all: detect both directions in one streaming pass. The section is defined by mode 0's zero set; upward and downward crossings are two disjoint sections of it, and the stepping — the entire cost — is shared. Collecting both costs one extra comparison per step and an interpolation per crossing, against an FFT. Then |lam2| is reported on both orientations with their spread as a measured systematic, no convention is needed, nothing is decided at the fourth decimal, and the ambiguity becomes a term in the error budget instead of a hidden choice. This is the run 029 move — separate the instrument's contributions and report them — applied to the term run 023 never had.
The consequence for run 022. Run 022 established partition-independence at L = 22 to a 0.002 floor, and run 023 leans on that as the settled base case. The orientation systematic at the same L is 0.026 ± 0.005 (N-030), thirteen times that floor. The 0.002 is not wrong; it measures how little |lam2| moves when the partition is perturbed. It is simply not a statement about how well |lam2| is determined, because a second instrument choice sitting in the same measurement was never varied and is thirteen times larger. A quoted precision that is finer than an unmeasured systematic in the same number invites exactly the reading it cannot support.
And run 023's L = 34 PASS was declared at "3- and 5-mode agreeing within 0.01" — the same magnitude as this shift. That agreement is real and, per the paragraph above, is not itself threatened. But a tolerance and a systematic of equal size should not be discovered in that order.
Scope, stated because the temptation is to generalise it. Everything measured here is at L = 22 with 14,326 crossings. The shift at L = 26, 30 and 34 has not been measured and could be larger or smaller; non-convergent L = 26 in particular has no reason to behave like L = 22. Nothing here says any filed number is wrong. It says a number was reported to a precision that a second, unvaried instrument choice does not support.
Where it goes
- Run 023 —
amended:, pointing here. - The fix —
R18c.py, one line incalibrate(), plus the convention in the output JSON. Implementation, so Claude Code. - OPS candidate — a run reporting a spectral quantity computed on a fitted section must record the section's orientation convention, and a quoted precision must name which instrument choices were varied to obtain it. This is the same shape as run 029's error decomposition, which separated grid, filter and scan contributions. Run 023 got no such decomposition, and this is the first term of the one it should have had.
159 source lines, no addenda.