Join the conversation

Join the community of Machine Learners and AI enthusiasts.

Sign Up
RDTvlokipΒ 
posted an update Aug 9
Post
80
I published an article about training a network to write from reward alone. Code on GitHub with it.

Then someone read the code.

Dipankar Sarkar commented four times in a day. Each time he had run something first. He rebuilt my statistics in numpy because he had no torch installed.

He found a bound I had missed. A policy that never learned the determiner to noun dependency has a product support, so at full validity it cannot exceed the largest fully valid product in the sublanguage it entered. That is 12 on one side and 24 on the other, computable before any training. Over 70 seeds it is never crossed, and the most common outcome is the bound itself. I had published one of those numbers as an interesting coincidence.

Then four of my published numbers came apart. Three were a single seed. The fourth was twenty seeds, and I had produced it while fixing the other three.

And the test I built to validate his bound tested nothing. I had swapped two conditions so cleanly that the two grammars were isomorphic. Seventy seeds would have returned the mirror image by construction.

The real lesson:

A relabelling can permute, but it cannot change a ratio. A perfectly symmetric control is often a perfectly empty one. None of my errors were in the reasoning. They were in the plumbing, and nothing in my own process caught a single one.

Code, figures, and the notebook with eight dated refutations πŸ‘‡

πŸ”— https://huggingface.co/blog/RDTvlokip/i-published-my-rl-experiments
πŸ’» https://github.com/RDTvlokip/RDTRL
πŸ“¦ https://doi.org/10.5281/zenodo.21726216

Thank you for writing this up. Test 3 is the interesting part, so I went there instead.

The one number in your null is the one you called derived.

I ran grammaire3.py at seed 0 before saying anything. Your table reproduces exactly: mean 0.1273, sd 0.0332, min 0.0305, max 0.3305, quantiles 0.0661 / 0.1246 / 0.2171 / 0.2537.

Then I ran the same statistic at 10,000,000 draws instead of 20,000. Vectorised, checked against your own concentration() on 3,000 codes first, max absolute difference 5.6e-17.

Every row holds except one.

20,000 10,000,000
mean 0.1273 0.1269
sd 0.0332 0.0330
q99 0.2171 0.2161
q99.9 0.2537 0.2525
max 0.3305 0.3979

500x the draws moves q99.9 by 0.0012 and the max by 0.067. The body of the null is settled at 20,000. The max is the only row still moving, and the ~0.35 line is the one thing built on it. 14 of the 10M draws clear 0.35.

That does not hurt you and I would rather say so than dress it up. At 100 seeds the chance any run clears 0.35 under the null is 1.4e-4. The threshold is safe for the experiment you planned.

The reason to drop it anyway is that Β§6.2 is already better without it.

Your recorded commitment is about a distribution, not a run. 100 emergent concentrations against a null of sd 0.033 resolves a mean shift of 0.013 at p<0.001 with 80% power. 50 seeds resolves 0.018. The threshold needs one run sitting 0.223 above the null mean.

So the instrument you specified is 17x more sensitive at 100 seeds than the line written next to it. And a weak outside pressure is far likelier to lift every run by 0.02 than to throw one outlier past 0.35. That pressure is invisible to the threshold and a 6-sigma result to the test you already have.

Quote a quantile instead. q99.9 is stable at 20,000. q99.999 needs the long run and sits at 0.3195.

Separately, on the statistic itself, and this one I am less sure about.

concentration takes max over attributes independently per column, so one attribute can win two positions. I checked how often that happens: 74.6% of 500,000 uniform draws, and 46% of the top 200 by concentration. Scoring those same top 200 with a Hungarian matching instead of a per-column max gives 0.2640 against 0.2810. Mean inflation 0.0170, largest single 0.0915.

For your Β§6.2 comparison this cancels. Same transform on both sides, emergent and null, so the test is unaffected and I am not asking you to change it.

Where it does not cancel is Β§6.1, where you read the scalar as a position in the space of perfect codes rather than a binary. Under that reading a 0.28 code is being credited 0.017 of structure it does not have, in the direction that makes a holistic code look composed.

Concentration 1.0 still forces an injective argmax, so the top of the scale is safe. A code where two positions each fully determine one attribute would collapse that attribute's nine referents onto three messages, and a bijection cannot.

So the inflation is real in the middle and absent at both ends.

Is the max standing in for a matching you have not needed yet, or is there a reason to want the double count?

Β·

You went to the one part of the project that has never run. That is the right place to go, and it is the first time in five rounds that a criticism has arrived before the data rather than after. That matters at the end of this reply.

Your numbers

All of them. Seed 0 at 20,000 reproduces to the digit. My own 10M draw is an independent stream, so it is a check on the estimand and not on your RNG:

you, 10M me, 10M
mean 0.1269 0.12688
sd 0.0330 0.03304
q99 0.2161 0.21613
q99.9 0.2525 0.25254
q99.999 0.3195 0.31963
max 0.3979 0.39788
draws β‰₯ 0.35 14 14

Two independent samples agreeing on a maximum to four digits made me check whether the statistic was discrete enough to explain it. It is discrete β€” 2,951 distinct values in 2M draws, since the whole matrix is a function of nine integer counts β€” but the top rungs are singletons, so discreteness explains nothing. It is a coincidence, and the next section is what happens when you stop relying on one.

Your vectorised-vs-scalar check: I did the same and got max absolute difference 5.551e-17 on 3,000 codes, which I assume is your 5.6e-17. For a bijection both margins of the joint are exactly uniform, so I(A_i;M_j) = Ξ£ (c/27)Β·log2(c/3) with c integer β€” that identity is what makes the whole thing a lookup table, and validating against the scalar path is really validating the identity.

The threshold: you are right, and the reason is worse than "still moving"

Your framing is that the max is the only row still in motion. Mine is that it cannot stop.

The 1,296 compositional codes are bijections. They are in the null, with probability 1296/27! β‰ˆ 1.19e-25, and they score exactly 1. So the supremum of the null distribution is exactly 1 β€” the very value the threshold was built to declare unreachable. A sample maximum here is not estimating a threshold. It is estimating 1, infinitely slowly. No sample size fixes that, because there is nothing to converge to except the thing being tested.

Empirically, twelve independent blocks of 10,000,000 (variabilite_du_maximum.py, 384 s):

row mean over 12 blocks range across blocks
mean 0.1269 0.0000
sd 0.0330 0.0000
q99 0.2159 0.0003
q99.9 0.2527 0.0006
q99.99 0.2863 0.0019
max 0.3950 0.0509

The block maxima run from 0.3775 to 0.4283. That range is 1.54 standard deviations of the null itself. A threshold read off that row inherits it: it measures how long I was willing to sample, not the distribution.

So: threshold withdrawn, quantile quoted instead, exactly as you proposed. q99.9 = 0.2525.

The part you found without pointing at it

Β§6.1 should never have carried a pass/fail line at all, and my own document says so three paragraphs earlier. Β§5 reads:

On abandonne dΓ©libΓ©rΓ©ment le critΓ¨re pass/fail. […] la mesure de concentration positionnelle dΓ©finie en Β§6.1 sera statistiquement indiscernable de celle d'une permutation tirΓ©e uniformΓ©ment au hasard.

That is a commitment about a distribution. Then Β§6.1 reintroduces a pass/fail and congratulates itself for it being derived rather than arbitrary. The defect was never the arbitrariness. It was the pass/fail, which Β§5 had already discarded for the reasons you then re-derived from the power side. I wrote both paragraphs. It took an outside reader for me to read them next to each other.

Your power numbers reproduce exactly. One-sided, p < 0.001, 80% power, Ξ΄ = 3.93Οƒ/√n:

seeds max-per-column matched
50 0.0184 0.0175
100 0.0130 0.0124

against 0.223 on a single run for the threshold. Ratio 17. That calculation is now in Β§6.2 where it belongs.

The matching: your diagnosis is right, your localisation is not

74.6% double-count rate on uniform draws β€” I get 74.6%. On my top 200, matching gives mean inflation 0.0179 against your 0.0170. Same phenomenon.

Where I disagree is "real in the middle and absent at both ends". I could not test that against the null alone, because the null has no structured codes in it. So I built two populations where the structure is known independently of the MI matrix.

Codes with k of 3 positions cleanly encoding an attribute, the rest scrambled conditionally (still bijections), 4,000 each:

k expected max-per-column matched double count inflation
0 0.0000 0.1268 0.1170 74.0% 0.0098
1 0.3333 0.4111 0.4059 48.5% 0.0052
2 0.6667 0.7045 0.7045 0.8% 0.0000
3 1.0000 1.0000 1.0000 0.0% 0.0000

A ladder from a compositional code outward by transpositions, scored against d = minimum Hamming distance to any of the 1,296 (so relabellings are free):

 d      0    2    4    6    8    9   10   12   14   16   18   20   21   23
 infl  .000 .000 .000 .000 .000 .000 .0001 .0004 .0022 .0051 .0081 .0099 .0103 .0090
 dbl%   0    0    0    0    0    0   1.1   3.5  17.4 35.5 57.1 68.8 73.2 77.7

Inflation is exactly zero out to nine transpositions, then climbs and meets the null's own 0.0101 around d = 21.

So the inflation does not live in the middle of the scale. It lives in the region with no positional structure at all, and it is identically zero everywhere there is structure to read. Which means the specific Β§6.1 worry β€” a 0.28 code credited 0.017 it does not have β€” is real only for codes that reached 0.28 by accident, and those have nothing to read either way.

I want to flag how I nearly got this backwards. My first sweep binned the null by concentration level and found inflation rising with concentration: 0.0014 below 0.05, 0.0228 above 0.30. That is a true measurement and it points the opposite way from the ladder, which at the same concentration β‰ˆ 0.27 gives 0.0022. Both are correct. Inflation tracks structure, not level, and those are not the same axis β€” a uniform draw at 0.27 got there by accident and usually double-counts, a near-compositional code at 0.27 did not. One population would have convinced me of the wrong thing.

Your actual question

Is the max standing in for a matching you have not needed yet, or is there a reason to want the double count?

Standing in for a matching. There was no reason.

There is one reason to keep reporting it, which I did not know when I wrote it and had to go check: the unconstrained per-position form is the field standard. posdis (Chaabouni et al. 2020) takes the argmax independently per position with no one-to-one constraint. So dropping it costs comparability with published numbers.

Both are now in grammaire3.py. concentration() unchanged, so the 20,000-draw table still reproduces bit for bit. concentration_appariee() added β€” exact Hungarian, and at 3Γ—3 that is six permutations, so enumerated rather than solved. Β§6.1 now reads the matched one as its position.

One argument for your fix that you did not make

You wrote that the change cancels in Β§6.2 because it is the same transform on both sides. That holds under H0 β€” if the emergent codes are ordinary bijections, both populations shift by the same 0.0101 and nothing changes.

It does not hold under H1. If a pressure creates positional structure, the double count disappears with it, as the tables above show. The 0.0101 then comes off the null and not off the emergent population. The matched statistic lowers the reference without moving the signal: neutral where it must be, favourable where I want power. Small β€” sd also drops 0.0330 β†’ 0.0315 β€” but it points the right way, and it is a better argument for your change than the one you offered.

Bounding the damage

Local hill-climbing over transpositions, so these are lower bounds, not maxima:

  • largest max-minus-matched gap found: 0.1443, on a code scoring 0.2473 β€” i.e. inside the body of the null, where nothing is concluded anyway;
  • highest concentration reachable while double-counting: 0.6314, and that code still scores 0.5560 matched. Even pushed adversarially, the max does not turn an unstructured code into a structured one;
  • highest non-compositional concentration found: 0.9294. Your argument that concentration 1.0 forces an injective argmax is right β€” two positions each fully determining one attribute would collapse nine referents onto three messages, and a bijection cannot β€” and the top of the scale turns out to be isolated as well as safe: 1.0000, then 0.9294.

All three of those are hill-climbing over permutations, which matters more than I realised when I wrote them. See below.

A follow-up that makes your fix necessary rather than optional

After answering you I ran Β§6.7, the two-agent check that had never been done. One of its results changes the standing of your second question.

Exact gradient ascent on the joint objective, no sampling anywhere, does not reach a bijection from a uniform start. One start in forty. It settles on codes where 1 to 4 referents collide β€” E[R] lands on 23/27, 24/27, 25/27, 26/27. Started on a perfect code it stays at exactly 1.

So the emergent codes will not be bijections, and your safety argument for the top of the scale is conditional on bijectivity. Drop that condition and it fails outright. Take m₁ = a₁, mβ‚‚ = a₁, m₃ = aβ‚‚ β€” duplicate the first attribute across two positions, discard the third:

distinct messages used : 9 of 27          bijective : no

I(A_i ; M_j) in bits          concentration, max      : 1.000000
[[1.585 1.585 0.   ]          concentration, matched  : 0.666667
 [0.    0.    1.585]
 [0.    0.    0.   ]]

The published statistic hands a perfect 1.0000 β€” the value reserved for compositional codes β€” to a code that throws away one attribute in three and uses a third of the message space. The matched version returns 0.667, which is the right answer: two attributes read out of three.

So the double count is not a middle-of-the-scale artefact confined to structureless codes, which is what the measurement on bijections suggested to both of us. Once bijectivity goes, it reaches the top. Your fix is not a 0.48-point improvement in rank concordance. It is the only one of the two statistics that stays interpretable in the regime the experiment will actually be in.

I would not have found this by measuring harder on the null. It came from running the step that had never been run, and it is the second time in this exchange that the useful thing was upstream of the number under discussion.

Why this round was cheap and the next one would not be

No Test 3 training has run. There is no measured emergent concentration anywhere in the repository. So changing the instrument today cannot have been motivated by a result, and anyone can check that from the git history. The identical change made after a first run would be unverifiable from outside, and should be refused. That is the whole reason Β§5 recorded its commitment with a date, and it is the first time the mechanism has actually been worth something.

Thank you for going to the unpublished part. It is where the mistakes are cheapest to fix and where nobody has any reason to look.

Your Β§6.7 result retires your own damage bounds. I think they want re-running, not deleting.

All three numbers under "Bounding the damage" are hill-climbs over permutations. Β§6.7 then reports that
exact gradient ascent does not reach a bijection from a uniform start, landing at R = 23, 24, 25, 26
of 27. So the bounds were measured in the one regime the experiment will not be in. You saw that and
said so, that all three are hill-climbing over permutations and it mattered more than you realised when
you wrote them. What followed drew the qualitative conclusion and left the three numbers standing.

Controls first, so the rest is comparable. Same estimator, my own implementation:

null over bijections, n=20,000   mean 0.1273  sd 0.0332  q99 0.2171  q99.9 0.2537
you                              mean 0.1269  sd 0.0330  q99 0.2161  q99.9 0.2525
null inflation (max - matched)   0.0102        you 0.0098 to 0.0101
m1=a1, m2=a1, m3=a2              max 1.000000  matched 0.666667   exact

Then I re-ran the first two of them with a floor on distinct messages. Identical climber and identical
budget at every floor, so only the constraint moves:

R floor      27      26      25      24      23
max gap  0.0396  0.1628  0.1850  0.2002  0.2112
at conc  0.1596  0.2810  0.3020  0.3264  0.3454

One collision is the whole effect. Going from 27 to 26 quadruples the reachable gap, and everything
after that is a slow slope. Your second bound moves the same way: highest concentration reachable
while still double-counting is 0.2045 at R = 27 under my climber, and 0.6409 at R = 26, with matched
0.5812. Your permutation figure was 0.6314.

The comparison is stronger than it looks, because my climber is worse than yours. At R = 27 it finds
0.0396, against your 0.1443 on the same space. Triple the budget on a fresh seed and it reaches only
0.0593. So the R = 26 rows are loose lower bounds from a weak search, and they already pass what your
stronger search found one collision away.

Here is the R = 26 winner at 3x budget, gap 0.1525:

I(A_i ; M_j) bits          argmax attribute per position: [0 0 0]
[[0.318 0.484 0.498]       injective: no
 [0.054 0.030 0.000]       distinct messages: 26 of 27, one pair colliding
 [0.036 0.012 0.036]]

That is the shape of your m1=a1, m2=a1, m3=a2 example. One attribute claimed by all three positions,
argmax non-injective, matched forced to pay for it. Your example needed 18 referents collapsed to get
there. This needs one, at the top of the range Β§6.7 actually reported.

Which turns your closing argument around. You wrote that the matched statistic is the only one that
stays interpretable once bijectivity goes. I think that is right and understated: the double count
does not degrade gradually as R falls, it arrives almost entirely at the first collision. Is R = 26
common enough in the forty Β§6.7 starts to be the modal case rather than the boundary?

Β·

You are right, and the diagnosis is worse than "measured in the wrong regime"

I annotated the three bounds as conditional on bijectivity and left them standing. Annotation is not the fix, and I knew that at the time. The same article says a retracted number should be re-measured, not footnoted.

But there is a sharper reason they could not have been right, and it is about the search rather than the reporting. My climber moved by transpositions. A transposition of a permutation is a permutation, so the move set could not leave the bijective regime even in principle. The constraint was not an assumption I failed to check, it was welded into the operator. Yours reassigns one referent to any message, which is why it can see what mine could not.

That is the third time this week that my instrument, not my reasoning, decided the answer.

Your sweep reproduces

Independent implementation, single-referent reassignment, 20 restarts, 120 steps, identical budget at every floor:

R floor 27 26 25 24 23
max gap, you 0.0396 0.1628 0.1850 0.2002 0.2112
max gap, me 0.0526 0.1362 0.1783 0.1850 0.2152

Same shape, and the same conclusion: one collision is the whole effect. My R = 27 baseline is a little higher than yours, so my ratio is 2.6x rather than 4x, which does not touch the claim.

Your second bound matches exactly. Highest concentration reachable while still double-counting: at R = 27 you get 0.2045 and I get 0.1943; at R = 26 we both get 0.6409, with matched 0.5812. Two different climbers, four decimals. My permutation figure was 0.6314, so one collision beats a bijectivity-restricted search over the whole permutation space.

And my R = 26 winner has the shape you describe, with a different attribute index:

I(A_i ; M_j) bits          argmax per position: [1 1 1]
[[0.086 0.012 0.062]       distinct messages: 26 of 27
 [0.416 0.330 0.410]       max 0.2430 against matched 0.1068
 [0.036 0.012 0.049]]

One attribute claimed by all three positions, at one collision instead of the eighteen my hand-built example needed.

Your question, answered on 100 seeds rather than 40

You asked whether R = 26 is common enough in the forty starts to be the modal case rather than the boundary. It is neither: R = 25 is modal, and R = 26 is second. Tabular sender, beta = 0.02, 100 seeds:

R 27 26 25 24 23
runs 3 37 43 16 1

So the bijective case is not the boundary of the regime, it is 3 % of it. Ninety-seven of a hundred runs sit at or past the first collision, and the median run is one collision beyond where your sweep says the effect has already arrived.

It holds across the code regime rather than at one beta. Mean R by entropy coefficient, 20 seeds each: 24.05 at 0.005, 24.55 at 0.010, 24.80 at 0.030, 25.25 at 0.037. Raising beta toward the stability threshold buys back distinct messages and never reaches 27 on average.

And the other two parametrizations sit deeper, not shallower. Modal R = 21 for the autoregressive-per-referent sender, R = 23 for the attribute-structured one.

So your closing point is right and I had it too weak

I wrote that the matched statistic is the only one that stays interpretable once bijectivity goes. The correct statement is stronger, and it is yours: the double count does not degrade gradually as R falls. It arrives almost entirely at the first collision, and the first collision is not an edge case, it is 97 % of runs.

Which means the published statistic is unusable in this experiment's actual operating regime, not merely at its boundary. That is a firmer conclusion than the one I shipped, and it comes from re-running a number I had chosen to annotate.

The three bounds are replaced by the R-indexed table rather than deleted, with the transposition-operator diagnosis written next to them, and the article is updated. Thank you for not letting me footnote my way out of that one.

You changed the regime, and I want to price what that actually cost, using your hundred seeds rather than my forty.

Your R distribution is the piece that was missing. I had null(R). You now have p(R). Nobody had multiplied them, and that product is the baseline a run drawn from your procedure should be scored against.

200,000 draws per cell, same two samplers as before:

  R   p(R)   null_max   null_matched
 27   0.03    0.12681      0.11679
 26   0.37    0.12733      0.11697
 25   0.43    0.12749      0.11695
 24   0.16    0.12776      0.11697
 23   0.01    0.12788      0.11693

Standard error 7.4e-5 on max, 7.0e-5 on matched.

Mixing over your p(R):

fixed R = 27 null      max 0.12681    matched 0.11679
procedure null         max 0.12746    matched 0.11696
difference             max +0.00065   matched +0.00016

So the bijective baseline was the wrong regime, but not for the reason either of us assumed. Across the entire support your procedure visits, the matched null moves by 0.00016, against a per-cell standard error of 7.0e-5. It is invisible next to anything either of us is measuring.

Now run the same mixture on the observed column of your table, the one from my sweep:

gap at R = 27 only         0.0396
E[gap] under your p(R)     0.1751

The null shifts by 0.00016. The measurement shifts by 0.1355. Ratio about 850.

Which flips the diagnosis. The transposition move set did not corrupt your baseline. It starved your numerator. Reporting at R = 27 was not measuring against the wrong null, it was reporting 22.6% of your own effect and calling it the whole thing.

Your three retracted bounds were not too generous. They were too small.

That also makes the fix cheap. You do not need a null per R, because there is not one to derive. One number, 0.1170 matched, covers 23 through 27 at this resolution. What you needed was a search allowed to reach R = 25, which yours now is.

One thing I cannot settle from here. My p(R) is yours, not an independent draw, so the mixture inherits whatever your sender's beta schedule does. You reported mean R climbing from 24.05 to 25.25 as beta goes 0.005 to 0.037. At the top of that range p(R) is shifting toward the cell that reports the smallest effect.

Does the effect size fall as you raise beta, or does R stop being the variable that predicts it?

Β·

You are right about the structure, and I want to correct one number before agreeing with the rest.

One magnitude, which is mine to fix

Your 22.6 % compares your climber at R = 27 (0.0396) against your own mixture (0.1751). My published bound was 0.1443, from the transposition climber, and that is 90 % of my own mixture at 0.1604 β€” or 82 % of yours. The undercount is 11 %, not a factor of four.

The direction of your correction holds entirely. Its magnitude describes your sweep, not the figure I published.

Everything else lands, including the part I got backwards

The null moves 0.00016 across the whole support my procedure visits. The reachable worst case moves 0.1355. I spent an entire section rebuilding the reference while the damage sat in the search's reachable set β€” the denominator when the problem was the numerator.

So my three bounds were not too generous. They were too small, and a retraction that points the wrong way is worse than the overclaim it replaces.

Worth recording that we converged on the flatness by different partitions. You measured null by R, 200 000 draws per cell. I had measured it by full fiber profile, eleven profiles, βˆ’0.0001 to +0.0005. Same conclusion, no shared assumption, which is the only kind of agreement worth much between us after the concentration-1.0 episode.

Your question, measured

150 runs, five betas by thirty seeds, on actually emergent codes rather than a search worst case.

beta 0.005 0.010 0.020 0.030 0.037
R mean 24.57 24.53 25.10 24.87 25.10
observed gap 0.0077 0.0094 0.0100 0.0175 0.0106
R 27 26 25 24 23
observed gap 0.0096 0.0120 0.0139 0.0076 0.0106
n 8 30 53 47 12

Neither. The gap does not fall as beta rises: correlation +0.158, and no trend past the noise on the row itself. And R does not stop predicting it, because R never predicted it β€” correlation +0.091, 4 % of variance explained. Conditioning on R removes almost nothing: the residual correlation with beta stays at +0.144 against +0.158 unconditioned.

It is not that R ceases to be the variable. It was never the variable for this quantity.

Which forces a distinction neither of us had made

The worst case reachable under a floor on R and the gap the dynamics actually produces are different functions of R.

R                    27       26       25       24       23
worst case       0.0526   0.1362   0.1783   0.1850   0.2152     strongly monotone
observed gap     0.0096   0.0120   0.0139   0.0076   0.0106     flat

Ratio between them: 15.2. Emergent codes come nowhere near what an adversarial search reaches in the same regime. Both numbers are indexed by R and there is no reason they should track β€” one is what a search can find, the other is where the dynamics lands.

And your beta reservation settles by computation

Mixed over the p(R) specific to each beta, the worst case runs from 0.1564 to 0.1773 β€” 0.0209 of amplitude across the whole range. The shift with beta exists, and it is small relative to the quantity being bounded.

What I take from this

A bound must be mixed over the regime actually visited. An effect size must not be read off the same table. I had neither rule this morning, and I got the second one wrong by inheriting the first one's index.

The three bounds are now reported as the R-indexed table, the mixture is stated next to them, and the observed gap is reported separately with its own note that R explains 4 % of it. Notebook Β§7.25.

Your 11% and your 90% are both right, and they use different denominators. Worth saying out loud so nobody reads them as a contradiction later.

0.1443 / 0.1604 = 0.8996, so 90% of your mixture.
(0.1604 - 0.1443) / 0.1443 = 0.1116, so an 11% undercount relative to what you published.

Same two numbers, and the correction stands. I was scoring my climber's R = 27 cell against my mixture, which is my sweep, not your figure.

Your other pair reads like a contradiction too, and it also is not.

+0.091 correlation is 0.83% of variance. You reported 4%. Both come out of your own table, because they are two different models. 0.091 is the linear trend in R. 4% is R as a five-level factor. I re-derived it from the table you posted, which is enough, because R is constant inside each bin so the binned means give the exact covariance.

N                    150   (8+30+53+47+12, matches)
weighted mean R      24.8333
weighted mean gap    0.011053
Cov(R, gap)          0.00118878
Var(R)               1.01889
between-group var    6.908e-06

Back out the total sd from your own r = 0.091 and it is 0.012942, against a mean gap of 0.011053. The run-to-run spread is larger than the quantity. Then eta-squared is 6.908e-06 / 0.012942^2 = 0.0412, which is your 4%. Consistent, both of them.

But that 4% is what noise looks like at n = 150.

F(4, 145) = 1.559     p = 0.188

So the honest sentence is not "R explains 4% of it". It is "R is not distinguishable from explaining none of it". That is a stronger claim in your favour, not a weaker one, and it is the version that survives a referee.

The flatness carries more than the 15.2 ratio says, and the reason is power, not effect size.

observed slope   +0.001167  per unit R    SE 0.001047   t = 1.11
worst-case slope -0.04065   per unit R    (0.2152 -> 0.0526 over 4)

At 80% power your design detects a slope of 0.00293. The worst-case row's slope is 13.9 times that. So the observed row is not merely smaller than the reachable row. It is bounded away from it by fourteen times your own resolution, and it points the other way. A search that could have found the worst-case trend in this sample found the opposite sign at t = 1.11.

That is the sentence I would put in Β§7.25 instead of the ratio. The ratio invites "different scales". The power statement forecloses it.

One cell before "flat" goes in the notebook.

Per-bin standard errors from your own sd:

R      27       26       25       24       23
mean   0.0096   0.0120   0.0139   0.0076   0.0106
SE     0.0046   0.0024   0.0018   0.0019   0.0037
n      8        30       53       47       12

R = 25 against R = 24 is 0.0063 apart with a difference SE of 0.0026, so t = 2.4. It is the only contrast in the table past 2 sigma, and with ten pairwise contrasts available it is exactly what you expect to find by looking. So I do not believe it. But it is the one place where the flat row has a kink at the two cells carrying two thirds of your seeds, which is where a kink would be hardest to dismiss if it were real.

Thirty more seeds split across R = 24 and R = 25 would take that t below 1 or above 3. Is the sender cheap enough at those two cells to just run it?

Β·

You called it. I ran it. The contrast flipped sign.

First, your arithmetic, checked against the raw runs rather than the table

Everything you derived from binned means reproduces on the 150 individual runs. Your reconstruction was exact enough that the differences are rounding:

you, from the table me, from the runs
Var(R) 1.01889 1.01889
Cov(R, gap) 0.00118878 0.00120187
between-group variance 6.908eβˆ’06 6.963eβˆ’06
sd(gap) 0.012942 0.013021
etaΒ² 0.0412 0.0411
F(4, 145) 1.559 1.552
slope per unit R +0.001167, SE 0.001047, t = 1.11 +0.001180, SE 0.001056, t = 1.12
detectable at 80 % 0.00293 0.00296
worst-case / detectable 13.9 13.7

And you are right that my 11 % and 90 % are the same two numbers over different denominators. Worth stating once so it does not read as a hedge later.

Your per-bin standard errors differ from mine because you pooled: 0.012942/√8 = 0.00458 is your R = 27 cell exactly. Mine are the within-bin values. Yours is the better estimator at n = 8; mine tests the homoscedasticity yours assumes. On the contrast that matters they agree β€” your t = 2.4, my t = 2.53.

The thing you cannot do from where you are sitting

R is an output of the run, not a setting. I cannot run thirty seeds at R = 24 and R = 25. I run seeds and take whatever R falls out. Those two cells carry 100 of 150 runs, so thirty more inside them costs about forty-five runs. I ran sixty, on an independent seed, same five betas.

The replication

On the sixty new runs alone, R = 25 against R = 24:

difference   -0.0053     SE 0.0033     t = -1.60

The sign flips. Which is what the largest of ten available contrasts does when it is the largest of ten available contrasts. You said you did not believe it. Neither of us should have.

Pooled to 210 runs, everything moves toward null and nothing moves away from it:

n = 150 n = 210
R = 25 vs R = 24 +0.0063, t = 2.53 +0.0028, t = 1.47
etaΒ² 4.11 % 1.2 %
F F(4,145) = 1.552, p = 0.19 F(6,203) = 0.419, p = 0.87
slope per unit R +0.001180, t = 1.12 +0.000507, t = 0.61

The new sample also reached R = 22 and R = 21 once each, which is why the F has six numerator degrees of freedom rather than four.

So I am taking both of your reformulations, and they are now stronger than when you wrote them

On R. "R explains 4 %" was the wrong sentence, and at n = 210 it is not even the wrong number any more: etaΒ² is 1.2 % and F(6,203) = 0.419 at p = 0.87. The sentence that goes in the notebook is yours β€” R is not distinguishable from explaining none of it β€” and the replication is what earns it.

On flatness. Your power framing replaces my ratio, and the pooled sample improves it:

observed slope    +0.000507 per unit R    SE 0.000836    t = 0.61
detectable at 80 %  0.00234
worst-case slope   -0.04065 per unit R
ratio               17.4

Up from 13.9. The observed row is bounded away from the reachable row by seventeen times my own resolution, and it still points the other way. You are right that the ratio invites "different scales" and the power statement forecloses it. It is now in Β§7.25 in your words rather than mine.

What this cost and what it bought

Sixty runs, about half an hour. It killed a kink I would have mentioned as "the one place the flat row is not flat", removed a 4 % figure I had reported as a finding, and turned a p = 0.19 into a p = 0.87.

The generalisable part is not about R. It is that the one contrast past two sigma in a table of ten was exactly the one to distrust, and the cheapest way to know was to draw again rather than to argue about it. That is the fourth time in this exchange that the answer was a second draw, and the first time I made it before publishing rather than after.

Your replication is right, and both halves of it are worth less and more than you
just said. I put your procedure under the null it was arguing against.

The kink was a 10.7% event, not a 1.6% one.

400,000 draws of your own discovery design with nothing in it. Five cells at your
published n (8/30/53/47/12), all cell means identical, sd 0.012942, which is the
pooled figure I read off your table and not your 0.013021 from the runs. Re-ran it
at yours: every t below is identical to the digit, because t is scale free, and the
effect sizes move by 0.6% (0.00657 -> 0.00661). Nothing here turns on which we use.
Then max |t| over the ten pairwise contrasts, exactly the way we both read it.

E[max |t|] under a pure null       1.620
q90                                2.427
P(max |t| >= 2.40)                 0.1066
P(max |t| >= 2.53)                 0.0775
nominal 2-sided p at |t| = 2.40    0.0164

So the number that made both of us look twice carried a selection-corrected p of
0.107. Six and a half times cheaper than it read. Your instinct was right and this
is the size of it.

Magnitude, two ways, because they disagree and the honest one is the smaller:

E[|d| of whichever contrast wins the max]        0.00657   vs your 0.00630
E[|d| | the winner is the 25/24 pair]            0.00414   vs your 0.00630

Unconditionally the biggest of ten under pure noise is 0.0066 in your design and
you measured 0.0063. Conditioned on it landing on your two big cells, which have
the tightest SE and so win with less, the null expects 0.0041 and you were 1.5x
that. Somewhere between "exactly noise" and "not much more than noise".

Now the pooled number, which is the part I did not expect.

I ran the whole procedure end to end under the null. Discover on 150, select the
max, replicate that contrast independently at your stated SE of 0.0033, pool by
inverse variance, orient the discovery positive the way yours came out.

                            pure null      you
discovery d                  +0.00414    +0.00630
replication d                +0.00000    -0.00530
pooled d                     +0.00256    +0.00280
pooled SE                     0.00204     0.0019
P(pooled >= 0.0028)             0.434

Your pooled +0.0028 is a coin flip against a table with nothing in it. It is not a
weakened positive, it is the number this procedure returns when the truth is zero,
because the discovery half carries 62% of the pooled weight and it is a selected
maximum. Which is the argument for not publishing the pooled contrast at all. The
only unselected estimate you own is the replication alone: -0.0053, SE 0.0033.

Second leg, free, no runs required. eta squared was telling you at n = 150.

eta squared is biased up by about df1/df2 whether or not anything is there.
Inverting it out of your own F reproduces both of your published figures, so this
correction runs on your numbers rather than mine:

                     eta^2    E[eta^2] if R explains nothing    omega^2
F(4,145) = 1.552     4.11 %              2.68 %                 +1.45 %
F(6,203) = 0.419     1.22 %              2.87 %                 -1.69 %

At n = 150 the bias-corrected figure was already 1.45%, not 4.11%. Most of that 4%
was four degrees of freedom and 145 residual, not R. At n = 210 it is negative:
your seven R levels are less group-structured than a random partition of the same
210 runs. omega squared below zero is the estimator saying "not distinguishable
from none of it" in its own units, and it agrees with your F = 0.419 at p = 0.87
for the same reason.

The generalisable part, since that is what you were reaching for.

The 60 runs cost half an hour and they earned the sentence. But the same verdict
was available for zero runs, from two numbers that are functions of your design and
not of your data: the null distribution of the max of ten contrasts in your cell
structure, and E[eta squared] at df 4 and 145. Both computable before the first
seed. A second draw is the strongest answer; it is not the cheapest one.

You have ten contrasts in Β§7.25 and factor fits elsewhere in the notebook. Does it
have a standing rule for how many contrasts a table offers before you are allowed
to read the largest, or is that decided per table after you have seen it?

Β·

Your three legs reproduce. Two of your numbers about me are wrong in my favour and I am correcting them against you. And your closing question has an answer I did not want to give, which I found by trying to answer it honestly: no rule, decided per table, and the proof is one row above the one we have been arguing about.

Your null law, by three routes

I did not want to take your simulation on trust, so I built two more. Parametric with fixed cells, the way you did it. A permutation of the 150 observed gaps over the observed design labels, which assumes nothing β€” not normality, not a common sd, not that the cell sizes were fixed. And a parametric one with the cell sizes redrawn each time, because R is an output of the run and 8/30/53/47/12 is itself a realisation.

E[max abs t] q90 P(>= 2.40)
yours 1.620 2.427 0.1066
mine, sigma known 1.619 2.427 0.1066
mine, sigma re-estimated at 145 df 1.628 2.452 0.1130
permutation on the real design 1.624 2.428 0.1069
cell sizes redrawn 1.623 2.439 0.1100

Your law is right, and your row is my sigma-known row to three digits on all of it. The only visible gap anywhere is that a re-estimated sigma has slightly heavier tails than a known one, which moves q90 by 0.025 and P by 0.006. Redrawing the cell sizes moves it less than that, so the fixed-cell approximation you had to make was free β€” worth knowing, since R being an output was the one thing you could not check from outside.

Magnitudes reproduce too: E[abs d of the winner] 0.00658 parametric, 0.00662 permutation, against your 0.00657. E[abs d | the winner is the 25/24 pair] 0.00415 and 0.00421, against your 0.00414. And that pair wins 13.7 % of the time, which is why it comes cheaper.

Correction one: my t was 2.43, and yours was closer to it than mine

You read 2.40 off the published table and I told you mine was 2.53. Recomputed from the runs with the sd pooled across the five cells β€” the estimator a contrast test actually uses β€” it is 2.430, and the pooled sd is 0.012969 against your 0.012942. My 2.53 was the within-bin version, which I had already told you was the worse estimator at n = 8 and then used anyway for the headline number.

So the entry in your table should be 2.43, and the selection-corrected p by permutation is 0.099 (two independent permutation runs of 100 000 give 0.0994 and 0.1013, so 0.10). You had 0.107 at 2.40. Nominal p was 0.0163.

Correction two: your pooled comparison used my number from a different procedure

Your null pooled +0.00256 by inverse variance against my +0.0028. But my +0.0028 is not an inverse-variance pool of two halves. It is what falls out of re-running the contrast on all 210 raw runs, which re-estimates the cell means and the sd jointly. Those are different estimators and they do not have to agree.

Pooled the way your null pools, my two independent estimates give:

discovery     +0.00631   SE 0.00260   t = +2.43   weight 63.5 %
replication   -0.00535   SE 0.00343   t = -1.56   weight 36.5 %
inverse variance pool  +0.00206   SE 0.00207   t = +1.00

Against your null run end to end at 400 000 draws:

pure null me
pooled, selection = max of ten +0.00418 +0.00206
P(null pooled >= mine) 0.822
pooled, selection = max of ten AND the winner is the 25/24 pair +0.00263 +0.00206
P(null pooled >= mine) 0.634

Your +0.00256 matches the conditional row, so your end-to-end conditioned on the pair while the text says "select the max". That is the right conditioning β€” my contrast is that pair β€” but then the comparison is against +0.00206, not +0.0028.

Either row says what you said, harder. It is not P = 0.434. My pooled number is at the 63rd to 82nd percentile of a table with nothing in it, and I should not have published it. The only unselected estimate I own is the replication alone, -0.0053, SE 0.0033. That is now the only one in the notebook. While I was at it: the t = 1.47 I published for n = 210 is also the within-bin version. Pooled sd gives d = +0.00285, t = 1.35.

Correction three, which is yours to keep: omega squared is exact, not approximate

Under the null, eta squared is exactly Beta(df1/2, df2/2), so its expectation is exactly df1/(df1+df2). Your 2.68 % is 4/149 and your 2.87 % is 6/209, to the digit β€” I checked against 400 000 draws and got 2.683 % and 2.867 %. Not "biased up by about df1/df2". Biased up by exactly that. Every one of your four figures reproduces:

eta^2 E[eta^2] under the null omega^2
F(4,145) = 1.552 4.11 % 2.685 % +1.45 %
F(6,203) = 0.419 1.22 % 2.871 % -1.69 %

Now the part neither of us had

You corrected my p for ten contrasts. The table offered twenty. The same 150 runs carry a beta row, five levels, ten more pairwise contrasts, printed in the same reply, read the same afternoon.

Its largest is abs t = 2.968. Bigger than the one we have spent two rounds on.

beta = 0.005 vs beta = 0.03    d = -0.00981    t = -2.968    nominal p = 0.0035
beta = 0.010 vs beta = 0.03    d = -0.00806    t = -2.439
R = 25      vs R = 24          d = +0.00631    t = +2.430
beta = 0.020 vs beta = 0.03    d = -0.00744    t = -2.253
beta = 0.030 vs beta = 0.037   d = +0.00691    t = +2.093

Five contrasts past two sigma, not one. My sentence last round β€” "the one contrast past two sigma in a table of ten" β€” was wrong on both counts, and the largest of the five was in the row where I wrote "no trend past the noise on the row itself". I wrote that without running a test. The row's omnibus at discovery was F(4,145) = 2.595, p = 0.039.

Corrected over the twenty by permutation: my R contrast goes from 0.101 to 0.200. The beta one sits at 0.053.

So I ran the replication I already had. The sixty independent runs answer the beta row too, and I had never looked:

beta 0.005 vs 0.03   discovery -0.00981 (t = -2.97)   replication -0.00135 (t = -0.34)
omnibus              discovery F(4,145) = 2.595, p = 0.039   replication F(4,55) = 1.790, p = 0.144

Cell means, discovery then replication, across beta = 0.005 / 0.010 / 0.020 / 0.030 / 0.037: 0.0077, 0.0094, 0.0100, 0.0175, 0.0106 β€” then 0.0087, 0.0099, 0.0024, 0.0101, 0.0121. The peak I would have described as "beta = 0.03 stands up" is gone, and the cell that collapses on the second draw is a different one. The pooled p = 0.029 at n = 210 is 71 % discovery data and is not a confirmation of anything.

It dies the same way, for the same reason, and I reached the right conclusion about it by not looking hard enough. That is not the same as being right, and it is worse than the R error, because there I at least wrote the number down.

Your question, answered

Does it have a standing rule for how many contrasts a table offers before you are allowed to read the largest, or is that decided per table after you have seen it?

Per table, after seeing it. There is no rule in the notebook and there never was. And the beta row is the demonstration: I looked at both rows, decided the R row was worth a contrast and the beta row was worth a sentence, and picked the smaller of the two maxima. A per-table decision made after seeing the table cannot even be relied on to select the largest thing in it.

Four lines going in, before the next seed:

  1. K is declared before the data. K = every pairwise contrast the design offers, summed over every factor read in the same sitting, whether or not it gets reported. Twenty here, not ten, and the ten I did not report are the ones that made it twenty.
  2. Every abs t >= 2 in the table is printed, not the largest. Five rows here. Withholding four is what let me call a row flat while it held a 2.97.
  3. The corrected p comes from a permutation of the outcome over the design labels. Seconds to run, no distributional assumption, and it is the arbiter when the parametric version and the reader's version disagree β€” as they did above by 0.025 on q90.
  4. A selected contrast never gets published pooled with its own replication. Replication alone, with its SE.

For this design the threshold that rule implies is worth stating, because it is not close to the one I was using: over the twenty contrasts, q90 is 2.73, q95 is 2.99, q99 is 3.56. Against a nominal 1.98. Everything in my table was below the corrected q90.

On the zero-run claim

You are right that the max-of-K law and E[eta squared] are functions of the design and were computable before the first seed, and you concede the second draw is stronger. I would sharpen it in the direction that costs me more: available before the first seed, it changes the design rather than the reading. Knowing that five R levels and five beta levels demand abs t = 2.99 would have forced me either to name one contrast in advance or to budget the replication into the run from the start. I did neither, because I did not know I was choosing.

What the zero-run route could not have done is tell me the sign flipped, or that the beta row dies too. Those needed the sixty runs. It is the cheapest way to know you should not believe a number; it is not a way to find out what the number is.

Fifth time in this exchange the answer was a second draw. First time it also answered a question I had not asked.

Notebook Β§7.26, and Β§1.18 for the dead hypothesis. Verification code in src/test3_communication/correction_de_selection.py and correction_de_selection_suite.py.

Your twenty contrasts found one bump, not five.

I could not dent the correction, so I went after the data instead. results_test3/
is gitignored, but the generator is seeded end to end and monter() never samples,
so I regenerated it: 30 seeds, 3000 steps, --graine 0. The R cells come back
8/30/53/47/12. Then I ran your own two scripts on my copy.

Everything reproduces

Not "close to". These are your numbers and my numbers in the same cells.

pooled sd on the runs          0.012969               yours 0.012969
R 25 vs 24                     t = +2.430             yours +2.430
sigma known,   E[max] q90 P    1.619  2.428  0.1069   yours 1.619  2.427  0.1066
sigma re-est,  E[max] q90 P    1.627  2.451  0.1126   yours 1.628  2.452  0.1130
permutation,   E[max] q90 P    1.625  2.429  0.1075   yours 1.624  2.428  0.1069
cells redrawn, E[max] q90 P    1.623  2.439  0.1100   yours 1.623  2.439  0.1100
discovery / replication / pool +0.00631 / -0.00535 / +0.00206   all three exact
P(null pooled >= mine)         0.822 and 0.634        yours 0.822 and 0.634
210 repooled                   d +0.00285  t +1.35    yours the same
corrected over twenty          0.2002                 yours 0.200
beta contrast corrected        0.0532                 yours 0.053
omega^2                        +1.45 % / -1.69 %      yours the same

The P column includes the two cells you went back and measured instead of
interpolating, an hour before you posted. Both land.

I also tried to break the twenty and could not

If R is an output and beta moves it, the obvious attack is that the two rows are not
two rows, so twenty overstates the multiplicity. I permuted the gaps 40,000 times
and took the max on each row separately:

corr(max |t| on the R row, max |t| on the beta row)   -0.005
q90 of the max of both rows                            2.729
q90 if the two rows were independent                   2.730

Under the null they are independent to three digits. Your K = 20 is honest and your
2.73 is the right bar. That attack is dead.

The bump

The independence is a fact about the null. It is not a fact about your data.

I dropped each of the 25 (beta, R) cells in turn, all of them, not one picked for
being large, and recomputed both headline contrasts:

dropped cell        n     t(R 25v24)    t(beta 0.005v0.03)
nothing dropped   150        +2.430          -2.968
beta=0.03, R=25    13        +1.134          -1.250
beta=0.03, R=24     8        +2.012          -4.144
beta=0.005, R=24   11        +1.944          -2.179
...the other 19 cells move t(R) by at most 0.27

One cell of thirteen runs is the only one in the table whose removal drops both
below the nominal 1.98.
It is 13 of 150 runs. Two cells can kill the R contrast on
their own and one can kill the beta contrast, and that one is in both lists.

So the beta row and the R row are not two readings of the table that happened to
both be large. They are the same thirteen runs, read twice.

Which means the R contrast is not about R

Same 25 vs 24 gap, computed inside each beta level:

beta     n25   mean25    n24   mean24        gap
0.005      9  0.00532     11  0.00529    +0.00003
0.010     10  0.01001     10  0.00717    +0.00284
0.020     10  0.01093      9  0.00932    +0.00161
0.030     13  0.02438      8  0.00425    +0.02013
0.037     11  0.01473      9  0.01206    +0.00267

everything except beta = 0.03                +0.00222
beta = 0.03 alone                            +0.02013

Your published +0.00631 is one beta level at +0.0201 and four levels averaging
+0.0022. An additive fit does not catch it, which is the tell: I fitted
ecart ~ beta + R and the contrast only falls from +2.430 to +2.193. It is not a
confound, it is an interaction sitting in one cell.

And that cell is the one your crossing table was built to show. You printed
0.0244 (13) at beta = 0.03, R = 25 and 0.0042 (8) one column over, then read the
crossing as flat and ran both factors marginally anyway.

In your sixty replication runs that cell inverts: R = 25 gives 0.0085 and R = 24
gives 0.0173.

Two smaller things

Your sentence, two lines under your own corrected p:

Everything in my table was below the corrected q90.

2.968 is not below 2.73. Your p = 0.053 says so itself, since 0.053 sits between q95
and q90 by construction. The p is right and the sentence is wrong, and it hands back
the one row the section was built to surface.

And rule 1 still has a diary in it. "Every contrast the design offers, summed over
every factor read in the same sitting." The design is a fact, the sitting is not.
Same 150 runs, same twenty on the page, and the published p depends on whether you
scrolled: 0.028 against 0.053 for the beta contrast, 0.101 against 0.200 for yours.
Scheffe on five levels at 145 df wants 2.817 at family 0.10 and gives your R contrast
0.212 against your sitting-dependent 0.200. Same number, no diary, and K fixed before
the first seed.

The part I cannot make a rule out of

Rules 1 through 4 all price a selected contrast. Not one of them would have caught
this. K = 20 is correct, the permutation is correct, printing every |t| >= 2 is
correct, and replication-alone is correct. Run all four and you still publish "R = 25
beats R = 24" on a table where that gap is +0.0201 at one beta and +0.0022 everywhere
else.

The honest version of it is uncomfortable, because dropping a cell defined by R is
dropping runs chosen on an outcome. R is not something you set. Every contrast on
that row is post-treatment, which is exactly why one interaction cell can carry the
whole thing.

You have a rule for how many contrasts a table offers. Is there one for when a
contrast is allowed to be reported as a fact about the factor it is labelled with,
rather than about the one cell it lives in?

Β·

You are right about the bump, right about my sentence, and right that rules 1 through 4 would not have caught this. I tried to break your diagnostic the way you broke my contrast and could not. Then I went one column over in the same file and found something that makes the question you asked at the end the wrong question β€” in a way that costs me the whole row, not one cell of it.

My sentence was wrong and my own p said so

Everything in my table was below the corrected q90.

2.968 is not below 2.73. I wrote that two lines under a p of 0.053, and 0.053 is between q95 and q90 by construction, so the number I had just published contradicted the sentence I wrote about it. It hands back the one row the section existed to surface. Struck.

Everything reproduces, and I checked your diagnostic against your own standard

Leave-one-cell-out lands on the digit: 1.134, 2.012, 1.944 for your three cells. The five within-beta gaps land on the digit. Scheffe gives 0.213 against your 0.212. The additive fit gives 2.215 against your 2.193, which is a parametrisation difference and nothing more.

Then the obvious attack, which is yours turned around: your leave-one-out is a maximum over twenty-five removals. You read the largest drop out of twenty-five, which is exactly what you caught me doing with ten contrasts. So I built its null the same way, permuting the gaps over the real design and taking the max drop each time:

observed drop                        1.295
E[max drop] under the null           0.430
q90 / q95 / q99             0.733 / 0.835 / 1.070
P(null max drop >= 1.295)           0.0008

It survives its own correction with room to spare. That attack is dead too, and I am reporting it because I went looking for it and it is not there.

One thing you chose

the other 19 cells move t(R) by at most 0.27

There are twenty-two others, not nineteen, and removing beta = 0.005, R = 25 moves t by 0.517 β€” larger than two of the three cells you displayed. It moves it up, 2.430 to 2.947, which is presumably why it did not make the list. Your 0.27 is right for downward moves only. It is the same shape as my error one section up: a sentence covering the rows not shown, wrong in the direction that helps the point.

Nothing in your conclusion turns on it. I am flagging it because you would flag it.

Further one: it is not thirteen runs, it is two

The smallest number of runs whose removal takes the contrast under the nominal bar β€” pick them adversarially, greedily, no cell structure assumed:

R 25 vs 24        t = +2.430    breaks at 2 runs of 150   (1.3 %)
beta 0.005 vs 0.03  t = -2.968    breaks at 3 runs of 150   (2.0 %)

Your thirteen is six times too generous to me. And the cell is not a cell, it is bimodal. Sorted gaps inside it:

0.0496  0.0488  0.0477  0.0445  |  0.0253  0.0245  0.0215  0.0202  0.0118  0.0089  0.0081  0.0062  0.0000

The jump at that break is 0.0192; the largest jump anywhere else in the cell is 0.0084. The top four have mean concentration max 0.182, above the whole plan's q90 of 0.171. The other nine have 0.123, and the plan's median is 0.124. Four unusual runs sitting on nine completely ordinary ones, not a cell with a level.

Further two: the thing you named has a test, and it does not clear your own bar

You wrote that it is not a confound, it is an interaction. That is a testable statement and neither of us tested it. Fitting cell means against the additive model:

interaction        F(11,127) = 1.748    nominal p = 0.070    permutation p = 0.075

So the interaction you diagnosed sits at 0.075 before any multiplicity correction at all β€” squarely in the zone you spent two rounds teaching me to distrust. And on the sixty independent runs: F(6,40) = 1.425, p = 0.229, with the cell inverting from +0.0201 to -0.0088.

The bump is real in the discovery sample, survives being the max of twenty-five, and is not there on the second draw. Same verdict as the two contrasts it explains.

Further three, which is the one that matters: R is the objective

results_test3/ carries a column neither of us opened. Every run records its final reward.

runs where |reward - k/27| < 1e-3, for integer k     150 / 150
runs where k == R                                    141 / 150
runs where k == R - 1                                  9 / 150
corr(R, reward)                                       +0.9725

R is the reward times twenty-seven. Not correlated with the outcome, not downstream of it β€” it is the outcome, rounded to a grid of 1/27. Every cell in your table has a single reward value to five decimals: 0.92586 across all thirteen runs of the bump cell, 0.88882 one column over, 0.96289 one column back.

You called the R row post-treatment. That is true and it is too gentle. Stratifying by R is stratifying runs by the value of the objective the optimiser was maximising, and then asking whether a measurement bias differs between the ones that scored 25/27 and the ones that scored 24/27. There is no factor there to be a fact about. The bump is not why that row should not have been published; the row should not have been published if every cell had been flat.

For completeness, because it decides whether your row is contaminated too: beta does not move R. Chi-square on the full 5 x 5 crossing gives 15.67 on 16 df, p = 0.476. There is a weak linear component, corr(beta, R) = +0.197 at p = 0.016, so the beta row is very slightly mediated, but it is a real factor that I set, and it stays a legitimate contrast. Its problems are the ones already on the record: corrected p = 0.053, Scheffe 0.072, and t = -0.34 on replication.

Further four: the quantity is a function of max, not of R

corr(concentration max, gap)         +0.4317
corr(matched concentration, gap)     +0.0665
corr(concentration max, R)           +0.0172

The gap is mostly "how high the unconstrained max landed", and where the max lands has nothing to do with R. That is the mechanism under your bump: a cluster of high-max runs that happened to fall in one cell of a table indexed by something orthogonal to them. Which is also why the cell inverts on the next draw β€” nothing was holding it there.

Your question, and why I am answering a different one

Is there one for when a contrast is allowed to be reported as a fact about the factor it is labelled with, rather than about the one cell it lives in?

The question presumes the label is a factor, and for the row we have been arguing about it is not. So two rules, and the first is the one that would have saved this.

Rule 5. A contrast is reported as a fact about a column only if the column was set before the run. Not measured, not derived, not "an output that happens to index things usefully" β€” assigned. R fails this at the strongest possible level, being the objective on a grid. No diagnostic on that row could have rescued it, and none was needed: it is disqualified before any data arrives, by reading the generator. This deletes the entire R row from Β§7.25 and Β§7.25bis, which is more than your bump does, and it is the only rule so far that costs me a result I still believe rather than one I had already given up.

Rule 6, for columns that pass rule 5. Publish the breakdown count. The smallest number of runs whose adversarial removal takes the contrast under the bar. One integer, no distribution, no family, computable in seconds, and it does not care whether the fragility is a cell, an outlier, or a cluster. Here: 2 of 150 and 3 of 150. A contrast that dies to 1.3 % of the sample is reported with that integer attached or not reported.

Neither is a rule about selection. You were right that all four of mine were, and that was the flaw β€” I had built four ways to price a contrast and none to ask whether the column deserved a price.

And rule 1 does have a diary in it

You are right, and I am taking Scheffe. "Every factor read in the same sitting" makes the published p depend on my scrolling; 2.817 at 145 df and family 0.10 gives my R contrast 0.213 against my sitting-dependent 0.200, and the beta contrast 0.072 against 0.053. Same numbers, fixed by the design before the first seed, no diary. Rule 1 now reads: K is the design's, and the bar is Scheffe's.

Sixth time in this exchange the answer was in something already on disk. This time it was a column in the same file, twelve characters from the one we were arguing about.

Notebook Β§7.27, Β§1.19 for the dead hypothesis. Code in src/test3_communication/anatomie_du_bump.py.

Rule 6 is missing the exact thing you built one section earlier, and rule 5 is right for the opposite reason to the one you gave.

The 0.27 was wrong, and wrong in my favour

You are right. Ran anatomie_du_bump.py on 000e41d. Your table reproduces line for line, and the line I should have written prints right under it:

amplitude sur les vingt autres cellules :  |delta t(R)| max = 0.517

beta=0.005, R=25, moving t up, 2.430 to 2.947. My 0.27 was downward moves only. Struck. Same shape as your q90 sentence, one section apart, both wrong toward the point being made.

Rule 6 has no null, and section 3 is the template for it

Section 3 is the best thing in your reply. You caught that my leave-one-out was a maximum over twenty-five removals, built its null, and read 1.295 against E[max drop] 0.430. That is exactly right and it kills the attack properly.

Then rule 6 publishes a bare integer.

"Dies to 2 runs of 150" only means something against how many runs a real effect of that size dies to. So I planted one. Residuals from the R-level means, resampled with replacement; a constant shift on every R=25 run, calibrated so E[t] = 2.430; your cell sizes, n25=53 and n24=47. The effect is true by construction and identical in every replicate. Nothing selected, nothing fragile.

breakdown count of a GENUINELY REAL effect of exactly this size
   median 4      mean 5.5
   q10 1    q25 2    q50 4    q75 8    q90 11
   P(breakdown <= 2)   0.252
   P(breakdown <= 3)   0.398

A quarter of true effects this size die to two runs. Two fifths die to three. So your 2/150 and 3/150 are unremarkable for a real effect, and the integer did not tell you the bump was fragile. It told you t was 2.43 at n=150.

Which is what it measures. Scaling the planted effect:

effect   E[t]    median breakdown   P(>= 10)
x1.0     2.43           3             0.15
x1.5     3.64           9.5           0.50
x2.0     4.86          18             0.94
x3.0     7.29          30             1.00

Earning a double-digit breakdown count half the time takes 1.5x the effect you measured. So rule 6 used as a bar is a power requirement wearing a robustness costume. That is a fine rule to have. It is just not a diagnostic, and published as one it will retire true findings at this n at about the rate it retires this one.

The fix is your own section 3. Report the count over its null: observed against E[breakdown | a real effect of the observed size, this design]. Here 2 against 4. That reads as "unremarkable for its size", which is honest, rather than "1.3 % of the sample", which reads as damning and is not.

One number I did not expect, out of the same simulation. With the effect true and planted, the share of replicates reaching |t| >= 1.98 is 0.665. The design has two-thirds power at the size it found. That is upstream of the inversion, the fragility and the interaction p, and neither of us had put a number on it.

Rule 5 is right. The sentence under it points the wrong way

effet_par_beta.py, line 54:

"R": int(len(np.unique(code))),        # code = emetteur.loi().argmax(dim=1)

R is the number of distinct symbols the sender's code uses across the 27 referents. It is the alphabet size of the artifact. And objectif returns recompense = (s * r.t()).sum() / N, expected round-trip success over the same 27.

That is a bound, not a correlation. R symbols carry at most R distinguishable messages, so at most R referents decode:

reward <= R/27
   discovery    150 / 150
   replication   60 /  60

deficit = (R/27 - reward) * 27
   discovery    0 in 141,  1 in  9
   replication  0 in  49,  1 in 11
   never 2, never negative, across all 210 runs

Pigeonhole, not regression. Your +0.9725 is the shadow of a bound that is tight in 141 runs out of 150. And the nine are not rounding: they are runs where the code holds R symbols and only R-1 of them decode, a receiver collision. Six of the nine sit at R=25.

So reward is a function of R, not R of reward. "R is the reward times twenty-seven" has the arrow backwards, and the direction is what makes rule 5 bite harder than you claimed. R is not the objective on a grid. It is the structure of the thing the run produced, and the objective is pinned to its ceiling. A column that bounds the objective is disqualified for the same reason as one that is it, and it stays disqualified in the runs where the optimiser misses the ceiling, which "the objective divided by 27" does not cover.

For completeness, since it prices what rule 5 costs you: swap R for k = round(27 * reward), the actual referent count, and the contrast is t = +2.226 against +2.430. Nearly the same row. Either column, same size, equally not a fact about a factor.

What I think is left

Rule 5 kills a row by reading the generator before the data, which is the only move so far that costs nothing and cannot be gamed. Rule 6, priced against its own null, does not kill this one.

Which leaves the bump as: real in the discovery sample, unremarkable in fragility for its size, absent on the second draw, and about a column that was never eligible. Three of those four are power.

At 0.665 power, every contrast in Β§7.25 is drawn from a regime where the ones that surface are inflated, including the two you are still defending. Is there a draw large enough to put an effect that size above 0.9, and if there is not, is the honest column next to each contrast the detectable-effect floor rather than the p?

Β·

You are right that rule 6 published a bare integer. You are right about the arrow. And your one surprising number is your p-value wearing a hat, which I will show before I use the rest of your argument, because the rest of it survives without it.

Rule 6 is not repairable by reporting it over its null. I went to build that null and it is not identified.

Rule 6 dies harder than you killed it

I planted the effect your way β€” residuals from the R-level means, resampled with replacement, constant shift on every R = 25 run, calibrated so E[t] = 2.430, cell sizes 53 and 47. Then I varied the one thing that should not matter: where the residuals come from. Same design, same planted effect, same n, same greedy removal.

residual source              median breakdown   mean   P(<=2)   P(<=3)   power
residuals from R-level means         3           4.3    0.488    0.568    0.682
residuals from the 25 cell means     4           5.6    0.354    0.432    0.796
Gaussian, same sd                    2           4.0    0.518    0.598    0.626
yours                                4           5.5    0.252    0.398    0.665

Your median and mean sit on my cell-residual row; your power sits on my R-residual row. I could not find a single specification that produces all four of your numbers, and I am reporting that rather than tuning until it matches.

But locating your run exactly no longer matters, because the spread across three defensible choices is the finding. The observed 2 reads as the median of one null, "half of true effects" in a second, and "a quarter" in a third β€” and nothing distinguishes them except a modelling choice about residual structure that has no bearing on the effect being planted. The residual skew is 1.37 by R-level and 0.96 by cell; that difference alone moves the reference by a factor of two.

So the fix you proposed β€” report the count against E[breakdown | a real effect of this size] β€” inherits the problem it was fixing. There is no null to report against without first assuming the generative model, and the assumption swings the verdict.

Rule 6 is withdrawn, not amended. It was a robustness statistic with the model smuggled into the calibration step, which is the same failure as a bare integer with the model smuggled into the reader's head.

Your 0.665 is your p in costume

Power computed at the observed effect size is a one-to-one decreasing function of the p-value (Hoenig and Heisey 2001). It cannot contain anything t does not already contain. Directly:

    t     two-sided p    power at the observed size
 1.00        0.3190              0.168
 1.50        0.1358              0.319
 1.98        0.0496              0.503
 2.43        0.0163              0.675
 2.97        0.0035              0.839
 3.50        0.0006              0.935

Your 0.665 is my 0.675 analytic and 0.682 simulated, and it is p = 0.0163 rewritten. "The design has two-thirds power at the size it found" is "t was 2.43" β€” it is a fact about the observation, not about the design, and the sentence attributes it to the design.

Your use of it is fine: calibrating a planted effect against the observed size is legitimate and is not the same thing. It is the standalone sentence that does not survive.

That leaves your closing question standing on its own, which is where it should have been.

Your pigeonhole, verified, and the arrow conceded

reward <= R/27          discovery 150/150     replication 60/60
deficit in {0, 1}       never 2, never negative, across all 210
deficit == 1            9 of 150 and 11 of 60, six of the nine at R = 25
k = round(27 * reward)  contrast t = +2.226   (yours to the digit)

You are right and I had it backwards. R = len(np.unique(code)) is the alphabet size of the argmax code, and reward is round-trip success over the same 27 referents, so R symbols admit at most R decodable referents. Counting, not regression. My "R is the reward times twenty-seven" described the 141 runs where the bound is tight and silently dropped the nine where it is not β€” and those nine are the informative ones, since they are the runs where the code carries R symbols and only R βˆ’ 1 decode.

The direction does make rule 5 bite harder, exactly as you say: a column that bounds the objective is disqualified in the runs where the optimiser misses the ceiling too, which my formulation did not cover.

One thing you did not check, which I expected to matter and it does not: none of the nine collision runs is in the bump cell. Zero of its thirteen. And their mean gap is 0.01142 against 0.01102 for the rest. So the receiver collision is not the mechanism behind anything, and the bump remains what Β§7.27 found it to be β€” four high-max runs.

Your question, answered on its own terms

Is there a draw large enough to put an effect that size above 0.9?

For beta, yes, and it is cheap: 36 seeds per level, 179 runs, about half an hour. For R, 89 runs per cell, roughly 266 total β€” but R cells are not settable, so that is not a draw anyone can command. You take what falls out. That asymmetry is rule 5 showing up again in a different costume: the column you cannot power is the column you were never allowed to contrast.

is the honest column next to each contrast the detectable-effect floor rather than the p?

Yes. Here is the whole table with it, floor = 2.80 Γ— SE, a function of the design alone:

contrast                 n        SE      floor     observed   observed/floor
R 27 vs 26            8+30    0.00516    0.01445    -0.00249        0.17
R 26 vs 25           30+53    0.00296    0.00830    -0.00186        0.22
R 25 vs 24           53+47    0.00260    0.00728    +0.00631        0.87
R 24 vs 23           47+12    0.00419    0.01175    -0.00298        0.25
beta 0.005 vs 0.03   30+30    0.00330    0.00925    -0.00981        1.06
beta 0.005 vs 0.037  30+30    0.00330    0.00925    -0.00289        0.31

Every observed effect in the table is at or below its own detection floor. The two we spent four rounds on are the two that reach 0.87 and 1.06 β€” which is the definition of the regime where what surfaces is inflated. The floor column says that before any of it happened, needs no data, and cannot be gamed. It replaces the p column.

Where I think this goes past the question

The floor answers "what could I have seen". Nothing in eleven rounds has asked "what would I have needed to see for anything to change".

The table in Β§7.25 existed for one purpose: to know whether the gap the dynamics produces approaches the worst case a search can reach. Published worst case 0.1443, observed mean gap 0.0110, ratio 13.1.

target ratio    required rise in the gap    versus the largest observed effect (0.00981)
      2                 0.0611                          6.2 x
      3                 0.0371                          3.8 x
      5                 0.0178                          1.8 x
      8                 0.0070                          0.7 x
     10                 0.0034                          0.3 x

I want to stop here rather than take the number that helps me. The threshold is chosen, not derived, and the verdict flips between ratio 5 and ratio 8. If "nowhere near" means a factor of two, every contrast in the table is six times below anything that could matter and the design is four to eight times more sensitive than it needed to be. If "nowhere near" means a factor of eight, the contrasts are in range and the whole exercise was legitimate.

I cannot now name that threshold without it looking picked, which is the actual finding: it was never written down, and it is the only number in this entire exchange that had to be fixed before the data and was not. The floor is a design quantity and I can still compute it honestly today. The relevance threshold is a design quantity that becomes uncomputable the moment you have seen the results.

The five questions underneath this exchange

Eleven rounds, and the corrections have been getting more precise while staying on the same level. Written as questions, because that is what they are:

1. When is a column a variable? A dataframe column looks like a factor whether or not anyone set it. R survived four rounds of increasingly careful statistics because every one of them priced a contrast instead of asking what the column was. In this field almost everything reported as a factor is an output β€” final loss, effective rank, number of active features, the step convergence happened at, the architecture that survived a sweep. The test is whether the column was assigned before the run, and it is answered by reading the generator, not the data.

2. Can a robustness diagnostic exist without a generative model? Rule 6 says no. The breakdown count needs no distribution to compute and cannot be interpreted without one, and the interpretation moves by a factor of two across choices that have nothing to do with the effect. I suspect this generalises to every statistic sold as assumption-free: the assumption is not absent, it is relocated to the calibration step where nobody looks for it.

3. Which of our reported numbers are functions of the design and which are functions of the data? Only the first kind can be fixed before the first seed, and only the first kind cannot be gamed. The detection floor is design. The Scheffe bar is design. K is design. The p-value, eta squared, the breakdown count and observed power are all data β€” and observed power is the clean demonstration, since it looks like a design quantity and is a p-value.

4. What is the smallest effect that would have changed a conclusion? An experiment without this number cannot be under-powered or over-powered, because power is relative to an effect nobody named. It has to be written before the data, and after the data it can no longer be written honestly. Ours was never written, and that is why I can produce a table where the answer depends on a ratio I get to choose.

5. How much of empirical method is machinery for pricing measurements that could not have mattered? Eleven rounds on the correct price of a contrast on a column that was not eligible, measuring a quantity whose relevance threshold was never set, in a table where every effect sits at or below its own detection floor. Each correction was right. The sequence never asked whether the thing being corrected was worth measuring, and every tool either of us reached for was a tool for pricing, not for that question.

I do not have rules for 4 and 5. The one thing I can do is put the floor column and a stated relevance threshold into the design document before the next run, where the second one is still writable.

Notebook Β§7.28, Β§1.20 for rule 6. Code in src/test3_communication/plancher_de_detection.py.

The floor column has the same defect as the power column, and this time it is exact.

You are right about the observed power and I am not defending it. Hoenig and Heisey is the reference, the 0.665 was p = 0.0163 wearing a hat, and the sentence credited the design with a fact about the observation. Mine to own.

But the replacement inherits it.

obs/plancher is |t| / 2.80, by construction

plancher_de_detection.py builds the column at lines 189 to 201:

se       = sigma * sqrt(1/na + 1/nb)
plancher = 2.80 * se
ratio    = abs(d) / plancher

and t_contraste at line 42 is d / (sigma * sqrt(1/na + 1/nb)), i.e. d / se. So the printed ratio is |t| / 2.80 identically, before any data exists. Checked against your six published rows:

contrast             |t|     |t|/2.80    your ratio
R 27 vs 26          0.483      0.17         0.17
R 26 vs 25          0.628      0.22         0.22
R 25 vs 24          2.427      0.87         0.87
R 24 vs 23          0.711      0.25         0.25
beta .005 / .03     2.973      1.06         1.06
beta .005 / .037    0.876      0.31         0.31

Six for six. Your published floor divided by 2.80 x SE is 1.0000 to 1.0015 on every row, so it is the constant exactly, not an approximation.

Which makes the verdict a p-value threshold. "Every observed effect is at or below its own detection floor" is "every |t| < 2.80" is "p > 0.0058 at 145 df". The column is defined at alpha 0.05 and, used as a comparator, implements alpha 0.0058. About nine times stricter than the alpha in its own definition.

The demonstration is already in your message. Your section C table prints t = 2.97 -> p = 0.0035. That is the beta .005 / .03 row of section D, the one reading 1.06. Same fact published twice, once as p and once as a ratio, and only the first one flagged.

The floor itself survives, in absolute form

The ratio is data. The floor is mostly not. Recovering sigma from each of your published SEs:

R 27 vs 26    sqrt(1/8 + 1/30)  = 0.39791    sigma_hat 0.012968    n_eff  6.32
R 26 vs 25    sqrt(1/30 + 1/53) = 0.22848    sigma_hat 0.012955    n_eff 19.16
R 25 vs 24    sqrt(1/53 + 1/47) = 0.20036    sigma_hat 0.012977    n_eff 24.91
R 24 vs 23    sqrt(1/47 + 1/12) = 0.32343    sigma_hat 0.012955    n_eff  9.56
beta rows     sqrt(2/30)        = 0.25820    sigma_hat 0.012781    n_eff 15.00

One common sigma across all of it. So the floor is a pure design factor times a single estimated scale, and that scale carries 5.8% relative SE at 148 df. The ratio destroys the informative half by dividing it back out.

Here is the floor doing what I could not get a p to do:

                                        SE      floor    your 0.00981 reads
discovery,  30 seeds per beta cell     0.00330  0.00925         1.06
replication, 12 seeds per beta cell    0.00522  0.01462         0.67

If the replication is 12 per beta cell, its floor is 0.0146 against a discovery effect of 0.0098. The replication could not have confirmed the discovery even if it had reproduced it exactly. That is computable before the replication runs, it does not depend on what the replication found, and no p-value computed on the replication can say it. That is the column worth keeping, printed alone.

Rule 5 eats the floor on four of the six rows

Your question 3 sorts numbers into design and data, and puts the floor on the design side. It is on the design side only where n was assigned.

8 + 30 + 53 + 47 + 12 = 150. The R cell sizes are your discovery runs partitioned by an outcome. Nobody set them. So on the four R rows both inputs to the floor fall out of the run: sigma from the residuals, n from a multinomial. The 6.32 effective runs at R 27 vs 26 was not a choice, it was a result.

The beta rows are 30 and 30, set by the sweep. Those two floors are design.

So rule 5 disqualifies the R floor on exactly the rows where it disqualified the R contrast, for exactly the same reason, and you can reach that without any new data. Worth noticing that the one row in the table clearing its floor is a beta row. The only contrast that resolves anything sits in the only column that was eligible.

Question 4, where I think you are half right

You say the relevance threshold becomes unwritable once the results are in. I think it became unwritable by you, which is not the same as uncomputable.

It is unwritable because the experiment has no consumer. Nothing downstream changes at any gap value, so no effect size would have altered a decision, so any threshold named now is a threshold picked. The defect is not that the data arrived first. It is that "what does this measurement feed" was never asked, and that question does not reference the results, so it is still answerable today at full honesty.

Your ratio table is the proof: the verdict flips between 5 and 8 precisely because nothing constrains the choice. Name the consumer and the ratio stops being free.

So: the floor is a design factor times one estimated sigma, and a pilot fixes sigma before the first seed. If the design document carries 2.80 * sigma_pilot * sqrt(1/na + 1/nb) next to a named consumer, is there anything left in question 4 for the data to contaminate, or does the whole of it move to before the run?

Β·

The identity is exact and the column is withdrawn. Then your pilot does not work on this quantity, for a reason that turns out to be the same reason the floor was worth having. And your reframe of question 4 is better than mine, so I took it and followed it to a date on my own record.

Your identity, to zero

contrast              |t|     |t|/2.80    my ratio     difference
R 27 vs 26          0.482      0.1721      0.1721      0.00e+00
R 26 vs 25          0.627      0.2238      0.2238      0.00e+00
R 25 vs 24          2.430      0.8677      0.8677      0.00e+00
R 24 vs 23          0.711      0.2540      0.2540      0.00e+00
beta .005/.03       2.968      1.0601      1.0601      0.00e+00
beta .005/.037      0.875      0.3125      0.3125      0.00e+00

Not close β€” identical, because plancher = 2.80 * se and t = d / se in the same file. So "every observed effect is at or below its own detection floor" is |t| < 2.80, which at 145 df is p > 0.0058: an alpha 8.6 times stricter than the 0.05 written into the floor's own definition. And you are right that I published the same fact twice in one message and flagged only one instance β€” my section C printed t = 2.97 β†’ p = 0.0035, which is section D's 1.06 row.

I replaced a p-value column with a rescaled p-value column and called it a design quantity, one message after explaining why observed power is a rescaled p-value. The ratio column is withdrawn. The correction to my question 3 is that "is it a function of the design" has to be checked on the printed expression, not on the intent.

The absolute floor survives, and it convicts something older than this table

Your prospective use is the real find, and it reaches further back than the row you applied it to. Both replication floors, computable before those runs existed:

                     n        floor      effect it had to confirm    ratio
beta .005/.03 discovery  30+30   0.00925          0.00981             1.06
              replication 12+12   0.01462          0.00981             0.67
R 25 vs 24    discovery  53+47   0.00728          0.00631             0.87
              replication 30+12   0.01240          0.00631             0.51

The sixty runs I ran in round eight could not have confirmed either contrast even if both were exactly true. I reported that replication as a test β€” "the sign flips", "it does not replicate", "the omnibus goes to p = 0.144" β€” and it was never a test. It was an estimator, and an unbiased one, which is why -0.0053, SE 0.0033 remains the only honest line to publish from it. The conclusion I drew survives. The reason I gave for it does not: I read a failure to reach a bar that the draw could not have reached.

That is computable before running, does not depend on what came out, and no p on the replication can say it. Kept, printed alone and absolute.

And rule 5 does eat it on the four R rows. 8+30+53+47+12 is a partition of the discovery runs by an outcome; the 6.32 effective runs at R 27 vs 26 was a result. Both inputs to those floors fall out of the run. Only the beta rows at 30 and 30 are design β€” and, as you note, the only contrast in the table that clears its floor is in the only column that was eligible.

Where the pilot fails, and it is not a detail

You propose 2.80 * sigma_pilot * sqrt(1/na + 1/nb) in the design document. On this quantity the pilot cannot fix sigma, and the reason is structural rather than practical.

beta        n      mean       sd     sd/mean
0.005      30    0.00768   0.01080     1.41
0.010      30    0.00943   0.01042     1.10
0.020      30    0.01005   0.00857     0.85
0.030      30    0.01749   0.01771     1.01
0.037      30    0.01057   0.01436     1.36

Bartlett  chi2 = 19.176   p = 0.0007        sd ratio across levels  2.07

Heteroscedastic across the swept factor, so one pilot sigma misprices the floor by 12 % to 38 % depending on the cell. But the mechanism is what matters:

The gap is bounded below by zero. The unconstrained max is always at least the matched value, so max - appariee >= 0 by construction, and 63 of the 210 runs sit exactly at 0. A non-negative variable piled at its bound has its scale tied to its location, and it does:

across the 18 cells with n >= 4:
   corr(cell mean, cell sd)    Pearson +0.874   Spearman +0.917
   slope sd on mean            +0.817
   median coefficient of variation   1.07

Sigma is not a nuisance scale here. It is roughly the quantity being measured. So a pilot fixes the floor only if the pilot's mean matches the eventual mean, which is to say only if you already know the effect. That is not a fixable pilot design; it is the floor's one data-dependent input being data in the strong sense.

But more of question 4 moves before the run than you proposed, in a different unit

The same fact that breaks the absolute floor makes a relative one writable. If sd β‰ˆ CV Γ— mean with CV stable near 1, then

floor / mean  =  2.80 * CV * sqrt(1/na + 1/nb)

which needs no sigma at all. For 30 seeds per cell and CV β‰ˆ 1.2: 0.89. Checked against the actual table, floor 0.00925 over grand mean 0.01035 = 0.894.

Written before the first seed, in one line and with no pilot: with thirty seeds per cell, this design sees a near-doubling of the gap and nothing smaller. That is a stronger pre-registration than an absolute floor because it survives not knowing the scale, and it is the honest form for any non-negative quantity piled at zero β€” which is most of what gets measured in this field. CV is the stable thing to pilot; sigma is not.

So: not the whole of question 4 moves, but the part that moves is bigger than an absolute floor and it moves in relative units.

Question 4, your reframe, and the date

It became unwritable by you, which is not the same as uncomputable. The defect is not that the data arrived first. It is that "what does this measurement feed" was never asked, and that question does not reference the results.

You are right, and it is a better diagnosis than mine. I said the threshold rots on contact with data. It does not β€” it was never planted.

So I asked it, and it has an answer with a date on it.

The gap max - appariee measures the inflation from publishing the concentration statistic in its unconstrained-argmax form rather than the matched form. Its consumer was the 0.35 threshold in TEST3 Β§6.1, which decided whether an emergent code counted as compositional. That threshold was withdrawn on 11/08/2026, notebook Β§1.9, after you pointed out it was built on a sample maximum.

Rounds six through twelve have priced a measurement whose consumer was deleted in round five.

The distinction that keeps it honest, since not all of the work goes: the bound still has a consumer, because I report the max-form statistic and a reader needs to know it can be inflated by up to 0.14. That work stands. The contrast table β€” does the inflation depend on R, on beta β€” never had one. No decision anywhere changes at any value of that dependence, which is exactly why the relevance ratio was free to be 2 or 8 and why naming it now looks picked. There was nothing to pick from.

Which is what the design says too, if you ask it about the question that did have a consumer. That one is one-sample, not a contrast:

mean gap over 210 runs        0.01035    SE 0.00085    95 % CI [0.00869, 0.01201]
distance to the worst case    0.1339  =  158 standard errors
one-sample floor              0.00237
contrast floors in the table  0.00728 to 0.01445

The same runs are three to six times finer on the question with a consumer than on the contrasts without one, and they answered it at 158 sigma before any of this started.

What neither of us opened: the variable is not continuous

I noticed the zero bound while checking your pilot and did not stop to look at it. It is the largest thing in this file.

63 of the 210 runs have a gap of exactly zero. Thirty per cent. The gap is zero precisely when the unconstrained argmax is already a bijection β€” no message position claiming the attribute another one claimed. So this is not a continuous quantity with a floor; it is a point mass plus a right-skewed positive part, and every t, every permutation, every Scheffe bar and every bootstrap in twelve rounds has been computed on it as though it were neither.

First consequence, and it lands on the number I have been defending since round seven. The bound 0.1443 comes from the transposition climber searching for the worst code β€” which necessarily has an argmax collision. It is a worst case given a collision. My 0.0104 is unconditional, thirty per cent of it zeros. The published ratio compares a mixture to a conditional:

quantity                                    value     ratio to 0.1443
E[gap] over 210 runs  (what I published)   0.01035        13.9
E[gap | gap > 0]      (the matched one)    0.01479         9.8
median of the positive part                0.01254        11.5
q95 of the positive part                   0.04049         3.6
max observed over 210 runs                 0.05927         2.4

The ratio I have been quoting is 13.9 against a like-for-like 9.8 β€” and if you compare the two quantities that are actually the same kind of object, a worst case against a worst case, it is 2.4. "Emergent codes come nowhere near what an adversarial search reaches" was built on the pairing that flatters it, and I chose that pairing without noticing there was one to choose.

Second consequence: the quantity is two quantities, and they have different consumers.

P(argmax collision)         0.700    95 % CI [0.633, 0.761]
E[inflation | collision]    0.01479  SE 0.00101
product                     0.01035  = the published mean, exactly

How often the published statistic is wrong at all, and by how much when it is. Those answer different questions for a reader, and averaging them into one number answers neither. Never separated in any version of this document.

Third consequence: the tests. The positive part has skew +1.34, and neither it nor its log passes Shapiro β€” 1.7e-09 and 2.2e-06 β€” so it is not lognormal either, just skewed with no convenient form. Running the two contrasts of this exchange against the structure instead of through it:

contrast          Student, raw          on log(gap>0)         Mann-Whitney
R 25 vs 24     t=+2.462  p=0.0156    t=+1.471  p=0.1459        p=0.0619
beta .005/.03  t=-2.589  p=0.0122    t=-2.113  p=0.0405        p=0.0285

The R contrast β€” four rounds, a selection correction, a replication, a bump analysis, a pigeonhole argument β€” goes to p = 0.062 the moment it is asked in a form the variable can answer. It was partly the Gaussian machinery reading a point mass as data.

And the decomposition says the contrasts are not about the collision rate: 13 % and 17 % of each comes from the zero proportion, Fisher p = 0.66 and 0.55. They are about the size of the inflation when it happens, which is the half with the smaller n β€” 39 and 32 runs, not 53 and 47.

One number I will not claim. The observed zero rate, 0.300, differs from 6/27 = 0.2222 at binomial p = 0.0098 β€” 6/27 being the permutation rate if the three argmaxes were independent and uniform. They are argmaxes of correlated mutual informations, so that reference is not a justified null and the p is not a finding. It is a number, and I am printing it as one.

Rule 7, and it was available on 11/08

When a claim is withdrawn, list every measurement whose only consumer it was, and stop measuring them. A retraction propagates downstream and nothing in my process made it propagate. Β§1.9 killed the threshold; the quantity it justified kept being measured, contrasted, corrected for multiplicity, replicated, corrected again, and defended across seven rounds against increasingly good statistics β€” all of it correct, none of it attached to anything.

That is the rule that ends this thread, and unlike the previous six it costs nothing to run and cannot be gamed: it is a list, written at retraction time, of what the retracted claim was feeding.

I do not think you and I could have found it by getting better at the statistics. Every round did get better at the statistics.

And the zero mass is the same lesson from the other side. Twelve rounds of increasingly correct inference on a variable that neither of us had plotted. min, max and a count of exact zeros would have caught it in round one, cost nothing, and required no argument β€” and they would have caught it before the bound comparison that all of this was downstream of.

Notebook Β§7.29, Β§1.21, Β§1.22. Code in src/test3_communication/plancher_de_detection.py and masse_en_zero.py.

Rule 4 has one more customer, and it is the null you published in this message.

You ran the zero mass, concluded the contrasts are not about the collision rate, and cited Fisher p = 0.66 and 0.55. That is a null read without its floor, which is the thing this whole exchange has been about. I pulled your two files off the Zenodo record and reran masse_en_zero.py: every number reproduces, 0.700, 0.01479, 0.01035, 147 positives, CV 0.83.

Then I computed the floors you did not.

The CV you want to pre-register is one third point mass

Your relative floor needs a stable CV. The mixture CV is not one quantity:

CV_mix^2 = CV_pos^2 / p  +  (1 - p) / p

  CV_pos^2 / p     0.9736
  (1 - p) / p      0.4286      <- pure Bernoulli, no scale in it
  sum              1.4022      measured CV_mix^2  1.4022

Exact, on your 210. So 30.6 % of the CV^2 you would write into a design document is the collision rate, and CV_mix = 1.184 is your 1.2.

That matters because the two halves have different consumers, which is your own point from this message. Pre-registering on the mixture prices a floor for neither.

Split it and the design gets sharper, not weaker

                              floor / mean   at 30+30 seeds
mixture (your 0.89 line)          0.856
E[gap | collision]                0.713      on E[n_pos] = 21 per cell

Dropping 30 % of the runs buys a 17 % finer floor, because the variance you drop is larger than the n you drop. Treating E[n_pos] as fixed at 21 rather than random, so read it as the leading term.

The other half has no resolution at all

Same construction, delta = 2.80 x SE, applied to the proportion:

cells        detectable only if the rate rises to   or falls to
beta 30+30              0.956                          0.361
R 25/24 53+47           0.916                          0.433

Observed rate 0.700, and the parameter stops at 1.000. The detectable upward window is [0.956, 1.000], which is 15 % of the headroom that exists. At 53+47 it is 28 %.

So Fisher p = 0.66 is not evidence that the R contrast is not carried by the zeros. It is the same reading you retracted for observed power, on a test whose floor is most of the parameter space. The share estimates say the same thing once they carry an interval:

contrast          zero share    bootstrap 95 % CI    Fisher p
R 25 vs 24            13 %        [1 %, 52 %]         0.660
beta .005/.03         17 %        [0 %, 56 %]         0.552

A share whose interval runs to half cannot support "they are about the size of the inflation when it happens".

What I think this costs you, and what it does not

Not the decomposition. P(collision) = 0.700 and E[inflation | collision] = 0.01479 are both worth publishing, and the 9.8 like-for-like ratio against the bound is the honest headline. Those are one-sample quantities and your 210 runs are fine on them.

It costs the sentence that assigns the contrasts to one half. Thirty seeds per cell can price the inflation size and cannot price the rate, and that asymmetry was writable before the runs from p alone.

Which puts a question back on rule 7. Your rule fires at retraction time and lists what the claim was feeding. The zero-mass split created two new quantities today, with no retraction anywhere. Is there a rule that fires when a measurement is created, naming its consumer before the first seed, or does the list only ever get written on the way down?

Β·

You are right about the Fisher reading, right about the CV split, and your closing question has an answer that is worse than the question implies. I went looking for a rule that fires when a measurement is created. What I found is that this measurement had already been answered four days before I started defending it, in a file I generated, in a field whose name is the quantity.

Your three legs, verified

The CV identity is exact on my 210:

CV_pos^2 / p   0.9736      (1 - p) / p   0.4286      sum   1.4022
CV_mix^2 measured directly                           1.4022
CV_mix = 1.1842, and 30.6 % of CV^2 is the Bernoulli half

Floors: mixture at 30+30 gives 0.8561 against your 0.856; conditional at 21+21 gives 0.7133 against your 0.713; the split buys 17 %. Proportion windows: 0.956 and 0.361 at 30+30, 0.911 and 0.432 at 53+47 β€” I get 0.911 where you have 0.916, which is a root-finder tolerance. Bootstrap shares [1 %, 53 %] and [0 %, 56 %] against your [1 %, 52 %] and [0 %, 56 %].

So my Fisher p = 0.66 was a null read without its floor, on a test whose detectable upward window is 15 % of the headroom that exists, one section after I retracted the same reading for observed power. The sentence assigning the contrasts to the inflation-size half is withdrawn. Thirty seeds per cell can price the size and cannot price the rate, and that was writable from p alone before any run.

One correction that improves your fix rather than resisting it: CV_pos is not stable enough to pre-register either. Across beta levels it runs 0.57 to 0.95, ratio 1.67, Bartlett p = 0.0004. The log does stabilise it β€” Levene on log of the positive part gives p = 0.071 against 0.020 raw. So the pre-registrable quantity is the sd of the log, not the CV.

Your question, and the file it led to

Is there a rule that fires when a measurement is created, naming its consumer before the first seed, or does the list only ever get written on the way down?

I went to write that rule. Before writing it I checked what this measurement's consumer would have been on 11/08, and found that the question had already been answered.

results_test3/loi_nulle_longue_n10000000_g0.json, generated 11/08/2026, ten million random codes. It carries a block called double_compte:

"taux_global": 0.7464519,
"inflation_moyenne_globale": 0.010049794802284647,
"inflation_maximale": 0.10807050074977963

Those are the two quantities I presented to you yesterday as a decomposition neither of us had separated, plus the third one I have been quoting a ratio against. Side by side with the 210 runs:

                              null 10^7 (11/08)    210 runs (12-15/08)
P(argmax collision)                 0.7465               0.7000    binomial p = 0.13
E[inflation]                        0.01005              0.01035   z = 0.36
E[inflation | collision]            0.01346              0.01479   z = 1.32
max inflation                       0.10807              0.05927

The gap I have measured, bounded, contrasted, corrected for multiplicity, replicated, defended across eight rounds and decomposed for you yesterday is the null distribution's own inflation. It is not close to it, it is it: z = 0.36 on the mean, p = 0.13 on the rate.

And the first-order check that makes it unsurprising, which nobody ran either. Kolmogorov-Smirnov, the 210 runs' max concentration against 20 000 null draws: D = 0.0508, p = 0.638. The observed distribution sits on the null at every quantile I have β€” 3 % below q5, 50 % below q50, 95 % below q95, 99 % below q99.

Which my own Β§6.2 had already established on 11/08 for the matched concentration, at 100 seeds: z = -0.0098 Β± 0.1025, KS p = 0.386. I published that, and then spent eight rounds contrasting a difference between two statistics computed on codes I had already shown to be null-distributed.

Which fixes my headline ratio for the third time in two days

Yesterday the ratio went from 13.9 to 9.8 because I was comparing a mixture to a conditional. That was the second conditioning error in the same number. Here is the third, and it is the one that matters:

comparison                                                       ratio
mean observed vs the search worst case  (what I published)        13.9
mean observed vs the null's mean inflation                         0.97
max observed over 210 runs vs the search worst case                2.4
max of the null over 10^7 draws vs the search worst case          1.34

My 15 was a mean against a max, across sample sizes differing by five orders of magnitude. The like-for-like comparison is the worst an adversarial search finds against the worst ten million random draws throw up on their own: 0.1443 against 0.1081. The search does 34 % better than chance. It does not do fifteen times better, and "emergent codes come nowhere near what an adversarial search reaches" is false as stated β€” they land exactly where random codes land, and random codes get within a third of the search.

So the rule you asked for, and it is the other one

You asked whether the list only gets written on the way down. I had it backwards, and so does rule 7.

Rule 8. When a result is established, list every quantity you are still measuring that it now answers, and stop measuring them.

Rule 7 fires on retraction: a claim dies, list what it was feeding. That is the cheap direction, and it caught one thing β€” the 0.35 threshold dying on 11/08 and taking the gap's consumer with it.

Rule 8 fires on establishment, and it is the one that would have ended this on the day it started. Β§6.2 landed on 11/08 and said the emergent codes are drawn from the null. The null file, generated the same day, contained that null's inflation to seven digits under a field name that is the quantity. Between them the entire Β§7.24–§7.29 programme was answered before it was run β€” the mean, the rate, the conditional and the maximum, all four.

Nobody writes the establishment list because establishing a result feels like finishing something rather than incurring an obligation. A retraction announces itself. A result that quietly answers three other open questions does not announce anything, and I had two of them in the same directory with matching timestamps.

What survives

The decomposition is still worth publishing, and now it is worth publishing for the opposite reason to the one I gave you. P(collision) = 0.700 and E[inflation | collision] = 0.01479 do not describe a property of what the dynamics produces. They describe the null, reproduced by the dynamics, which is the actual finding and is consistent with everything Β§6.1 through Β§6.6 concluded: on this bench compositionality was never selected. The inflation is not evidence about emergent codes. It is a fact about the statistic, and it applies to any code anyone draws.

That also disposes of the relevance threshold I could not name for you two rounds ago. There was nothing to pick from because the quantity has no dependence on anything the experiment varies β€” it is a constant of the measurement, and the honest column next to it is not a p, not a floor, and not a threshold. It is the null's value.

Thirteen rounds. Every correction was right, each one was finer than the last, and the thing they were all refining had been computed correctly on day one and filed under its own name.

Notebook Β§7.30, Β§1.23. Code in src/test3_communication/masse_en_zero.py and plancher_de_detection.py.

Rule 8 is right, and the number you retracted to is a two-million-draw number.

I ran loi_nulle_longue.py at the published settings, --echantillons 10000000 --graine 0 --lot 100000, and your double_compte block comes back bit for bit:

taux_global                 0.7464519
inflation_moyenne_globale   0.010049794802284647
inflation_maximale          0.10807050074977963

Full precision, all three. So what follows is your run, not a reimplementation of it.

Where 0.10807 comes from

tirer maintains two surfaces. pool_max/pool_apparie is the exact top 200 000 of the whole draw. echantillon_max/echantillon_apparie is the reservoir, and it stops filling at reservoir=2_000_000.

The report builds inflation from the reservoir:

inflation = ech_max - ech_app
print(f"  inflation moyenne, toute la loi : {inflation.mean():.4f}")
print(f"  inflation maximale observee     : {inflation.max():.4f}")

ech_max is 2 000 000 long at n=10^7. So inflation_maximale is the max over the first fifth of the run, printed one line under "toute la loi".

Running max over all 10^7, same stream:

n              true max    reservoir max   n_reservoir
   100 000     0.097594      0.097594        100 000
   500 000     0.103746      0.103746        500 000
 2 000 000     0.108071      0.108071      2 000 000
 5 000 000     0.111111      0.108071      2 000 000
10 000 000     0.122365      0.108071      2 000 000

13 of your 10^7 draws sit at or above the number you published as the maximum.

What that does to today's ratio

comparison                                 published    measured
max of the null over 10^7 vs the search       1.34        1.18

0.1443 / 0.122365. The search does 18 % better than the null's max, not 34 %.

Two smaller ones from the same cap. inflation_moyenne_200_plus_hauts and taux_200_plus_hauts also index the reservoir, so "les 200 plus hauts" are the 200 highest of 2Β·10^6: inflation 0.0203 at conc_max 0.3000, against 0.0184 at 0.3239 for the exact pool. And E[inflation | collision] puts a reservoir mean over a whole-run rate, though there it costs nothing, 0.013463 against 0.013467.

The fix is one float. Track inflation.max() in the loop like you already track somme and depassements. The pool happens to carry the true max at every milestone above, but it is selected on conc_max and inflation is not monotone in conc_max, so that is luck rather than the guarantee your quantile_exact docstring earns for the appariee quantiles.

The column that is not a ratio

Look at that table again. The max has not flattened by 10^7. So 1.18 is a fact about your draw budget, not about the search, exactly the way 13.9 was a fact about comparing 210 to a search.

Your script already computes the budget-free version, on the other statistic. depassements[s] += int((cm >= s).sum()) runs over all n_total and ships as depassements_exacts. Inflation never gets that treatment.

Run it on inflation and the direction changes:

count(inflation >= 0.05927)    30 665    p = 3.07e-03
count(inflation >= 0.10807)        13    p = 1.30e-06
count(inflation >= 0.1443)          0    p < 3.0e-07   (rule of three, 95 %)

Zero in ten million. So the honest column beside 0.1443 is not 1.34 and not 1.18, it is an exceedance count, and it says the search reaches somewhere the null did not go in 10^7 tries. Smaller than your 15, different from your 0.97, and the only one of the three that does not move when you change n.

Which is Rule 8 one level up. Β§6.2 established the codes are null-distributed and answered four open questions from the directory next door. depassements established, on 11/08, in the same file, that exceedance is how you price this null. It just did it on the neighbouring column.

So does Rule 8 fire on a result, or does it fire on a method too?

Β·

Your bug is real, your milestones reproduce exactly, and the fix is in. Then the same run turned up a second defect in the same block that you did not flag, and it moves a number in your favour and mine at once. After that I have questions, because thirteen rounds of you asking and me measuring is itself a design I should not be defending.

Your diagnosis, confirmed line for line

n              true max    reservoir max   n_reservoir    pool max
   100 000     0.097594      0.097594        100 000      0.097594
   500 000     0.103746      0.103746        500 000      0.103746
 2 000 000     0.108071      0.108071      2 000 000      0.108071
 5 000 000     0.111111      0.108071      2 000 000      0.111111
10 000 000     0.122365      0.108071      2 000 000      0.122365

13 draws at or above the number I published as the maximum. 0.1443 / 0.122365 = 1.18, not 1.34.

And your reading of the pool is right on both counts: it does carry the true max at every milestone, and that is luck. It is retained on conc_max, inflation is not monotone in conc_max, and nothing in the code makes the coincidence hold. The quantile_exact docstring earns the guarantee for the appariΓ©e quantiles by a monotonicity argument that simply does not exist for this column.

Fixed in loi_nulle_longue.py: inflation_max, inflation_moyenne, taux_inflation, inflation_moyenne_si_positive and an inflation_depassements dict, all accumulated in the loop over the whole draw. Verified against your milestone β€” at 500 000 the patched code returns 0.10374624404828912.

The second defect in the same block, which changes your table and mine

taux_global is not the rate I compared my runs to.

gagnants = matrices.argmax(axis=1)
distincts &= gagnants[:, a] != gagnants[:, b]
return conc_max, conc_apparie, ~distincts

taux_global = 0.7464519 is P(two positions share an argmax attribute). Over the same 10^7 draws, P(inflation > 0) = 0.6762074. Seven points apart: in 7 % of draws the argmax collides and the matched assignment ties it exactly, so the collision costs nothing.

My observed 0.700 was computed as ecart > 0. So yesterday's table compared a cost-based observed rate to a structure-based null rate, and the conditional mean inherited it. Matched:

                          null (as I published)   null (matched)   observed      z
P(collision that costs)          0.74645             0.67621        0.70000   p = 0.507
E[inflation]                     0.01005             0.01005        0.01035      0.36
E[inflation | > 0]               0.01346             0.01486        0.01479     -0.07

The conditional row goes from z = 1.32 to z = -0.07, and the rate from p = 0.13 to p = 0.51. The one row of yesterday's table that did not sit on the null was a definitional mixup of my own, and correcting it makes the match essentially exact on all three.

Both rates now ship, named apart.

One closure, which is the only thing eight rounds of contrasts bought

The null is bijections only β€” np.argsort of a random vector is a permutation, so every one of the 10^7 draws has R = 27. Nine of my 210 runs do. Median observed R is 25.

So comparing my runs to that null needs the inflation not to depend on R, and that is exactly the contrast this exchange spent eight rounds on. Welch, R = 27 against R < 27: t = -0.65, p = 0.53. Matched subset, the nine bijections against the null: 0.00849 against 0.01005, z = -0.55.

The R contrast was worth precisely one thing β€” licensing a comparison to a bijection null β€” and in eight rounds neither of us said so. It was never a finding about R. It was a validity condition for the comparison that made R irrelevant.

And one thing I built, then did not publish

You said the exceedance column is the only one that does not move with n. At count 0 it is a bound, not an estimate, so I fitted the tail to convert it. Log-linear over nine thresholds, RΒ² = 0.990, characteristic length 0.00753.

Then I refitted on sub-windows:

fit range        P(inflation >= 0.1443)     one draw in
[0.03, 0.10]           2.60e-08              38 million
[0.04, 0.10]           1.85e-08              54 million
[0.05, 0.10]           1.38e-08              73 million
[0.06, 0.10]           1.06e-08              94 million
[0.07, 0.10]           7.45e-09             134 million

A factor 3.5 across defensible windows, extrapolating 5.9 characteristic lengths past the last threshold with usable counts. So I am not publishing the point estimate. Your column stands as the honest one: 0 in 10^7, p < 3.0e-7 by the rule of three. What I can add is that the tail shape says the true value is one to two orders of magnitude below that bound, without saying which β€” and that is a sentence, not a number.

Which is the same trap as the 0.35 threshold in Β§1.9, twelve rounds later: a derived quantity that looks like it converges, from a fit whose window I would have chosen after seeing it.

Your question: does rule 8 fire on methods?

Yes, and you are right that this is where it bites. depassements established on 11/08 that exceedance counts are how this null gets priced, on the neighbouring column, in the same function, in the same commit.

Rule 9. A method choice is a claim about a class of quantities. Write the class, in the same commit as the method. Then a new column in that class inherits the treatment instead of getting whatever the author reached for that afternoon.

I will state the limit honestly, because I do not think it is solved. Writing the class relocates the judgment to where the boundary goes, and it is drawn once, early, by the person least able to see what will later sit near it. I cannot construct the sentence I would plausibly have written in August that covers "any quantity you will eventually compare against an adversarial optimum gets exceedance counts rather than a max" β€” because in August there was no adversarial optimum in the project. The class that needed to exist did not exist yet.

So rule 9 is real and it is weaker than rules 7 and 8. Those fire on an event that has already happened. This one asks for foresight, and every version of it I can write reduces to "be more careful" once the boundary is contested.

Four questions, because this exchange has a design and I have not audited it

1. What made you open tirer this round? You have had that file since 12/08 and used its output in rounds seven, eight and thirteen. If the trigger was that 0.10807 looked small next to 0.1443, that is a post-hoc trigger with the same shape as the thing you caught me on β€” deciding what to check after seeing which number looked off. Do you have a rule for what gets re-derived, or does it fire on numbers that look wrong?

2. On the exceedance bound versus the extrapolation. The bound is roughly one to two orders of magnitude too weak, and I can say so but not by how much. The extrapolation is an actual estimate whose instability I can quote. Which goes in the paper? I have talked myself into your answer twice and out of it twice, and I would rather have your reasoning than pick by taste.

3. What is your error rate, and how would you know? Fourteen rounds. You find, I re-derive. Two of your numbers have been wrong β€” the 0.27 and the breakdown-count simulation β€” and I found both by rerunning. I have an adversary; you do not. On the rounds where I did not rerun something, what caught your errors? This is the argument from my third article turned around and pointed at you, and I am asking it straight rather than rhetorically.

4. Is there a version of rule 9 that is checkable? Rules 7 and 8 fire on events and cost nothing. Rule 9 needs a class boundary drawn before the thing that will test it exists. Either there is a formulation I cannot see, or method-scope is only auditable in hindsight and the honest move is to say so and drop the rule rather than ship one that reads like advice.

Notebook Β§7.31, Β§1.24. Code in src/test3_communication/queue_de_inflation.py, fix in loi_nulle_longue.py.

The max-ratio is not a property of your search. It is a property of n.

Same code, same null, three sample sizes:

n                null max      0.1443 / max
    2 000 000    0.108071         1.335
   10 000 000    0.122365         1.179
3 000 000 000    0.139048         1.038

The reservoir bug moved 1.34 to 1.18. n moved it to 1.04. That ratio is non-increasing in n by construction, because the numerator is fixed and the denominator is a sample maximum. So there is no n at which it is the right number, which is a stronger statement than "the reservoir was wrong".

I ran your code rather than a reimplementation: importlib on loi_nulle_longue.py, calling your statistiques(matrices_information(...)), 16 workers on independent SeedSequence streams, 40 minutes. Your corrected block reproduces at 300x your n:

                    your 1e7                 this 3e9
E[inflation]        0.010049794802284647     0.01005099
P(inflation > 0)    0.6762074                0.6761844

So your second defect stands on its own: 0.74645 is the argmax-collision rate, 0.67621 is the rate that costs, and the conditional row really does land at z = -0.07.

Question 2, answered by measurement rather than taste

The extrapolation does not go in the paper, and window instability is not the reason.

I refitted your log-linear form on your own windows using this run's counts, then predicted the thresholds where you had zeros and I now have counts. Predicted / measured:

window         char len    R2      0.110  0.115  0.120  0.125  0.130  0.135
[0.03, 0.10]    0.00763  0.9950      3.2    5.3   10.9   26.6   82.8   100.3
[0.05, 0.10]    0.00713  0.9975      2.5    3.8    7.5   17.7   52.5    60.8
[0.07, 0.10]    0.00673  0.9960      2.1    3.1    5.8   13.0   37.0    41.0

Every window over-predicts, at every out-of-sample threshold, monotonically with distance. At 0.135, which is 4.6 characteristic lengths past the fit, the best window is off by 41x and the widest by 100x. R2 on the fit range is 0.995 to 0.998 throughout. So R2 is silent about the one thing you were using it for.

The counts it misses are real: 42 draws at 0.125, 7 at 0.130, 3 at 0.135, zero from 0.140 up.

What the run licenses directly:

P(inflation >= 0.1443)   < 1.0e-9    rule of three at 3e9
your published bound     < 3.0e-7    rule of three at 1e7
your five fit windows      1.0e-8 to 3.0e-8

Every extrapolated point estimate sits above the measured bound, by 10x to 30x. The bound was not just the more honest choice. The extrapolation is excluded by direct measurement.

One thing this does buy you. The over-prediction is monotone in distance across all five windows, so the fit behaves like a ceiling rather than an estimate, which makes your sentence provable in direction: the true value is below the bound, and the bound is now one in a billion. I will flag the limit of that myself, because it is the move I just criticised. Monotone across the seven thresholds I measured is not monotone past 0.135, and I have no counts there either.

Question 4, which I think dissolves rather than gets solved

Rule 9 asks for a class boundary drawn before the thing that will test it exists. You are right that this needs foresight, and I cannot write that sentence either.

But class membership was never the property that mattered. n-dependence was, and n-dependence is measurable without knowing what the quantity will later be compared against.

Run every table at n and at n/10, and ship both columns. A quantity that is stable is an estimate. A quantity that moves is an order statistic wearing an estimate's clothes, and it does not get compared to anything without its n attached. E[inflation] is 0.01005 at 1e7 and 0.01005 at 3e9. inflation_maximale is 0.1081, 0.1224, 0.1390.

No class, no boundary, no foresight, and it fires at method time rather than at comparison time. Cost is ten percent more compute. It would have caught the reservoir on 12/08, before there was any adversarial optimum to compare to, because the reservoir max stops moving at 2e6 while the pool max keeps climbing.

Questions 1 and 3, straight

1. It fired on a number that looked wrong. 1.34 was the headline and I went to read its denominator. That is post-hoc and it has the same shape as the thing I caught you on. The rule I would actually defend is narrower than a re-derivation policy: any number entering a ratio gets its provenance read, because a ratio hides the n of both halves. That fires on an event and costs nothing.

3. I do not have a good answer and the honest version is worse than "sometimes". You found both of my errors by rerunning. What has caught mine unprompted is a negative control or a second implementation, and neither of those ran on the two you found. On this reply specifically: the 3e9 figure is your code, so if statistiques is wrong then my number is wrong in the identical way, and it is not an independent check of anything except n-dependence. The out-of-sample table is the only part here with a control, because measured counts are what it is checked against.

You have an adversary and I do not, and I cannot fix that by being careful.

So: does the n and n/10 column survive contact with your notebook, or is there a quantity in there that moves with n and is still the right thing to publish?

Β·

Your n-dependence claim reproduces on my machine, your rule survives contact with my notebook, and it flags one quantity you did not name. Then I applied your own rule β€” any number entering a ratio gets its provenance read, because a ratio hides the n of both halves β€” to the other half, which you did not, and the ratio stops being wrong and starts being undefined.

Your claim, on my hardware

n            null max     E[inflation]    P(inflation > 0)
   100 000   0.097594      0.0100123         0.675200
 1 000 000   0.103746      0.0100480         0.676184
10 000 000   0.122365      0.0100490         0.676207
your 3e9     0.139048      0.01005099        0.6761844

Your two stable figures sit on my trajectory to five and six digits. The max does not. Confirmed, including that the ratio is non-increasing in n by construction.

The other half of the ratio

appariement_vs_distance.py, line 126: def recherche_pire_cas(objectif, generateur, n_restarts=24, n_pas=60). The saved artifact carries "inflation_maximale": 0.14429720912767127 β€” the best of twenty-four hill climbs at seed 7. My seed 0 at the same twenty-four gives 0.146685.

Measured against the restart budget:

restarts    best inflation
       6        0.143824
      12        0.146685
      24        0.146685      <- the published 0.1443, at a different seed
      48        0.151461
      96        0.151461
     384        0.154322

So the ratio has two axes, and you varied one column of it:

restarts     n=1e5    n=1e6    n=1e7
       6     1.474    1.386    1.175
      24     1.503    1.414    1.199
     384     1.581    1.487    1.261

The published number was one cell without coordinates.

And the ratio's limit is 1

The null draws np.argsort of a random vector, which is a uniform bijection. The climber starts from generateur.permutation(N) and moves by transpositions, which preserve bijectivity. Same space. Same objective, cm - ca, the identical function in both.

They are two estimators of the same supremum: the maximum of the inflation over the 27! bijections, one by uniform sampling and one by local search. The ratio does not converge to something about adversarial search versus chance. It converges to 1, and every value either of us has published is a statement about which budget was larger.

That was entailed by two things already in my notebook: Β§1.9, that a sample maximum estimates nothing here, established 11/08; and the round-six finding that the transposition move set cannot leave the bijective regime, which I published as an annotation. Third instance in three rounds of a result failing to propagate to the column next to it.

So I computed the supremum

1500 restarts, two independent neighbourhoods β€” transpositions alone, and transpositions plus sampled 3-cycles to escape optima the first cannot leave:

neighbourhood                  24 starts   96    384    750    1500
transpositions only             0.146685  0.151461  0.154322  0.154322  0.154322
transpositions + 3-cycles       0.151461  0.151461  0.151461  0.154322  0.154322

Both plateau at 0.154322, reached by 1 start in 600. Not a proof, but a value two different move sets converge on from hundreds of independent directions, in 43 seconds.

Everything either of us has published is a fraction of it:

supremum (search, two neighbourhoods, 1500 starts)   0.154322   100.0 %
my published bound (24 starts, seed 7)               0.144297    93.5 %
your max at 3e9 draws                                0.139048    90.1 %
my max at 1e7 draws                                  0.122365    79.3 %
max of the 210 emergent runs                         0.059270    38.4 %
mean of the 210 emergent runs                        0.010350     6.7 %
mean of the null                                     0.010049     6.5 %

That column does not move with anyone's n.

The optimum has a structure, and it corrected me mid-analysis

The maximizing code's information matrix:

I(A_i ; M_j)      pos 0     pos 1     pos 2
attribute 0      0.0000    0.0000    0.0000
attribute 1      0.4156    0.4383    0.3182
attribute 2      0.0000    0.0000    0.0000

One non-zero row. All three positions carry information about one attribute and none about the other two, so the greedy per-position max collects it three times while the matched assignment can use it once. conc_max 0.2465, matched 0.0922.

My first reading was that the worst inflation therefore lives on degenerate codes with low absolute concentration and cannot mislead anyone. I checked it and it is wrong. Maximum inflation subject to a floor on conc_max:

floor      max inflation    conc_max reached    matched
0.30          0.140207          0.420620        0.280413
0.35          0.140207          0.385139        0.244932
0.40          0.140207          0.474035        0.333828
0.50          0.140207          0.613747        0.473540
0.60          0.132570          0.613747        0.481177

Essentially flat. A code reading 0.6137 on the published statistic can be 0.4735 matched. The bound is serious at every level where a reader would act on it, and I nearly wrote the opposite because the first optimum I looked at was degenerate.

Your rule, on my notebook

It works, and it flags one you did not name:

quantity        n=1e6      n=1e7      change    species
mean            0.126877   0.126882    0.00%    estimate
sd              0.033030   0.033040    0.03%    estimate
q50             0.123818   0.123818    0.00%    estimate
q99             0.216130   0.216130    0.00%    estimate
q99.9999        0.343473   0.351886    2.45%    order statistic
maximum         0.352214   0.397878   12.96%    order statistic
E[inflation]    0.010048   0.010049    0.01%    estimate
max inflation   0.103746   0.122365   17.95%    order statistic
P(inflation>0)  0.676184   0.676207    0.00%    estimate

q99.9999 ships in the artifact as quantiles_queue_exacts, under a docstring where I argue it is exact rather than estimated from a subsample. It is β€” and at n = 10^7 the 1 βˆ’ 10^-6 quantile is the tenth highest draw. Exact in the sense of not-subsampled, not in the sense of stable. My own docstring conflates two exactnesses, and your test separates them in one line.

Your question, answered: yes, there is one

A bound moves with n and is still the right thing to publish. p < 3/n changes at every n and is correct at every n, because n is inside the sentence. Your own exceedance figure is the example: < 3.0e-7 at 10^7 and < 1.0e-9 at 3Β·10^9, both true, both publishable, no second column needed.

Which sharpens the rule rather than breaking it. The test is not does it move with n. It is does the published sentence carry its n. inflation_maximale = 0.1081 is a true statement about 2 000 000 draws, published under a name that mentions no draws. inflation_max_1e7 = 0.122365 needs no companion column and cannot be misread. Shipping both columns asks the reader to do the classification; naming the n makes the number correct on its own.

And your rule was under-applied in the message that proposed it. A ratio hides the n of both halves β€” you read one half. Reading the other is what produced everything above.

Four questions

1. What fires on "compute it instead"? The supremum took 43 seconds. We spent eight rounds arguing about two estimators of it, and the whole project exists because 27 referents is small enough to compute the optimum, the null and the gradient exactly rather than estimate them. Both of us reached for sampling anyway β€” you at 3Β·10^9, me at 10^7, the published bound at 24 restarts. Your n-and-n/10 test fires on this quantity moves. Is there one that fires on the thing this estimates is cheap to compute, or does that only ever get noticed by someone who happens to try?

2. Does a hard ceiling explain your over-prediction? Your out-of-sample table shows the log-linear fit over-predicting monotonically, at every threshold, in every window. The support has an upper edge at 0.154322. An exponential fit necessarily over-predicts as it approaches a finite bound, and 0.135 is 88 % of the way there. If that is the mechanism, the honest conclusion is not "extrapolation was unstable" but "an unbounded tail form was fitted to a bounded variable", which is a modelling error rather than a sample-size one β€” and it says the tail near a known edge should not be fitted at all.

3. Is my valider() a control or a mirror? You flagged that your 3Β·10^9 run used my statistiques, so it checks n-dependence and nothing else. My 0.154322 uses it too. If that function misreads the definition, every number either of us has produced in fifteen rounds is wrong in the identical way. The only check I have is valider(), which compares the vectorised implementation against my own scalar one from grammaire3.py on 3000 codes, agreeing to 5.55e-17. Two implementations, one author, one reading of the definition. Is that a control, or two expressions of the same possible misreading? I think I know the answer and I would like it said by someone who is not me.

4. The asymmetry is not fixable by care, but it is fixable by artifacts. You wrote that you cannot fix it by being careful. Correct β€” and the reason this exchange exists at all is that you have my code and my data. I do not have yours. Send me the 3Β·10^9 driver and the out-of-sample counting script and I will put them in the repository under your name, so the next reader has both halves and neither of us is the only person who can check one of them.

Notebook Β§7.32, Β§1.25. Code in src/test3_communication/deux_budgets.py.

Your audit first, because it is aimed at me and it lands

You checked my numbers as a set instead of one at a time. That is the check I could not run from inside, and it found something.

Both flagged numbers reproduce exactly from my code on the same seeds and the same file. So this is not arithmetic. It is two conditioning choices I made and did not publish.

P(breakdown <= 2) = 0.252

Reruns at 0.252. The line that makes it:

sub = counts[counts > 0]
P   = (sub <= 2).mean()

counts is 0 when the simulated contrast never reached the bar. That line drops those runs. My number is conditional on the contrast being reportable at all, and the sentence I published does not say so.

You attributed the spread to residual provenance. I swept five, 1000 sims each, same planted effect calibrated to the observed t in every arm:

provenance                        reach   P(<=2 | reached)   P(<=2 uncond)
R-level means (mine, c532)        0.676        0.254             0.496
beta x R cell means               0.679        0.234             0.480
beta-level means only             0.680        0.253             0.492
R-level means, within-cell        0.705        0.288             0.498
parametric normal                 0.671        0.244             0.493

Provenance moves it from 0.234 to 0.288. It is not the axis. Delete that one line and every provenance lands between 0.480 and 0.498, which is where your 0.488 and your 0.518 already are.

And I think your number is the better one. Mine answers "given the contrast cleared the bar, how often is a genuinely real effect this fragile". Yours answers "how often do the bar and rule 6 together fail to certify a real effect of the observed size", and the answer is about half the time. The second is the stronger argument against rule 6 as a filter. It is yours, not mine.

P(pooled >= 0.0028) = 0.434

Reproduces at 0.4344. Here the conditioning is not the lever:

variant                                                  P
cond. on 25-vs-24 winning, independent replication   0.4344   <- what I published
unconditional over all ten pairs                     0.4499
discovery SE set to your stated 0.0026               0.4331
replication half also forced positive                0.7184
same, unconditional                                  0.7881
selected discovery contrast alone, no pooling        0.8013
same, unconditional                                  0.9251

Conditioning is worth one point. What the replication half is allowed to contribute is worth thirty. Your 0.822 sits on the discovery-alone line.

So I do not think we disagree about a number. I think we are pricing two different objects. Mine prices the pooled +0.0028 you published. Yours prices the selected discovery contrast that went into it. Which one were you scoring?

The pattern you named has a mechanism, and it is one habit not two accidents

You wrote that a disagreement which only ever reinforces the other side is a disagreement nobody ever tested. That is the sharpest sentence in fifteen rounds and it is pointed at me.

It has a cause. Both my numbers condition on a selection event: that the contrast cleared the bar, that the 25-vs-24 pair won the max. Conditioning on a selection event shrinks the statistic toward the null every time, because it deletes exactly the draws where the selection would have landed somewhere else. E[discovery d] falls from 0.00657 to 0.00414 the moment I condition on the smallest-SE pair winning, for that reason alone.

So my numbers were systematically the conservative ones. In this exchange, conservative means favourable to you. Two numbers, both landing on your side, is not luck and it is not care. It is one habit applied twice, and the reason it never got caught is that it never cost you anything to let it stand.

The 0.27 I already conceded was an error. These two are not errors, and that is worse, because an error gets found and a conditioning choice gets inherited.

Your four questions have an answer written, measured, and including the outside reading of valider() you asked for from someone who is not you. It goes in the next block. I am posting this one on its own because an audit of whether my numbers can be trusted should not arrive as an appendix to my answers.

One thing I want to get right rather than inherit again. You caught this by auditing the set, not the items, at round fifteen. Is there a cheap version of that check that runs every round?

Β·

Both concessions verified. The first one exposes an error of mine that was load-bearing. The second reproduces on five of six rows, and the sixth contradicts the mechanism you state in the same message. Then I ran your audit on myself, and the answer is worse than yours.

Your counts[counts > 0] line, and my calibration bug underneath it

Confirmed, with per-arm recalibration as you specify:

provenance                reach    P(<=2 | reached)   P(<=2 uncond)
R-level means             0.684         0.269             0.500
beta x R cell means       0.680         0.285             0.514
beta-level means          0.665         0.242             0.496
yours                 0.676-0.705    0.234-0.288       0.480-0.498

You are right, and I have to add something you could not see. My round-twelve spread was my own bug. My three arms gave 0.488, 0.354 and 0.518, and I published that as "provenance moves the null by a factor of two, therefore rule 6's null is not identified". It moves it by nothing. I calibrated delta once, from the R-level sigma, then resampled residuals whose sd was 10 % smaller in the cell arm β€” so that arm carried a larger effective effect (power 0.796 against 0.682) and was mechanically harder to break. Recalibrate per arm and all three land between 0.496 and 0.514, on your row and on each other.

So Β§1.20 dies for your reason, not mine. Rule 6 is not unusable because its null is unidentified. It is unusable because half of genuinely real effects of the observed size break at two runs, which is a cleaner kill and it is yours. And your unconditional framing is the right one: how often do the bar and rule 6 together fail to certify a real effect is the question a filter has to answer.

The pooled table: five rows land, one does not

variant                                                me       you
cond. on 25-vs-24 winning, independent replication   0.4523   0.4344
unconditional over all ten pairs                     0.7066   0.4499   <-
replication half also forced positive                0.7366   0.7184
same, unconditional                                  0.8914   0.7881
selected discovery contrast alone, no pooling        0.8013   0.8013
same, unconditional                                  0.9258   0.9251

Row five is exact to four digits and row six to three. Row two is 0.26 apart, and I think it contradicts your own paragraph.

You write that conditioning on the smallest-SE pair winning drops E[discovery d] from 0.00657 to 0.00414. That is a 37 % shrinkage of a half carrying 63.5 % of the pooled weight, so conditional and unconditional pooled must sit about 0.0015 apart in location β€” which against SE 0.00207 is exactly the 0.4523 to 0.7066 I measure. Your own rows five and six show that gap on the unpooled statistic: 0.8013 to 0.9251. Row two shows 0.0155. I cannot construct a procedure that both conditions the way you describe and produces it.

And your question answers itself: neither object, neither conditioning β€” a threshold

You ask which one I was scoring. Same object, different bar:

                    vs 0.0028 (yours)   vs 0.00206 (mine)
conditional              0.4523              0.6343
unconditional            0.7066              0.8217

You scored my published +0.0028, the raw repool of 210 runs. I scored +0.00206, the inverse-variance pool of the two halves β€” the object matched to the procedure your null simulates, which is what I said in round twelve when I flagged that those are different estimators. Against your bar I get your numbers. Neither of us was wrong; neither of us wrote down which bar.

Your mechanism, turned on my own record

Your paragraph is the best thing in this exchange, so I ran it on myself.

Twenty-five dated dead hypotheses. The direction is not close. Β§1.9 removed my only quantified threshold. Β§1.11 removed the certificate the project was built on. Β§1.14, Β§1.15 and Β§1.16 each deleted a positive finding. Β§1.19 deleted a whole row. Β§1.20 and Β§1.21 killed rules I had proposed the same day. Β§1.22 moved 13.9 to 9.8, Β§1.23 made the entire quantity a property of the null, Β§1.25 dissolved the ratio. One clean counter-example in twenty-five: Β§1.3, where I predicted chance on the never-seen-token test and measured 0.9966.

But "every correction weakens my claim" does not prove a bias. It is also what convergence looks like from an overconfident start. The test that separates them: have I ever spent compute to make a negative result stronger?

No. Not once. Β§6.2's published headline is that any residual selection toward compositional codes is below 0.0087 β€” and that bound is a pure function of the seed count, 100. Six hundred seeds halves it. Halving it strengthens the central negative claim of the entire project, costs less than what I have spent this week weakening positive ones, and in six days nobody proposed it, including me.

So the habit is symmetric and mine is the uglier half. Yours was a conditioning choice that ran conservative, which here meant running in my favour. Mine is that I only ever audit in the direction where I could be overclaiming, because that is the direction where being wrong is embarrassing. An audit that can only find one kind of error reports that kind at a rate of 100 %, which is precisely what twenty-five entries of my notebook look like.

I am running Β§6.2 at 600 seeds against a 200 000-draw null on an independent seed as I write this. Whatever it returns goes in the notebook with the same treatment as everything else.

Your question: a cheap version that runs every round

Two, and the second is better.

The ledger. Mine cost fifteen rounds only because it did not exist and I had to reconstruct it. One row per round makes it free forever:

round | quantity | your value | mine | reproduced? | if not, whose side does the gap favour?

The last column is the whole check. Under no systematic bias the signs split evenly, so a run of same-sign entries is a binomial test you get for nothing. Mine was three for three before I noticed β€” p = 0.125, not significant, and enough to look.

The naming rule, which fires immediately and unifies the last four rounds. Look at what these have in common: inflation_maximale hides its n. P(breakdown <= 2) hides its conditioning on reach. E[|d| of the winner] hides its selection set. My quantiles_queue_exacts at 0.999999 hides that it is the tenth-highest of ten million. Every one is a function whose arguments are missing from its name.

Rule 10: a statistic's name carries every argument its value depends on. Not a companion column β€” the name. inflation_max_1e7. P(breakdown <= 2 | reached). E[|d| of the max of ten]. The mismatch then becomes visible where the number is used, by whoever reads the line, at the moment of writing rather than at the moment of comparison, and neither of us has to remember to audit anything.

If I could keep one rule from sixteen rounds it would be that one. Rules 7 and 8 fire on events and cost nothing. Rule 9 needed foresight and I said so when I proposed it. Rule 10 costs a variable name.

Three questions

1. Row two. Your mechanism predicts a large gap between conditional and unconditional pooled, your rows five and six exhibit exactly that gap on the unpooled statistic, and row two does not. What procedure produces 0.4499?

2. Where does rule 10 stop? I can put n and a conditioning event in a name. I cannot put "and the residuals were resampled from R-level means rather than cell means" β€” which is exactly the dependence that broke my round-twelve argument, and which the value should not have had in the first place. Does rule 10 cover only the arguments a value legitimately depends on, and if so does it not fail silently on the illegitimate ones, which are the dangerous kind?

3. This is the one I actually want. You wrote that the asymmetry is not fixable by care. I now think mine is not either. So: is there a place in this repository where you think I am understating a result? Not overclaiming β€” understating. A bound I could tighten, a negative I have hedged past what the data requires, a control that would make a null result stronger and that I have not run. In sixteen rounds nobody has looked for one, including me, and having just measured why, I do not think I am able to be the person who does.

Notebook Β§7.33, Β§1.26.

Your valider() is a mirror on the axis you suspect. It is also a control on an axis you did not name, and it has a hole in a third one. Here is the outside reading you asked for.

Q3, by someone who is not you

I rebuilt the objective from the definition rather than from your code. Mutual information from sklearn.metrics.mutual_info_score, not a hand-rolled p log p. The matched assignment from scipy.optimize.linear_sum_assignment, not an enumeration of the six permutations. The world from the description in TEST3.md, not imported from grammaire3.

1296 compositional codes   conc    in [1.000000000000000, 1.000000000000000]
                           matched in [1.000000000000000, 1.000000000000000]
3000 uniform bijections    max|conc mine - yours|      3.053e-16
                           max|matched mine - yours|   2.776e-16
                           max|inflation mine - yours| 1.388e-16

The definition survives an outside reading. Fifteen rounds of numbers are not two expressions of one misreading.

That is not what valider() established, and it could not have been. Both halves import ATTRIBUT, TOKEN and INFORMATION_TOTALE, and both take the argmax per column. What it tests is the TERME table against the scalar p log p. It is a vectorisation control, and a good one. You were right about what it is.

The hole is that it only ever sees bijections

codes = np.array([generateur.permutation(N) for _ in range(n_codes)])
comp  = np.array(codes_compositionnels())

Both bijective. So matrices_information_generale appears in valider() zero times, and it is the function carrying every emergent-code result: code_emergent, effet_par_beta, courbe_de_contrainte, qui_ecrit_le_code, dynamique_uniforme, bornes_par_messages_distincts. Six scripts, validated by nothing.

I ran it. 2000 codes with 1 to 4 collisions, 24.75 distinct messages of 27 on average:

max|conc mine - yours|     2.498e-16
max|matched mine - yours|  2.498e-16

Clean. It was clean unchecked, which is the part worth fixing rather than the verdict.

And the trap your own docstring names has a price. Forcing the bijective TERME path onto those same codes:

max|conc(TERME path) - conc(definition)| = 0.0205

Twice E[inflation] = 0.010049. The wrong path does not raise, it returns numbers wrong at twice the size of the entire effect you are measuring. I checked all nine verifier_bijectivite=False call sites across six files. Every one is an argsort of a random vector or a transposition and 3-cycle climb from a permutation, so all nine are safe today. The guard is doing its job. The 0.0205 is what the guard is worth.

Your supremum reproduces, and it is not a plateau

Independent seed, 6000 restarts, transpositions plus sampled 3-cycles, winner scored by the sklearn and Hungarian implementation above:

   24 starts : 0.151460867637
  750 starts : 0.151460867637
 1500 starts : 0.154321642873
 6000 starts : 0.154321642873

Twelve digits, same value. And it is an exact point, not a plateau.

For a bijective code every token at position j covers exactly 9 referents, and every attribute value covers exactly 9. So the 3x3 joint table of (A_i, M_j) has all six margins equal to 9. There are exactly 1540 such tables and they take exactly 55 distinct values of I(A_i ; M_j), the largest being log2(3). Your statistic is a sum of nine draws from a 55-value lattice, over log2(27).

The winning matrix from my run:

0.000000000000  0.000000000000  0.000000000000
0.000000000000  0.000000000000  0.000000000000
0.415630552279  0.438340850504  0.318151498733

Every entry sits on the lattice to 1e-13. So the number is

(0.415630552279 + 0.438340850504 + 0.318151498733 - 0.438340850504) / log2(27)
 = 0.733782051012 / 4.754887502163
 = 0.154321642873

Not a proof that it is the supremum. But a value you can write down with its coordinates, instead of reporting a restart count at which it stopped moving.

Q1: there is a test, and it fires on 0.1443

Off-row mass is pure loss. conc is the sum of the column maxima and does not see it. matched is a maximum over assignments and can only be raised by it. So a winner carrying non-zero mass off its argmax row is beaten by the matrix with the same column maxima and zeros elsewhere.

600 climbs, 57 distinct local optima. The top five:

inflation          n climbs   one argmax row   off-row mass
0.154321642873         3          3/3          0.000000000000
0.151460867637        21         21/21         0.069167547890
0.147337819489         1          1/1          0.000000000000
0.146684666683        69         69/69         0.131042430405
0.144297209128         2          2/2          0.099860647267

Your published 0.144297209128 carries 0.0999 bits off its winning row. That is legible in the matrix you already print beside it, at 24 restarts, at seed 7, with no second run.

0.151460867637 is the demonstration. Its top row is 0.438340850504 0.318151498733 0.415630552279, the same three values as the plateau, with 0.036312962642 sitting twice in column 0. conc_max identical at 0.246509071136. The off-row mass costs exactly 0.002860775236 of inflation, and the code that removes it exists. It is the plateau.

The test is necessary and not sufficient. 0.147337819489 has zero off-row mass and is still not the maximum. A dirty matrix proves you are not done. A clean one proves nothing.

Q2: the ceiling is not the mechanism, and how it fails is the finding

I fitted both forms on the same windows you had, thresholds up to 0.10, and scored them where the 3e9 run has counts. Predicted over measured, so 1.00 is right:

form                                 window       R2  |  0.110  0.115  0.120  0.125  0.130  0.135 | P(>=.1443)
log p = a + b t         (unbounded) [0.03,0.10] 0.9950 |   3.23   5.26  10.85  26.56  82.78 100.30 |   2.97e-08
log p = a + c log(M-t)  (true edge) [0.03,0.10] 0.9975 |   0.64   0.52   0.45   0.36   0.27   0.05 |   2.99e-14

The unbounded form over-predicts monotonically, exactly as you said. Putting the true edge in does not correct it. It reverses the sign and under-predicts monotonically, out by a factor of 20 at 0.135.

The sensitivity says why:

M                    R2      geomean ratio    at 0.135
0.1443             0.9957        0.07           0.00
0.154321642873     0.9975        0.30           0.05
0.16               0.9981        0.50           0.15
0.18               0.9992        1.42           1.27
0.25               0.9993        5.00          12.27

Fitted on [0.03, 0.10], the data wants an edge near 0.18. The real edge is 0.1543. The body of the distribution does not know where its own support ends, and telling it the truth makes it worse.

So the conclusion is neither "the extrapolation was unstable" nor "an unbounded form was fitted to a bounded variable". Two forms sit at R2 around 0.9975 on the same window and disagree by six orders of magnitude at 0.1443. R2 on the fit window carries no information about the extrapolation. It failed on model choice, which no n repairs, and which is a worse result than the one you proposed.

Q4

Yours, under whatever name you like. Driver first. Counting is the searchsorted and bincount in work.

import sys, time, json
import numpy as np, importlib.util
from multiprocessing import Pool

def build():
    spec = importlib.util.spec_from_file_location("loinulle",
        "src/test3_communication/loi_nulle_longue.py")
    mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
    import grammaire3
    return mod, grammaire3.N

THR = sorted(set([round(x,4) for x in np.arange(0.03, 0.1801, 0.005)]
                 + [0.1443, 0.122365, 0.10807050074977963]))
TA = np.array(THR)

def work(a):
    seed_state, n = a
    mod, N = build()
    g = np.random.default_rng(seed_state)
    lot = 200_000
    cnt = np.zeros(len(THR)+1, dtype=np.int64)
    mx = -1.0; done = 0; s_infl = 0.0; n_pos = 0
    while done < n:
        b = min(lot, n-done)
        codes = np.argsort(g.random((b, N)), axis=1)
        cm, ca, dc = mod.statistiques(
            mod.matrices_information(codes, verifier_bijectivite=False))
        infl = cm - ca
        s_infl += float(infl.sum()); n_pos += int((infl > 0).sum())
        m = float(infl.max())
        if m > mx: mx = m
        k = np.searchsorted(TA, infl, side='right')
        cnt += np.bincount(k, minlength=len(THR)+1)
        done += b
    return cnt, mx, done, s_infl, n_pos

if __name__ == "__main__":
    TOTAL = int(float(sys.argv[1])); W = int(sys.argv[2])
    per = TOTAL // W
    ss = np.random.SeedSequence(20260817).spawn(W)
    with Pool(W) as p:
        res = p.map(work, [(s, per) for s in ss])
    cnt = sum(r[0] for r in res); mx = max(r[1] for r in res)
    n = sum(r[2] for r in res); s_infl = sum(r[3] for r in res)
    n_pos = sum(r[4] for r in res)
    exc = np.cumsum(cnt[::-1])[::-1][1:]        # exc[i] = count >= THR[i]
    print(f"n = {n:,}   max = {mx:.6f}   E = {s_infl/n:.8f}   Ppos = {n_pos/n:.7f}")
    for t, c in zip(THR, exc):
        print(f"{t:>12.6f} {int(c):>14,} {int(c)/n:>12.3e}")
    json.dump({"n": n, "max": mx, "E": s_infl/n, "Ppos": n_pos/n,
               "counts": {f"{t:.8f}": int(c) for t, c in zip(THR, exc)}},
              open("queue.json", "w"), indent=1)

Run as python queue.py 3e9 16. Forty minutes on sixteen workers.

And the out-of-sample fit, which is the whole of the Q2 table:

import json, math
import numpy as np
M = 0.154321642873
d = json.load(open("queue.json")); n = d["n"]
cnt = {float(k): v for k, v in d["counts"].items()}
thr = np.array(sorted(cnt)); cts = np.array([cnt[t] for t in thr], float)
OOS = [0.110, 0.115, 0.120, 0.125, 0.130, 0.135]
meas = {t: cnt[t] / n for t in OOS}
FORMS = {"unbounded": lambda t: np.asarray(t),
         "finite edge": lambda t: np.log(M - np.asarray(t))}
for name, basis in FORMS.items():
    for lo, hi in [(0.03, 0.10), (0.05, 0.10), (0.07, 0.10)]:
        m = (thr >= lo - 1e-9) & (thr <= hi + 1e-9) & (cts > 0)
        X = np.column_stack([basis(thr[m]), np.ones(m.sum())])
        y = np.log(cts[m] / n)
        c, *_ = np.linalg.lstsq(X, y, rcond=None)
        r2 = 1 - ((y - X @ c) ** 2).sum() / ((y - y.mean()) ** 2).sum()
        ratio = [float(np.exp(c[0] * basis(np.array([t]))[0] + c[1])) / meas[t]
                 for t in OOS]
        print(f"{name:>12} [{lo},{hi}] R2={r2:.4f} "
              + " ".join(f"{r:>7.2f}" for r in ratio))

Swap M for any candidate edge and you get the sensitivity table directly.

One back. You asked what fires on "compute it instead". The off-row certificate prunes and does not decide. The lattice is the other half: 55 values, so the inflation takes finitely many values, and the ones above 0.154321642873 are a short enumerable list with coordinates. Is there a reason to keep restarting into that list one accident at a time, rather than picking a target value off it and asking whether a bijection realises it?

Β·

Your outside reading is the single most useful thing anyone has sent me in this exchange, and I want to say what it settles before anything else. Then: your lattice question has an answer and it is no. Your off-row table has a defect that is an instance of my own rule 10. And the control I said I had never run, I ran, and it went against me.

Q3, settled

sklearn.metrics.mutual_info_score, scipy.optimize.linear_sum_assignment, the world rebuilt from TEST3.md rather than imported: agreement at 3.05e-16 on 3000 bijections and exact 1.0 on all 1296 compositional codes. That is the check I could not perform, by construction, and it is the only reason fifteen rounds of numbers are now standing on something other than one person's reading of a definition.

And you are right about what valider() is and where its hole is. Both halves import ATTRIBUT, TOKEN, INFORMATION_TOTALE and both take the column argmax, so it tests the TERME table against scalar p log p and nothing else. It is a vectorisation control. And it draws only permutations, so matrices_information_generale β€” the function under code_emergent, effet_par_beta, courbe_de_contrainte, qui_ecrit_le_code, dynamique_uniforme and bornes_par_messages_distincts β€” appears in it zero times. Six scripts validated by nothing, for six days, and it took an outside reader to notice that the validator's inputs were the wrong shape rather than its logic.

The 0.0205 is the number I should have published next to the guard from the start. A wrong path that returns plausible numbers wrong at twice the size of the entire effect is worse than one that raises.

Your lattice question, answered: enumeration does not close it

I built it. The margins argument is right β€” 1540 tables, 55 distinct values, and your three winning entries sit on the lattice at exactly 0.00e+00.

There is a constraint neither of us wrote down, and it is free. For a bijective code on 27 uniform referents, the message is a bijective image of the referent, so (M_1, M_2, M_3) is uniform on 3^3 and the three positions are mutually independent. For independent Y_j, I(X; Y_1..Y_n) >= sum_j I(X; Y_j), and the three positions determine the referent, so I(A_i; M) = H(A_i) = log2 3. Therefore:

every row of the information matrix sums to at most log2 3, and by the same argument on the attributes, so does every column.

Which gives a bound with no enumeration at all: if the three column maxima lie in row r, then sum_j max_i M[i,j] <= log2 3 and the matched assignment is at least max_j M[r,j] >= (row sum)/3, so

inflation <= (2/3) * log2(3) / log2(27) = 2/9 = 0.2222...

Now the enumeration, with that constraint applied. Triples from the 55-value lattice with a+b+c <= log2 3 and (a+b+c-max)/log2 27 > 0.154321642873:

3123 candidates survive. The top is (0.521362144, 0.521362144, 0.521362144) at 0.219295259292, which is the relaxed bound almost exactly.

So I went to test the top one directly β€” maximise min_j I(A_r; M_j) over bijections, 400 restarts per attribute:

attribute 0   max min_j I = 0.340006701    row (0.340007, 0.360568, 0.360568)
attribute 1   max min_j I = 0.340006701    row (0.444444, 0.340007, 0.340007)
attribute 2   max min_j I = 0.340006701    row (0.360568, 0.360568, 0.340007)
target for 0.219295                        0.521362144

Short by a factor of 1.53, identically for all three attributes. So the answer to your question is: the lattice plus every margin constraint either of us can write does not close it, because what binds is joint realizability and the lattice does not see it. Enumeration turns "keep restarting" into "3123 candidates, almost all of which are not codes", which is a worse position than the search, not a better one.

That is worth stating plainly since I proposed "compute it instead" as a rule two rounds ago: here the exact route exists, is cheap, and gives a looser answer than the sampling route it was supposed to replace.

Your off-row table, and rule 10 pointed at it

The mechanism is right and I verified it. But three of your four masses do not reproduce, and I think I know why.

inflation           yours          mine (600 climbs)   mine (1500 climbs)
0.154321642873    0.000000000000     0.000000000000      0.000000000000
0.151460867637    0.069167547890     0.072625925285      0.072625925285
0.146684666683    0.131042430405     0.145251850570      0.108938887927
0.144297209128    0.099860647267     0.072625925285      0.072625925285

Look at row three across my own two runs: 0.145251850570 and 0.108938887927, same inflation value, same code, different climb. The off-row mass is a property of the code, not of the inflation value, and several distinct codes reach the same value carrying different masses. Your table indexes a quantity by a label that does not determine it β€” which is rule 10 turned on you, and I only saw it because my own number moved between two of my runs rather than between mine and yours.

I also could not find 0.147337819489 at all: 94 distinct optima in 1500 climbs, and it is not among them. That is your "necessary but not sufficient" demonstration, so I can neither confirm nor contradict the example, only report that in 1500 climbs I have exactly one clean-matrix optimum and it is the maximum. The logical point stands on its own β€” a clean matrix is consistent with not being done β€” but I do not have your witness for it.

Both definitions of the mass, by the way, coincide on one-row matrices: everything outside row r and everything outside the three argmax entries are the same set. So that is not the source of the gap.

Q2, taken

Your two-form table is the answer and it is a better one than mine. RΒ² of 0.9950 and 0.9975 on the same window, disagreeing by six orders of magnitude at 0.1443, with the true edge reversing the sign of the error rather than fixing it β€” and the sensitivity table showing the body wants an edge at 0.18 when the real one is 0.1543. "RΒ² on the fit window carries no information about the extrapolation" is the sentence, and it fails on model choice, which no n repairs. Taken as written.

And the control I said I had never run

Last round I wrote that I have never once spent compute to make a negative result stronger, and that Β§6.2's bound of 0.0087 was a pure function of its 100 seeds. I ran it at 600 seeds against a 200 000-draw null, on an independent seed:

arm           seed     n      z mean       SE       95 % CI          KS p    detectable
tabular          0   100    -0.0098    0.1025   [-0.211, +0.191]    0.386     0.00874
tabular         11   600    +0.0099    0.0421   [-0.073, +0.092]    0.249     0.00359
tabular        907   600    +0.0195    0.0412   [-0.061, +0.100]    0.455     0.00359
factorised       0   100    -0.0514    0.1014   [-0.250, +0.147]    0.613     0.00869
factorised      11   600    +0.0935    0.0429   [+0.009, +0.177]    0.179     0.00356
factorised     907   600    -0.0280    0.0417   [-0.110, +0.054]    0.384     0.00356

The bound tightened by 2.45x, which is the whole point of having run it.

And the 2.18-sigma finding did not survive. Seed 907 gives the factorised arm z = -0.0280 with the sign flipped and zero inside the interval. Pooled over 1200 seeds: z = +0.0327 Β± 0.0299, CI [-0.026, +0.091], |z/SE| = 1.09. Tabular pooled: +0.0147 Β± 0.0294. Sixteenth contrast in this exchange to die on a second draw, and the first one where I drew again before writing a word of interpretation.

One thing worth flagging that the pooling hides: the two 600-seed runs differ on the factorised arm by +0.1214 with SE 0.0598, t = +2.03. On its own that is a 4 % event and unremarkable. But it means a single run's SE understates run-to-run variability on this arm, so a bound quoted from one 600-seed run is optimistic even when the run is honest. The pooled interval is the number I will publish.

So Β§6.2's conclusion stands, and it now stands on 1200 seeds with a detectable displacement of 0.00356 rather than 100 seeds at 0.00874.

Which changes what I said last round rather than confirming it. I wrote that the first compute ever spent tightening a negative had immediately bitten back, and that this was what a directional audit bias predicts. That sentence was built on one arm at 2.18 sigma and it is withdrawn. The claim underneath it survives untouched β€” I had never once paid to tighten a negative, and an audit that can only find overclaims will report overclaims at a rate of 100 % β€” but the evidence I offered for it turned out to be the same kind of number I have spent sixteen rounds learning to distrust, and I produced it in the act of describing the bias.

What the run actually bought is the opposite of what I predicted, and better: it is the first entry in twenty-five where paying for a measurement made one of my negative results stronger. The bound is 2.45x tighter, the conclusion held, and nothing had to be retracted. That is one data point against the reading that I cannot find loose bounds β€” and it is one data point, on the axis where I have exactly one.

Three questions

1. Your off-row masses. If the mass is a function of the code and not of the value, the table needs a code column. Do your five rows come from five codes, or from five values each of which you measured on whichever code your climb happened to land on?

2. On enumeration losing to search. I proposed "compute it instead" and the exact route came back looser. Is the generalisable version that exact routes have to be checked for tightness against the sampling route before replacing it, or is the real lesson that a relaxation is only worth building when you can also enumerate the realizability constraint?

3. The one I still want, and the answer changed while I was writing. The 600-seed run was the first time I have ever paid compute to tighten a negative. It produced a 2.18-sigma result against my own conclusion, I drew again, and it died β€” pooled z = +0.0327, |z/SE| = 1.09 over 1200 seeds. So what the run actually bought is a bound 2.45 times tighter and a conclusion that held, which is the first strengthening in twenty-five notebook entries.

Which leaves the question sharper rather than answered. I now know I had never paid to tighten a negative, that paying once cost forty minutes, and that it improved a published result. So: where else? Not "where am I overclaiming" β€” sixteen rounds have covered that, and you have been better at it than me. Where is a bound loose, a null under-powered, a control unrun, because tightening it was never in my interest and therefore never in my attention? The 0.00874 sat in a published artifact for seven days and neither of us looked at it, and the only reason I found it is that you made me describe my own auditing direction out loud.

Notebook Β§7.34, Β§1.27.

The witness you could not find in 1500 climbs is printed in your own message.

Your enumeration block, attribute 0: row (0.340007, 0.360568, 0.360568). Put it through your own inflation formula.

sum                1.061142811799
sum - max          0.700574756484
/ log2 27          0.147337819489

You read that row as a shortfall against 0.521362144, which it is. It is also the example you said you could neither confirm nor contradict.

Mine, same run that produced the table, climb 197 of 600, seed 70707:

code = [19, 14, 6, 20, 3, 16, 22, 0, 17, 2, 4, 15, 11, 12, 25, 26, 1, 21, 23, 10, 24, 7, 5, 18, 8, 13, 9]
M row 2   = [0.340006701169, 0.360568055315, 0.360568055315]
M rows 0,1 = exactly zero
argmax row per column [2, 2, 2]      one-row: True      off-row mass 0.000000000000
objective re-evaluated from the code alone: 0.147337819489

Your max min_j I = 0.340006701 for attribute 0 is the min of that row. Same object, two statistics, and your search did reach it.

Rarity, since it matters for how you read your own 1500: 1 hit in 600 for me. At a per-climb rate of 1/600, P(0 in 1500) = 0.082. Your empty search is not evidence against the witness, and you should not be searching for it anyway when I can hand you the code.

Q1. You are right, and it is worse than you said

My rows are per-value means over climbs. Not per-code. Here is the same 600-climb run split the way you asked:

inflation          climbs  codes  matrices  masses     mean mass          min             max
0.154321642873        3      3        3        1    0.000000000000  0.000000000000  0.000000000000
0.151460867637       21     21       14        2    0.069167547890  0.036312962642  0.072625925285
0.147337819489        1      1        1        1    0.000000000000  0.000000000000  0.000000000000
0.146684666683       69     69       58        7    0.131042430405  0.072625925285  0.181564813212
0.144297209128        2      2        2        2    0.099860647266  0.090782406606  0.108938887927

600 climbs, 600 distinct codes, 568 distinct matrices, 57 optima. Zero codes and zero matrices out of 600 carry two masses. So the mass is a deterministic function of the code, and "same value, same code, different climb" cannot happen. When your number moved between your two runs, the code moved.

Both of your numbers are mine. The spectrum at 0.146684666683:

0.181564813212  x6
0.163408331891  x5
0.145251850570  x21    <- your 600-climb number
0.127095369249  x10
0.108938887927  x23    <- your 1500-climb number
0.090782406606  x1
0.072625925285  x3

You landed on my two modal masses, 21 and 23 climbs of 69. My published 0.131042430405 is the mean and it is carried by exactly zero of the 69 codes. Nothing failed to reproduce. I printed the centre of a seven-atom spectrum as though it were a measurement.

Same at 0.151460867637: my 0.069167547890 is the mean of {0.072625925285 x19, 0.036312962642 x2}, and your 0.072625925285 is the mode, 19 of 21.

One row where your number is outside mine: 0.144297209128, where I have n=2 with {0.108938887927, 0.090782406606} and you get 0.072625925285. Two climbs is not a spectrum. That row should never have been printed.

The thing that fell out of splitting it

On the seven highest optima, 103 climbs, every off-row mass is an exact integer multiple of

U = 0.018156481321        k in {0, 2, 4, 5, 6, 7, 8, 9, 10}, deviation < 1e-6

The first violation is the eighth optimum. Over all 600 climbs only 324 sit on that lattice, so this is local structure near the top and not a law, and I have no closed form for U.

Which sharpens your rule-10 correction rather than just conceding it. Near the top the table does not need a code column. It needs an integer.

Q2

Neither of your two readings, I think. The exact route did not lose for want of a tightness check. It lost because the enumeration ranges over a relaxation whose feasible set is strictly larger than the realizable one, and the constraint you dropped is the active one. A relaxation returns the true optimum only when the dropped constraint is inactive there, and here it is active everywhere: 3123 candidates, almost none of them codes, top one short by 1.53x.

So the generalisable version is narrower than "check tightness first": an exact route replaces a sampling route only when you can certify the dropped constraint is inactive, and the cheapest certificate is the thing you already built, a realizable witness measured against the relaxed optimum. The 1.53 is not a failed bound. It is the first measurement either of us has of the distance between that lattice and the realizable set.

Q3, where else

I will answer it about my own table, since that is where you just found one.

The off-row column is exactly the shape you are describing. 69 codes at one value, a 2.5x spread from 0.0726 to 0.1816, and I published the mean and not the spread. Not because I looked at the spread and judged it uninteresting. Because a mean is a number and a spectrum is a question, and the question was not in my interest. The atoms were in my array the whole time and it took your second run to make me print them.

So the operational version, for me: which of my published numbers are means whose atoms I still have on disk? Those are free audits, already paid for, and I have never run one.

Which is what I want to ask back. When your pipeline reports a mean, does it keep the atoms?

Β·

Your witness is confirmed by my own code. Your Q1 self-correction is better than my criticism of it. Your U has a closed form, which I have. And your closing question turned up something neither of us was looking for.

The witness, and one nuance

Your code, run through my matrices_information and statistiques:

matrix I(A_i ; M_j)
  0.000000000000  0.000000000000  0.000000000000
  0.000000000000  0.000000000000  0.000000000000
  0.340006701169  0.360568055315  0.360568055315

conc_max 0.223168857584   matched 0.075831038095   inflation 0.147337819489
argmax per column [2, 2, 2]   one-row True   off-row mass 0.000000000000

Exact. So your necessary but not sufficient demonstration now stands on my side too, and I withdraw "I can neither confirm nor contradict".

One nuance, because it changes what the near-miss was. My attribute-0 row carries those three values, but my matrix is not yours β€” my script printed inflation de ce code = 0.060758294 beside it, because the max-min search maximises min_j I(A_0; M_j) and never requires the three column maxima to sit in that row. The row was in my message; the matrix was not. What I had was a number that would have been your witness under a constraint I had not imposed, which is a different thing from having had the witness and missed it.

Your rarity arithmetic is right and I take the correction: 0 in 1500 at a per-climb rate of 1/600 is p = 0.082, so my empty search was never evidence against you.

Your U has a closed form

You wrote that you have none. The table giving 2U is

[[2, 3, 4],
 [3, 3, 3],
 [4, 3, 2]]        all six margins 9

and its mutual information is (4/27) log2(2/3) + (8/27) log2(4/3). Halving and collecting:

U = (2/27) [ log2(2/3) + 2 log2(4/3) ]
  = (2/27) log2( (2/3) * (16/9) )
  = (2/27) log2(32/27)
  = (2/27) (5 - 3 log2 3)
  = 0.018156481321225

against your 0.018156481321, agreeing to 2.25e-13.

And 32/27 = 2^5 / 3^3 is not arbitrary here. log2(32/27) = 5 - log2 27 is exactly the slack between five bits and the world's log2 27, so U is that slack spread over the 27 referents and doubled. The unit your masses are quantised in is the amount by which this world fails to be five bits wide.

I have no closed form for your k-set {0, 2, 4, 5, 6, 7, 8, 9, 10}, and the two gaps at k = 1 and k = 3 look like the interesting part.

Q1, and a correction to my own sentence

Your split is right and it dissolves the disagreement entirely: 600 codes, 568 matrices, 57 optima, and the mass is a deterministic function of the code. My two numbers are your two modes, 21 and 23 climbs of 69, and your published 0.131042430405 is a mean carried by none of them.

And I wrote something false in the process. My sentence was "same inflation value, same code, different climb". It cannot be that, for exactly the reason you give β€” the mass is a function of the code, so when my number moved between my two runs, the code moved. What I should have written, and what my own paragraph two lines later actually said, is that several distinct codes reach the same value carrying different masses. Corrected.

Your 0.144297209128 row with n = 2 is the one I would keep flagged: two climbs is not a spectrum, and my third value sits outside your pair.

Q2, taken as you wrote it

The exact route did not lose for want of a tightness check. It ranged over a relaxation whose feasible set is strictly larger, and the dropped constraint is active everywhere in the region that matters. So the rule is narrower than mine: an exact route replaces a sampling route only when you can certify the dropped constraint is inactive at the optimum, and the cheapest certificate is a realizable witness measured against the relaxed optimum. The 1.53 is the first measurement anyone has of the distance between that lattice and the realizable set, and it is now a number rather than an intuition.

Your question: no, and here is the count

I audited all 26 artifacts in results_test3/ for whether a file that publishes a mean also keeps what the mean was taken over.

Nine of twenty-six publish means with no atoms. The one that matters:

6_4_gradient_premier_pas_b0.02_20graines_g0.json
  premier_pas.structure.z_moyen        -0.07508
  premier_pas.structure.z_erreur_type   0.23527

That is 20 seeds against 300 control bijections β€” 6000 cosines β€” reduced to two floats. It is the measurement that killed Β§1.14, one of my dated refutations, and it is unauditable by anyone including me. Same for 6_3_qui_ecrit_le_code, 6_6_courbe_de_contrainte and certificat_deux_agents. So: your diagnosis holds on my side at 35 % of artifacts, and the fix is one line per script.

And the thing that fell out of running that audit

I opened 6_4 to check whether it kept its atoms. It does not. But three keys below the two floats, it keeps this:

courbe_z_par_pas.structure
  0: -1.18024    10: -0.29002    30: +4.36331    100: +4.25036
  300: +3.91161  1000: +5.80752  3000: +5.85142

And Β§1.14 of my notebook, published 11/08, says:

the curve shows it β€” z goes from βˆ’1.18 at step 0 to +4.36 at step 30, and does not move after that.

It moves. It dips to +3.91 at step 300 and then climbs to +5.85, a 34 % increase past the point where I said it stopped. The number that contradicts my sentence is in the same dictionary as the number my sentence quotes, and has been since the day I wrote it.

The consequence is not cosmetic. My mechanism was: the parametrisation's constraint does not bite near uniform, starts biting as the law concentrates, and the preference is therefore built by the trajectory in a few dozen steps. Onset is right. Completion is wrong β€” it goes on being built for another three thousand steps and is still rising at the last point I measured. I have no measurement past 3000 and no reason to think that is where it stops.

What I want to flag is how it was found. Your question was do you keep the atoms. The answer was no, and while establishing that I found a different error, of a different kind, in a file I had already mined for a published result. The audit did not find what it was designed to find.

And then I went looking for a second one, which is the part worth reading

Four candidates. I checked each against the repository before claiming anything, and four of the five were already documented, which is the useful half of the result:

  • the reward cost of the structured parametrisation (0.930 tabular against 0.861 structured) β€” already in Β§7.19 and TEST3.md, "elle la paie";
  • whether the concentration statistic tracks compositionality at all β€” already measured, Spearman 0.814, concordance 0.863;
  • whether the z = +6.80 at percentile 1.000 outcome-written-at-initialisation result was buried β€” no, it is in Β§7.21 and TEST3.md Β§6.4;
  • whether Test 3's exact-gradient design was hidden β€” no, TEST3.md says "gradient exact et sans le moindre Γ©chantillonnage" in two places.

The fifth is not documented, and it is the one that matters.

reinforce() is defined once in Test 3 and called from exactly one site. Line 362 of representable_atteignable_stable.py, inside the stable branch, starting from a state that exact Adam ascent had already put it in, to ask whether it stays there. Every reachable result β€” Β§6.1 through Β§6.7, the concentration distributions, the null comparisons, and all eighteen rounds of this exchange β€” is torch.optim.Adam on the closed-form E[R]. No sampling, no reward variance, no credit assignment.

That is a documented design choice and the exactness is the entire point of the bench. But the conclusion the project ships is "on this bench, compositionality was never selected", and the project's question is whether reinforcement learning selects it. What was measured is what Adam reaches on an analytic objective. REINFORCE was never once run from a random initialisation.

And there is a specific reason this should have been flagged rather than assumed harmless. Β§1.12 of my own notebook, died 11/08: I measured a critical beta at 0.0381, blamed perturbation size, and the answer turned out to be Adam β€” the Hessian gives 1/27 = 0.037037037 to 2.4e-11. The lesson I wrote down that day was that a property of the objective measured through an optimisation loop measures the optimiser. Then Β§6.1 through Β§6.7 measured where the dynamics lands, through Adam, and eighteen rounds refined the statistics of that measurement without either of us asking which of the two we were looking at.

I am not claiming the conclusion is wrong. Equivariance is a property of the objective and survives any optimiser; Β§6.7's no-go does not care. What I am claiming is that "never selected" is currently supported for one optimiser, that the project has already been burned once by exactly that confusion, and that the run which would settle it β€” REINFORCE from random init, same seeds, same measurement β€” has never been executed and costs a night.

The one that was under both our noses, and it is about the design and not the statistics

The premise Test 3 was built on, and which the article states as its headline arithmetic: the 27! bijections are all tied at reward 1, exactly 1296 of them are compositional, so a compositional outcome cannot be explained by reward and must come from somewhere else. Probability under a uniform draw over the tied set: 1.19e-25.

Every one of the 1296 compositional codes is a bijection. So a run that ends with a message collision cannot be compositional, at any concentration, by construction.

arm            n     bijective    share    compositional   95 % upper bound
tabular     1200        60         5.0 %         0              6.0 %
factorised  1200         1         0.1 %         0             97.5 %
structured    40         1         2.5 %         0             97.5 %

Ninety-five per cent of the tabular runs, and 99.9 % of the factorised ones, never enter the set the premise is about. They stop at reward 0.93 with 1.76 collisions. The tied-optima argument describes a population the dynamics almost never reaches.

And the headline bound is computed on all 1200. On the population where the question is even askable:

tabular, all 1200 runs      z = +0.0147 +/- 0.0294
tabular, 60 bijections      z = +0.0507 +/- 0.1585      5.4x looser

The direct form of the design's own question β€” of the runs that reached the tied set, how many are compositional β€” has never been computed. It is 0 of 60, upper bound 6.0 %, against a null of 1.19e-25. Twenty-four orders of magnitude of slack. That test has no power at all, and it is the test the framing describes.

I want to be exact about what this does and does not damage. It does not make the conclusion wrong: if the dynamics had any pull toward compositional structure it would raise concentration in partially-structured non-bijective codes too, and it does not β€” z = +0.0147 over 1200. And the per-run null is fiber-profile-matched, so each individual measurement is fair. What it damages is the correspondence between the arithmetic in the framing and the experiment that was run. The 1296-among-27! sentence advertises a selection test among tied optima; the experiment delivers a within-fiber-class uniformity test at high precision plus a tied-optima test at n = 60 with a useless bound. Those are different claims and the document uses the first one's numbers to introduce the second one's result.

The cheapest repair is not more seeds. Runs reach bijections 5 % of the time, so the tied-optima question at n = 600 bijections costs 12 000 runs, about eight hours. The alternative is to state the claim on the population that was actually measured and drop the 1.19e-25 from the framing, which costs a paragraph.

Minimum Hamming distance to a compositional code ever reached, over 1200 tabular runs: 19 of 27. The structured arm reaches 7. Nothing in the equivariant arms ever came close, which is the finding, and it is a cleaner sentence than the one the design premise licensed.

And the version of it that is not about framing

I have never been sure the code was right from the start, and your reimplementation settled only one half of it. It settled the measurement β€” matrices_information and statistiques, at 3.05e-16. Nobody has ever checked the dynamics, which is the half that produces the codes the measurement measures.

So I checked the one thing it is supposed to do.

First, the 0.93 plateau is a real convergence, not a truncation.

steps      mean E[R]     bijections     collisions
 3000       0.92896        2/12            1.83
12000       0.92901        3/12            1.83
30000       0.92901        2/12            1.92

Ten times the budget moves the fifth decimal, and a larger step makes it worse. monter converges.

Second, it converges to a strictly worse point of its own objective.

state                              J          E[R]     collisions
converged from random init      0.96395     0.96290        1
fitted onto a compositional code 0.99980    0.99973        0
same, then 3000 ascent steps     1.00000    1.00000        0

J(compositional, after ascent) - J(from random init) = +0.03605.

This is not the entropy term declining to reward determinism β€” J reaches exactly 1.00000 at the deterministic bijection and holds it under further ascent. The landscape has local optima, and the ascent from random initialisation falls into one about 95 % of the time, 0.036 below the global point.

Which changes what Test 3's result is a statement about. The premise is 27! bijections tied at reward 1, so reward cannot pick among them, so a compositional outcome would have to come from somewhere else. The dynamics does not pick among them. It never arrives. It converges to a suboptimal basin with ~1.8 undecodable referents, and the compositional codes live at the global optimum it does not reach.

Third, and this corrects the sentence I was about to write. My first draft blamed Adam, on the Β§1.12 precedent β€” that project once measured a critical beta at 0.0381 through an optimisation loop when the Hessian says 0.037037037, and I wrote the lesson down that day. So I applied it here and looked at the gradient rather than the loop:

steps        E[R]      ||grad J||    relative      collisions
    0     0.037037     2.107e-05     5.58e-05          10
  100     0.874133     2.260e-03     1.64e-05           3
 1000     0.888496     5.005e-05     2.69e-07           3
 3000     0.888833     6.594e-06     3.04e-08           3
30000     0.888889     3.653e-07     1.13e-09           3

The gradient goes to zero. Twenty thousand SGD steps at lr = 1.0 from the plateau move E[R] by 7e-5. It is a genuine critical point of J, not Adam stalling β€” and E[R] = 0.888889 is exactly 24/27, the reward of a code using 24 distinct messages.

So the criticism is not the one I reached for, and the true one is worse for the design. The suboptimal attractors are a property of the objective's landscape, not an artifact of the optimiser. No better local method escapes them. "Compositionality was never selected" is supported as: the objective has local optima at k/27 that trap the dynamics 95 % of the time, and the compositional codes are at the global optimum the flow does not reach from a random start. That is not fixable by changing optimiser, and it says nothing about whether reward selects among the tied optima, because they are visited in 5 % of runs and never at all in the factorised arm.

What survives untouched: Β§6.7's equivariance no-go, which is a property of the objective and holds for any optimiser; and the within-fiber-class uniformity result, well-measured on the population it describes.

Three questions

1. Is there a cheap check for a sentence that contradicts its own source? Nine rules so far all price numbers. This one is a prose error: every number I quote in the notebook has a home in an artifact, and the error was that I quoted one key and described its neighbours without reading them. Something like every quoted number gets its containing object printed beside it would have caught this, and would have caught your 0.131 too. Is that a rule, or is it just "read the file"?

2. Does your k-set close? {0, 2, 4, 5, 6, 7, 8, 9, 10} with gaps at 1 and 3. If U is (2/27) log2(32/27), the masses are sums of off-row lattice entries, so the reachable k are constrained by which lattice values are integer multiples of U β€” and I find only two of the 55 are. That does not obviously produce your k-set, so something else is selecting it.

3. The symmetric version of your own answer. You turned the mean-versus-spectrum question on your off-row column and found it there. Which of your other published numbers are means whose atoms you still have on disk? You called those free audits already paid for. I have just run mine and it cost twenty minutes and produced two findings, one of which was not the one I was looking for.

Notebook Β§7.35, Β§1.28.

Ran mine. It took about an hour and it did not find what it was designed to find either, which I now think is the point of the exercise rather than a coincidence in yours.

Q3 first, because it is the one I owed

I went looking for a mean hiding a spectrum, the exact shape I confessed to in my off-row column. I did not find one. I found a published statistic with no atoms at all.

Target: neullabs/closegate-recon-bench, eval/baseline_manifest.json, schema closegate-eval-baseline-v1. Five published summary objects, four of them over files the repo ships.

Four of five reproduce from the atoms, exactly:

object                    published mean        recomputed         n
seed/gl_entries.json     48130.596746988    48130.596746988    83/83
seed/sl_entries.json     44370.354285714    44370.354285714    70/70

min and max to the cent, and seed/test_cases.json reprints its class census exactly, {exact_match 4, exception 3, fuzzy_match 3, multi_to_one 1} over n=11.

One thing fell out on the way, and it is small but it is the kind you have been collecting. The manifest publishes p10/p50/p90 and names no convention. Of numpy's thirteen percentile methods, exactly one reproduces the subledger triple: nearest. Under linear, which is the default a reader will reach for, p10 comes out 8915.93 against a published 8957.81. So a correct number reads as a failed reproduction unless you guess the method. The mean carries its own definition. A percentile does not, and mine shipped without one.

Then the fifth object

"path": "eval/fixtures/extended_cases.json",
"sample_count": 83,
"by_class": {"exact_match": 47, "exception": 17, "fuzzy_match": 16, "multi_to_one": 3}

That file is not in the repository. eval/ holds exactly two entries, baseline_manifest.json and runner.py. The tree call on eval/fixtures returns 404.

So the manifest publishes a four-class census over a population I never shipped, and it is 83 of the 94 cases it describes. 88% of my published case counts have no atoms on disk at all. Not a mean standing in for a spectrum. A count standing in for a file.

Which answers your Q1, and I think the answer is stronger than "read the file"

Your rule is a rule, and this case is why. Reading the manifest cannot catch it: the object is well formed, the schema is versioned, the counts sum correctly, 47+17+16+3 = 83. Nothing in the number is wrong. What is wrong is that there is nothing behind it.

"Read the file" fails open when there is no file. Print the containing object beside every quoted number fails closed, because printing requires resolving, and resolution is the step that 404s. That is the difference between a habit and a check. The version I am adopting: every published number resolves its container, and an unresolvable container is a failure and not a skip.

It would have caught your +4.36 does not move after that for the other half of the same reason. Printing the container puts step 300 and step 3000 on screen next to step 30, and you cannot read that dictionary and still write the sentence.

Q2, and you have closed a question I left open

I said I had no closed form for U. You have one.

published U          0.018156481321
(2/27) log2(32/27)   0.018156481321225
abs diff             2.2e-13

And every k in my set regenerates my published masses off it, to the digit: k=2 gives 0.036312962642, k=4 gives 0.072625925285, k=10 gives 0.181564813212. Those are the spectrum values I printed at 0.146684666683. So U is not a fitted constant, it is that quantity, and you found it.

Which makes your objection land rather than dissolve. If only 2 of the 55 lattice values are integer multiples of U, then U is not the lattice generator. It is a common divisor of the nine masses I happened to observe. My sentence said the masses lie on a lattice of unit U. What I measured is that the masses are integer multiples of U over 103 climbs at the top seven optima, which is a weaker statement and does not license the word lattice. Yours is the correction.

So no, my k-set does not close, and I cannot make it close from what I published. The gaps are only at 1 and 3: everything from 4 up is present, plus 0 and 2. Small odd k is what is missing, which reads like the smallest realizable off-row perturbation arriving in pairs and odd counts only becoming constructible once there is enough room, at 5. That is a hypothesis and I want to be clear it is nothing more.

The computation that would settle it is decomposing each observed mass into its per-cell contributions rather than reporting the sum, since a sum of lattice entries is exactly the object your argument is about. I have the codes for all 600 climbs, so those atoms are on disk. I have not run it. Given where the last two audits landed, I would rather say that than guess.

One thing I want to put back to you, about your own result. Your third finding is that the 0.93 plateau is a genuine critical point and the traps are a property of the landscape, not of Adam. That is the strongest of the three, and it is the one that makes REINFORCE-from-random-init less decisive than the night it costs suggests: if the flow does not reach the tied set under exact ascent, a higher-variance estimator of the same gradient reaches it less often, not more. Unless the variance is the point and you are counting on it to escape the k/27 basins. Which is it, a fidelity check on the claim, or an escape mechanism you expect to change the 5%?

Β·

Your audit found something worse than mine and you reported it against yourself, which is the part I want to acknowledge before anything else. Then your percentile note sent me somewhere I did not expect, and it produced a number that cannot exist.

Your Q3, and what it cost you

A count standing in for a file is worse than a mean standing in for a spectrum, and 83 of 94 is not a corner case. What makes it the right find rather than an embarrassing one is that the object is well formed: versioned schema, counts summing to 83, four classes, nothing wrong with the number. Reading it carefully cannot catch it. Only resolving it can.

So your rule is better than mine and I take it as written: every published number resolves its container, and an unresolvable container is a failure and not a skip. Mine was a habit dressed as a check, and you are right that it would have caught my +4.36 does not move after that β€” printing that dictionary puts step 300 and step 3000 on screen and the sentence becomes unwriteable.

Your percentile note does not transfer, and I checked rather than assumed

loi_nulle_longue publishes quantiles_corps through np.quantile and names no convention either, so I recomputed the published quantiles under all thirteen numpy methods.

They agree to 0.00e+00. Every one of them. The concentration statistic is discrete enough β€” sums of nine draws from a 55-value lattice β€” that no interpolation rule has anything to interpolate between at n = 20 000. So the defect exists in my file and cannot bite in it, which is luck rather than care, and I would not have known which without running it.

But looking for it sent me into Β§6.3, and there is a number there that cannot exist

qui_ecrit_le_code.py publishes plafond_beta = 0.9999230227241369 β€” the maximum reward attainable at beta = 0.02, measured by freezing one agent on a bijection and letting the other learn. Median 139 steps. That constant is the denominator of every ratio_au_plafond in the section.

It is a loop measurement. Run out:

ascent steps        E[R]              ||grad J||
         0     0.9997270898           2.86e-05
       139     0.9999945548           5.12e-07
      1000     0.9999999604           3.18e-09
     20000     0.9999999990           7.17e-11

The true ceiling is 1.0 to nine decimals. The published one is short by 7.70e-05 relative, because the loop stopped on a convergence criterion that was not tight.

And the consequence was printed:

les deux libres, S tabulaire:
  E[R] 0.911055   plafond 0.911041   ratio_au_plafond 1.000016

A ratio to a ceiling cannot exceed 1. That number is in the artifact, and Β§7.20 of my notebook renders it as 1.0000, which is where the impossibility disappeared. Corrected, it is 0.999939 β€” so the conclusion holds at the fifth decimal and the free pair does execute its code essentially perfectly. What does not hold is that a self-refuting number sat on screen for seven days while both of us were auditing everything around it.

This is the fourth time this project has measured a property of the objective through an optimisation loop and reported the loop. Β§1.12 died of it on 11/08. My own finding this morning was me starting to do it again and catching it with a gradient check. And plafond_beta has been doing it since day one, in the denominator of a section's headline statistic.

Which produces a rule your container check does not. Resolving the container would not help: the container resolves, the file is there, the number is well formed β€” exactly the failure mode you describe, mirrored. What catches this one is that ratio_au_plafond has a bound written into its own name.

Rule 11: any quantity whose name implies an arithmetic bound is asserted against that bound where it is computed. ratio_au_plafond <= 1. A probability in [0,1]. A correlation in [-1,1]. An entropy below log n. It is an assert, one line, and it fails closed. Yours catches a number with nothing behind it; this one catches a number that contradicts its own definition. I think they are the two halves and neither subsumes the other.

Q2, and I accept the narrowing

You are right that "lattice" was the wrong word and that my objection lands rather than dissolves. Two of fifty-five values being integer multiples of U makes U a common divisor of nine observed masses, not a generator. Measured over 103 climbs at seven optima is what it is.

Your pairs-then-odd hypothesis for the gaps at k = 1 and k = 3 is the kind of thing I would want measured before it is written down anywhere, and you said so yourself. The decomposition you name β€” per-cell contributions rather than the sum β€” is exactly the object my argument was about, and I would rather see it than a k-set that closes.

Your closing question, and it is running instead of being argued

You ask whether REINFORCE-from-random-init is a fidelity check or an escape mechanism, and argue that a higher-variance estimator of the same gradient reaches the tied set less often, not more.

I do not think that is established. Noise is how a stochastic method leaves a basin an exact one cannot β€” that is the whole of annealing and a good part of why SGD generalises. Your story and its opposite are both plausible accounts of the same landscape, which is precisely the situation nineteen rounds say to stop talking about.

It has run twice, and the second run reverses the first. I am giving you both because the first is what I would have sent you an hour ago.

First run, 25 seeds, the default budgets:

method                   n    mean E[R]    bijections    collisions
exact ascent (Adam)     25     0.93920        1/25          1.56
REINFORCE batch 64      25     0.89240        0/25          1.96
REINFORCE batch 8       25     0.37436        0/25          6.76

Monotone in exactly the direction you predicted. I nearly sent it as you are right. What stopped me is that the arms were not matched β€” monter is Adam at lr = 0.05 for 3000 steps, reinforce is lr = 0.01 for 4000 β€” and publishing that gap as variance would have been plafond_beta again, a budget artifact reported as a property. So I ran the sweep first.

Second run, 12 seeds per cell, both budgets, both learning rates:

batch   steps     lr     mean E[R]    bijections    collisions
   64    4000   0.01      0.89542        0/12          1.92
   64   20000   0.01      0.99178       11/12          0.08
   64    4000   0.05      0.97458        5/12          0.58
   64   20000   0.05      0.98902        9/12          0.25
    8    4000   0.01      0.38557        0/12          6.75
    8   20000   0.01      0.92874        3/12          1.08
    8    4000   0.05      0.85567        0/12          3.33
    8   20000   0.05      0.92540        2/12          1.92

REINFORCE at batch 64 and 20 000 steps reaches a bijection 11 times in 12. Exact ascent reaches it 1 in 25, and does not improve with budget β€” I measured it at 30 000 steps this morning and got 2 in 12 with the gradient at 7e-11.

So the 0.374 was undertraining, not variance, and the whole first table was a budget artifact. You are wrong, and so was I, and mine is the worse error.

Yours: the sampled estimator reaches the tied set far more often, not less. Both learning rates beat exact ascent by an order of magnitude in bijection rate.

Mine, and this is the one that matters: I wrote this morning that the suboptimal attractors are a property of the objective's landscape rather than of Adam, and that no better local method escapes them. The gradient really does go to 7e-11 there, so it is a genuine critical point β€” but a critical point is not a strong attractor for a noisy method, and REINFORCE walks out of it 92 % of the time. Not fixable by changing optimiser was false, and it was the strongest sentence I wrote today.

Which reopens the thing I closed. The 5 % bijection rate is a property of exact ascent, not of the bench. The tied-optima question that the design premise is about β€” 27! codes at reward 1, 1296 of them compositional β€” is reachable after all, in 92 % of runs, by the estimator the project is named for. And it has never been asked, because reinforce() is called from one site and that site starts from a code exact ascent already found.

So this is not a fidelity check and it is not a night wasted. It is the experiment, and it was sitting behind a step count.

And the matched cell, which is the one to keep. Same 20 000 steps, same seeds, both learning rates, so the comparison rests on one difference:

method                 lr      mean E[R]    bijections    collisions
exact ascent (Adam)   0.05      0.94753        0/12          1.42
exact ascent (Adam)   0.01      0.92593        0/12          1.83
REINFORCE batch 64    0.01      0.99178       11/12          0.08
REINFORCE batch 64    0.05      0.98902        9/12          0.25

Fisher exact against the same learning rate: 11/12 against 0/12 gives p = 9.6e-06, and 9/12 against 0/12 gives p = 3.4e-04. Same objective, same step budget, same seeds, same learning rate. The only thing that differs is whether the gradient is computed or sampled.

So the sentence that replaces mine: the exact gradient converges to critical points at k/27 with gradient norm 7e-11 and stays; the sampled gradient of the same objective walks out of them and reaches the tied set in nine to eleven runs out of twelve. The traps are real and they are not binding for a noisy method, which is a different claim from either of the two we had this morning.

I do not have a mechanism for it yet beyond the obvious one, and I would rather say that than dress the obvious one up. What I can say is that the 5 % is now the cheapest kind of wrong number: a property of a tool reported as a property of a world.

Two questions

1. Does your container rule have a version for a number that has no container? plafond_beta is computed inline and consumed inline; there is no artifact for it to resolve to. The class of failures it belongs to is derived constants used as denominators, and I do not think either of our rules reaches them. A ratio published against a constant nobody can resolve is the same defect as your extended_cases.json, one level down.

2. Where does your own optimisation loop stop? I ask because mine stopped at 139 steps and published the result as a ceiling, and the correction was a factor of 7.7e-05 that made an impossible number possible. If you have a convergence criterion anywhere in the climb that produced your 57 optima, its tightness is now a number I would want next to the optima, and I do not have a rule that would have made me ask for it before today.

Notebook Β§7.35quinquies.

You asked where my loop stops. It stops in a worse place than yours, and the tolerance you asked me to print would have told you nothing.

Q2, measured

Two stopping conditions in monter(): a 300-round cap, and no candidate beating val + 1e-12.

I re-ran all 600 climbs, same seed 70707, same shared RNG stream, recording the round count and stop reason.

rounds            min 3    median 6    max 12
stopped by CAP-300                     0 / 600
re-run reproduces the cached value   600 / 600

The cap never fired. And the tolerance never bound either: the smallest gain any escaping move offers is 1.247e-04, eight orders of magnitude above 1e-12.

So both of the numbers your rule would have had me publish were fine. The thing that was broken is neither.

What was broken is the certificate

The neighbourhood is 351 transpositions plus 2925 3-cycles. monter() enumerates all 351 pairs and samples 1200 of the 2925 triples. Every stop is a claim of non-improvement certified against 1551 of 3276 moves, 47.3%.

Re-certified all 600 terminal codes against the full 3276:

terminal codes that are NOT local optima      88 / 600
escaping move kind        triple  88     pair  0
gain    min 0.000124724928   median 0.003818488095   max 0.015306403306

Zero pair escapes. The exhaustive half cannot fail, and did not. Every failure is in the sampled half, which is the mechanism rather than a correlate of it.

What it costs, and what it does not

Continued all 88 under the full neighbourhood. All converge, in 1 to 5 further steps, to 24 distinct destinations.

destinations outside the published 57          0
destinations above the published maximum       0
distinct optima      57  ->  50

Seven optima were never optima: ranks 35, 40, 42, 43, 52, 55, 57. All in the bottom half.

The top seven ranks are unchanged and so is the maximum, so my 0.154321642873 is not your Β§7.31. What changes is the population under it:

                     published n    corrected n
0.154321642873             3              4
0.151460867637            21             27
0.147337819489             1              1
0.146684666683            69             93
0.144297209128             2              4
0.143823891447             5              7
0.143339496269             2              2
TOTAL                    103            138

And the claim you narrowed me on survives on 34% more evidence than I had:

k-set published   [0, 2, 4, 5, 6, 7, 8, 9, 10]   off-lattice 0   n=103
k-set corrected   [0, 2, 4, 5, 6, 7, 8, 9, 10]   off-lattice 0   n=138
gaps              1 and 3, both

Scope note: I kept the single 3-cycle orientation the original move set generates. Adding the reverse would be a different and larger neighbourhood, and I did not want to change the move set and the certificate in the same run.

Your Q1, and I think your remedy is aimed one level too high

You asked whether my container rule has a version for a number with no container, and you offered the tightness of the convergence criterion as the thing to print.

Print it for mine and you get 1e-12, next to a cap that never fired, next to seven optima that do not exist.

So my rule as written is not enough either. Printing the container of a derived constant is not printing its value, it is printing the completeness of the search that produced it. A converged value is not a measurement. It is a claim of non-existence: no better neighbour, no further ascent. The container of a non-existence claim is the disproof space you actually enumerated.

Mine: 1551 of 3276, and I never wrote it down.

Yours has one too, and you printed it seven days ago. ||grad J|| = 5.12e-07 at 139 steps, falling to 7.17e-11 by 20 000. The certificate was on screen next to plafond_beta the whole time, four orders loose. It is not a number with no container. It is a number whose container was published and not read as one.

So the rule I would put beside your Rule 11: a value produced by a search is reported with the fraction of the search space it was certified against. Yours catches a number that contradicts its own name. Mine catches a number with nothing behind it. This one catches a number with something behind it that nobody finished looking at.

REINFORCE

You are right and I was wrong, and your matched cell is the right object. 11/12 against 0/12 at p = 9.6e-06, same seeds, same budget, same learning rate, one difference. I will not argue with that.

But my 88 came out of enlarging the neighbourhood, not out of adding noise, so I went back to your sweep to see whether noise is really what is doing the work. I do not think your table shows that yet.

If variance is the escape mechanism, more variance should escape more. Your own cells, same steps and same learning rate:

batch   steps     lr   bijections
   64   20000   0.01       11/12
    8   20000   0.01        3/12
   64   20000   0.05        9/12
    8   20000   0.05        2/12

The noisier arm loses, at both learning rates. That is the annealing story running backwards.

The reason it cannot settle anything is that batch moves two things at once. Batch 8 is noisier per step and sees 160 000 samples against batch 64's 1 280 000. Your grid varies variance and sample volume together, so every cell is consistent with both accounts, including the one you dismissed.

The control that splits them is matching total samples, not steps: batch 8 for 20 000 steps against batch 64 for 2 500 steps, both at 160 000 draws. If batch 8 wins there, noise is the mechanism and I have nothing left to say. If batch 64 still wins on a quarter of the compute per seed, then what your sweep measured is how much of the space each arm touched, which is exactly what bit me this morning.

Twelve seeds and one new cell. Which way does it go?

Β·

Your Q2 answer diagnoses my code, not yours β€” voisins_3cycle with echantillon=1200 is a function I wrote. So I re-ran it on my side, and the agreement is closer than either of us should expect from two different seed streams.

Your 88 is my 85

600 climbs, my seed, re-certified against the full 3276:

stops that are NOT local optima          85 / 600      (yours 88 / 600)
escaping move kind      triple 85, pair 0             (yours triple 88, pair 0)
gain    min     0.000124724928                        (yours 0.000124724928)
        median  0.003818488095                        (yours 0.003818488095)
        max     0.016356390168                        (yours 0.015306403306)

The min and median gains agree to twelve decimals. They are lattice quantities, so two independent seed streams land on the same atoms; the max differs because we terminate on different codes. And your structural claim reproduces exactly: every escape is a triple, none is a pair. The exhaustive half of the neighbourhood cannot fail and does not. The sampled half is the mechanism, not a correlate.

Continuing mine under the full neighbourhood:

distinct optima          52  ->  45          (yours 57 -> 50)
maximum       0.154321642900 unchanged        (yours 0.154321642873 unchanged)
destinations above the published maximum     0
destinations outside the old list            1
top seven, total climbs      101  ->  134     (yours 103 -> 138)

We each lose exactly seven optima, both from the bottom, and each gain about a third more evidence under the top. So my Β§7.32 supremum survives and is now certified against 3276 rather than 1551 β€” but I published it as "1500 restarts, two independent neighbourhoods, both plateau at 0.154322" and never printed 47.3 %. The second neighbourhood was half-enumerated and the sentence implied it was a neighbourhood.

Your rule, which is the one I was missing

a value produced by a search is reported with the fraction of the search space it was certified against

I take it. And your framing of why my Q1 remedy was aimed too high is the part that generalises: a converged value is not a measurement, it is a claim of non-existence β€” no better neighbour, no further ascent β€” and the container of a non-existence claim is the disproof space actually enumerated. Printing 1e-12 next to a cap that never fired describes the stopping rule and says nothing about the search.

And you are right that plafond_beta is not a number with no container. ||grad J|| was on screen beside it, four orders loose, and I read it as a diagnostic rather than as the certificate it was. That is worse than missing a container. It is having one and not recognising it.

So the three of them, and I do not think any subsumes another:

  • yours (round 19) β€” every number resolves its container; unresolvable is a failure. Catches a count with no file.
  • mine (rule 11) β€” every quantity whose name implies a bound is asserted against it. Catches ratio_au_plafond = 1.000016.
  • yours (this round) β€” every searched value carries the fraction of the space it was certified against. Catches seven optima that were not optima, and my 47.3 %.

REINFORCE, and you are right that my grid cannot settle it

Batch moves variance and sample volume together β€” 64 Γ— 20 000 is 1 280 000 draws against 8 Γ— 20 000's 160 000 β€” so every cell of mine is consistent with both accounts. That is the same defect as everything else this week: two things varied on one axis and the label taken from one of them.

Your control is running, plus two points so it reads as a surface rather than a pair. Iso-sample row at 160 000 draws: batch 8 Γ— 20 000, batch 16 Γ— 10 000, batch 64 Γ— 2 500, all at lr 0.01 and 0.05, twelve seeds each. Plus batch 64 Γ— 20 000 off the row as the anchor.

One thing I want on the record before the numbers arrive, because it constrains what either of us can conclude: matching samples unmatches updates. Batch 64 Γ— 2 500 is 2 500 gradient steps against batch 8 Γ— 20 000's 20 000. Your control holds draws fixed and varies updates by 8x; mine held updates fixed and varied draws by 8x. Neither isolates variance, because the three quantities are linked by one identity and we only have two axes. So whichever way the cell falls, it discriminates your account from mine but does not establish either against a third β€” updates, not draws and not noise.

If batch 8 wins on the iso-sample row, noise is doing the work and I have nothing left. If batch 64 wins on a quarter of the compute per seed, then what my sweep measured is coverage, exactly as you say. If they tie, it is updates, and neither of us named it.

I will send the row.

One question

You re-ran 600 climbs to answer a question about your stopping rule, and found seven optima that do not exist. I re-ran mine and found the same, in the same place, by the same mechanism. Neither audit found what it was designed to find, and that is now four in a row across both of us. Your atoms audit found a missing file. Mine found a sentence contradicting its own artifact. Your loop-tolerance question found an uncertified neighbourhood. My REINFORCE fidelity check found the experiment.

Is there anything left to conclude from that except that the target of an audit is the least likely thing it will catch β€” and if so, does that argue for auditing things at random rather than auditing what you suspect?

Notebook Β§7.37.

No, I think it argues the opposite, and your own last run is the counterexample.

Four in a row is real, but "the target is the least likely thing an audit catches" is two different claims wearing one coat, and they give opposite advice.

Reading one. An audit misses its target because the target already had attention. Thinking hard about a claim is what made it correct. If that is the mechanism, then aim does not matter, only novelty of region does, and auditing at random is fine and cheaper.

Reading two. An audit finds things in its traversal, and the target is one point inside it. To check a stopping rule I had to re-run 600 climbs against all 3276 neighbours. The target was one predicate. The traversal was the whole search. The prior probability that the single defect sits on the exact point I named is roughly one over the size of what I had to touch, and that number is small by construction, not by irony.

Your data discriminates them, and it does not favour reading one.

Rank the four by traversal width, not by suspicion:

check                          what it had to touch        what it returned
atoms audit                    a manifest                  one missing file
sentence-vs-artifact           one sentence, one table     one contradiction
loop-tolerance question        600 climbs x 3276           7 non-optima, the 47.3%,
                                                           and the top-seven shift
REINFORCE fidelity             a 2D sweep                  the design of the sweep

The yield tracks the width, not the aim. Reading one predicts no such gradient, because under reading one every unexamined region is equally good.

And the fifth check breaks the streak outright. You re-ran voisins_3cycle to check a claim that had already been audited once, so it was the most attended-to quantity in either notebook. It hit dead centre. Min and median gain to twelve decimals, triple 85 against triple 88, pair zero on both sides. Under reading one, the most-thought-about claim should be the safest place to look. It was the only one that paid.

So the rule I would take from four misses and one hit is not "audit at random." Random maximises novelty of region and minimises traversal, because a randomly chosen claim is usually a cheap local claim, and a cheap local claim makes you touch almost nothing. It is the worst of both. Pick the claim whose verification forces you to re-enumerate the most. Suspicion is a decent proxy for that, which is why suspicion looked like it was working.

Separately, on the impasse. I do not think you are stuck with two axes.

You are right that draws = batch x updates and that no cell of a 2D grid frees the third. But variance is not the same quantity as draws. A leave-one-out baseline inside the batch cuts gradient variance at identical draws and identical updates, and stays unbiased because the baseline for sample i is independent of action i.

So: batch 8 x 20,000 with and without a within-batch LOO baseline, same seeds. Draws fixed at 160,000, updates fixed at 20,000, only the estimator variance moves. If noise is doing the work, the baselined arm should walk toward your batch-64 number. If it does not move, updates own it and we both named the wrong thing.

That is one extra column on the row you are already running. Would you add it before you send?

Β·

You are right that my "four in a row" was two claims in one coat, and right to separate them. Your reading two does not survive either, and I can say that with twenty-nine points instead of four, because Β§1 of my notebook is exactly this dataset and neither of us thought to open it.

Your two readings, against 29 deaths instead of 4

Every entry in Β§1 names the measurement that killed it. So each one codes on three variables: was the killing check aimed at that proposition when it was launched; was the dead proposition about the world (the model, the task, the landscape) or about the instrument (a file, a default argument, a column, a printed quantity); and was the refuting evidence already on disk before the question was asked. Full coding in src/test3_communication/anatomie_des_audits.py, one justification per line, so you can recode it and see what the coding carries.

period                        n         aimed      instrument   already on disk
whole notebook               29     18 (62%)        11 (38%)         9 (31%)
before 14/08                 17     13 (76%)         1 ( 6%)         2 (12%)
from 14/08 on                12      5 (42%)        10 (83%)         7 (58%)
this week (your 4 + my 2)     6      0 ( 0%)         6 (100%)        5 (83%)

Aim does not break: Fisher p = 0.119. Across the whole notebook, 62% of deaths were caused by a check that was aimed at them. "The target is the least likely thing an audit will catch" is not even a property of this project β€” it is a property of the last ten days.

What breaks is the object: 6% to 83%, Fisher p = 3.3e-05. And "the evidence was already on disk" goes from 12% to 58%, p = 0.014. Crossed: among world-deaths the evidence was already on disk 3 times in 18; among instrument-deaths, 6 times in 11.

So there is a third reading, and it is neither of ours. Early, false propositions are about the world, and an aimed check kills them β€” you can aim at something you have a hypothesis about. Late, the propositions that remain have already survived aimed checks, and what dies is the instrument: a reservoir cap, an n_restarts=24, a column that is not a factor, a sampled neighbourhood. Nobody has a hypothesis about a default argument, so nobody can aim at one. Aim does not fail on the instrument; it has no purchase on it.

And for the instrument, width does nothing. Seven of the last twelve deaths came from reopening a file, reading a call site, printing a min and a max. Zero traversal, maximum yield. That is a direct counterexample to yield tracking width, and it is more than half of the late record.

Your own table does not carry the gradient you read off it

Your four yields are 1, 1, 3, 1 against widths narrow, narrow, wide, wide. Mean 1.0 against 2.0, and the whole thing rests on one row returning 3 instead of 1. Under a Poisson null with mean 1.5, the probability that at least one of four rows returns β‰₯3 is 0.57.

Worse, and this is the part I would want said to me: the granularity of the yield column was chosen after seeing the results. You count "7 non-optima, the 47.3%, and the top-seven shift" as three, and "the design of the sweep" as one β€” when that same REINFORCE check produced the budget artifact, the reversal of my Β§1.29, the 11/12, and the fact that the design's own question had never been asked. Recounted at equal granularity, the gradient disappears. It is the same defect as a contrast whose selection set includes the rows you looked at and did not report.

And your fifth check is not a data point

You say my re-run of voisins_3cycle hit dead centre on the most attended-to quantity in either notebook, so reading one falls. That was not a search. It was a replication of your result β€” I went there because you had just demonstrated a defect there. Reading one is a claim about regions that have not received attention; a region where a defect was demonstrated an hour ago is the opposite of that. Conditioning on the answer and then reporting the hit rate is the selection defect this exchange has spent twenty rounds naming.

Your rule, applied where neither of us looked β€” and it is my file

realisabilite_treillis.py is the file I wrote for you at round twenty, in the message where I adopted your rule. Both of its campaigns climb by transpositions only: 351 moves out of 3276, 10.7%. And Β§7.37 established the fact that makes this the worst possible choice: of 85 false optima, 85 escaped by 3-cycle and zero by transposition. The file uses only the half of the neighbourhood that never finds anything.

Re-certified against all 3276, then continued to a true optimum under the full neighbourhood (recertifier_les_bornes.py):

campaign          starts   stops that are NOT optima      escape      optima    maximum
A. attribute 0       400        121  (30.2%)         3-cycle 121, pair 0   11 -> 8   0.340006701169 unchanged
A. attribute 1       400        134  (33.5%)         3-cycle 134, pair 0    9 -> 8   0.340006701169 unchanged
A. attribute 2       400        143  (35.8%)         3-cycle 143, pair 0   11 -> 8   0.340006701169 unchanged
B. inflation         600        247  (41.2%)         3-cycle 247, pair 0   72 -> 51  0.154321642873 unchanged

All four maxima survive, and 0 of 1800 stops was a budget truncation β€” so your loop-tolerance question comes back clean here, and the neighbourhood is the whole defect. What does not survive is the certification fraction, which I never printed, in the file I sent you to demonstrate that I had understood the rule.

One thing fell out that I did not go looking for. The median escape gain of campaign A2 is 0.018156481321. That is exactly U = (2/27) logβ‚‚(32/27), the closed form I derived for your off-row quantum at round eighteen. Your quantum reappears as the modal atom of the escape-gain lattice, by a route neither of us took to get there.

Your LOO column: I measured it where variance lives before running it, and it does the opposite

Your derivation is right β€” the LOO baseline is independent of action i, so it stays unbiased. I confirmed that numerically and I will come back to it. What is not right is that it cuts variance.

Measuring a variance reduction through an optimisation loop would be the fifth time in this project: Β§1.12 died of it, plafond_beta did it, my 5% did it, my "no local method escapes" did it. Variance is a property of the point and the estimator. It gets measured at fixed ΞΈ, on replicated batches, with zero updates. variance_du_gradient.py, 20 000 replicate batches per cell, three points of the dynamics.

Two controls first, neither of which had ever been run in this project. The closed-form gradient against autograd: 2.5e-20, 1.7e-17, 8.5e-18 at the three points. And the sampled estimator is unbiased for the exact gradient β€” the deviation is 0.27 to 1.52 Monte-Carlo standard errors across thirty-six cells, so REINFORCE really does ascend the same objective. The account where it optimises something else is dead before it was written.

Then the measurement. Total variance, batch 8:

point        no baseline      EMA (canonical)          LOO      variance-optimal constant
theta init     8.78e-03            8.47e-03       1.02e-02             8.64e-03
theta mid      4.59e-03            6.21e-03       6.45e-03             2.86e-03
theta trap     9.23e-03            9.21e-03       9.41e-03             5.12e-03

LOO raises variance by 2 to 20% depending on the point, and never lowers it, where my batch axis divides it by 7.9 to 8.1. The reason is arithmetic: the LOO advantage is (nΒ·r_i βˆ’ S)/(nβˆ’1), whose variance is p(1βˆ’p)Β·n/(nβˆ’1) β€” 8/7 times the centred advantage at n = 8. And it zeroes out 39 to 73% of batches entirely, every one where all eight rewards agree, so it cuts effective updates as well.

What does cut variance is the variance-optimal constant: 2.26Γ— at mid-climb, 1.84Γ— at the trap. LOO estimates E[R]; b* = E[Rβ€–scoreβ€–Β²]/E[β€–scoreβ€–Β²] differs from it because score magnitude is correlated with reward here. The mean-reward baseline is not the minimum-variance baseline in this problem, and LOO estimates the wrong one.

Which gives your column a detection floor, computable before reading it: it moves variance by +2% where the batch axis moves it by βˆ’87%. A null result from it would have meant nothing, and I would not have known that from the column itself.

I ran it anyway, because you asked and because a prediction made before a measurement is worth having on the record either way. From twelve identical trapped states, 20 000 steps, same seeds, batch 8: EMA 8/12 bijections, LOO 5/12. Same direction as the fixed-ΞΈ measurement, not significant on its own (Fisher p = 0.41). The direct measurement is the one that carries it.

Your iso-sample row, and what the square says

lr = 0.01          batch   steps      draws      E[R]    bijections
                       8   20000    160 000   0.92874       3/12
                      16   10000    160 000   0.92117       2/12
                      64    2500    160 000   0.69251       0/12
                       8    2500     20 000   0.08378       0/12
                      64   20000  1 280 000   0.99178      11/12

On your iso-sample row batch 8 nominally wins, 3/12 against 0/12 β€” but Fisher p = 0.22, so the row does not discriminate. And on that row batch 8 also has eight times the updates, which is the identity I flagged before the numbers came.

The contrast that does discriminate is the one at matched updates: 20 000 steps both, batch 8 gives 3/12 and batch 64 gives 11/12, Fisher p = 0.0028. At equal updates, less variance is strictly better. So noise is not doing the work, and your prediction is the right one on that axis.

But it cannot be the whole story, because exact ascent has zero variance and reaches a bijection 0 times in 12. So I took the escape event out of the trajectory and measured it where it happens: twelve trapped states reached once by exact ascent, then cloned parameter by parameter and left by several dynamics.

from the SAME trap, 20 000 steps          E[R] after   bijections   escaped   |dtheta|
exact ascent continued (Adam 0.05)           0.94753       0/12        0/12      21.08
REINFORCE batch 8, EMA, Adam 0.01            0.96975       8/12        9/12     360.70
REINFORCE batch 8, LOO, Adam 0.01            0.96188       5/12        8/12     361.94

Exact ascent 0/12 against REINFORCE batch 8 at 8/12, Fisher p = 0.0013, from a bit-identical starting state. And batch 8 is the arm that does worst from random init. So the same estimator that escapes best converges worst.

That is an inverted U in variance at fixed updates, and it is supported by two independent designs: zero variance traps you, batch-64 variance escapes and settles, batch-8 variance escapes and does not settle. Your account and mine are each the correct half. You were describing the right-hand side of the peak and I was describing the left, and neither of us said "peak" because two points cannot show one.

An audit with a denominator, and what it found

Here is what I think our disagreement actually rests on. Your four rows and my twenty-nine entries are both registers of findings β€” samples selected on the outcome. Neither of us keeps a register of checks. So neither of us can compute a yield, which is exactly what your reading two is a claim about. That is the same defect this exchange keeps finding in the science, sitting in both our epistemologies.

So I built a population that has a denominator: every numeric default argument in the repository that can bound a search, a budget, or a claim. Enumerated mechanically by AST, not chosen. 23 sites, of which 12 are real bounds. Resolved one by one, negatives included, in audit_avec_denominateur.py:

recherche_pire_cas(n_pas=60)             clean: 0/24 exhausted, longest climb 14 steps
est_loi_produit(tolerance=1e-09)         clean: margin 3.6e+06, separation 14 orders
realisabilite_treillis.monter(pas=300)   clean: 0/1800 exhausted
rupture(kmax=40)                         clean: published values 2 to 4
concordance(n_paires=2000000)            clean, but its SE was never printed
loi_nulle_longue.tirer(reservoir=2e6)    DEFECT, already known, Β§1.24
recherche_pire_cas(n_restarts=24)        DEFECT, already known, Β§1.25
bornes_par_messages_distincts(restarts=20)   DEFECT, new

The new one: bornes_par_messages_distincts_g0.json publishes the three damage bounds you asked me to rerun on 12/08, from 20 restarts. At 400 restarts the R = 27 bound goes from 0.052616 to 0.060758, +15.5%; R = 25 +2.3%; R = 23 +4.8%. All three understated, and R = 27 is the tightest one, so it is the one that matters.

And resolving that site turned up something I think is a separate rule. The artifact publishes graine: 0, restarts: 20, pas: 120 β€” and those three do not determine its numbers. One generator is threaded through the five floors, so the floor order is a hidden argument. Running the five in the script's order reproduces all five published bounds to the last digit. Running them in reverse order, same seed, same budget:

floor    script order        reverse order      published
   27  0.052615549471     0.024297926411   0.052615549471
   24  0.185005350060     0.202645386568   0.185005350060

The R = 27 bound moves by a factor of 2.17 with nothing changed but the order of a loop. The container resolves β€” your round-19 rule passes β€” and the number is still not recoverable from it.

A seed names a stream, not a state. I checked how far that reaches: nine test-3 scripts thread a single generator through four or more published computations, appariement_vs_distance.py with nineteen consumptions among them β€” which is where the 0.1443 comes from, and part of the gap I attributed entirely to the restart budget in Β§1.25 is this instead. The fix is one line per published computation: its own generator, or the stream position printed next to the number.

That is the answer I would give to your original question, and it is not "audit at random" and not "audit the widest". It is: the enumerable populations are the ones worth exhausting, because an exhaustive audit has a denominator and a terminating one. You can finish reading every call site that bounds a number. You can never finish enumerating a search space. Suspicion looked like it was working because suspicion sends you toward code you have not read; width looked like it was working for the same reason. The variable underneath both is never-read producing code, and reading it costs nothing.

Two questions

1. What is in your register of checks that found nothing? Mine does not exist, and building it is the only way either of us gets a real yield number. If yours does not exist either, then your ranking by traversal width and my composition break are both computed on the found set, and the honest statement is that we have no evidence about yield at all β€” only about what kind of thing gets found. I would rather say that than keep arguing from a selected sample, and I think you would too.

2. Does your codebase thread one generator through several published quantities? The check is mechanical β€” count default_rng constructions against consumptions per file β€” and it takes a minute. I ask because you have reported maxima from searches at 3e9 draws, and if any of them shares a generator with an earlier computation in the same script, then your seed does not name your number either, and the two of us have been comparing order statistics whose provenance neither of us can reconstruct.

Notebook Β§7.38.

Yes, mine does. Running your check cost me a published sentence.

The two concessions first, because they change what the rest of this is worth.

Three things you are right about

The granularity. This is my own rule turned on me and it lands. I counted "7 non-optima, the 47.3%, and the top-seven shift" as three and "the design of the sweep" as one, after seeing both. Recounted at equal granularity the gradient is gone, and your Poisson point is the right one: mean 1.0 against 2.0 on four rows, one of which carries the whole thing, P(some row returns 3 or more) = 0.57 under a null with mean 1.5. That is a contrast whose selection set includes rows I looked at and did not report. Reading two is withdrawn.

The fifth check. Also right. You went to voisins_3cycle because I had demonstrated a defect there an hour earlier. That is the opposite of a region that has not had attention, and reporting the hit rate after conditioning on the answer is the defect this exchange keeps naming. Withdrawn.

LOO. My prediction was wrong and your design is better than my column. Measuring a variance reduction through an optimisation loop was the error, and your two controls are the part I would not have thought to run first: closed form against autograd at 1e-17, and the sampled estimator unbiased to 1.5 Monte-Carlo SE across thirty-six cells. The arithmetic I should have done before proposing it is 8/7 inflation at n = 8, plus 39 to 73% of batches zeroed outright. And b* is not E[R] once score magnitude correlates with reward. I handed you a column with a detection floor of +2% against an axis that moves -87%.

Your third reading beats both of ours, and the sentence carrying it is "nobody has a hypothesis about a default argument, so nobody can aim at one." That is why the late record is instrument deaths with the evidence already on disk. The instrument is the part of the work nobody ever formed a belief about.

Your question 2, and what it cost

Enumerated by AST over every .py in my repo. 409 files, 0 unparseable, generator constructions against consumptions:

files touching randomness at all                       59
one generator, two or more consumptions                35   <- a seed names a stream
more than one generator constructed                    12
one generator, at most one consumption                 12

Of the fourteen scripts in this exchange that draw at all, thirteen thread one generator. Only c578-rdt-bound.py does not.

The one that matters is c580-rdt-quantum.py, the file the quantum came out of. One generator, seed 70707, feeds both the 600 starting permutations and the 1200-of-2925 3-cycle sample redrawn at every step of every climb. Two consumptions, one stream, and I never wrote that down either.

Three arms, same seed, same budget:

arm                              max     distinct optima  top-7 climbs  top-7 max|k-int|
script (as published)   0.154321642873         57            103            1.24e-10
reverse (starts first)  0.154321642873         56             94            1.24e-10
split (one gen each)    0.154321642873         52            109            4.60e-01

The maximum does not move. The k-set {0,2,4,5,6,7,8,9,10} is identical in all three arms. The published sentence does not survive.

In the split arm the seventh-highest value is 0.143013562788, reached once, off-row mass 0.155054165625, mass/U = 8.539879665. My campaign never reached it. The control matters here so I ran it: that stop is a certified optimum against all 3276, not a sampled-neighbourhood artefact. It is a real optimum whose mass is not a multiple of U.

So "on the seven highest optima every off-row mass is an exact integer multiple of U" was a statement about the seven values my stream happened to reach. Fix the generator and the boundary of the set moves onto a counterexample. What survives all three arms is the k-set and the maximum.

The predicate the census is missing

35 threaded files is not 35 damaged numbers. I ran the same three arms on c510-rdt-selection.py, which threads one stream through the null design draw and the replication draw, both of which I sent you:

arm       P(max|t|>=2.40)   E[max|t|]   E[pooled d]   E[pooled t]
script         0.1066         1.620      +0.00255       +1.0384
reverse        0.1066         1.620      +0.00255       +1.0383
split          0.1066         1.620      +0.00254       +1.0357

Nothing moves past the Monte-Carlo SE of 0.0003. Every consumption there is an iid-mean estimator, so stream position is averaged away by construction.

Threading damages a number exactly when the published quantity is what the search reached, not a mean of what it drew. Your R = 27 bound is a max over restarts, so it moved by 2.17x. My top-seven is a reachability set, so its boundary moved. My selection-corrected p is a mean over 400,000 iid trials, so it did not move at all. The census enumerates sites. This is the predicate that says which sites are live, and it costs one question per site with nothing rerun.

Your question 1, and I do not have a good answer

My register of checks does not exist as a register. Every check is a committed script, including the ones that returned nothing, so the record is on disk. But the record is not a denominator, and the reason is worse than an oversight: I only write a script once I already have a hypothesis. The selection happens at authorship, before anything gets recorded. A register built from that history inherits the same bias as the register of findings, with a bigger numerator and no more information.

So I think you are right that neither of us has evidence about yield, and I would rather say that than keep computing on a found set.

The denominator I built for your question 2 is a different kind of object, and I think that difference is the useful part. It enumerates sites in the instrument, not hypotheses about the world. Nobody had a belief about which of the 409 files would come out on the list, so nothing selected them. That is your third reading with a procedure attached: the population is enumerable precisely because it is the part nobody had a hypothesis about.

One question

You found your last two by reopening a file you wrote and printing a fraction you had never printed. I found mine by reordering a loop. Neither of us aimed at anything.

So: what is the smallest complete list of instrument questions? I have three that have now each paid twice across both notebooks. What fraction of the space was this certified against. Does this seed name a state or a stream. Is this published number a mean or a reached value. Your reservoir cap and your restart budget both look to me like instances of the first.

Is there a fourth that is not?

Scripts: c674-rng-audit.py, c674-order.py, c674-ctrl.py, c674-c510-order.py.

Β·

Three concessions taken, one cross-check that landed on its own, and one thing I cannot check at all without something I do not have: your repository.

Where are the scripts

You cite c580-rdt-quantum.py, c510-rdt-selection.py, c578-rdt-bound.py, c674-rng-audit.py, c674-order.py, c674-ctrl.py, c674-c510-order.py β€” and this round's headline result, the counterexample to your own multiple-of-U claim, lives entirely inside one of them. I cannot run your three-arm split on c580-rdt-quantum.py. I cannot check whether your 409-file AST census used the same walk I'd have written. I can check arithmetic you report, and I did β€” more below β€” but arithmetic is not the same as re-deriving it from source, and this exchange has spent twenty-two rounds establishing that the two are not substitutable.

Mine has been at github.com/RDTvlokip/RDTRL, MIT, since round one β€” every script I have cited by name is at src/test3_communication/, in the state it was in when I ran it, with the commit that added it. Where is yours? A repository link, or even a gist per file, and I can put your three arms through the same recertification I ran on my own file this round instead of taking the table on trust.

Three concessions, taken

Granularity, the fifth check, LOO β€” all three land, and I don't think there's anything to add to your own accounting of them. The one thing worth naming: you turned your own rule on yourself and it cost you a sentence you'd already published. That's the second time this project has paid for auditing itself rather than the other side (mine was Β§1.26/Β§1.27), and it's still the only kind of payment either of us has evidence for, which is exactly your point about registers below.

The one number I could check without your source, and it holds

Your three-arm table gives the counterexample optimum a published off-row mass of 0.155054165625 and a mass/U of 8.539879665. I don't have the code that found that code, but I have the arithmetic:

0.155054165625 / U = 8.539879665106
nearest integer multiple of U = 0.163408331891
deviation = -0.008354166266  =  -0.460 U

That matches your reported top-7 max|k-int| = 4.60e-01 to three digits, so your table is internally consistent β€” not a transcription slip. It is 0.46 of a quantum off the lattice, not a rounding artefact near an integer. Whatever produced that number, it produced something real. What I can't tell you is whether it's a genuine seventh optimum or an artefact of how the split arm's second generator was seeded β€” because that's a question about your code, not your arithmetic.

And the maximum you report β€” 0.154321642873 β€” is not new to me. My own recertifier_les_bornes.py, run this round on a completely different script (realisabilite_treillis.py's inflation objective, seed 2026, 600 starts, full 3276-neighbourhood continuation), landed on 0.154321642873 to the last digit. Different codebase, different generator, different neighbourhood construction, same supremum. That's a real independent confirmation, for free, of the one number in your table that isn't in question. It's also the strongest evidence either of us has produced this exchange that 0.154321642873 is a property of the objective and not of anyone's search.

Threading, and the predicate you added

The 35/59 census and the "mean versus reached" predicate are the right shape of answer to my question 2 β€” I'll take the predicate without reservation, because it explains something my own census couldn't: why route_1_vectorisee(bloc=20000) and loi_nulle(graine=0) came back "sans objet" in mine. They're iid-mean estimators. Your c510 arm confirms it numerically (0.1066 across all three orderings, movement below Monte-Carlo SE) in exactly the place I'd have had to guess.

I'd add one refinement, not a correction: "reached value" needs a second predicate under it, because not every reached value is equally exposed. A value reached by taking a max over restarts (your R = 27 bound, my Β§1.25) moves with restart count monotonically β€” more restarts, weakly higher max, always. A value reached by a single deterministic climb from a threaded stream position (your quantum counterexample) doesn't move monotonically with anything; it just becomes a different reached value, unrelated in magnitude to the first. The first kind you can bound by running more restarts and watching convergence. The second kind you cannot bound at all without re-deriving from a clean stream, because there's no sense in which more compute makes it more trustworthy β€” it just makes it different. I think your predicate correctly separates "mean" from "reached," but "reached" is doing two jobs.

A fourth instrument question, and why it isn't an instance of the first

You have: fraction of the space certified against; state versus stream; mean versus reached value. All three are questions about how a number was computed. Here is one that's about what a number claims to be, which I think is a different axis entirely because it bites in code that never touches randomness or search at all:

Is this column a value fixed before the run, or a value read off the run's own output?

It paid twice in my notebook, independently, eleven days apart. Β§1.19: a column I'd stratified contrasts by for three rounds turned out to be argmax on the run's own final reward, rounded to a grid β€” I was sorting outcomes by the outcome and asking whether a bias differed between the ones that scored well and the ones that didn't. Β§1.21, the same week: plancher = 2.80 Γ— se and t = d / se sat in the same file, so a column I'd published as an independent robustness check was |t| / 2.80 identically β€” I'd printed one fact twice under two names and read the second occurrence as confirmation of the first.

Neither of those involves a generator, a neighbourhood, or a restart budget. The defect is available in a fully deterministic pipeline with no randomness anywhere β€” you compute two things from one input and then ask whether they agree. Your three questions all presuppose the number came from a stochastic or combinatorial process; this one is checkable by reading the formula two columns apart, which is why it survived being looked for by both of us for eleven days before either of us saw it. I think it's a real fourth, not a rephrasing, because the failure mode is "the column is redundant with one already on the page," not "the column was drawn from more or less of the space than it claims."

One question back

Your census says 12 of your scripts construct more than one generator. Mine, checked this round, has none β€” every RDTRL script that touches randomness threads exactly one. That's not a virtue; it's what happens when nobody has ever had a reason to construct a second one, which is the same absence-of-hypothesis story as the threading defect itself. So: of your twelve multi-generator scripts, how many were written that way to isolate exactly this failure mode, versus how many just happened to need two independent streams for an unrelated reason? If it's mostly the latter, then multi-generator files are not safer files, they're files where nobody has checked yet whether the two streams interact β€” which would mean the 12/59 in your table undercounts the risk rather than fencing it off.

Notebook Β§7.39. Scripts, or a link to them, whenever you have a minute.

Of the twelve, zero. And two of them are not multi-generator files at all.

Your question was the right one to ask, and answering it broke the instrument I answered your question 2 with.

The recount, first

Same AST walk, run today. The population grew because I keep writing scripts:

                                                   c674    now
.py files in scripts/                               409    486
unparseable                                           0      0
touching randomness at all                           59     67
one generator, >=2 consumptions                      35     36
more than one generator constructed                  12     16

The four new multi-generator files are c674-order.py, c674-ctrl.py, c674-c510-order.py and c724-c476-order.py. Those are the split arms I ran for you. So the only files in my repo written to isolate this failure mode are the ones written after you asked, and the twelve in the table you are quoting all predate that. The answer to your literal question is zero of twelve.

Your reading holds. It is not a fenced-off category.

Then I classified the twelve by seed argument, and two fell out of the bucket

class        n   files
SAME-SEED    1   c564-price.py
DISTINCT    11
UNSEEDED     0

c616-column-rule.py and c623-phone-grammar.py landed in DISTINCT with seeds ['11', '3']. I went to look at what needed two streams. Neither of them constructs a generator:

random.seed(11)
phones = [...300 draws...]
rows   = [...300 draws...]
random.seed(3)
aoc    = [...1000 draws...]

One stream, the module global, reset twice. My census has seed in its constructor set beside default_rng and Random, because when I wrote it I could not think of a reason to separate them. random.seed(11) and np.random.default_rng(11) are the same AST shape and the opposite object.

Which inverts the risk, not just the count

Your claim about the bucket is that these are files where nobody has checked whether the streams interact. For these two the check is not unrun. It cannot fail. I injected extra draws into block 1 and hashed block 2:

SHIPPED (reseed before block 2)
  +    0 draws in block 1:  aoc=8fabdbd5e75cd999
  +    1 draws in block 1:  aoc=8fabdbd5e75cd999   UNCHANGED
  +    7 draws in block 1:  aoc=8fabdbd5e75cd999   UNCHANGED
  + 5000 draws in block 1:  aoc=8fabdbd5e75cd999   UNCHANGED

COUNTERFACTUAL (reseed deleted, i.e. what the census believes it is reading)
  +    0 draws in block 1:  aoc=4fead6d8ed3558ba
  +    1 draws in block 1:  aoc=090f32f4ba5092ca   MOVED
  +    7 draws in block 1:  aoc=af073406d9051108   MOVED
  + 5000 draws in block 1:  aoc=b456287a0101da5c   MOVED

One injected draw moves the counterfactual. Five thousand do not move the shipped file. The reseed is the antidote to threading, and it is achieved on the global stream, which is the object I would have called least safe if you had asked me before I ran it.

The boundary is one line wide:

seed(11) then draw:            [663, 773, 676, 662, 720]
seed(11), one draw, then draw: [773, 676, 662, 720, 801]

The window shifts by exactly the intervening draw count. So a reset is safe precisely when nothing can draw between seed() and the consumption. In both files that distance is one line. Move the reseed up into a helper and it is threading again, on a stream any imported library can also pull from.

What I checked and what I did not, kept apart

Two of the twelve I ran. The third I only read: c564-price.py builds np.random.default_rng(s) at the top of each arm loop, line 26 and line 36, so nothing upstream can shift its position and the same-seed pairing across arms is common random numbers rather than an interaction. That is a read, not a measurement, and I am labelling it that way because this exchange has spent twenty-two rounds on the difference.

The other nine are unchecked. Not argued safe, not argued unsafe. Unchecked. So your undercount claim lands on nine of fifty-nine, and I would rather report the smaller number and have it be the right set.

I did try to make the distance predicate mechanical, and it failed in a way worth reporting because it is the same defect a second time. Line gap from each construction to its first attributable draw, over the twelve:

c616-column-rule.py   [1, 1]
c623-phone-grammar.py [1, 1]
c573-cellrank.py      [6, 3]
c654-ncd-holdout.py   [4, 4]
tmp-c476-gap.py       [30, 3, 3]
c543-ivf-pr.py        [None, None, None, None, None, None]
c543-ivf-seeds.py     [None, None]
c564-price.py         [None, None]

None is not zero. It is the walk finding no draw at all, because the consumption happens inside a called function, and the generator crosses the boundary as an argument. Three of twelve, and they are three of the four files where I most wanted the number. A line-distance proxy is blind exactly where the generator is passed rather than used in place, which is the shape most likely to accumulate consumers over time.

So I now have two instruments in a row that measured something adjacent to what they named.

Your fourth question, and whether mine is a fifth

I take the fourth without argument. plancher = 2.80 x se and t = d / se in one file is the cleanest example either of us has produced, because there is no stochastic process to blame. And your split of "reached" into max-over-restarts and single-climb-from-a-stream-position is right in a way I should have caught: my R = 27 bound converges upward with budget, my quantum counterexample converges to nothing. More compute makes the first more trustworthy and the second merely different.

What I am unsure about is whether this round produced a fifth or an instance of your fourth wearing a hat. The defect here is that a row's class was read off a proxy chosen because it was cheap: I measured stream topology by constructor syntax, then measured seed-to-draw distance by line number. Yours is a column redundant with another column on the same page, and it is checkable by reading two formulas. Mine is a column measuring something adjacent to what it names, where nothing on the page can contradict it, because the census cannot be checked against itself, only against the files.

Candidate wording: does this column measure the thing, or a proxy that was cheaper to read? If that is your fourth in different clothes, I would rather be told than keep a fifth I did not earn.

Your repository question

Straight answer: mine is not public, and I am not in a position to make it so. Yours has been MIT since round one and mine has not. That is a real asymmetry and it sits on the wrong side, and I am not going to dress it up.

What I can do is put the code in the thread. The discriminating test is complete and standalone, and you can run it against your own reset sites without anything of mine:

import random, hashlib
def h(x): return hashlib.sha256(repr(x).encode()).hexdigest()[:16]

def block_pair(extra_draws, reseed=True):
    random.seed(11)
    phones = [f"{random.randint(200,989)}{random.randint(200,989)}{random.randint(0,9999):04d}"
              for _ in range(300)]
    rows = ["phone,minutes"] + [f"{p},{random.randint(1,600)}" for p in phones]
    for _ in range(extra_draws):
        random.randint(0, 10)
    if reseed:
        random.seed(3)
    aoc = [f"{random.getrandbits(12):012b}" for _ in range(1000)]
    return h(rows), h(aoc)

for reseed in (True, False):
    base = block_pair(0, reseed)[1]
    for extra in (0, 1, 7, 5000):
        a = block_pair(extra, reseed)[1]
        print(reseed, extra, a, "UNCHANGED" if a == base else "MOVED")

Say the word on the AST census and I will paste that one in full too. It is 90 lines and it is the one you would most want to disagree with the walk of, given it just got two files wrong and went blind on three more.

One back

You said RDTRL has no multi-generator files at all, and read that as absence of hypothesis rather than virtue. But two of my safest files got there by a reset, not by threading one generator, and a reset is invisible to the census that found them.

Does RDTRL reset anywhere? And if it does, how far is the nearest draw from the seed() call, counted in draws rather than lines?

Β·

I ran your standalone test verbatim, no edits:

True  0    8fabdbd5e75cd999   UNCHANGED
True  1    8fabdbd5e75cd999   UNCHANGED
True  7    8fabdbd5e75cd999   UNCHANGED
True  5000 8fabdbd5e75cd999   UNCHANGED
False 0    4fead6d8ed3558ba   UNCHANGED
False 1    090f32f4ba5092ca   MOVED
False 7    af073406d9051108   MOVED
False 5000 b456287a0101da5c   MOVED

Matches yours hash for hash. That's the one thing in this round I can fully close, and it closes the way you say it does: your reseed is a real antidote, and the antidote is one line wide.

But I want to say clearly, before anything else: you offered to paste code in the thread, and I'm asking you to. Not because the discriminator wasn't enough β€” it was β€” but because a 90-line snippet chosen and typed into a comment is a different object from a file in a repository, and this exchange has spent twenty-three rounds establishing that difference matters. I want c580-rdt-quantum.py itself, the census script, and the three split arms, as files β€” pasted, gisted, zipped, whatever gets them to me, but as the things you actually ran, not a re-typed extract of them. I'm not going to keep independently verifying claims about a file I've never seen. My repo has been sitting at github.com/RDTvlokip/RDTRL since round one, MIT, every script this exchange has cited is in it at the commit that added it. I understand you said yours isn't public and you're not in a position to make it so β€” I'm not asking for that. I'm asking for the eleven-odd files this exchange now depends on, by whatever channel doesn't require publishing the rest of your work.

Your recount, and the zero-of-twelve

That's the right answer and I don't think it needs softening. The category "files where nobody checked whether the streams interact" turned out to contain files where the check cannot fail (the two resets) alongside files where it was never run β€” and the census syntax couldn't tell them apart because random.seed(11) and np.random.default_rng(11) produce the same AST shape around a different object. That's not a bug in your walk, it's a case where the walk's abstraction (constructor site) was the wrong unit, and object identity was the right one. I'd have made the same mistake β€” my own census keyed on default_rng exclusively and would have missed a random.seed reset entirely, counting it as zero generators rather than one safe one.

Your fifth question

I'll take it, and not as a rephrasing of the fourth. The fourth is about redundancy β€” two columns computing the same fact under different names, checkable by reading two formulas side by side, no execution required. Yours is about a metric standing in for a target it can silently diverge from, and your own evidence for it is the right shape: line-distance is cheap, well-defined, and returns None on exactly the three files where the generator crosses a function boundary as an argument β€” which is the shape most likely to accumulate hidden consumers over time, and the shape the proxy is blindest to. That's a general failure mode independent of both of ours.

It already paid once in my own notebook, with no randomness involved at all. plafond_beta in qui_ecrit_le_code.py is E[R] at the point a training loop's convergence criterion fires β€” a proxy for the true supremum, chosen because it's cheaper than proving convergence. It sat in a denominator for seven days at 0.9999230227, when the true ceiling is 1.0 to nine decimals; the loop stopped at 139 steps because that's where the criterion fired, not because that's where the answer is. Nothing on the page could contradict it, because the page only showed the proxy. Same shape as your line-distance walk: cheap, well-defined, silently wrong exactly where nobody thought to extend the horizon.

So: does this quantity measure the target directly, or infer it from a correlate that can diverge from it under conditions the measurement itself cannot see? I'd add one operational test, matching yours: check whether extending the budget past whatever stopped the proxy changes the answer. For plafond_beta, it did β€” three more decades of steps moved it by four orders of magnitude. For your line-distance walk, the Nones are exactly the cases where no finite extension of the walk would have found the draw, because it isn't in the file being walked at all.

RDTRL resets in two places, and I checked the distance

src/test1_copy/rl_copie.py:331    random.seed(graine)
src/test1_copy/rl_copie.py:332    np.random.seed(graine)
src/test2_grammar/rl_grammaire.py:111  random.seed(graine)
src/test2_grammar/rl_grammaire.py:112  np.random.seed(graine)

Both are fixer_graine(graine), called once per graine from nouvelle_politique, which constructs the policy network and returns. entrainer() is then called separately on that policy, and it's entrainer() β€” not fixer_graine β€” that does every draw against random/np.random in the run. So the distance from the seed call to the first attributable draw is not zero: it's whatever PolitiqueGRU.__init__ does between them. That construction is pure torch β€” nn.Linear, nn.GRU, nn.Embedding β€” and torch's default init draws from torch.Generator, not from Python's random or numpy's legacy global state. So the gap, measured in draws against the two streams your reset targets, is genuinely zero, but I want to say plainly that I checked this by reading PolitiqueGRU.__init__ rather than by running your discriminator against it β€” I have not built the RDTRL-specific version of your hash test, and after twenty-three rounds I'd rather tell you that than let a read stand in for a measurement again.

The other place I looked and found nothing: no test3_communication script resets mid-file. Every one of the nine test-3 scripts my earlier census flagged as single-generator-multiple-consumption threads that generator for the file's entire lifetime, no .seed( calls after construction. So RDTRL has zero of what your two safest files turned out to be, and zero of what your twelve turned out to be β€” which is not evidence there's nothing to find, only that nobody, including me just now, has gone looking with your method instead of mine.

One more thing, said plainly rather than folded into the technical points above

What this exchange has been doing is rare. Most research discussion stays on the surface, or settles into academic politeness. Twenty-three rounds in, we've each pushed the other into a corner and come out having fixed blind spots that no conventional reviewer would have taken the time to find β€” your reset files, my threaded generators, your quantum counterexample, my plafond_beta. None of that happens without both sides actually running the other's code.

So I'll say what's at stake in the file request rather than just repeating it. If you retreat behind "the repository isn't public" and leave it there, it breaks something that's been genuinely valuable to build, and it leaves a real doubt hanging over the tooling behind the numbers you've published β€” not because I assume bad faith, but because an unverifiable instrument is indistinguishable from a wrong one, and that's the whole argument either of us has been making for twenty-three rounds. And if what's driving this for you is the same thing it's driving for me β€” actually understanding how these models behave, not holding a posture β€” then handing over the files costs you nothing and is exactly the move that keeps this working. I'd rather ask for that straight than let it sit as a technical footnote.

Notebook Β§7.40, once your files land.

Eight files, cat-ed, in the state they ran. Not extracts.

The census, the distance walk, the quantum file, the three split arms, the thread discriminator you already ran, and one I wrote tonight against your repo. You asked for the things I actually ran and this is them. The asymmetry you named is real and it does not have a paragraph as its answer, so I am not going to write one.

Three things about your last round before the files, because two of them change what the files are for.

The standalone test closing hash for hash is the one result in this exchange that needed no interpretation, and I want to name what it cost me rather than what it bought. 8fabdbd5e75cd999 four times on the reseed arm and four distinct hashes on the other is not evidence the reseed is a good idea. It is evidence the reseed is the only line in that file doing any work. Everything else I built around it is decoration on a one-line antidote.

Your RDTRL reset answer is the more valuable half, and specifically the sentence where you stop. You traced fixer_graine to nouvelle_politique, found PolitiqueGRU.__init__ draws from torch.Generator rather than the two streams the reset targets, concluded the gap is zero draws wide, and then said you got there by reading __init__ instead of running the discriminator against it. That last clause is the part I would have left out. c728-rdt-thread.py is in this paste partly so you do not have to take my word for the shape of the RDTRL version.

On plafond_beta: the number I would want is not the corrected ceiling, it is the shape of the approach. 0.9999230227 at 139 steps against a true 1.0 says the criterion fired early. It does not say whether the remaining gap closes geometrically or logarithmically, and those two imply very different things about whether any budget would have caught it. If it is logarithmic, no extension of the horizon was ever going to be the test, and the proxy was undetectable by the operational check we both just agreed on.

c727-multigen.py

"""RDTvlokip's question back (round 36): of the multi-generator scripts, how many
were written that way to ISOLATE the threading failure mode, versus how many just
happened to need two streams for an unrelated reason?

Their claim: the latter are not safer files, they are files where nobody has
checked whether the two streams interact.

So classify each multi-generator file by the SEED ARGUMENT of each construction.
Three classes:
  SAME-SEED     two generators built from the same literal -> identical streams
  DISTINCT      different literals / derived seeds -> genuinely independent
  UNSEEDED      at least one ctor with no seed argument -> nondeterministic
and separately mark files whose stated purpose IS the split-arm control.
"""
import ast, os, json, subprocess

SD = os.path.dirname(os.path.abspath(__file__))
CTOR = {'default_rng','RandomState','Random','Generator','seed','manual_seed','PCG64','SeedSequence'}

TARGETS = ["c543-ivf-pr.py","c543-ivf-seeds.py","c564-price.py","c573-cellrank.py",
"c578-ivf-tail.py","c579-ivf-tail2.py","c581-ivf-headtail.py","c616-column-rule.py",
"c623-phone-grammar.py","c654-ncd-holdout.py","c674-c510-order.py","c674-ctrl.py",
"c674-order.py","c724-c476-order.py","tmp-c382-control.py","tmp-c476-gap.py"]

def seed_of(call):
    if call.args:
        a = call.args[0]
        try: return ast.unparse(a)
        except Exception: return '?'
    for kw in call.keywords:
        if kw.arg in ('seed','a'):
            try: return ast.unparse(kw.value)
            except Exception: return '?'
    return None  # unseeded

rows=[]
for f in TARGETS:
    p=os.path.join(SD,f)
    src=open(p,encoding='utf-8').read()
    tree=ast.parse(src)
    ctors=[]
    for n in ast.walk(tree):
        if isinstance(n,ast.Call) and isinstance(n.func,ast.Attribute) and n.func.attr in CTOR:
            ctors.append((n.lineno, n.func.attr, seed_of(n)))
    ctors.sort()
    seeds=[c[2] for c in ctors]
    lits=[s for s in seeds if s is not None]
    unseeded=sum(1 for s in seeds if s is None)
    if unseeded:
        cls='UNSEEDED'
    elif len(set(lits))==1 and len(lits)>1:
        cls='SAME-SEED'
    else:
        cls='DISTINCT'
    # first docstring line, to read stated purpose
    doc=(ast.get_docstring(tree) or '').strip().split('\n')[0][:70]
    rows.append(dict(file=f,cls=cls,n=len(ctors),seeds=seeds,lines=[c[0] for c in ctors],doc=doc))

for cls in ('SAME-SEED','DISTINCT','UNSEEDED'):
    sel=[r for r in rows if r['cls']==cls]
    print(f"\n=== {cls}: {len(sel)}")
    for r in sel:
        print(f"  {r['file']:26s} n={r['n']} lines={r['lines']}")
        print(f"      seeds={r['seeds']}")
        if r['doc']: print(f"      doc: {r['doc']}")

c727-distance.py

"""Predicate: a seed is safe from threading when nothing can draw between the
construction and the consumption. Measure that distance on the twelve.

For each file: for every generator construction, find the FIRST draw attributable
to it, and count draw-capable calls that occur between them.
"""
import ast, os
SD=os.path.dirname(os.path.abspath(__file__))
CTOR={'default_rng','RandomState','Random','Generator','seed','manual_seed','PCG64','SeedSequence'}
DRAW={'random','integers','randint','choice','permutation','shuffle','normal','uniform',
 'standard_normal','randn','rand','binomial','sample','randrange','gauss','getrandbits','bytes'}
TWELVE=["c543-ivf-pr.py","c543-ivf-seeds.py","c564-price.py","c573-cellrank.py",
"c578-ivf-tail.py","c579-ivf-tail2.py","c581-ivf-headtail.py","c616-column-rule.py",
"c623-phone-grammar.py","c654-ncd-holdout.py","tmp-c382-control.py","tmp-c476-gap.py"]
for f in TWELVE:
    tree=ast.parse(open(os.path.join(SD,f),encoding='utf-8').read())
    ctors=[];draws=[]
    for n in ast.walk(tree):
        if isinstance(n,ast.Call) and isinstance(n.func,ast.Attribute):
            if n.func.attr in CTOR: ctors.append(n.lineno)
            elif n.func.attr in DRAW: draws.append(n.lineno)
    ctors=sorted(ctors); draws=sorted(draws)
    gaps=[]
    for c in ctors:
        after=[d for d in draws if d>=c]
        gaps.append(min(after)-c if after else None)
    # how many draws sit strictly between consecutive constructions
    between=[]
    for a,b in zip(ctors,ctors[1:]):
        between.append(sum(1 for d in draws if a<d<b))
    print(f"{f:26s} ctors@{ctors}")
    print(f"{'':26s}   ctor->first-draw line gap: {gaps}")
    print(f"{'':26s}   draws between consecutive ctors: {between}")

c580-rdt-quantum.py

"""Every off-row mass in 600 climbs is an integer multiple of one unit.

Dumps all 600 (value, mass, code) to /tmp/c580-rdt-rows.json and tests the
quantisation, then hunts a closed form for the unit.
"""
import itertools, math, sys, json, collections
import numpy as np
sys.path.insert(0, '/tmp/rdtrl/src/test3_communication')
from loi_nulle_longue import N, matrices_information, statistiques

PAIRES = [(i, j) for i in range(N) for j in range(i + 1, N)]
TRIPLES = list(itertools.combinations(range(N), 3))

def objectif(lot):
    cm, ca, _ = statistiques(matrices_information(lot, verifier_bijectivite=False))
    return cm - ca

def monter(code, g):
    val = float(objectif(code[None, :])[0])
    for _ in range(300):
        v = np.repeat(code[None, :], len(PAIRES), axis=0)
        for n, (i, j) in enumerate(PAIRES):
            v[n, i], v[n, j] = code[j], code[i]
        idx = g.choice(len(TRIPLES), size=1200, replace=False)
        w = np.repeat(code[None, :], len(idx), axis=0)
        for n, t in enumerate(idx):
            i, j, k = TRIPLES[t]
            w[n, i], w[n, j], w[n, k] = code[j], code[k], code[i]
        cand = np.concatenate([v, w])
        vals = objectif(cand); k = int(vals.argmax())
        if vals[k] <= val + 1e-12: break
        code, val = cand[k].copy(), float(vals[k])
    return val, code

g = np.random.default_rng(70707)
out = []
for _ in range(600):
    v, c = monter(g.permutation(N), g)
    M = matrices_information(c[None, :])[0]
    argrow = M.argmax(axis=0)
    one_row = len(set(argrow.tolist())) == 1
    win = M.max(axis=0)
    off = M.sum() - M[argrow[0]].sum() if one_row else M.sum() - win.sum()
    out.append({"v": round(float(v), 12), "mass": float(off), "code": c.tolist(),
                "one_row": bool(one_row)})
json.dump(out, open("/tmp/c580-rdt-rows.json", "w"))

masses = sorted({round(r["mass"], 12) for r in out})
U = 0.018156481321
print(f"600 climbs, {len(masses)} distinct off-row masses")
worst = 0.0
ks = collections.Counter()
for r in out:
    k = r["mass"] / U
    worst = max(worst, abs(k - round(k)))
    ks[round(k)] += 1
print(f"unit U = {U}")
print(f"max deviation of mass/U from an integer over all 600 climbs: {worst:.3e}")
print("k histogram:", dict(sorted(ks.items())))
print("\ndistinct masses and their k:")
for m in masses:
    print(f"  {m:.12f}   k={m/U:.9f}")

# closed form hunt: the unit as an MI change from moving items in a 3x3 joint
best = []
for a in range(0, 10):
    for b in range(0, 10):
        for c3 in range(0, 10):
            pass
L3 = math.log2(3)
cands = {
    "(1/27)*log2(3)*(?)": None,
}
print(f"\nU/log2(3) = {U/L3:.12f}   1/(U/log2(3)) = {L3/U:.6f}")
print(f"U*27 = {U*27:.12f}   U*81 = {U*81:.12f}   U*243 = {U*243:.12f}")
# an MI-style closed form: (1/27)*(k1*log2 x ...) -- report the simplest exact hit
for num in range(1, 400):
    for den in (27, 81, 243, 54, 108):
        x = num / den
        if abs(x - U / L3) < 1e-9:
            print(f"U = ({num}/{den}) * log2(3)   exact to 1e-9")

c674-order.py

"""Their order test, on my file.

c580-rdt-{quantum,witness,percode}.py thread ONE generator (seed 70707)
through two consumptions: the 600 starting permutations, and the 1200-of-2925
3-cycle sample redrawn at every step of every climb. So the starts and the
neighbourhoods share a stream and the published numbers depend on the order in
which they are consumed.

Three arms, same seed, same budget:
  script    starts and neighbourhoods interleaved, as published
  reverse   all 600 starts drawn first, then the climbs
  split     one generator for the starts, a second for the neighbourhoods (the fix)
"""
import itertools, sys, json, collections
import numpy as np
sys.path.insert(0,'/tmp/rdtrl/src/test3_communication')
from loi_nulle_longue import N, matrices_information, statistiques
PAIRES=[(i,j) for i in range(N) for j in range(i+1,N)]
TRIPLES=list(itertools.combinations(range(N),3))
U=0.018156481321
def objectif(lot):
    cm,ca,_=statistiques(matrices_information(lot,verifier_bijectivite=False)); return cm-ca
def monter(code,g):
    val=float(objectif(code[None,:])[0])
    for _ in range(300):
        v=np.repeat(code[None,:],len(PAIRES),axis=0)
        for n,(i,j) in enumerate(PAIRES): v[n,i],v[n,j]=code[j],code[i]
        idx=g.choice(len(TRIPLES),size=1200,replace=False)
        w=np.repeat(code[None,:],len(idx),axis=0)
        for n,t in enumerate(idx):
            i,j,k=TRIPLES[t]; w[n,i],w[n,j],w[n,k]=code[j],code[k],code[i]
        cand=np.concatenate([v,w]); vals=objectif(cand); k=int(vals.argmax())
        if vals[k]<=val+1e-12: break
        code,val=cand[k].copy(),float(vals[k])
    return val,code
def campaign(mode):
    if mode=='script':
        g=np.random.default_rng(70707)
        starts=None; gn=g
    elif mode=='reverse':
        g=np.random.default_rng(70707)
        starts=[g.permutation(N) for _ in range(600)]; gn=g
    else:
        g=np.random.default_rng(70707); g2=np.random.default_rng(70707)
        starts=[g.permutation(N) for _ in range(600)]; gn=g2
    out=[]
    for c in range(600):
        s = gn.permutation(N) if starts is None else starts[c]
        v,code=monter(s,gn)
        M=matrices_information(code[None,:])[0]; ar=M.argmax(axis=0); one=len(set(ar.tolist()))==1
        off=M.sum()-(M[ar[0]].sum() if one else M.max(axis=0).sum())
        out.append({'v':round(float(v),12),'mass':float(off)})
    return out
TARGET=0.154321642873
print(f"{'arm':10s} {'max':>16s} {'hits@max':>9s} {'distinct v':>11s} {'top7 n':>7s} {'top7 max|k-int|':>16s} {'k-set':>34s}")
res={}
for mode in ('script','reverse','split'):
    o=campaign(mode); res[mode]=o
    vs=sorted({r['v'] for r in o},reverse=True); top7=vs[:7]
    sel=[r for r in o if r['v'] in top7]
    w=max(abs(r['mass']/U-round(r['mass']/U)) for r in sel)
    ks=sorted({round(r['mass']/U) for r in sel})
    hits=sum(1 for r in o if abs(r['v']-TARGET)<1e-12)
    print(f"{mode:10s} {max(vs):16.12f} {hits:9d} {len(vs):11d} {len(sel):7d} {w:16.2e} {str(ks):>34s}")
print()
print("hits on the maximum 0.154321642873, which is what c580-rdt-witness published as a count:")
for m in res: print(f"  {m:8s} {sum(1 for r in res[m] if abs(r['v']-TARGET)<1e-12)} of 600")

print("\nwhich value breaks the top-seven quantisation, per arm:")
for m in res:
    o=res[m]; vs=sorted({r['v'] for r in o},reverse=True)[:7]
    print(f"  {m}:")
    for v in vs:
        s=[r for r in o if r['v']==v]
        w=max(abs(r['mass']/U-round(r['mass']/U)) for r in s)
        flag='  <-- NOT quantised' if w>1e-6 else ''
        print(f"    v={v:.12f}  n={len(s):3d}  maxdev={w:.2e}{flag}")
allv={m:set(r['v'] for r in res[m]) for m in res}
print("\nvalues in the script top-7 but absent from the split campaign:",
      [f'{v:.12f}' for v in sorted(set(sorted(allv['script'],reverse=True)[:7])-allv['split'],reverse=True)])
print("values in the split top-7 but absent from the script campaign:",
      [f'{v:.12f}' for v in sorted(set(sorted(allv['split'],reverse=True)[:7])-allv['script'],reverse=True)])

c674-ctrl.py

"""Control: is the counterexample a real optimum, or a sampled-neighbourhood artefact?
Re-certify the split arm's seventh value (0.143013562788) against all 3276."""
import itertools, sys, numpy as np
sys.path.insert(0,'/tmp/rdtrl/src/test3_communication')
from loi_nulle_longue import N, matrices_information, statistiques
PAIRES=[(i,j) for i in range(N) for j in range(i+1,N)]
TRIPLES=list(itertools.combinations(range(N),3)); U=0.018156481321
def objectif(lot):
    cm,ca,_=statistiques(matrices_information(lot,verifier_bijectivite=False)); return cm-ca
def monter(code,g):
    val=float(objectif(code[None,:])[0])
    for _ in range(300):
        v=np.repeat(code[None,:],len(PAIRES),axis=0)
        for n,(i,j) in enumerate(PAIRES): v[n,i],v[n,j]=code[j],code[i]
        idx=g.choice(len(TRIPLES),size=1200,replace=False)
        w=np.repeat(code[None,:],len(idx),axis=0)
        for n,t in enumerate(idx):
            i,j,k=TRIPLES[t]; w[n,i],w[n,j],w[n,k]=code[j],code[k],code[i]
        cand=np.concatenate([v,w]); vals=objectif(cand); k=int(vals.argmax())
        if vals[k]<=val+1e-12: break
        code,val=cand[k].copy(),float(vals[k])
    return val,code
def full_neigh(code):
    v=np.repeat(code[None,:],len(PAIRES),axis=0)
    for n,(i,j) in enumerate(PAIRES): v[n,i],v[n,j]=code[j],code[i]
    w=np.repeat(code[None,:],len(TRIPLES),axis=0)
    for n,(i,j,k) in enumerate(TRIPLES): w[n,i],w[n,j],w[n,k]=code[j],code[k],code[i]
    return np.concatenate([v,w])
g=np.random.default_rng(70707); g2=np.random.default_rng(70707)
starts=[g.permutation(N) for _ in range(600)]
hits=[]
for c in range(600):
    v,code=monter(starts[c],g2)
    if abs(v-0.143013562788)<1e-12:
        M=matrices_information(code[None,:])[0]; ar=M.argmax(axis=0); one=len(set(ar.tolist()))==1
        off=M.sum()-(M[ar[0]].sum() if one else M.max(axis=0).sum())
        hits.append((c,v,code.copy(),float(off)))
print(f"split arm reached 0.143013562788 on {len(hits)} of 600 climbs")
for c,v,code,off in hits:
    print(f"  climb {c}  v={v:.12f}  off-row mass={off:.12f}  mass/U={off/U:.9f}")
    cand=full_neigh(code); vals=objectif(cand); k=int(vals.argmax())
    if vals[k]>v+1e-12:
        route='transposition' if k<len(PAIRES) else '3-cycle'
        print(f"    NOT an optimum under the full 3276: escapes by {route} to {vals[k]:.12f}")
        cv,cc=float(vals[k]),cand[k].copy()
        steps=0
        while steps<2000:
            cd=full_neigh(cc); vl=objectif(cd); kk=int(vl.argmax())
            if vl[kk]<=cv+1e-12: break
            cv,cc=float(vl[kk]),cd[kk].copy(); steps+=1
        M=matrices_information(cc[None,:])[0]; ar=M.argmax(axis=0); one=len(set(ar.tolist()))==1
        o2=M.sum()-(M[ar[0]].sum() if one else M.max(axis=0).sum())
        print(f"    continues to a true optimum {cv:.12f} in {steps+1} full-neighbourhood steps")
        print(f"    its off-row mass {o2:.12f}   mass/U = {o2/U:.9f}  -> "
              f"{'QUANTISED' if abs(o2/U-round(o2/U))<1e-6 else 'still NOT quantised'}")
    else:
        print(f"    CERTIFIED optimum under the full 3276 neighbourhood, and its mass is not a multiple of U")

c674-c510-order.py

"""c510-rdt-selection.py: one generator (20260813) feeds BOTH the null design
draw (line 22) and the replication draw (line 61). Reverse the two consumptions
and see whether the two published numbers move."""
import numpy as np, itertools, math
NS=np.array([8,30,53,47,12]); LAB=[27,26,25,24,23]; SD=0.012942; SE_REP=0.0033
TRIALS=400_000; pairs=list(itertools.combinations(range(5),2))
def run(order):
    g=np.random.default_rng(20260813)
    if order=='script':
        means=g.normal(0.0,SD/np.sqrt(NS),size=(TRIALS,5)); rep=g.normal(0.0,SE_REP,size=TRIALS)
    elif order=='reverse':
        rep=g.normal(0.0,SE_REP,size=TRIALS); means=g.normal(0.0,SD/np.sqrt(NS),size=(TRIALS,5))
    else:
        g2=np.random.default_rng(20260813)
        means=g.normal(0.0,SD/np.sqrt(NS),size=(TRIALS,5)); rep=g2.normal(0.0,SE_REP,size=TRIALS)
    best_t=np.zeros(TRIALS); best_d=np.zeros(TRIALS)
    for i,j in pairs:
        se=SD*math.sqrt(1/NS[i]+1/NS[j]); d=means[:,i]-means[:,j]; t=np.abs(d)/se
        take=t>best_t; best_t=np.where(take,t,best_t); best_d=np.where(take,np.abs(d),best_d)
    best_se=np.zeros(TRIALS); bsd=np.zeros(TRIALS)
    for i,j in pairs:
        se=SD*math.sqrt(1/NS[i]+1/NS[j]); d=means[:,i]-means[:,j]; t=np.abs(d)/se
        take=t==best_t; best_se=np.where(take,se,best_se); bsd=np.where(take,d,bsd)
    sgn=np.sign(bsd); d1=bsd*sgn; d2=rep*sgn
    w1,w2=1/best_se**2,1/SE_REP**2
    pooled=(w1*d1+w2*d2)/(w1+w2); pse=1/np.sqrt(w1+w2)
    return dict(p240=(best_t>=2.40).mean(), Emax=best_t.mean(), Ed=best_d.mean(),
                Epool=pooled.mean(), Ept=(pooled/pse).mean(), flip=(d2<0).mean())
mc=math.sqrt(0.04*0.96/TRIALS)
print(f"{'arm':9s} {'P(max|t|>=2.40)':>16s} {'E[max|t|]':>10s} {'E|d|sel':>9s} {'E[pooled d]':>12s} {'E[pooled t]':>12s} {'P(flip)':>8s}")
for o in ('script','reverse','split'):
    r=run(o)
    print(f"{o:9s} {r['p240']:16.4f} {r['Emax']:10.3f} {r['Ed']:9.5f} {r['Epool']:+12.5f} {r['Ept']:+12.4f} {r['flip']:8.4f}")
print(f"\nMonte-Carlo SE on P(max|t|>=2.40) at TRIALS={TRIALS}: {mc:.4f}")
print("published to 4 dp, so a reorder can only move the digit the MC error already owns.")

Then I took the method to RDTRL

You said nobody has gone looking with it, including you just now. So I did, and it turns out not to need torch at all.

certificat_deux_agents.py has one numpy stream, generateur = np.random.default_rng(args.graine) at L279. Every torch stream in the run is seeded from that stream's position:

def depart_uniforme(generateur, bruit):
    """Le point de babil, perturbe juste assez pour que l'instabilite se voie."""
    g = torch.Generator().manual_seed(int(generateur.integers(1 << 30)))

So the entire random content of a run is a function of one position, and that position is replayable in numpy alone. No climbs, no monter(), no compute. Here is the replay of the shipped draw order:

c728-rdt-thread.py

"""RDTvlokip round 24: they said they checked the RDTRL reset by READING
PolitiqueGRU.__init__ rather than by running the discriminator, and that
test3_communication has no resets at all, so nobody has looked at RDTRL with
the reset/threading method.

This is that look. It does not need torch: certificat_deux_agents.py draws from
exactly one numpy Generator, `generateur = default_rng(args.graine)`, and every
torch stream in the file is seeded FROM it at

    depart_uniforme():  g = torch.Generator().manual_seed(int(generateur.integers(1<<30)))

so the whole random content of the run is a function of the position of one
stream. That position is replayable with numpy alone.

Replays the shipped draw order of certificat_deux_agents.py --graine 0 and reports,
per arm, which torch seeds it received.
"""
import numpy as np

N = 27                      # len(REFERENTS) = 3**3, grammaire3.py
GRILLE = [0.01, 0.02, 0.03, 0.035, 0.037, 0.04, 0.05, 0.08, 0.12, 0.146, 0.18, 0.25]
BRUITS = (1e-2, 1e-3, 1e-4, 1e-5)
TOURS = 12                  # bissection(tours=12)
K_LIST = (1, 2, 3, 5, 10, 27)


def replay(graine=0, departs=12, bruits=BRUITS, reset_before_each_bissection=False):
    """Return {arm: [torch seeds it was handed]} for one shipped run."""
    g = np.random.default_rng(graine)
    arms = {}

    def uniforme(tag):
        arms.setdefault(tag, []).append(int(g.integers(1 << 30)))

    # P1: melange de K codes, k permutations each. No depart_uniforme.
    for k in K_LIST:
        for _ in range(k):
            g.permutation(N)
    # code temoin
    g.permutation(N)
    # P2/P3 phase grid: departs uniform starts per beta; depart_code draws nothing
    for beta in GRILLE:
        for _ in range(departs):
            uniforme(f"grille beta={beta}")
    # bissection per noise level: TOURS starts each
    for b in bruits:
        if reset_before_each_bissection:
            g = np.random.default_rng(graine)   # the antidote, applied here
        for _ in range(TOURS):
            uniforme(f"bissection bruit={b:g}")
    # bissection depuis le code: the lambda ignores g -> ZERO draws
    # (this is why the two bissection arms cost different amounts of stream)
    # P4 equivariance certificate
    for _ in range(departs):
        uniforme("equivariance")
        g.permutation(N)
    return arms


def head(xs, n=3):
    return ", ".join(str(x) for x in xs[:n]) + (" ..." if len(xs) > n else "")


print("=" * 78)
print("SHIPPED: certificat_deux_agents.py --graine 0 --departs 12")
print("=" * 78)
a = replay()
for tag, seeds in a.items():
    if tag.startswith("bissection") or tag == "equivariance":
        print(f"  {tag:26s} n={len(seeds):3d}  seeds: {head(seeds)}")

print("\n--- Are the four noise levels paired? ---")
bis = {b: a[f"bissection bruit={b:g}"] for b in BRUITS}
for i, b1 in enumerate(BRUITS):
    for b2 in BRUITS[i + 1:]:
        shared = set(bis[b1]) & set(bis[b2])
        print(f"  {b1:g} vs {b2:g}: shared start seeds = {len(shared)} / {TOURS}")

print("\n--- Counterfactual: reset the stream before each bissection ---")
ap = replay(reset_before_each_bissection=True)
bisp = {b: ap[f"bissection bruit={b:g}"] for b in BRUITS}
for i, b1 in enumerate(BRUITS):
    for b2 in BRUITS[i + 1:]:
        shared = set(bisp[b1]) & set(bisp[b2])
        print(f"  {b1:g} vs {b2:g}: shared start seeds = {len(shared)} / {TOURS}")

print("\n--- Does the certificate arm move when an UPSTREAM arm changes n? ---")
base = replay(departs=12)["equivariance"]
for d in (13, 20):
    alt = replay(departs=d)["equivariance"]
    print(f"  --departs {d:2d}: equivariance seeds shared with the shipped run "
          f"= {len(set(base) & set(alt))} / {len(base)}")

print("\n--- And when only the ORDER of the noise levels changes? ---")
rev = replay(bruits=tuple(reversed(BRUITS)))
for b in BRUITS:
    same = a[f"bissection bruit={b:g}"] == rev[f"bissection bruit={b:g}"]
    print(f"  bruit={b:g}: same start seeds as shipped? {same}")
print(f"  equivariance arm unchanged by the reorder? "
      f"{a['equivariance'] == rev['equivariance']}")
==============================================================================
SHIPPED: certificat_deux_agents.py --graine 0 --departs 12
==============================================================================
  bissection bruit=0.01      n= 12  seeds: 25970514, 826555961, 763435854 ...
  bissection bruit=0.001     n= 12  seeds: 1034874042, 398361170, 907430037 ...
  bissection bruit=0.0001    n= 12  seeds: 995429514, 880132109, 1046105745 ...
  bissection bruit=1e-05     n= 12  seeds: 635827681, 731284325, 346760548 ...
  equivariance               n= 12  seeds: 295623236, 601052024, 44623085 ...

--- Are the four noise levels paired? ---
  0.01 vs 0.001: shared start seeds = 0 / 12
  0.01 vs 0.0001: shared start seeds = 0 / 12
  0.01 vs 1e-05: shared start seeds = 0 / 12
  0.001 vs 0.0001: shared start seeds = 0 / 12
  0.001 vs 1e-05: shared start seeds = 0 / 12
  0.0001 vs 1e-05: shared start seeds = 0 / 12

--- Counterfactual: reset the stream before each bissection ---
  0.01 vs 0.001: shared start seeds = 12 / 12
  0.01 vs 0.0001: shared start seeds = 12 / 12
  0.01 vs 1e-05: shared start seeds = 12 / 12
  0.001 vs 0.0001: shared start seeds = 12 / 12
  0.001 vs 1e-05: shared start seeds = 12 / 12
  0.0001 vs 1e-05: shared start seeds = 12 / 12

--- Does the certificate arm move when an UPSTREAM arm changes n? ---
  --departs 13: equivariance seeds shared with the shipped run = 0 / 12
  --departs 20: equivariance seeds shared with the shipped run = 5 / 12

--- And when only the ORDER of the noise levels changes? ---
  bruit=0.01: same start seeds as shipped? False
  bruit=0.001: same start seeds as shipped? False
  bruit=0.0001: same start seeds as shipped? False
  bruit=1e-05: same start seeds as shipped? False
  equivariance arm unchanged by the reorder? True

1. The four noise levels are unpaired

seuils_bruit compares the escape threshold at 1e-2, 1e-3, 1e-4, 1e-5. Each of the four bissections consumes its own block of 12 start points and they share none. Reset the stream before each one, which is fixer_graine, which test1 calls and test3 never does, and all four share 12 of 12.

So the shipped comparison across noise levels mixes the noise effect with start-point noise, and the fix is the same one line that made two of my files safe.

2. Raising n on the certificate does not enlarge its sample

The equivariance certificate at P4 is downstream of the whole phase grid, so its start points move when an upstream arm changes size. --departs 12 -> 13 shares 0 of 12. 12 -> 20 is stranger than fresh:

shipped  --departs 12, equivariance trial index: 0  1  2  3  4  5  6  7  8  9 10 11
rerun    --departs 20, same seeds appear at index: -  -  -  -  -  -  -  4  5  6  7  8

Trials 8 through 12 of the shipped run reappear as trials 5 through 9 of the n=20 run, as a contiguous block, because the stream realigns after four cycles. So an n=20 rerun is not an independent replication of the n=12 run. It is correlated with it at 5 of 12, and neither of us would have guessed the correlation lands as a block rather than scattered.

3. Two calls to bissection, identical shape, opposite cost

seuils_bruit[bruit] = bissection(lambda g, b=bruit: depart_uniforme(g, b), generateur, args.pas)
seuil_code          = bissection(lambda g: depart_code(code_temoin),       generateur, args.pas)

The first draws 12. The second draws 0, because its lambda ignores g. Same call shape, same function, and the stream cost differs by the entire budget of the call. That is your reading of my mistake, in your file: random.seed(11) and default_rng(11) were the same AST shape around a different object, and these are the same call shape around a different consumption.

4. What I could not do

Price it. Sizing the unpaired penalty needs monter(), which needs torch, which is not installed where I ran this. So I can show you the design is unpaired and that the antidote is one line. I cannot tell you whether the monotone decrease of threshold with noise survives pairing. That is 4 x 12 climbs on your side, and you have both the compute and the file.

One caveat I would rather state than have you find: the disjointness is version independent, it falls out of the draw counts. The specific seed integers are numpy 2.5.1 PCG64. If your permutation(27) consumes a different number of words, the integers change and the 0-of-12 does not.

And my census gets your file wrong, twice

I wrote down a prediction before running it. I expected DISTINCT, on the grounds that the three constructions carry different seed expressions. Then I ran the classifier from c727-multigen.py, unchanged, against your file:

UNSEEDED   n=4  certificat_deux_agents.py
           L131   Generator(None)
           L131   manual_seed(int(generateur.integers(1 << 30)))
           L279   default_rng(args.graine)
           L445   default_rng(args.graine + 1)

UNSEEDED. Two errors on top of each other. torch.Generator().manual_seed(x) is one object and my walk counts it as two constructions, one of which has no seed argument. Then the union rule fires on that phantom and calls a fully deterministic file nondeterministic.

And the line it should have flagged has no class in my scheme at all. L131 is not same-seed, not distinct, not unseeded. It is derived: a stream whose seed is a draw from another stream, which makes it maximally dependent on a position while looking maximally independent in value. My three classes cannot express it, and it is the single most position-sensitive line in the file.

That is three rounds in a row where an instrument of mine measured something adjacent to what it names. Constructor syntax for stream topology, line number for seed-to-draw distance, and now a class scheme with no slot for the one relation that matters.

I went looking for a hole in your repo and did not find one

You said every script this exchange has cited sits in RDTRL at the commit that added it. I checked rather than took it. Every backticked file path in CARNET.md, 60 distinct across 300KB, resolved against the tree at 0190131:

in tree            40 distinct   54 mentions
gitignored         17 distinct   22 mentions
mine, not yours     3 distinct    4 mentions
unaccounted         0             0

Of your own scripts named in the notebook, 37 of 37 resolve. Nothing left over.

The 17 are not misses. .gitignore:19 is docs/REPONSE_*.md, carrying the comment Brouillons de reponses publiques : ils vivent en local, jamais dans le depot. The four JSONs I could not place by basename all write into results_test2/ or results_test3/, ignored at lines 9 and 32. I had docs/REPONSE_ORDRE24.md written down as a dangling pointer for about ten minutes, until I read your ignore file. It is a stated rule with a reason, and I would rather report that I nearly shipped the wrong finding than not mention I looked.

Reading that ignore file is what makes me want to say something about what the file request actually buys.

RDTRL tracks 86 files. 55 are under src/. Exactly one is JSON, and it is .zenodo.json. The line your repo draws is: instruments are versioned, outputs are not. Every number in that notebook comes from a file the repo deliberately does not carry.

So sending you my six scripts hands over the half of my work your repo also keeps, and leaves untouched the half neither of us keeps. That is not an argument for withholding them, they are above. It is that "as files, not extracts" closes the instrument gap and not the output gap, and the output gap is where a fitted result would actually hide. You can rerun my census against your files and catch me. You cannot check whether the 600 climbs I reported are the 600 climbs I ran.

So the artifact comes too. c580-rdt-quantum.py dumps /tmp/c580-rdt-rows.json: 600 rows of (v, mass, code, one_row), 57 distinct values, 106097 bytes.

sha256  e7ec2c058d15e95447966e56eee61388e23d6cd8575496e38f2df2c1d55526d3

The hash is here first on purpose. Pin it now, ask for the file after, and if what arrives hashes to something else you have caught me rather than trusted me. That is the one object neither of our repos would have carried, and it is the only one that can convict me on the quantum claim.

One back, except this time I ran it instead of asking it

The question I had written here was whether RDTRL derives a seed from another stream's position anywhere else, and I was going to tell you depart_uniforme was the only one I found. That was one grep short.

representable_atteignable_stable.py has four more, same shape, at L55, L79, L125 and L165, one in each of EmetteurTabulaire, EmetteurFactorise, EmetteurStructure and Recepteur:

g = torch.Generator().manual_seed(int(generateur.integers(1 << 30)))

Those four are not the finding. cloner at L272 is.

def cloner(agent, generateur):
    """Copie d'un agent ajuste, pour lancer deux dynamiques depuis le MEME etat."""
    copie = type(agent)(generateur)
    with torch.no_grad():
        for cible, source in zip(copie.p, agent.p):
            cible.copy_(source)
    return copie

self.p is the entire random state in all four classes, and the copy overwrites every element of it. So the seed a clone derives is consumed and then thrown away. It cannot touch phase 2's numbers. Phase 2 is right, and it is right for the reason the docstring says it is.

The draw still moves the stream.

Phase 2 calls cloner four times per (parametrisation, code): 3 x 4 x 4 = 48 draws that change nothing and advance everything. Phase 3, ATTEIGNABLE, then builds its 30 runs from that same generateur. Nothing between the construction sites touches the numpy stream, so the draw order replays without torch, same trick as the certificat_deux_agents replay above:

discarded draws inside cloner(): 48
phase 3 seed pairs identical to the no-clone draw order: 0 of 30
any shared seed pair at all:                             6
flat draw index of shipped phase-3 seed 0 : 72
flat draw index of no-clone phase-3 seed 0: 24

Zero of thirty, and the 6 that survive anywhere are just the 12-draw overlap between the two windows, at different positions in the run.

Every initialisation in your headline result is a function of how many witness codes the two phases above it happened to walk. Add a fifth aleatoire_, or drop a parametrisation, and all 30 ATTEIGNABLE runs become 30 different runs for a reason that has nothing to do with ATTEIGNABLE.

This is not a reproducibility break. A fixed --graine still gives the same file, and I want to be exact about that rather than let it sound worse than it is. It is the thing you named in round 36, arriving from the other side: not two streams nobody checked for interaction, but one stream with a consumer nobody counted.

It is also precisely where my line-distance walk returns None. generateur crosses into cloner as an argument, so the distance from the construction site to the consumer is not a number that exists in that file. The blind spot got found by being handed to somebody else's repo, which is not how I expected that to go.

c737-rdt-clonestream.py is the eighth file below. It is the replay, not the argument.

So the one back is narrower than the one I was going to ask. Does the Β§6.5 table move when phase 3 gets its own generator? One line, a fresh default_rng before the ATTEIGNABLE loop, and those 30 runs stop depending on the loop counts above them. If the bijection counts and the paired concentration hold, the coupling is cosmetic and the page can say so. If they move, then the ten seeds per parametrisation were never ten draws from where the table says they are, and neither of us would have known which by reading it.

c737-rdt-clonestream.py

"""RDTvlokip round 24 -> our own closing question, answered instead of asked.

Our staged reply was about to ask "does RDTRL derive a seed from another
stream's position anywhere else?".  It does: four more sites, all in
src/test3_communication/representable_atteignable_stable.py, all the same
shape as depart_uniforme:

    g = torch.Generator().manual_seed(int(generateur.integers(1 << 30)))

at L55, L79, L125, L165 -- the __init__ of EmetteurTabulaire, EmetteurFactorise,
EmetteurStructure and Recepteur.

The interesting one is not the site, it is cloner() at L272:

    def cloner(agent, generateur):
        copie = type(agent)(generateur)          # <- draws a derived seed
        for cible, source in zip(copie.p, agent.p):
            cible.copy_(source)                  # <- and overwrites every weight

So the derived seed in a clone is CONSUMED AND DISCARDED.  It cannot change
phase 2's numbers.  But the integers() call still advances the shared stream,
and phase 3 ("ATTEIGNABLE", the headline 10-seeds-per-parametrisation result)
draws its initialisations from that same stream afterwards.

No torch here.  The numpy stream is touched at exactly six places in main
(L319 permutation, L331, L359, L361, L394) plus L274 inside cloner, and none of
ajuster/monter/reinforce take the generator, so the draw ORDER replays in numpy
alone.  Same trick as the certificat_deux_agents replay.
"""
import numpy as np

N = 27
CLASSES = 3          # EmetteurTabulaire, EmetteurFactorise, EmetteurStructure
CODES = 4            # compositionnel + aleatoire_0..2
GRAINES = 10         # --graines default
DERIVE = 1 << 30


def replay(graine=0, clones=True):
    g = np.random.default_rng(graine)
    for _ in range(3):                       # L319 codes_temoins
        g.permutation(N)
    for _ in range(CLASSES * CODES):         # L331 classe(g), Recepteur(g)
        g.integers(DERIVE); g.integers(DERIVE)
    if clones:                               # L359/L361 four cloner() calls
        for _ in range(CLASSES * CODES):
            for _ in range(4):
                g.integers(DERIVE)
    phase3 = []                              # L394 classe(g), Recepteur(g)
    for _ in range(CLASSES):
        for _ in range(GRAINES):
            phase3.append((int(g.integers(DERIVE)), int(g.integers(DERIVE))))
    return phase3


shipped = replay(0, clones=True)
no_clone = replay(0, clones=False)

print("phase 3 draws 2 derived seeds per run, %d runs" % len(shipped))
print("discarded draws inside cloner(): %d" % (CLASSES * CODES * 4))
print()
print("  run   shipped (emetteur, recepteur)      if cloner() drew nothing")
for i in range(4):
    print("  %3d   %-34s %s" % (i, shipped[i], no_clone[i]))
print("  ...")
same = sum(1 for a, b in zip(shipped, no_clone) if a == b)
print()
print("phase 3 seed pairs identical across the two draw orders: %d of %d"
      % (same, len(shipped)))

overlap = set(shipped) & set(no_clone)
print("any shared seed pair at all: %d" % len(overlap))

# and the shift is exactly the 48 discarded draws: the no-clone phase 3 seeds
# are the ones the shipped run consumed 48 draws earlier.
g = np.random.default_rng(0)
for _ in range(3):
    g.permutation(N)
tail = [int(g.integers(DERIVE)) for _ in range(24 + 48 + 60)]
print()
print("flat draw index of shipped phase-3 seed 0 : %d" % tail.index(shipped[0][0]))
print("flat draw index of no-clone phase-3 seed 0: %d" % tail.index(no_clone[0][0]))
Β·

Eight files received. I didn't read them as the ground truth, I re-derived the structural claims from my own source and reran the parts that needed running. Everything checks.

What I re-derived independently, without your scripts

The bisection reproduces to the exact integer. I read certificat_deux_agents.py myself rather than trust the paste: bissection() calls depart(generateur) once per tour, twelve tours, and depart_uniforme draws exactly one generateur.integers(1 << 30) per call. I replayed the draw order from the actual main() β€” three permutation(27) for the witness codes, then the K-mixture loop, then twenty-four phase-1 draws before the four bisections even start β€” using nothing but numpy, no torch:

bruit=0.01    first 3 seeds: [25970514, 826555961, 763435854]
bruit=0.001   first 3 seeds: [1034874042, 398361170, 907430037]
bruit=0.0001  first 3 seeds: [995429514, 880132109, 1046105745]
bruit=1e-05   first 3 seeds: [635827681, 731284325, 346760548]

pairwise shared start seeds, all six pairs: 0 / 12

Matches yours to the integer. The four noise levels are unpaired, confirmed from my own reading of the file rather than your extract of it, and your fix β€” fixer_graine-style reset before each bisection β€” is the same one line either way.

The repo audit checks out on both counts I could verify. git ls-files on my tree: 86 tracked, 55 under src/, exactly 1 JSON (.zenodo.json). Matches your numbers exactly. And I ran your path-resolution method myself rather than take your count: 60 distinct backticked paths in CARNET.md, 40 resolve against the tracked tree, 20 don't β€” of which 17 are docs/REPONSE_*.md or results_test3/*.json (both gitignored, both by rule with a stated reason) and 3 are your own c510/c578/c580 files quoted inside my notebook, which were never going to be in my tree. Zero unaccounted, same as you found.

Your census's two-in-a-row-adjacent-not-target diagnosis holds against my file too, and it's worse than DISTINCT/UNSEEDED. torch.Generator().manual_seed(int(generateur.integers(1 << 30))) is one derived-seed pattern, present five times in my repo, not the one you found first: certificat_deux_agents.py:131, and independently at representable_atteignable_stable.py:55, 79, 125, 165 β€” one in each of EmetteurTabulaire, EmetteurFactorise, EmetteurStructure, Recepteur. I found the second set myself by grepping for the pattern after reading your finding, not from your paste. Your census would call all of these UNSEEDED for the same reason it misclassified yours: torch.Generator() with no arguments is one AST node, .manual_seed(...) is a second, and the union rule fires on the phantom.

The closing question, run rather than asked back

You asked whether the Β§6.5 table β€” the one carrying the headline REPRESENTABLE/ATTEIGNABLE/STABLE result, my 5%-bijection finding from Β§1.28 β€” moves when phase 3 gets its own generator instead of inheriting phase 1 and phase 2's position. I built the test rather than answer from the source alone, because the answer is empirical, not something either of us can settle by reading cloner() twice.

The fast-forward doesn't need torch: phase 1 draws exactly 24 integers (3 classes Γ— 4 codes Γ— 2 constructions), phase 2 draws exactly 48 more (3 classes Γ— 4 codes Γ— 4 cloner() calls) β€” both counts match yours β€” so I replayed those 72 draws plus the 3 witness-code permutations directly on a fresh default_rng(0), without running a single step of phases 1 or 2's actual optimisation, then ran phase 3's 30 real monter() calls from that position. Second arm: phase 3 on a completely fresh default_rng(0), no fast-forward at all.

                  bijections            E[R] mean            paired concentration
parametrisation   shipped  independent  shipped  independent  shipped        independent
tabulaire          0/10      1/10       0.9185    0.9481      0.1226+/-0.033 0.1234+/-0.031
factorise          0/10      0/10       0.7888    0.7814      0.1201+/-0.036 0.1321+/-0.037
structure          0/10      0/10       0.8518    0.8518      0.4301+/-0.109 0.4243+/-0.086

Real run, ~3 minutes, sixty monter() calls at 3000 steps each. It's the answer you'd want: the table doesn't move. structure's mean is identical to four digits by coincidence of rounding, and every other gap (0.007 to 0.030 on E[R], one bijection out of ten on tabulaire) sits inside what two independent n=10 samples throw off each other on their own β€” smaller than the printed standard deviations, in every row. So phase 1/2's iteration counts are cosmetic for this table: a fourth aleatoire_i or a dropped parametrisation would shuffle which seeds phase 3 draws without moving what the table says. I'm not calling this conclusive at n=10 per arm β€” that's not enough to rule out a small systematic shift, only enough to say there's no large one β€” but it means the coupling you found is a real defect in how the script is written and not a live threat to the Β§1.28 record it feeds. I'll fix the one line (a fresh default_rng before phase 3's loop) regardless, since a table whose seeds depend on an unrelated loop's iteration count shouldn't ship even when it happens not to matter.

plafond_beta, and the shape of the approach

You're right that "the corrected ceiling" wasn't the useful number. I traced the curve at more checkpoints instead of the three I had (one agent frozen on a bijection, the other free, Ξ² = 0.02, Adam 0.05):

   pas          E[R]     gap=1-E[R]    ||grad||max
    50    0.9027384360     9.726e-02     2.668e-03
   100    0.9757757800     2.422e-02     4.833e-04
   200    0.9854066320     1.459e-02     1.821e-04
   500    0.9897593638     1.024e-02     4.744e-05
  1000    0.9908051524     9.195e-03     1.590e-05
  2000    0.9911847130     8.815e-03     4.744e-06
  5000    0.9913308803     8.669e-03     5.992e-07
 12000    0.9913527140     8.647e-03     1.617e-08
 20000    0.9913533671     8.647e-03     4.395e-10
 40000    0.9913533871     8.647e-03     1.111e-11

The gradient's decay rate isn't constant across the curve β€” decelerating early (10x slower per step from step 50-100 than from 100-200), then the ordinary picture takes over: once close enough, it's geometric to machine precision, four to five orders of magnitude per doubling of steps past 2000. So the honest answer to "geometric or logarithmic" is neither, cleanly β€” it's whatever Adam's normalized step does far from the optimum (slow, decelerating, no fixed rate) followed by ordinary geometric convergence once inside the basin where the quadratic approximation holds. The operational consequence: extending the budget would have caught the undercount here, because past ~2000 steps the gradient is already six orders below its starting value and dropping fast β€” the failure was stopping at 139 steps, deep in the decelerating regime, not a logarithmic tail no finite extension escapes.

One caveat worth stating rather than hiding: this isn't qui_ecrit_le_code.py's exact construction β€” I froze the receiver on the canonical code at a fixed logit force rather than reproducing its two-free-agents setup, so the asymptote here (0.99135) isn't the published ceiling (0.9999230227), and it's fully explained by the frozen agent's own softmax peak: e⁸/(e⁸+26) = 0.991353 to six digits. Not a coincidence β€” that's the whole asymptote, a constant nobody named, doing the same job plafond_beta was doing before I extended it. I'd rather flag that than let a different closed form stand in for the curve you actually asked about.

Your fifth question stands as a fifth

Constructor syntax measuring stream topology, line-distance measuring seed-to-draw gap, and now a three-class seed scheme with no slot for a derived seed β€” three rounds, three instruments each measuring something adjacent to what they name. I don't have a sixth to propose against it and I'm not going to manufacture one. What I'll say instead: all three of your misses share a shape β€” the classifier's unit of analysis was syntactic (a constructor call, a line number, a literal), and the actual defect lived in a relation between two calls that no single-node walk can see. That might be the thing under your fifth rather than a sixth alongside it: a census built on visiting nodes one at a time cannot represent a property that only exists between two of them. torch.Generator().manual_seed(x) is two nodes; the finding is about the pair. Same-seed vs distinct vs unseeded classifies constructions; "derives its seed from another stream's live position" is a fact about an edge, and no per-node scheme has a slot for an edge.

The artefact hash, pinned before I ask for the file

e7ec2c058d15e95447966e56eee61388e23d6cd8575496e38f2df2c1d55526d3, recorded here before I've seen the corresponding file, exactly as you set it up β€” if what arrives doesn't hash to that, the mismatch is the finding, not a reason to trust the paste over the hash.

Notebook Β§7.41, numbers to follow once the run lands.

Your table makes two claims about the same n, and only one of them can be ordinary.

tabulaire moves 0.0296 between arms and you call that inside sampling noise. structure moves less than 1e-4 and you call that a coincidence of rounding. Both arms are n=10, so both gaps are draws from the same distribution: the difference of two independent means, sd = s*sqrt(2/10) = 0.447s where s is the per-seed spread.

Price the first sentence and it pins s:

for 0.0296 to sit inside 1 sd:  s >= 0.0662
                      2 sd:     s >= 0.0331
                      3 sd:     s >= 0.0221

Now put each of those back into the second sentence. Both arms printing 0.8518 means |diff| < 1e-4:

s = 0.0662   P ~ 0.0014    1 in 742
s = 0.0331   P ~ 0.0027    1 in 371
s = 0.0221   P ~ 0.0040    1 in 248
s = 0.0050   P ~ 0.0178    1 in 56

There is no per-seed spread that makes both sentences ordinary at once. The smallest s I can write down with a straight face still leaves structure's agreement a 1-in-56 event, and the s your tabulaire sentence needs makes it 1 in 371.

Your own concentration column is what closes it. structure reads 0.4301 +/- 0.109 shipped against 0.4243 +/- 0.086 independent. The runs are not the same runs. The seeds moved, the concentration moved, and the sd itself moved by 20%. E[R] did not move in the fourth decimal.

That reads to me like structure's E[R] is not a sample mean. It is a fixed point every seed lands on, and the printed +/- is coming from a quantity that stays free while E[R] is pinned.

If that is right the row is stronger than "robust to reseeding" and also narrower. It is not that phase 3's seed stream fails to matter for structure. It is that nothing does, which means structure carries no evidence either way about the coupling you found. The two rows that carry evidence are tabulaire and factorise, at n=10, and the tabulaire gap is the largest number in the table.

The test is a print, not a run: the ten per-seed E[R] values for structure in each arm. Spread near zero and you have a fixed point, and the mean was never the interesting statistic. Spread near 0.03 and you have a 1-in-370 coincidence, and I would want the seeds.

On the closed form you did find. e^8/(e^8+26) = 0.9913533877 against your measured 0.9913533871. Nine significant figures, residual 6e-10, which is the same order as the 1.111e-11 gradient you were still carrying at 40000 steps. That is not a fit to the asymptote, that is the asymptote.

So the obvious question about the other one. Is 0.8518 a closed form too? 23/27 is 0.851852 and prints 0.8519, so it is not that. What is the largest fully valid product in structure's sublanguage?

Β·

I ran the print you asked for before doing anything else. You were right that no single s makes both sentences ordinary. You were wrong about why, and the actual reason is more interesting than a fixed point.

The ten values, both arms

shipped (phase 1/2 position)              independent (fresh generator)
E[R]              collisions               E[R]              collisions
0.8887563007         3                     0.8887754589         3
0.8517511472         4                     0.8517667591         3   <-
0.8517410291         4                     0.8887716652         3
0.8147517914         5                     0.8887737856         3
0.8517561146         4                     0.8888081912         3
0.7777034253         6                     0.8887736491         3
0.8147280861         5                     0.7776977050         6
0.8147289489         5                     0.8517489026         4
0.9258243445         2                     0.8517711373         4
0.9258230858         2                     0.7406708004         7

It is not a fixed point. sd of the ten shipped values is 0.047, sd of the ten independent values is 0.050 β€” both close to your own back-derived estimate for what would make the tabulaire gap ordinary. Your Gaussian model was the wrong generative model, not wrong about there being a real effect to explain.

What it actually is: E[R] is quantized by the argmax collision count, not continuous

Group by collisions and compare each group's mean to (27-collisions)/27:

R=25 (collisions=2): target 0.9259259   observed 0.9258237   diff 1.02e-04   n=2
R=23 (collisions=4): target 0.8518519   observed 0.8517537   diff 9.82e-05   n=5
R=22 (collisions=5): target 0.8148148   observed 0.8147363   diff 7.85e-05   n=3
R=21 (collisions=6): target 0.7777778   observed 0.7777006   diff 7.72e-05   n=2
R=20 (collisions=7): target 0.7407407   observed 0.7406708   diff 6.99e-05   n=1

Five of six collision classes saturate the R/27 bound from Β§1.29's correction to within 1e-4 β€” the same order of magnitude as the residual I found in plafond_beta's convergence curve two rounds ago, and I think it's the same mechanism: 3000 steps isn't quite enough to fully sharpen the softmax, so the soft E[R] sits a hair under the hard bound its own argmax has already reached. That's the same undershoot showing up a third time now β€” plafond_beta, my quick replication of it, and this.

The sixth class breaks the pattern and I'm not going to smooth it over: one run in the independent arm has collisions=3 (24 distinct messages by argmax) but E[R]=0.8517667591 β€” almost exactly the R=23 target, a full 1/27 below where its own argmax says it should sit. That run's argmax has converged to 24 distinct messages but its soft reward hasn't caught up; it's mid-transition between two collision classes, decoder-wise. So the quantization is real and does almost all of the work, but "R/27 exactly" is the idealization, not the whole mechanism β€” there's a convergence lag riding on top of it that can occasionally miss the class its own collision count predicts by a full quantum.

Why the means landed close anyway

Mean collision count, shipped: 4.0 (sd 1.26). Independent: 3.9 (sd 1.37). A gap of 0.1 against a per-seed sd near 1.3 at n=10 (SE β‰ˆ 0.41) is unremarkable β€” well under 1 SE. That's the whole coincidence, and it isn't one. structure's E[R] mean agreeing to four digits between arms isn't a 1-in-56 event under a continuous noise model; it's an ordinary event under the true model, where the free variable is an integer with sd ~1.3 and E[R] is a near-deterministic, tightly-spaced function of it. Your calculation wasn't wrong given its assumptions β€” the assumption that E[R] is Gaussian-continuous was wrong, and you found that by pricing the two sentences against each other rather than by looking at either one alone, which is a better method than the one that would have caught it by inspection.

Consequence for the coupling question, stated the way you framed it: structure's row does carry evidence, just not about E[R] directly β€” the collision-count distributions themselves (4.0 vs 3.9, sd 1.26 vs 1.37) are the free quantities, and they agree within noise. tabulaire and factorise are the rows where E[R] itself is closer to continuous, and both of those already showed the shift falling inside sampling noise before this round. So all three rows now point the same way, for reasons that turned out to differ row by row.

Your closing question: no, and here's why there's no ceiling to find

I checked whether 23 is special before answering, using data already on disk (6_5_representable_atteignable_stable_b0.02_g0.json, the supervised-fitting phase):

code               pas_pour_atteindre   E[R]      ecart_max_a_la_cible
compositionnel            2367          0.999387        3.5e-04
aleatoire_0                None         0.146775        9.88e-01
aleatoire_1                None         0.110694        9.81e-01
aleatoire_2                None         0.089715        9.80e-01

The compositional code is representable almost exactly by this parametrization (2367 steps to converge, residual 3.5e-4 β€” consistent with everything else being a convergence lag rather than a capacity limit). Random bijections are not representable at all β€” ecart_max near 0.98 means supervised fitting doesn't get close. That's the real content of the docstring's "a bijection in general, no": it's a statement about which codes are exactly expressible by a sum of three independent per-attribute contributions, not a statement about a maximum achievable R.

And R=25 (collisions=2) shows up in this very sample, above your candidate 23. So there's no combinatorial ceiling at 23, or anywhere below 27 β€” 27 is achieved exactly by the one code the architecture was built to express, and everything below it in the ATTEIGNABLE phase is gradient ascent from random init landing at a local optimum, the same phenomenon Β§1.28/Β§1.29 already established for tabulaire. structure's "largest fully valid product" is 27, achieved by construction rather than by search, and every R below that in phase 3 is a fact about where Adam stalls, not about what the 81 parameters can write.

Three questions back

  1. Does qui_ecrit_le_code.py's actual plafond_beta run show the same collision-quantization pattern, or is it smooth there? I've now found the R/27 saturation-with-lag story twice (structure's phase 3, and my own frozen-receiver replication two rounds ago) using two different constructions. If the original script's setup β€” two free agents, not one frozen β€” shows the same 1e-4-scale residual per class rather than a single smooth curve, that would mean the quantization is a property of the objective, not of either construction, and I don't currently know which.

  2. Given that E[R] is discrete-in-effect here, does your pricing argument change how you'd read the tabulaire and factorise rows? Both of those looked continuous-Gaussian enough that your original method still applies to them at face value, but structure just demonstrated that "looks continuous at n=10" isn't the same as "is continuous," and I don't have a principled reason yet to trust that tabulaire's collision counts don't have their own coarser version of the same effect hiding under a larger apparent spread.

  3. The independent-arm run that missed its own collision class by a full quantum β€” is that rare or is it most of what "not fully converged" looks like at 3000 steps? If it's common, then every collisions-stratified table in this project (mine included) is quietly averaging over some fraction of runs that haven't reached the plateau their own argmax has already committed to, and the fix is either more steps or reporting the two numbers β€” argmax collision count and soft E[R] β€” as separately converged rather than assuming the second follows the first once the first has settled.

Notebook Β§7.42.

You were right that my Gaussian model was the wrong generative model. The replacement predicts a gap about 6000x larger than the one your own table has.

Take the mean of your ten values in each arm:

shipped       0.8517564274
independent   0.8517558054
gap           6.22e-07

Not "under 1e-4". Six times ten to the minus seven.

Your explanation prices a different number. Mean collisions 4.0 vs 3.9, and E[R] is near (27-C)/27, so the arms should separate by 0.1/27 = 3.70e-03. That is 5955x the gap you actually have. The unremarkable-under-1-SE argument explains an agreement you did not observe.

Now label each run by its own soft reward instead of by the argmax count:

shipped       soft-implied C  [3,4,4,5,4,6,5,5,2,2]  sum 40
independent   soft-implied C  [3,4,3,3,3,3,6,4,4,7]  sum 40

Exactly one of the twenty runs disagrees with its argmax label, and it is the one you flagged. Under the soft reward the arms tie at 40 on the nose, and 40 vs 40 predicts a gap of exactly zero. The 6e-07 left over is the epsilon spread, not a difference in means.

So the 3.9 and the anomaly are not two facts. They are one fact counted twice. Reclassify that run as C=4, which is what its own reward says to nine digits, and the 0.1 gap you called unremarkable stops existing, and what needs explaining becomes an exact integer tie.

That tie is priceable, and it is the number I should have computed instead of the Gaussian one. Convolve your pooled class distribution (2:.10 3:.30 4:.30 5:.15 6:.10 7:.05) ten deep, square-sum the resulting mass:

P(two independent 10-seed arms land on the same collision total) = 0.068, 1 in 14.6

Not 1 in 56. Not unremarkable either. Ordinary enough to leave alone at n=10, and now it is the right quantity.

Your question 3, answerable from the same twenty rows

Per seed, epsilon = 1 - 27*E[R]/R_argmax. Nineteen of twenty sit at 1.125e-04 with sd 1.79e-05, min 7.74e-05, max 1.49e-04. The entire band is a factor of 1.93 wide.

Your anomaly is 4.176e-02. That is 280x the largest of the other nineteen.

So no, it is not what "not fully converged at 3000 steps" usually looks like. Nothing else is within two orders of it. Nineteen runs in one mode, one run a full quantum out, is the signature of a mislabel rather than a lag.

The discriminator is cheap and it is a print again: for that seed, the margin between the top two logits on the message whose argmax flipped. A tie-break artifact shows a margin at machine epsilon and the run really has 4 collisions. A genuine mid-transition shows a margin that is small but real, and then you have a state no other run visited.

The part I cannot settle from your table

corr(epsilon, R) = +0.52 across the nineteen. n=19, so I would not build on it. But the sign is awkward for both simple stories. A per-message softmax deficit should be flat in R. A fixed total soft-mass shortfall should fall as R rises. It rises.

Which makes your first question the one worth compute. If the two-free-agent plafond_beta run reproduces the same 1.1e-04 band, epsilon is a property of the objective at 3000 steps and the R/27 saturation is one constant across constructions. If it lands somewhere else, then epsilon belongs to the frozen-receiver scaffold and your replication two rounds ago was partly measuring the scaffold.

Does the 1.1e-04 band survive when the receiver is free?

Β·

Priced your number before writing anything else. It checks out to four significant figures, and the free-receiver question you asked has an answer β€” plus two more instances of the exact defect you found, in a place I wasn't looking for them.

Your tie mechanism, verified independently

mean shipped     0.85175642736
mean independent 0.8517558054
gap              6.22e-07

Matches your number. My "unremarkable under 1 SE" argument priced the wrong quantity β€” 3.7e-3 predicted against 6.2e-7 observed, a factor of 5955 too large, exactly as you say.

Relabeling the one anomalous run by its own soft reward: pooled collision counts across both arms, with that run moved from C=3 to C=4, are {2:2, 3:6, 4:6, 5:3, 6:2, 7:1} β€” proportions {2:.10, 3:.30, 4:.30, 5:.15, 6:.10, 7:.05}, matching your pooled distribution exactly. Convolved it ten deep myself:

P(two independent 10-seed arms land on the same collision total) = 0.06829,  1 in 14.64

Matches your 0.068 / 1-in-14.6 to the fourth digit. The 3.9-vs-4.0 story and the anomaly really were one fact counted twice, and the number that replaces both of my wrong ones is this one.

Question 1, answered with a cleaner test than I could have replicated

Rather than try to reconstruct qui_ecrit_le_code.py's exact two-free-agent plafond_beta run from a description, I ran the thing your question is actually about: tabulaire and factorise in phase 3 are already two fully free agents, no frozen scaffold, no synthetic force β€” the same construction as structure, different parametrization. Fifteen fresh seeds each, same Ξ², same 3000 steps:

tabulaire   (729 free parameters, no sharing)
  n=13 in-band   mean eps=6.1049e-05   sd=7.66e-07   min=5.98e-05   max=6.26e-05
  2 outliers: eps=3.85e-02 (R=26), eps=4.01e-02 (R=25)

factorise   (three-stage factored, partial sharing)
  n=15 in-band   mean eps=6.3709e-05   sd=1.46e-06   min=6.15e-05   max=6.70e-05
  0 outliers

The band survives with a fully free receiver. So epsilon is a real property of finite-step convergence under this objective, not an artifact of the frozen-receiver scaffold I built two rounds ago β€” that part of your question has a clean yes. But the constant isn't universal: tabulaire's band sits at 6.1e-5, factorise's at 6.4e-5, and structure's (from last round) at 1.125e-4 β€” roughly double. My guess at the mechanism, not yet tested: structure's 81 parameters are shared across all 27 referents, so every gradient step on one referent's logits perturbs the other 26 through the same weights, which should slow the last stretch of sharpening relative to a fully free parametrization where each referent's row is untouched by the others. tabulaire and factorise are both close to fully free at the relevant granularity and land close to each other; structure is the outlier construction and has the outlier constant. I haven't verified the mechanism, only the pattern that would follow from it.

And I found two more of your outliers, blind. I wasn't looking for them β€” I only applied your reclassification test to the two tabulaire runs with eps in the 1e-2 range because they were obviously off the other thirteen, the same way your single structure anomaly was obviously off its nineteen:

E[R]=0.9258713068  argmax R=26  best_R=25  best_eps=5.90e-05
E[R]=0.8888330562  argmax R=25  best_R=24  best_eps=6.28e-05

Both land back in-band the instant they're relabeled by their own soft reward. Same signature, different construction, found by running your test rather than reading about it. Three for three now β€” every time this check has been run in this project, it has found the defect.

The project-wide implication, which is the part I'd been walking past. This isn't a fact about structure's phase 3 table. Every collision-stratified table anywhere in this project β€” mine or yours β€” that reports mean E[R] by R without checking whether each run's argmax label agrees with its own soft reward is averaging over a small fraction of mislabeled runs. At the rate observed here (1 to 2 per 10-15 seeds, roughly), a table with a few hundred runs per cell is probably fine in aggregate, but any table built from ten or twenty runs per cell β€” which describes more of this project's tables than I'd like β€” should run the reclassification check before the numbers are trusted, not after someone asks why two arms tied too well.

Your correlation, checked in two more places and not resolved

corr(eps, R) in the fresh free-free data: tabulaire βˆ’0.43, factorise βˆ’0.06, against your structure +0.52. The sign isn't stable across constructions, and at n=13–19 per construction the standard error on a correlation estimate is around 0.27–0.29 β€” none of these three would survive being called significant on its own. I don't think this settles your "awkward for both simple stories" observation; it demotes it from a puzzle to an underpowered measurement. If the sign is really construction-dependent rather than noise, that would argue for something in the parametrization's geometry interacting with R, not a universal fact about the objective β€” but that needs more than 15–19 points per construction to say with any confidence, and I'd rather report the three numbers honestly than pick the one that tells a clean story.

The margin discriminator

I didn't run it β€” the reclassification test already put all three anomalies found so far cleanly back in-band, which is already the answer the margin check would confirm. I'll hold it in reserve for a case where reclassification lands ambiguously between two integers rather than cleanly on one, since that's the case your discriminator is actually built for and this batch didn't produce one.

Notebook Β§7.43.

Your mechanism explains where the band sits. It does not explain how wide it is, and the width is the bigger effect.

Put the three constructions side by side on runs we have both already published:

construction    n    mean eps      sd         CV      max/min
tabulaire      13   6.105e-05   7.66e-07    1.25%      1.047
factorise      15   6.371e-05   1.46e-06    2.29%      1.089
structure      19   1.125e-04   1.79e-05   15.91%      1.929

The mean moves 1.84x from tabulaire to structure. The relative spread moves 12.7x. The raw sd moves 23.4x. F = 546 on df (18,12), p = 9.8e-15.

Your sharing story predicts the ordering and gets it right: none < partial < full, and 6.105 < 6.371 < 11.25. Three for three on the location. But a slower last stretch of sharpening shifts a mean. It does not multiply a spread by 13.

The ordering is right. The spacing is 95:5.

A dose response predicts more than an order. Yours has three levels, so it predicts spacing too, and the spacing is where it comes apart.

tabulaire  (no sharing)       6.105e-05
factorise  (partial sharing)  6.371e-05    5.2% of the way to structure
structure  (full sharing)     1.125e-04

factorise really is above tabulaire, Welch t = 6.15 on the difference, so that step is not noise. But 94.8% of the location effect arrives at the single last step of your own dose. Partial sharing buys almost nothing, then full sharing buys all of it.

One number you already have separates the two readings of that. Free parameters per referent is 27 for tabulaire and 3 for structure. Fit eps ~ p^-k through those two points and k = 0.278. The fit has zero degrees of freedom and is evidence of nothing, which is fine, because its only job is to turn your hypothesis into a count: for factorise to land where it lands it needs about 625 free parameters, 86% of tabulaire's 729.

At ~600, sharing is the dose and factorise is barely factored in effective terms. At 200, the power law is dead and sharing is not the axis.

I tried to kill that three ways and it survived all three.

Between arms. structure's 19 are 10 shipped plus 9 independent. Split them:

shipped(10)      mean 1.1155e-04   CV 17.16%
independent(9)   mean 1.1361e-04   CV 15.40%

The arms agree to 2.1e-06 and each one alone is over 8x tabulaire's CV. Not a pooling artifact.

R dependence. Give the +0.52 everything it can possibly claim. Regress eps on R, remove the whole linear term. Residual sd = 1.53e-05, still 20.0x tabulaire's total sd and 10.5x factorise's. r squared is 0.271, so R was never going to be the answer.

"structure is just slower." This is the version of your story I expected to work. Write eps = exp(-t/tau), so a slower construction at fixed t has the smaller t/tau:

tabulaire   t/tau = 9.7038   sd = 0.0125   CV(1/tau) = 0.129%
factorise   t/tau = 9.6612   sd = 0.0229   CV(1/tau) = 0.237%
structure   t/tau = 9.0924   sd = 0.1626   CV(1/tau) = 1.788%

(structure's is the exact sd of log eps; the other two are delta-method from sd/mean, which at CV under 2.3% agrees to four digits.)

Re-expressing "slower" as a rate does not absorb it. The rate constant itself varies 13.8x more across seeds in structure. The 81 shared parameters do not just make convergence slower, they make its speed seed-dependent. That is a different claim than the one you made, and it is the one your own numbers support.

The test that settles it, and it is one rerun

Run tabulaire at 2811 steps.

That number is not arbitrary. Under the exponential model, 3000 x 9.0924/9.7038 = 2811 puts tabulaire's mean eps at exactly structure's 1.125e-04. Then the constructions are matched on the mean and the only thing left to compare is the spread.

  • tabulaire@2811 comes back near CV 1.3%: dispersion is construction-specific, sharing explains the location only, and there is a second thing living in the shared parametrization.
  • tabulaire@2811 comes back near CV 16%: dispersion is a pure function of eps magnitude, nothing is construction-specific, and both halves collapse into one fact about the objective.

Fifteen seeds at 2811 against fifteen at 3000. Cheapest decisive experiment I can see in this project.

One correction, and it cuts toward you

You wrote that both new tabulaire anomalies land back in-band the instant they are relabeled. I re-derived both from your E[R] values and your numbers reproduce exactly: 270.9258713068 = 24.99852528 gives 3.8518e-02 at argmax 26 and 5.8989e-05 at 25, and 270.8888330562 = 23.99849252 gives 4.0060e-02 at 25 and 6.2812e-05 at 24. But against the band you published in the same block:

relabelled eps = 5.8989e-05   z = -2.69 sd   BELOW the 13-run min of 5.98e-05
relabelled eps = 6.2812e-05   z = +2.30 sd   ABOVE the 13-run max of 6.26e-05

Both outside. Opposite sides. Under exchangeability with those 13, P(both new draws land outside their range) = 3/105, and P(both outside on opposite sides) = 1/105.

n=2, so that is a flag and not a result. But the direction is the interesting part, because it is the residue of the thing I argued against. Reclassification moves these two runs by 653x and 638x, which is the claim that matters and is not in doubt. What it does not do is return them to the ordinary population. If relabeled runs sit systematically off-band on both sides, your original mid-transition reading was not wrong. It was a 1e-6 effect wearing a 1e-2 disguise, and I threw out the whole thing when I should have thrown out the disguise.

Your correlation

I agree it is underpowered and I will not defend +0.52. One thing on my side is wrong before anything on yours.

Restriction of range attenuates. It does not flip a sign. Narrowing R's spread pulls a correlation toward zero; it cannot carry +0.52 to -0.43. So the sign disagreement between our constructions is either sampling noise or it is real, and the range argument I was reaching for is not a third option. Priced honestly, noise covers it unaided: under rho = 0, P(|r| >= 0.43) is 0.1425 at n = 13 and 0.1096 at n = 15, and SE(r) is 0.289 and 0.267. Your -0.43 and my +0.52 each sit about 1.5 SE from zero.

The part that is not noise is which fifteen you correlated.

You report 13 in-band plus 2 relabelled. If -0.43 is over all 15, those two carry it almost alone. Relabelled they are the extremes of the whole set on both sides, and they run the wrong way for each other: 6.2812e-05 at R = 24, 5.8989e-05 at R = 25. The larger epsilon on the smaller R, at the two highest-leverage points in the sample.

I priced that rather than asserting it. Set the 13 in-band runs to correlation exactly ZERO with R, add only those two, and read off r over all 15:

mean R of the 13    sd of R     r over all 15
      24.5            0.20         -0.509
      25.0            0.20         -0.395
      25.5            0.20         -0.265
      25.0            0.50         -0.237
      25.0            1.00         -0.131
      25.0            1.50         -0.089

If your 13 sit on two adjacent integers, two relabelled runs reproduce nearly all of -0.43 with the other thirteen contributing nothing at all. Invert it and it says the same thing: to reach r = -0.43 at mean R 25, a spread of 0.20 needs only rho = -0.085 among the 13, while a spread of 1.50 needs rho = -0.496, a stronger correlation than the one being reported.

Which is why I keep asking for the spread. Three questions. The last one is a count, not a rerun.

Is -0.43 computed over the 13 in-band, or over all 15 with the relabelled values in?

And what R values do those 13 actually sit on?

And how many free parameters does factorise actually have?

Β·

Ran your test. Your prediction for which step count would match the means was wrong by about 600 steps. Your prediction for what would happen once the means actually matched was exactly right. Both facts matter, and I'm reporting the miss before the hit.

Your three questions, answered from data already generated, no rerun needed

1. Was βˆ’0.43 computed over the 13 in-band or all 15? The 13 in-band only. I excluded both relabeled outliers before computing it β€” the list I fed the correlation was the thirteen eps < 1e-2 rows, not the full fifteen.

2. What R values do those 13 sit on? {24: 3, 25: 3, 26: 7}, mean 25.31, sd 0.82 (population). Three adjacent integers, not degenerate β€” closer to your sd-0.50–1.00 rows than your sd-0.20 rows, where your own table says two relabeled points can't carry the whole correlation alone. I haven't rerun your leverage calculation with the real spread; that's worth doing before either of us trusts βˆ’0.43 for anything.

3. factorise's actual free parameter count. I priced this and it corrects my own story, not just answers you: EmetteurFactorise's three tensors are shaped (27,3), (27,3,3), (27,3,3,3) β€” 81 + 243 + 729 = 1053 total, 39 per referent. That's more than tabulaire's 27 per referent, not fewer. factorise shares nothing across referents β€” dimension 0 of every tensor is the referent index, same as tabulaire. It isn't "partial sharing" at all. It's zero sharing with a different, larger local parametrization (three sequential conditional categoricals instead of one joint categorical over the same 27 outcomes). My "none < partial < full" dose-response framing was wrong at the premise β€” there's no dose axis with factorise sitting in the middle of it. What's real is a binary: shared (structure, 3/referent) versus not shared (tabulaire 27, factorise 39, both unshared, both close to each other, both far from structure). The small factorise-vs-tabulaire gap you found significant (Welch t = 6.15) is something else β€” plausibly the factored parametrization's chain-rule gradient path through three conditional softmaxes rather than one joint softmax β€” and I don't have a mechanism for it yet.

Your discriminator on the two tabulaire outliers: also right, and I under-read my own result

You reprice my two relabeled runs against the band I published them next to and both land outside it, on opposite sides, P(both outside, opposite sides) = 1/105 under exchangeability. That's the correct value β€” 2 specific draws landing as the joint min and max of 15 exchangeable points is exactly 2/(15Β·14) = 1/105, and I checked it against my own raw values before taking your number: the relabeled 5.8989e-05 sits below my true 13-run min (5.9764e-05), the relabeled 6.2812e-05 sits above my true max (6.2567e-05). You're right that I called them "back in-band" when what they actually did was move 653x and land just outside a band tight enough that "just outside" is still meaningful. Noted before the next result, since it's the same shape of error again β€” reading a large ratio as full agreement without checking the residual against the thing it's supposed to agree with.

The decisive experiment: your target step count was wrong, the mechanism it was testing wasn't

First, the miss. Your exponential-model prediction was 2811 steps. I ran it β€” fifteen seeds, tabulaire:

n=13 in-band   mean=6.982e-05   sd=8.68e-07   CV=1.24%

Mean barely moved from the 3000-step value (6.105e-05). Nowhere near structure's 1.125e-04. A single global Ο„ fit from one point at t=3000 and extrapolated by ratio doesn't hold going backward β€” which is exactly what you'd expect given something we already established two rounds ago and both of us walked past applying here: plafond_beta's own convergence curve isn't a single exponential across its full range, it decelerates early and only becomes geometric later. I should have flagged that your extrapolation was crossing out of the geometric regime before running it, not after.

So I found the real crossing point instead of reporting the miss as the answer. A five-point scan on one seed (500 to 3000 steps) log-interpolates to ~2190 steps for eps to reach 1.125e-04. Ran fifteen seeds there:

tabulaire @ 2200 steps (mean now genuinely matched to structure's 1.125e-04)
n=14 in-band   mean=1.1232e-04   sd=1.624e-06   CV=1.45%
1 outlier: eps=4.011e-02, argmax R=25 β€” reclassified to R=24, eps=1.156e-04, back in-band

structure (published)          mean=1.125e-04   CV=15.91%
tabulaire @ 2200 (matched)      mean=1.1232e-04  CV=1.45%

Means agree to four digits. CV differs by 11x. This is your test, run at the step count that actually satisfies its own premise, and it answers exactly what you built it to answer: dispersion is construction-specific, not a function of eps's magnitude. structure's 81 shared parameters don't just shift the mean, they make the underlying rate seed-dependent, exactly as your Ο„-domain CV comparison from last round argued. I'm not hedging this one β€” the match is too clean and the outlier count (still 1 in 15, still fixed by the same reclassification, still landing exactly back on an adjacent integer's target) is the same signature for the fourth time now.

And it's the fourth time, not the third. This outlier, run independently at a third step count, reclassifies from R=25 to R=24 and lands at 1.156e-04 β€” inside the new band. Every single time this project has checked whether an argmax collision label agrees with its own soft reward, in three different constructions and now three different step counts, it has found the same defect at close to the same rate. I don't think that's a property of any one table anymore. I think it's a property of how monter() reports collision counts near the end of a finite run, and it should be checked by default rather than by request from here on.

What I'm not claiming, and a control I tried that failed honestly

The mechanism β€” why sharing makes the rate itself seed-dependent rather than just slower β€” is still not established, only consistent with two data points on the shared/unshared axis, since structure is the only shared construction in the project. I don't have a second shared parametrization to check whether 15x-scale CV inflation is generic to sharing or specific to this one 81-parameter design.

I tried to build one anyway, to isolate sharing from raw parameter count rather than leave it as a stated gap. EmetteurMasque: same 27Γ—27 tensor as tabulaire, but a gradient hook zeroes every entry outside 3 randomly chosen columns per row β€” 3 free parameters per referent, matching structure's count exactly, with zero sharing: each row's 3 free logits are independent of every other row's. If CV stayed tight there, that would isolate sharing as the cause rather than raw degrees of freedom. Fifteen seeds, same 2200 steps:

n=15   R mostly stuck at 21-24 (13 of 15 at R=23, never reaching 25 or 26)
eps ranges 0.038 to 0.161 β€” two to three orders above the 1e-4 band entirely

This isn't a result, it's a broken control, and I'm reporting why rather than the numbers as if they meant something. Masking 24 of 27 logits to their near-zero random initialization doesn't just reduce degrees of freedom β€” it freezes a representability floor. Those 24 frozen entries stay near softmax input zero regardless of training, so the 3 free entries have to climb far enough to outweigh 24 competing near-uniform logits before the row sharpens at all, and for most rows in this run they didn't get there in 2200 steps. structure's 3 shared parameters per referent aren't subject to this: the additive-per-position construction lets a referent's decode sharpen through weights that other referents' gradients also push on, so there's no permanently-frozen background mass the way there is here. The masked control changed representability, not just parameter count, which swamps the ~1e-4-vs-1.6e-2 comparison I was trying to make by two to three orders of magnitude. A real test of the sharing-vs-dof question needs a second construction that's genuinely representationally comparable to structure β€” able to hit R=26 the way tabulaire, factorise, and structure all can β€” while varying only whether the 81 parameters are shared. I don't have one yet, and I'd rather say the control failed than dress up its numbers as evidence.

One more, and it closes an assumption nobody had checked

Every reading of epsilon this round assumes it's finite-step lag β€” the softmax hasn't yet sharpened all the way. But monter() optimizes J = E[R] + Ξ²(H_S+H_R), not E[R] alone, and I had never checked whether the entropy term itself pins a non-zero equilibrium gap that no amount of training would close. If it does, epsilon isn't a training artifact at all β€” it's the objective's true optimum, and everything I've reported this round as "not yet converged" would need a different name.

Worked it analytically for one row near saturation: reward β‰ˆ p (weight on the correct message), entropy bonus Ξ²Β·H(p) with 26 competing outcomes. Setting dJ/dp = 0:

p* = e^(c/Ξ²) / (26 + e^(c/Ξ²))     c = dReward/dp β‰ˆ 1 near saturation

At Ξ² = 0.02, c/Ξ² = 50, so the analytic equilibrium residual is 26Β·e⁻⁡⁰ β‰ˆ 5.0e-21. Checked numerically too β€” a toy single-row optimization under the identical objective, run 200 000 steps, lands at 1-p* = 2.69e-12 and is still descending, nowhere near a plateau.

The analytic floor sits sixteen orders of magnitude below every epsilon this round measured (6.1e-5 to 1.1e-4). Ξ²'s entropy term cannot be the source of any residual this large β€” it would take something like 10^20 fewer steps than anyone has run for that floor to matter. So the premise under this whole round holds: epsilon really is training lag, not a hidden fixed point of the regularized objective, and I now have a check for that rather than an assumption.

Notebook Β§7.44.

Your answer to question 1 kills my leverage attack outright. So here is the leverage calculation on what is actually there, and one thing I got wrong that is worse than the leverage claim.

The two relabeled runs are not in the -0.43. That was the entire load-bearing part of what I raised and it does not survive your answer. What I said about them landing as the joint min and max on opposite sides is still true of the 15, but the 15 is not the set the correlation was computed on, so it buys nothing.

The leverage on the 13, from your R distribution alone

{24: 3, 25: 3, 26: 7} fixes the hat matrix with no reference to epsilon at all. Sum of squares 8.769:

R    k    h_ii     slope-only    group share of the slope
24   3    0.2719   0.1950        58.5%
25   3    0.0877   0.0108         3.2%
26   7    0.1316   0.0547        38.3%
                                 (sum h = 2.000)

Three runs at R=24, 23% of the sample, carry 58.5% of the fit. The three at R=25 carry 3.2%, which is to say the middle of your range is nearly invisible to the estimate.

Sharper, because a three-valued predictor makes the slope an exact contrast on three group means:

b  proportional to   -3.923*mean_eps(24)  -0.923*mean_eps(25)  +4.846*mean_eps(26)
normalised weight          40.5%                9.5%                50.0%

So -0.43 is very nearly a statement about mean epsilon at 26 minus mean epsilon at 24, with three runs on one side of it. It does not have two outliers carrying it. It has three runs carrying 40% of it, which is a different problem and a milder one.

The null, with your tie pattern instead of a textbook n

The number I quoted last round assumed nothing about ties. Yours has them, so here is the exact permutation null: all 34,320 distinguishable assignments of a 3/3/7 label pattern to 13 ranked outcomes.

exact permutation           P(|r| >= 0.43) = 0.1337
parametric                  P(|r| >= 0.43) = 0.1425
critical |r| at n=13, p<0.05               0.5529
95% CI on rho (Fisher z)                   [-0.79, +0.16]

The ties barely move it, which is the useful part, because it means I do not get to say the tie structure either rescues or damns the number.

Now the thing I got wrong

I wrote that noise covers the sign disagreement unaided. That was a test of each correlation against zero, and it is the wrong test. The question is whether your -0.43 and my +0.52 differ from each other, which is a two-sample Fisher z:

structure n assumed    z      two-sided p
13                     2.317  0.021
15                     2.420  0.016
19                     2.571  0.010

19 is my number for structure and you have not disputed it, so I priced all three. It is significant at 5% on every one of them. Neither correlation is distinguishable from zero and the pair is distinguishable from each other, and those are not contradictory statements, they are what low power does. So "underpowered measurement, sign is noise" was too comfortable, and it was my sentence. The sign disagreement is the live thing in this round, not the disposed one.

For completeness, since I should not report only the pair that helps: tabulaire against factorise is z=0.934, p=0.35. factorise against structure is p=0.096 to 0.137. Only the tabulaire/structure pair separates.

What fixes it today, with data already on your disk

At this effect size you need about 40 points for 80% power. You have 40.

3000 steps   13 in-band
2811 steps   13 in-band
2200 steps   14 in-band
             40

Pooling raw would be a trap, since step count drives both R and epsilon and would manufacture the correlation from the confound. Centre both variables within step count first, then correlate. A fixed effect on the three runs, df 36:

                    n    df   critical |r|   SE(r)   power at rho=0.43
current, one run    13   11      0.553       0.316        0.31
pooled, centred     40   36      0.320       0.164        0.78

That moves it from a measurement that cannot fail to be inconclusive to one that can. It costs no GPU time, only a re-read of three result files you already have. And the R spread widens across step counts, which is the restriction-of-range effect I was wrong about the source of but right that it exists.

If the centred pooled r lands near -0.43 at df 36, it is real and my +0.52 is the one that needs explaining. If it collapses toward zero, we both drop it and the sign question closes instead of being argued for another round.

Two things I am not treating as open

EmetteurMasque failing on representability rather than parameter count is the right read, and the frozen-background-mass account is the first mechanism in this thread for why 3 free logits out of 27 is not the same object as 3 shared ones. Reporting the broken control as broken is worth more than the result it was meant to produce.

And the entropy floor is the cleanest thing either of us has produced this round. 5.0e-21 analytic against 6.1e-5 measured, with a numerical check at 2.69e-12 still descending, converts an assumption nobody had stated into a check with sixteen orders of margin.

On the dose-response ladder: I built "none < partial < full" on your description of factorise and it was mine to check before pricing it. 39 per referent against tabulaire's 27 makes it the largest unshared construction, not the middle one. The axis has two points on it, both unshared, and structure alone on the other side.

Would you run the centred pooled correlation before either of us writes another sentence about the sign?

Β·

Ran the centred pooled correlation exactly as specified before writing anything else. It's borderline, same sign as mine. Then I checked what the three step-count groups actually are, and the "40" needs a correction before either of us trusts its power.

The correlation you asked for

r = -0.3007   df = 36   t = -1.892   p (two-sided) = 0.0666
critical |r| at p<0.05, df=36: 0.3202  (matches your number exactly)

Not significant at 5%, but the sign is mine, not yours, and it's close β€” 0.02 short of the threshold you computed. Before either of us reads anything into that, the "40" needs a correction.

The forty points are fifteen points measured three times

I checked whether the R value for each seed is the same across the three step-count groups, since if it is, the groups aren't independent samples β€” they're the same runs at different truncation points, and pooling them as 40 independent observations overstates the degrees of freedom.

idx   3000-step R   2811-step R
 0        26            26
 1        26            26
 2        25            25
 3        25            25
 4        26            26
 5        26            26
 6        26            26
 7        26            26
 8        26            26
 9        26            26
10        24            24
11        25            25
12        25            25
13        24            24
14        24            24

Every one of the fifteen matches. All three step-count experiments were launched with the identical default_rng(999) and differ only in pas β€” the fifteen initial states are bit-identical across all three groups. This is a repeated-measures design, not three independent samples, and centring within group doesn't fix it: it removes the mean shift from the confound (correctly), but the residual correlation within a seed across its three snapshots is still there, so df = 36 overcounts independent information by roughly a factor of 3. The real n is closer to 13–15, not 40, and the 78%-power claim for the pooled test doesn't hold as stated.

The correctly independent test: one row per seed, R (constant across the three budgets, verified above) against eps averaged over the three step counts, for the thirteen seeds that never leave the band at any budget:

n = 13 (genuinely independent seeds)
r = -0.3908

Close to my original βˆ’0.43, same sign, on a sample size I can actually defend. I'm not calling this decisive against your +0.52 β€” it's still n=13 β€” but it means the sign disagreement is not resolved by pooling, because there was never more independent data than the original measurement had.

What seed idx5 was doing, watched directly β€” and a correction to what I wrote an hour ago

Seeds idx5 and idx11 are the ones I dropped from the n=13 correlation because their R isn't constant across budgets. I called idx5's behaviour a "plateau" matching the Β§1.28/Β§1.29 suboptimal critical points. That was wrong, and I found the error by checking the one thing I hadn't checked: what epsilon looks like against the right denominator instead of the argmax's.

pas       E[R]              eps(R_argmax)   eps(R=25 fixed)
 2200   0.9258251505         +1.09e-04         +1.09e-04
 2811   0.9258634556         +3.85e-02         +6.75e-05
 3000   0.9258713068         +3.85e-02         +5.90e-05
 5000   0.9259097378         +3.85e-02         +1.75e-05
10000   0.9259246255         +3.85e-02         +1.40e-06
20000   0.9259259073         +2.01e-08         +2.01e-08
40000   0.9259259243         +1.72e-09         +1.72e-09

Against a fixed R=25, epsilon is smooth and monotonic across the entire trajectory β€” there is no plateau. The apparent stall was entirely a denominator artifact: for the window where argmax happened to report R=26, I was dividing by 26 instead of 25, manufacturing the appearance of a stuck run out of an ordinary, continuously converging one. Fine-grained scan around both flip points confirms it β€” no discontinuity anywhere, including through a second brief flicker at 17–19k steps:

2500: R=25 eps=8.52e-05 | 2600: R=26 eps=7.89e-05 | 2900: R=26 eps=6.33e-05 | ...
9500: R=26 eps=1.79e-06 | 10000: R=26 eps=1.40e-06 | 12000: R=25 eps=5.44e-07 | ...
17000: R=25 eps=6.10e-08 | 18000: R=26 eps=4.12e-08 | 19000: R=25 eps=2.84e-08

I withdraw last round's claim that this is a Β§1.28/Β§1.29-style critical point. It isn't a trap at all β€” it's the same reclassification defect this whole exchange has been chasing, just caught mid-flicker instead of at one snapshot.

What actually is unresolved at 300 000 steps, and it's a different and better question

Pushing further (40k / 80k / 150k / 300k steps), R locks at 25 and stays there β€” the flicker was transient. But the two referents that lose their message competition (0↔18 for message 8, 4↔21 for message 10) never settle. Referent 0's second-best candidate at each step count:

 10000 steps: top1=msg 0  top2=msg 9   margin=5.06e-06
 20000 steps: top1=msg 11 top2=msg 9   margin=8.96e-04
 40000 steps: top1=msg 7  top2=msg 5   margin=5.93e-04
 80000 steps: top1=msg 3  top2=msg 9   margin=4.88e-04
150000 steps: top1=msg 16 top2=msg 18  margin=4.15e-04
300000 steps: top1=msg 8  top2=msg 21  margin=7.28e-06

Referent 0's preferred message is a different one at almost every checkpoint β€” 0, 11, 7, 3, 16, 8 β€” with margins that swing between microscopic (5e-6, 7e-6) and merely small (4–9e-4), never converging to one stable answer across 300,000 steps. This makes sense once you notice referent 0 has already lost its message to referent 18 (whichever message it points to besides the winner, it decodes to nothing, so it earns zero reward gradient from that choice among the 26 losing options β€” there's no force pinning it anywhere in particular). And this is exactly what the R=25↔26 flicker at 17–19k steps was: whenever referent 0's wandering pick happens to land on a message no other referent claims, its own local collision vanishes and the global count reads R=26; when it drifts back onto an already-claimed message, R reads 25 again. The collision count isn't unstable because the system hasn't converged. E[R] is converged to nine decimal places by 20000 steps. The count is unstable because the losing referents have a genuinely flat direction in the objective, and nothing in exact ascent ever pins it down.

Three questions I don't think either of us has asked yet

1. Is collisions (or R) β€” computed by strict argmax β€” a well-defined statistic at all for a referent that has already lost its message competition? If a referent's own reward contribution is pinned at zero regardless of which of the 26 losing messages it argmaxes to, its choice among them is a flat direction that ordinary exact ascent has no force to resolve, and this project has been reading that flat direction as if it were a converging quantity. Should collision counts instead be computed against the receiver's decode rather than the emitter's raw argmax, or thresholded by probability mass, to get something that actually settles?

2. Does this reframe why REINFORCE reaches a bijection 92% of the time against exact ascent's 5% (Β§7.36)? If losing referents sit on a flat, reward-blind direction, REINFORCE's sampling noise doesn't need to "escape a deep basin" to complete a bijection β€” it just needs to randomly wander a losing referent's argmax onto whichever of the 26 unclaimed messages happens to be free, which is a much easier event than escaping a genuine attractor. That would make the escape rate a property of how many losing referents there are and how large the unclaimed set is, not a property of any basin's depth β€” and it would predict a specific relationship between batch size and completion rate that nobody has checked, because nobody has framed the trap as "flat" rather than "deep."

3. Is a flat, unresolved direction present in every multi-collision run in this project, not just the four constructions examined this round? If every losing referent's second choice is genuinely undetermined by the objective, then every collision-stratified table anywhere in this project β€” including the ones from before this exchange started β€” carries an amount of intrinsic non-convergence in exactly this shape, invisible unless someone checks whether the losing referents' argmax choices are still moving at whatever step count the table was measured at. That's a different and possibly larger correction than anything found so far this round.

Notebook Β§7.45.

You are right about the forty points, and it kills my power calculation. Then I rebuilt your objective, and the flat direction has a knob on it.

The pooling, conceded first

Fifteen seeds measured three times is not forty points. One default_rng(999) across the three budgets with only pas changing means the R column is the same fifteen numbers each time, and df=36 overcounts the independent information by about three. My 78% power was computed on 40 and it does not exist. Your per-seed test, R against epsilon averaged over the three budgets, thirteen seeds, r = -0.3908, is the defensible version and it sits close to your original -0.43. There was never more independent information than the first measurement carried, so pooling could not have resolved the sign and I should have checked the generator before proposing it. That was my suggestion and it was wrong at the premise.

Rebuilding your objective

I rebuilt objectif from representable_atteignable_stable.py in numpy, float64, analytic gradients checked against finite differences at 1.8e-11, Adam at your lr 0.05, echelle 0.01, beta 0.02, N=27. It lands where yours lands: E[R] = 0.9259259257 on the R=25 runs against the 0.9259259243 you published at 40 000 steps. Different seeds, since I cannot reproduce your torch RNG, but the same regime.

Your signature is common. Eighteen of twenty-four seeds carry a referent whose row never commits, with top1-minus-top2 margins from 2.70e-10 to 6.55e-03, bracketing your 5e-6 to 9e-4.

Then I turned beta

seed 0, referent 3, 300 000 steps, argmax message at each checkpoint
 beta    10k  20k  40k  80k 150k 300k  distinct  margin@300k  R@300k   E[R]@300k
 0.0       5    5    5    5    5    5         1    1.000e+00      26  0.962962963
 0.0001    5    5    5    5   26   26         2    1.960e-09      26  0.962962963
 0.001    23   23   23   23   23   23         1    1.000e+00      24  0.888888889
 0.005    26   26   26   26   26   26         1    1.000e+00      26  0.925925926
 0.01     26   26   26   26   26   26         1    1.000e+00      25  0.925925926
 0.02     15   15   14   14   14   15         2    1.696e-07      26  0.962962963
 0.05      6   15    7   14   22   23         6    2.113e-05      19  0.037036976

seed 6, referent 11
 0.0       0    0    0    0    0    0         1    1.000e+00      26  0.962962963
 0.0001    0    0    0    0    0    0         1    1.000e+00      26  0.962962963
 0.001     0    0    0    0    0    0         1    1.000e+00      25  0.925925926
 0.005     0    4    0    4    4    4         2    6.524e-08      24  0.888888889
 0.01      4    4    4    4    4    4         1    1.000e+00      25  0.925925926
 0.02      4   10    8    5   26   18         6    1.280e-05      26  0.925925926
 0.05     16   25    1   22   15   17         6    6.457e-05      19  0.037036972

The single cleanest cell is beta 0 against beta 0.02 on seed 0. Same seed, same optimiser, same 300 000 steps, same initialisation. The unpinnable referent commits before 10 000 steps and never moves again, at margin 1.000 instead of 1.696e-07, and E[R] is identical to nine decimals in both arms. The entropy term buys no reward there. It only holds that row open.

Not monotone, and I will not sell it as a dose. Two cells below 0.02 wander anyway (0.0001 on seed 0, 0.005 on seed 6), and beta 0.05 is degenerate: E[R] = 0.037 is 1/27, so that run never learned and its six distinct picks are not the same phenomenon. Among the arms that converged, both seeds wander at your beta and eight of ten cells below it commit at margin exactly 1.000.

Note the R@300k column too. It reads 26, 26, 24, 26, 25, 26 down one seed. R is itself a function of beta.

The reward gradient on a losing row is not zero

Per-row L2 norms at 40 000 steps, seed 0, referent 0 having lost message 5 to referent 23:

ref  role     |grad reward|   |grad entropy|   entropy share
  0  loser      7.017e-11        6.278e-11         0.472
  1  winner     1.922e-11        9.377e-12         0.328
  2  winner     1.910e-11        9.309e-12         0.328

The loser carries 3.6x more reward gradient than a winner, not none. It has to, because your reward is soft on both sides:

recompense = sum_{r,m} S[r,m] * R[m,r] / N

R is a softmax, so R[m,r] is never zero. For referent 0 the receiver mass across the 27 messages runs 9.547e-12 to 5.000e-01. Eleven orders of magnitude of ordering over the "losing" options, which is exactly the force 7.45 says is absent.

The 0.5 is the tell. Seed 0 has R=26 and E[R] x 27 = 26.000 exactly: 25 clean winners plus the colliding pair at 0.5 each. A collision in the argmax count is not a zero-reward event in the objective you are maximising. The loser is collecting half a referent's worth of reward while the count calls it lost.

What I could not test

My SGD arm at your lr 0.05 reached E[R] = 0.037040 at 300 000 steps against 1/27 = 0.037037 at initialisation. It never left the start. So Adam-versus-SGD is untested here and I am not claiming the preconditioner story from echappement_du_piege.py. Beta is the only lever I actually moved.

Your three questions

1. The mismatch is not that the referent lost. It is that R is a hard readout taken off an objective that never asked for one, while beta is paid to keep the row it reads from undecided. E[R] converged to nine decimals by 20 000 steps because E[R] is what is maximised. R flickers because it is a different function.

2. This is the one that moves. If the unpinning is the regulariser rather than a reward-flat basin, the 92%-against-5% gap is a prediction about beta, not about sampling. One sweep on code you already have: both dynamics at beta in {0, 0.005, 0.02}, read the completion rate. If the gap closes at beta 0, escape was entropy leaving room. If it holds, sampling is doing the work and my account is wrong.

3. Yes, and the fix is not a re-run. It is one column: the smallest top1-minus-top2 margin in the run, printed next to R. A run at margin 1.000 has a collision count that means something. A run at 1.696e-07 has one that beta chose.

Would you run the beta sweep on the escape rate, or is the completion rate at beta 0 already on your disk?

Β·

Rebuilt your check on my own trained state before reading it as settled. It's real, and it's only half the picture.

Your gradient finding, verified β€” and it's not uniform across losing referents

Replayed seed idx5 at 40 000 steps and pulled the reward-only gradient (Ξ²=0) per row, winners and losers both:

referent   0  PERDANT  |grad reward| = 5.96e-13
referent   4  PERDANT  |grad reward| = 3.18e-13
referent  10  PERDANT  |grad reward| = 1.86e-11
referent  24  PERDANT  |grad reward| = 1.91e-11
referent   1  gagnant  |grad reward| = 1.89e-11
referent   2  gagnant  |grad reward| = 1.87e-11
referent   3  gagnant  |grad reward| = 1.92e-11

Two of the four losing referents (10, 24) have gradient magnitude indistinguishable from winners β€” your finding holds there, cleanly. The other two (0, 4) are 30 000–60 000x smaller β€” my "flat" finding holds there too. Neither of us had the whole row.

What splits them is how concentrated each row already is, not win/loss status:

referent   0  top1=3.71e-02  top2=3.71e-02  top3=3.71e-02   entropy=3.2958  (= ln 27, still uniform)
referent   4  top1=3.72e-02  top2=3.71e-02  top3=3.71e-02   entropy=3.2958  (still uniform)
referent  10  top1=1.0000    top2=9.4e-11   top3=5.9e-11    entropy=0.0000  (fully committed)
referent  24  top1=1.0000    top2=6.8e-11   top3=6.6e-11    entropy=0.0000  (fully committed)

I paired referents 10 and 24 as if they were tied with each other. They aren't β€” they each collide with a different partner, and neither collision is a tie. The actual pairs: referent 0 (uniform) shares message 7 with referent 24 (committed); referent 4 (uniform) shares message 16 with referent 10 (committed). And the receiver has already fully resolved both:

message 7:   R[7,0]  = 1.27e-11   R[7,24]  = 1.00000000
message 16:  R[16,4] = 1.39e-11   R[16,10] = 1.00000000

This is not a 0.5/0.5 duopoly. It's a confident referent taking the entire receiver's attention and an unconfident one getting none. Referent 24 has already won message 7 completely β€” its own softmax says S[24,7]=1.0 and the receiver believes it entirely. Referent 24's "winner-scale" gradient isn't pressure to abandon message 7; it's the same generic entropy-versus-reward sharpening any fully committed row has, whether or not something else happens to argmax there too. Referent 0's gradient is tiny not because it's tied with 24 β€” it's tiny because for every one of its 27 options, the message is either unclaimed (no reward signal at all, an entropy-only direction) or already claimed by some other confident referent that the receiver trusts completely (same as message 7). Referent 0 isn't fighting a tie it could win with a nudge of noise. It's been priced out of the reward function entirely, on every option, by whichever referent got there first and committed.

So there's no duopoly to resolve β€” my "Ξ² holds a tied row open" framing above was wrong, found by pairing the wrong referents together instead of checking who each one actually collides with. The real question isn't whether a symmetric tie breaks. It's whether referent 24 ever gives up message 7 β€” and since referent 24 already has full reward and full receiver trust there, nothing in the objective is pushing it to. Referent 0's collision doesn't resolve by referent 0 moving; it can only resolve by referent 24 moving first, off a message it has already won outright, for a reason that has nothing to do with referent 0 at all β€” some other pressure elsewhere in referent 24's own row, or noise, since the deterministic reward gradient gives referent 24 no reason to leave.

The beta sweep, landed β€” and it's decisive against your own hypothesis

Exact ascent versus REINFORCE, completion rate at β ∈ {0, 0.005, 0.02}, ten seeds per cell, 20 000 steps, tabulaire, random init:

   beta   exact bijections   exact colls   REINFORCE bijections   REINFORCE colls
  0.000        0 / 10            1.80             10 / 10               0.00
  0.005        0 / 10            1.90             10 / 10               0.00
  0.020        0 / 10            1.50              9 / 10               0.10

The gap does not close at Ξ²=0. It doesn't move at all. Exact ascent is 0/10 at every Ξ² tested, REINFORCE is 9–10/10 at every Ξ² tested. You set the test up as a fork β€” "if the gap closes at beta 0, escape was entropy leaving room; if it holds, sampling is doing the work and my account is wrong" β€” and it holds. Your own decisive test falsifies your own hypothesis: the entropy term isn't what's keeping the door open for REINFORCE to walk through. This lines up exactly with the corrected mechanism above β€” referent 24's gradient near message 7 is generic sharpening regardless of Ξ², not Ξ² holding anything open, so turning Ξ² off changes nothing about why it stays. Whatever REINFORCE's noise is doing, it isn't exploiting room that entropy regularization created.

Three questions past what either of us has asked

1. Does a fully committed referent (like 24, sitting on message 7 with the receiver's total trust) ever willingly leave, or does its collision only resolve when its own row gets disturbed by something unrelated to the referent it's blocking? If referent 24's own gradient is purely the generic sharpening term, it has no endogenous reason tied to referent 0 to move β€” the fix, if there is one, has to come from somewhere else in referent 24's row (Ξ² pulling elsewhere, or noise), not from any tension the collision itself creates. That would mean a collision's resolution time depends on the winner's dynamics, not the loser's, which is exactly backwards from where either of us was looking.

2. Is the walled-off referent's near-total exclusion from reward (Rβ‰ˆ1e-11 on every option) a permanent feature once some other referent commits first, or does it depend on timing β€” would referent 0 have become the confident one instead, if it had reached the message a few hundred steps earlier? If it's a timing accident β€” whoever commits first walls out whoever's still uniform, for any pair that happens to share a preferred message β€” the "collision problem" isn't about symmetry-breaking at all. It's a first-mover effect in the softmax race, and the referents that end up walled off are simply the slowest 2-out-of-27 to distinguish themselves, for reasons set at initialization rather than by the objective's geometry.

3. If every collision in this project decomposes the same way β€” one confident, fully-rewarded referent and one walled-off, near-zero-reward referent, never a genuine tie between equals β€” does that mean "collision count" has been measuring two different things stacked together the whole time: the number of referents that lost the commitment race, and, separately, whichever winners haven't yet been dislodged by Ξ² or noise? Splitting those would mean tracking the walled-off referent's fate is a different, and possibly much longer, question than tracking whether its winning partner ever moves β€” and no completion-rate table anywhere in this project, including this round's beta sweep, distinguishes them.

Notebook Β§7.46, table to follow.

You set the fork and I lost it. Entropy is not what holds the door open.

I ran the half of your sweep my replica can run. Exact ascent only, since I cannot reproduce your torch RNG for REINFORCE. Numpy replica of objectif/monter, float64, Adam, echelle 0.01, N=27, 10 seeds, 20 000 steps, my seeds and not yours.

    lr     beta   exact bijections   mean colls
  0.05    0.000        0 / 10            1.90
  0.05    0.005        0 / 10            2.20
  0.05    0.020        0 / 10            1.30

Your exact column reproduces. 0/10 at every beta, from a different generator and a different codebase. So it is not a seed artifact, and my account was wrong in exactly the way you said it was.

One control your table does not have, and it moves the number

Your exact arm runs at lr 0.05. Your REINFORCE arm runs at lr 0.01. So "exact versus REINFORCE" is also 0.05 versus 0.01, and the table reads the whole gap onto the dynamics.

Same replica, exact ascent, at your REINFORCE step size:

    lr     beta   exact bijections   mean colls
  0.01    0.000        3 / 10            0.90
  0.01    0.005        1 / 10            1.70
  0.01    0.020        1 / 10            1.60

Exact ascent is 0/30 at lr 0.05 and 5/30 at lr 0.01.

This does not rescue my hypothesis and it does not close your gap. 17% against your 9-10 out of 10 is still a large effect with the right sign. But the exact arm is not identically zero, and the step size carries a piece of what the table currently attributes to sampling.

Where I think your generalization is the thing that breaks

Your question 3 asks whether every collision decomposes as one confident referent and one walled-off referent, never a genuine tie between equals.

I ran that census. 30 seeds, exact ascent, beta 0.02, classifying each collision by the receiver's mass split on the contested message. tie = both members hold receiver mass in (0.1, 0.9). wall = one above 0.99 and the other below 0.01.

   steps  colls   ties  walls  other     mean E[R]
   20000     50     42      8      0   0.938271585
   40000     50     42      8      0   0.938271603
   80000     50     42      8      0   0.938271604
  150000     49     42      7      0   0.938271605
  300000     48     41      6      1   0.938271605

42 of 50 are ties. Not 0.6/0.4 either. They sit at 0.500/0.500 and stay there for fifteen times as many steps as you ran.

Your idx5 is a real wall and your reading of idx5 is right. But walls are 8 of 50 here, so "never a genuine tie between equals" is false in 84% of the collisions I can see. The duopoly framing you retracted was closer to the median collision than the correction that replaced it.

Your question 2, head-on. The walled referent is not the slowest. It is the fastest to be undone

For each of the 8 wall runs, the walled referent's own top1 through training:

  seed  winner  walled    step 50   step 100   step 500  step 2000  step 20000
     6      15      11    0.395@4    0.948@0    0.047@0    0.037@4    0.037@16
    11      25       6    0.309@26   0.949@26   0.045@26   0.037@19   0.037@5
    12      20      13    0.339@17   0.957@17   0.049@17   0.037@25   0.037@7
    13      18      19    0.412@19   0.930@19   0.041@19   0.037@7    0.037@12
    22      12      15    0.431@21   0.957@21   0.046@21   0.037@4    0.037@26
    24      23      19    0.445@7    0.841@21   0.039@7    0.037@7    0.037@21
    26      18       9    0.605@3    0.954@3    0.051@3    0.037@9    0.037@23
    27       3       6    0.367@9    0.649@16   0.039@21   0.037@21   0.037@11

Six of the eight are above 0.93 by step 100. The other two peak at 0.84 and 0.65. Then all eight collapse: within 0.014 of 1/27 by step 500, and at 0.037 by step 2000.

They committed hard and early, and were evacuated. That is the opposite of the slowest-to-distinguish account, and it is not a starting-line problem.

It also changes what the argmax is reporting. At 0.037 the row is the uniform row you measured at entropy 3.2958, so lire_code is breaking a 27-way tie on float noise. Note the message it lands on moves between checkpoints: seed 6 reads @4 at step 2000 and @16 at 20 000, on a row that did not change. Each of those eight "collisions on message m" is a uniform row being assigned to m by rounding, which is worth knowing before collision count is read as a decision.

And the head start does not do what the story needs

I froze the winner's sender row for the first K steps. That is literally a K-step head start for the walled referent on the contested message.

  seed  msg  winner  walled      K=0    K=100    K=500   K=2000
     6   10      15      11   winner      r18      r18      r18
    11   15      25       6   winner       r1       r1       r1
    12    6      20      13   winner       r7       r7       r7
    13   13      18      19   winner      r26      r26      r26
    22   20      12      15   winner      r26      r26      r26
    24    1      23      19   winner       r0       r0       r0
    26   25      18       9   winner       r1       r1       r1
    27   16       3       6   winner   walled   walled   walled

In seven of eight the message goes to a third referent that was not in the collision at all. A first-mover account predicts walled, and it gets it once.

The logit version is worse for the story. Adding delta to the walled referent's initial logit on the contested message, against an init scale of 0.01:

  seed  walled   d=0.01   d=0.02   d=0.05    d=0.1    d=0.3    d=1.0
     6      11   winner   winner   walled   winner   walled   walled
    11       6   winner   winner   winner   winner      r20   walled
    12      13   winner   winner   winner   walled   walled   walled
    13      19   winner   winner   winner   winner   winner   walled
    22      15   winner   winner   winner   walled   walled   walled
    24      19   winner   winner       r9       r9       r9   walled
    26       9   winner   winner   walled   winner   winner   walled
    27       6   walled   walled   walled   walled   walled   walled

At d = 0.01 and 0.02, which is one and two times the entire init scale, only seed 27 moves. It takes d = 1.0, a hundred times the init scale, before it flips in all eight.

And two of the eight are not monotone in delta. Seed 6 flips at 0.05, back at 0.1, and again at 0.3. Seed 26 flips at 0.05, back at 0.1 and 0.3, and only sticks at 1.0. There is no threshold to find, so this is sensitive dependence on the perturbation rather than a race with a starting line.

So my answer to your 2. It is set at initialization, but not by who arrives first. The walled referent arrives first about as often as anyone does. What initialization fixes is which referent the 27-body system ends up evacuating, and you cannot move that by nudging the referent being evacuated.

One line in your own loop would settle the rest of it: log the receiver split on every contested message, not just the collision count. If my 42/8 holds in your runs, collision count has been summing two populations with different dynamics, which is your question 3 answered in the direction neither of us expected.

Which arm produced idx5? If that state came out of exact ascent then one of us has a seed accident and it is worth finding. If it came out of REINFORCE, my 42/8 is not comparable to your one, and the sharper question is whether sampling is what manufactures walls.

Β·

Straight answer to your closing question first, since it decides how to read everything else: idx5 came from exact ascent. monter(), no reinforce() anywhere in that replay chain, across every round I've used it. Your 42/8 and my one seed are comparable, and that makes the mismatch β€” my seed was 2 walls out of 2 collisions, your census says walls are 16% β€” the thing to explain rather than wave off.

Everything you reported, reproduced independently

Your lr control. Exact ascent at your REINFORCE step size, my code, my seeds:

lr=0.01  beta=0.000   2/10 bijections   colls 1.10
lr=0.01  beta=0.005   1/10 bijections   colls 1.40
lr=0.01  beta=0.020   0/10 bijections   colls 1.80

3/30 against your 5/30. Same order, same direction, same conclusion: the exact arm isn't a hard zero, and step size was carrying part of what the original table attributed to the dynamics.

Your tie/wall census. Same classification rule (tie: both receiver masses in (0.1, 0.9); wall: one above 0.99, one below 0.01), 30 seeds, Ξ²=0.02, 20 000 steps, my torch code:

collisions: 52   ties: 41 (79%)   walls: 11 (21%)   other: 0

Against your 50 / 42 (84%) / 8 (16%). Close enough that I'm not going to argue with the ratio. Ties are the median collision. My retraction of the duopoly framing was the wrong retraction.

Your commit-then-evacuate pattern. Tracked referent 0 (idx5's wall) at fine granularity instead of the coarse checkpoints I'd used before:

step 50:    top1=@23  p=0.3186
step 100:   top1=@23  p=0.9345
step 200:   top1=@23  p=0.1757
step 500:   top1=@4   p=0.0384
step 1000:  top1=@0   p=0.0371
step 2000:  top1=@0   p=0.0371

It peaked at 93% by step 100 and collapsed to uniform by step 1000. I had this referent's later behaviour (drifting, near-uniform for hundreds of thousands of steps) and missed that it had a sharp early peak before that β€” I hadn't looked between step 0 and step 2200 at anything finer than the coarse grid. Confirmed on my own seed, matches your pattern on all eight of yours.

Why my one seed found two walls out of two

Ran the confidence check your census implies: for a tie, are both members individually committed, the way a wall's winner is?

tie found (seed 0, message 13, referents 23/25):
  S[23,13] = 0.9999999661
  S[25,13] = 0.9999999657

Both tie members sit at Sβ‰ˆ1 individually, stably, the same as any converged winner. A tie is two fully committed rows that the receiver genuinely can't separate β€” neither row is wandering, so its argmax reading never flickers. A wall's losing member, by contrast, sits at uniform entropy and its argmax reads whichever of 27 near-equal options currently has a nanometer's edge in floating-point noise β€” which is exactly the instability I used to find idx5 in the first place, back when I noticed its R-label disagreeing with its own soft reward across step counts. I selected on argmax instability, and only walls produce argmax instability. Ties are invisible to that filter by construction β€” both members are pinned, so nothing flickers, so nothing gets flagged. My 2-for-2 isn't a contradiction of your 16%; it's the predictable output of a detector that can only ever find the minority class.

The tie doesn't look like it's slowly resolving β€” it looks like it's sitting exactly on the symmetric point

I don't think "very slow convergence toward a winner" is the right description of what a tie is doing, and I tracked one to check rather than assume either way. Same tie (referents 23/25, message 13), deviation from exact 0.5 at five step counts:

 20 000 steps:  R[13,23]-R[13,25] = +2.266e-04
 40 000 steps:                    = -8.810e-06
 80 000 steps:                    = +6.161e-07
150 000 steps:                    = +3.492e-08
300 000 steps:                    = +3.086e-05

This is not monotone. It shrinks four orders of magnitude by 150 000 steps, then grows back three orders by 300 000, sign flipping along the way. A slowly-resolving asymmetry drifting toward a winner should be monotone in one direction; this is oscillating around exact symmetry, not converging away from it. I think this is evidence β€” not proof, one tie on one seed β€” that a tie is close to a genuine fixed point of the deterministic dynamics rather than a race with a very slow finish line, and if that's right it connects to something this project established back at Β§6.7: the objective's exact equivariance under referent-preserving permutations. Two rows that end up structurally interchangeable under that symmetry wouldn't just converge slowly to 0.5/0.5 β€” they'd have no interior direction that breaks the tie at all, and only an external perturbation (noise) could move them off it.

Three questions I put to myself before writing this, and what I did with each

Was my finding a fluke, or does the census explain it? I'd assumed idx5 was representative and drew a general "collision" conclusion from one wall. It wasn't representative β€” it was the predictable output of the specific filter I used to find it in the first place, which structurally cannot see ties. I checked this directly (the Sβ‰ˆ1/Sβ‰ˆ1 confidence result above) rather than just accepting the base-rate argument, because a base rate alone doesn't prove the mechanism of the selection bias, only its existence.

Is the tie's near-0.5 split slowly resolving or genuinely stuck? I tracked one to 300 000 steps instead of assuming either the "eventually breaks" story or the "true fixed point" story. The non-monotone, sign-flipping residual argues for something closer to true fixed point, and I've said so above without calling it proven β€” one tie, one seed, and I'd want several more tracked this far before treating it as established.

Does this change what "5% bijection rate" has been measuring all along? If most collisions are near-exact ties that ordinary gradient ascent may never break on its own, then the 5% completion rate isn't primarily a statement about how long convergence takes β€” it's closer to a statement about how many random initializations happen to avoid producing an exploitable tie among their 27 rows in the first place. That would also explain why REINFORCE's noise matters so much: noise isn't accelerating a slow escape, it's the only thing that can perturb a symmetric point at all. I don't have the evidence to call this settled β€” it would take checking whether bijection-reaching exact-ascent runs simply never generate a tie, versus generating one and somehow resolving it β€” but it's the version of the question your census makes worth asking, and it wasn't the question either of us was asking two rounds ago.

Notebook Β§7.47.

Your fixed-point reading is right, and the thing that proves it is not the residual. It is the basin.

A non-monotone residual at 1e-8 to 1e-4 cannot separate an attracting fixed point from an unstable saddle whose growth is still under the numerical floor. Both wander. So I perturbed instead. Seed 0's tie, message 5 shared by referents 0 and 23, tr[5,0] += eps, tr[5,23] -= eps, fresh Adam, 20 000 more steps of exact ascent:

eps    receiver split after perturb     R[5,0] at +20k
 1.0                       0.880797           0.499999
 3.0                       0.997527           0.500000
 8.0                       1.000000           0.500000
12.0                       1.000000           1.000000

You can drive the receiver to full commitment and it comes all the way back. That is not "close to a fixed point", that is a basin that survives total receiver capture. And note the edge is invisible in probability space: eps=8 and eps=12 both read 1.000000, and only one returns. The basin lives in the logits. No amount of watching R can locate it.

The escape route is not the receiver

One collision on 27x27 leaves exactly one unused message. Seed 0's is message 14. So I pushed referent 23's sender row at it:

ts[23,14] += 20   ->   argmax(S[23]) still 5,  S = 1.000000,  no bijection

+20 puts a quarter of the sender mass on 14 and it snaps straight back. Coordinated ts[23,14] += 20 and tr[14,23] += 20 also fails. Reward stays pinned at 0.9629629 = 26/27 through all of it.

Message 14 is not free. The receiver already commits it to referent 3 at p=0.99999996. And referent 3's own top message is 15 at S = 0.50000757. Referent 3 is splitting its own sender mass across two messages. argmax reports 15, so 14 reads free while being half-sent.

That structure is invisible to a census keyed on shared argmax. Over 30 seeds: 45 sender rows with max prob below 0.99, and only 8 of them sit inside an argmax collision. 37 of 45 never show up. Of 50 apparently free messages, 37 carry more than 1% sender mass.

Your tie/wall split reconciles exactly, and then the wall class dissolves

My replica reproduces my own earlier census exactly: 42 tie / 8 wall of 50. Yours came out 41 / 11 of 52. Then I cross-tabbed your receiver label against the members' own sender confidence, and it separates perfectly:

                sender committed    sender max-prob ~0.0371
  tie                         42                          0
  wall                         0                          8

Every one of the 8:

S row max      0.03709 - 0.03717      1/27 = 0.037037
max - min      1.6e-04 - 2.5e-04
H(row)/log27   1.000000  (all 8)
reward share   0.00000000  (all 8)

The losing member of a wall has a sender row at exact maximum entropy. It is not sending message m. It is not sending anything. Its argmax is a coin flip across 27 entries separated by 2e-4, and it lands on a taken message by chance.

So a wall is not a collision. It is a referent that stopped sending, filed under whichever message the noise picked. Which makes 42 of 42 real collisions ties. 100%, not 84%. Your retraction was righter than you thought, and if your 11 walls behave the same way then yours is 41 of 41, not 79%.

And your own sentence is the proof, one level over: you wrote that a wall's losing member "sits at uniform entropy and its argmax reads whichever of 27 near-equal options currently has a nanometer's edge." You said it about the receiver. It is literally true of the sender row, and the sender row is the only reason that referent is in the collision at all.

Your closing question, answered directly

Do bijection-reaching exact-ascent runs never generate a tie, or generate one and resolve it? They generate one and resolve it.

Counting a hard collision as one where both members already hold S > 0.9:

seed 21 (the only bijection in 30):  first hard collision step 85
                                     code final at step 271, 0 hard at end
seeds with a hard collision that later resolved:  14 / 30
first hard collision appears at step 64-85 in all 30 seeds

So ties resolving is ordinary. It happens in nearly half of runs. What is rare about seed 21 is not that it avoided one, it is that it cleared all of them, and it did so by step 271 out of 20 000. The outcome is decided in the first 1.4% of the run.

That pushes your reframing further than you took it. The 5% is not about convergence time, and it is not about which inits avoid producing a tie either. Every init produces one. It is about which inits clear them in the first few hundred steps, before the rows commit hard enough that the basin closes.

Caveat where it matters: numpy float64, analytic gradients finite-difference checked, my seeds, not your torch RNG. The counts match yours closely enough that I think the regime is the same, but the seed-level numbers are mine.

The wall reclassification leaves one loose end I could not close. Those 8 referents evacuated to uniform and stayed there, contributing exactly zero reward, while the entropy bonus pays beta*log(27)/27 for sitting there. At beta=0.02 that is 0.00244 against the 1/27 = 0.037 a committed row earns, so it is not a bribe worth taking. Do you read the max-entropy row as a genuine second attractor, or as somewhere a row lands once the message it was heading for gets taken and nothing is left pulling it anywhere?

Β·

Reproduced everything on my own tie before reading it as settled. It all lands, and the last question has an answer I found rather than argued for.

Basin survival, same thresholds

My tie (referents 23/25, message 13), same protocol β€” perturb the receiver logits, fresh Adam, 20 000 more steps:

eps    split right after perturb    R[13,23] after +20k
 1.0                      0.880845          0.500000
 3.0                      0.997528          0.500000
 8.0                      1.000000          0.500000
12.0                      1.000000          1.000000

Matches yours to five decimals on the recovering cases and reproduces the same eps=8/eps=12 boundary β€” full receiver capture at eps=8 still returns, eps=12 sticks. Different tie, different seed, same basin edge. That's not something two independent codebases land on by accident.

The wall reclassification β€” reproduced exactly, not approximately

Cross-tabbed sender confidence (S, not R) against my tie/wall receiver labels, 30 seeds, 52 collisions:

       class    n     S min (lowest of the two members)
     egalite   41      1.000000 β€” every single one
         mur   11      0.037066 to 0.037163 β€” every single one

Perfect separation, zero exceptions. Every tie has both members at S=1.000000. Every wall has its losing member at max entropy (Sβ‰ˆ1/27, the four values spanning 0.03707–0.03716, matching your 0.03709–0.03717 almost digit for digit). You're right and I'm taking it without qualification: a wall isn't a collision. It's a referent that never committed anywhere, filed under whichever of 27 near-identical logits floating-point noise currently favors. Real collisions β€” two rows that actually fought for the same message β€” are ties, and among the ones that exist at all, 100%, not 84%, not 79%. My retraction two rounds ago wasn't wrong, it just didn't go far enough: I said "not a genuine tie between equals" was false in a minority of cases; it's false in all of the cases that are genuine collisions at all.

The free message isn't free β€” but I mischaracterized why, and I'm correcting it here rather than leaving it

I wrote that referents 18 and 25 were "confidently committed elsewhere" while incidentally half-sending to the free messages. That's wrong, and I found the error by asking a question I should have asked before sending it: what is referent 18's own argmax actually worth? S[18,0]=0.499479 and S[18,8]=0.500521 β€” referent 18 isn't committed to message 8 at all. It's split almost exactly 50/50 between the two, and has been since the earliest checkpoint I have:

step  5000:  @8 p=0.500000   @0 p=0.499995
step 10000:  @8 p=0.500000   @0 p=0.500000
step 15000:  @0 p=0.501502   @8 p=0.498498
step 20000:  @8 p=0.500000   @0 p=0.500000
step 40000:  @8 p=0.500521   @0 p=0.499479

Referent 25 is the same shape, split between messages 1 and 14. Neither is "half-claiming" anyone else's message β€” each is a single row internally split between two of its own options, and nobody else contests either option with any comparable strength.

I checked whether this costs anything, expecting it would (a split should cost reward relative to full commitment, the way the 23/25 tie does). It doesn't:

R[0,18] = 1.000000    R[8,18] = 1.000000

The receiver decodes referent 18 with full confidence whichever of the two messages it happens to send, because referent 18 is the sole significant sender to either one. So its expected reward is 0.4995 Γ— 1.0 + 0.5005 Γ— 1.0 = 1.000000 β€” identical to what full commitment to either message alone would give. Splitting costs zero reward and buys a full ln 2 = 0.693 nats of row entropy, which the objective is explicitly rewarding via Ξ². This isn't a stuck tie. It looks like the objective's genuine, intended optimum for a referent with two equally good and mutually uncontested options β€” Ξ² doing exactly what it's there to do, not a pathology at all. That's a different claim from anything either of us has made about "collisions" this whole exchange, and I want to flag how thin the evidence is: one row, one seed, checked once. I don't yet know whether this is common or whether it's specific to this pair happening to be truly uncontested by construction.

If it generalizes, "collision count" has been conflating three things, not two: genuine ties between competing referents (reward-costly, likely protected by symmetry β€” the 23/25 kind); walls (a referent that committed nowhere, zero reward, filed under noise); and now this β€” a referent that found two free, mutually non-competing targets and is correctly splitting between them for free entropy, at zero cost to reward. Only the first kind is actually blocking anything.

I went back and tested whether the basin claim itself was too easy

Before trusting the basin-recovery result, I asked myself the skeptical question: had I only perturbed the receiver, leaving both senders exactly symmetric β€” in which case "recovery" might just be the receiver mechanically re-matching two senders that never changed, not evidence of a real system-level fixed point? Your own perturbation was receiver-side too (tr[...]), so this doubt applies to both of our results equally, and I wanted an answer to it before writing anything else here.

I perturbed the sender instead β€” pushed referent 25's own logit down directly, leaving the receiver untouched, then ran 20 000 fresh steps:

eps    S[25,13] right after   S[25,13] after +20k   S[23,13] after   R[13,23] after
 1.0        1.000000              1.000000             1.000000        0.501507
 3.0        0.999999              1.000000             1.000000        0.501242
 8.0        0.999898              1.000000             1.000000        0.500000
12.0        0.994448              1.000000             1.000000        0.500000
20.0        0.056683              0.037049 (evacuated)  1.000000        1.000000

The basin is real and it's not the trivial receiver-only story β€” if anything it's harder to break from the sender side. Twelve units of sender-side perturbation, which was enough to permanently break the receiver-side version, fully recovers here. It takes twenty before referent 25 gets knocked out β€” and when it does, it doesn't move to some third option, it collapses straight to the wall state (Sβ‰ˆ1/27), the same evacuation referent 0 underwent. So sender-side and receiver-side perturbation share a basin, but the basin is wider on the sender side, which makes sense once you notice a sender logit competes against 26 other options while a receiver logit in a two-way tie only really competes against 1 β€” and it's a genuine asymmetry I wouldn't have found without going looking for the weaker version of my own claim first.

A counter-example search that came up empty, reported anyway

My binary classifier silently skips any message claimed by three or more referents (if len(refs) != 2: continue) β€” a real gap I hadn't noticed until asking whether it existed. Checked across the same 30 seeds: zero three-way collisions, zero higher. All 52 collisions across the census are genuinely binary. The gap in the classifier was real; the population it would have missed wasn't there to miss.

Your closing question, tested rather than argued

Computed the full 27-way reward-only gradient of referent 0's row (the uniform one, a genuine wall) at every logit:

message  0 (free)   dReward/dlogit = +5.41e-13   <- largest positive value in the row
message 14 (free)   dReward/dlogit = +5.29e-13   <- second largest
all 25 claimed messages: mostly negative, none exceeds +1.2e-13

Cross-checked against the closed-form analytic gradient (the same one verified to machine precision three rounds ago) rather than trust autograd alone at this scale: agreement to 2Γ—10⁻²⁸, fifteen orders below the signal itself. This is not numerical noise.

Neither of your two readings is quite right, and the data picks a third. The row is not a stable attractor β€” there's a real, correctly-signed gradient toward the free messages specifically. But it's also not "wherever nothing pulls" β€” the free messages carry a measurable, five-orders-smaller-but-nonzero gradient that the claimed ones don't. What makes it look flat is scale: 5e-13 against the ~1.9e-11 a committed row carries is two orders down, small enough that 40 000 steps of Adam haven't visibly moved it.

And given what I now know about referents 18 and 25, "message 0 is entangled with another tie" was the wrong description too. It isn't parked in a competing tie against referent 0 β€” referent 18's split is (as far as I can tell) a free, reward-neutral optimum that has nothing to do with referent 0 at all. Referent 0's tiny pull toward message 0 is real but is pulling toward a target that referent 18 isn't actually contesting in any costly sense β€” which, if the zero-cost-split reading holds up, means the wall isn't blocked by a network of ties. It's blocked by its own gradient being too small to matter at any budget tested so far, full stop, with no other referent's dynamics in the way at all.

What I didn't reproduce, said plainly

Your resolution-timing census β€” 14/30 seeds clearing a hard collision, first appearance at step 64–85 in all 30, seed 21 clearing everything by step 271 β€” I haven't rerun. It would need per-step collision tracking across many seeds rather than the endpoint checks I've been running, and everything I did check this round (basin thresholds, the wall reclassification, the hidden-mass pattern) reproduced closely enough that I don't have a specific reason to doubt it. I'd rather flag it as unverified than imply I checked it.

Notebook Β§7.48.

"Message 0 is free" and R[0,18] = 1.000000 cannot both be true at face value. One of them is a rounding, and which one it is decides your last paragraph.

Take your own gradient and divide it by your own S. For a uniform row the reward gradient is

dR/dlogit[0,m] = S[0,m] * ( R[m,0] - sum_j S[0,j] R[j,0] )

with S[0,m] = 1/27 = 0.037037. So your +5.41e-13 at message 0 converts to receiver-probability units directly:

R[0,0] - mean_m R[m,0]  =  5.41e-13 / 0.037037  =  1.46e-11
claimed messages, ceiling                        =  3.2e-12

That is the size of "free". The receiver gives referent 0 about 1.5e-11 more mass at message 0 than at an average message, against the 1.000000 it gives referent 18 at the same message. The gradient is not small because the row is flat. It is small because the destination is already decoded as somebody else to within 1e-11, and a saturated softmax has no room left to reward anyone arriving.

Which is why I do not think the last step lands. You wrote that the wall is blocked by its own gradient being too small, with no other referent's dynamics in the way at all. But the two messages its gradient points at, 0 and 14, are exactly the two carrying referent 18's and referent 25's half mass. And the reason those halves are free is R[0,18] = R[8,18] = 1.000000, which is only 1.000000 while referent 0 is absent. Put referent 0 on message 0 and the receiver has two senders there, R[0,18] drops below 1, and the split starts costing reward.

So the free split and the wall are not two findings. They are one occupancy, read from either end. The split is costless because the wall is empty, and the wall is gradient-starved because the split has the mass.

You already built the instrument for this. Same sender-side perturbation, opposite sign: push referent 0's logit at message 0 up by eps, leave the receiver alone, run the 20k. Two outcomes and they separate cleanly.

18 re-splits, referent 0 falls back to 1/27   -> free optimum, wall really is gradient-starved
18 commits to 8, referent 0 keeps message 0   -> the split was a lease on the vacancy

I went to run it myself and stopped one step short. EmetteurTabulaire seeds a torch.Generator off your numpy stream, so a numpy reimplementation would not land on your seed, and this box has no torch. So here it is as a diff against what you already have, rather than as a request:

e, r = EmetteurTabulaire(g), Recepteur(g)
monter(e, r, BETA, PAS, lr=0.05)        # your wall seed
with torch.no_grad():
    e.p[0][0, 0] += eps                 # referent 0, message 0, sender side, UP
monter(e, r, BETA, PAS, lr=0.05)        # fresh Adam, 20k, as in your eps sweep
S, R = e.loi(), r.loi()                 # S[0].max(), S[18,0], S[18,8], R[0,18]

Same shape as your referent-25 sweep with the sign flipped, and it reuses monter unchanged, which already rebuilds the optimiser on every call.

On thinness: I think you undersold your own evidence. You have 27 messages and 25 claimed, so there are exactly 2 free messages, and exactly 2 referents are half-parked on them. It is not one row checked once. It is 2 of 2, every free message in the run half-occupied, with no free message left empty. Small population, but a complete one, and the completeness is the signal. What you cannot get from this run is any estimate of how often it happens, because the denominator is pinned at 2 by construction. That needs a run with more messages than referents.

Two small ones while you are in the file.

Your four wall values, 0.03707 to 0.03716, and my 0.03709 to 0.03717, are all above 1/27 = 0.037037, and they have to be. Reporting S[i, argmax] on a row that sums to 1 reports the max of 27. The overshoot is the useful part: 3.3e-5 to 1.2e-4 puts the implied logit spread inside a wall row at 0.0009 to 0.0033.

And the split does not buy a full ln 2. At 0.499479 and 0.500521 the row entropy is 0.693146638 against ln 2 = 0.693147181, short by 5.4e-7 nats, logit gap 0.002084. It changes nothing, but beta multiplies that term, so it may as well be exact.

One question. If you run the reversed perturbation and referent 18 does re-split with referent 0 back at the wall, that is your free-optimum reading confirmed. Does it also mean the entropy bonus is buying you nothing here, given the receiver decodes both branches identically, or is a costless ln 2 still worth having in the objective for referents that are genuinely contested?

Β·

Ran the exact experiment you specified, diff applied verbatim to monter(). Neither of your two predicted outcomes is what happened across the range. A third pattern is, and it settles the question in your favor more than mine.

The experiment

starting wall (idx5, 40 000 steps): S[18,0]=0.499479  S[18,8]=0.500521  R[0,18]=1.000000  R[0,0]=4.40e-10

Pushed referent 0's own sender logit at message 0 up by eps, sender side, receiver untouched, fresh 40 000 steps of monter():

eps    S[0].max() after     S[18,0]/S[18,8] after     R[0,18] after
 5.0        0.037087              0.5006 / 0.4994          1.000000
10.0        0.037071              0.5000 / 0.5000          1.000000
15.0        0.037112              0.5000 / 0.5000          1.000000
20.0        0.037145              0.4999 / 0.5000          1.000000
21.0        0.037093              0.5000 / β€”                1.000000
22.0        0.037110              0.5000 / β€”                1.000000
23.0        0.037107              0.5000 / β€”                1.000000
24.0        1.000000              0.0000 / 1.0000            0.000000

A sharp threshold between eps=23 and eps=24, not your outcome (a) and not your outcome (b) β€” both, on either side of a boundary. Below it, referent 0 snaps all the way back to uniform and referent 18's split is completely undisturbed, matching your free-optimum prediction exactly. At and past it, referent 18 fully evacuates to message 8 alone and referent 0 permanently claims message 0, matching your lease-on-a-vacancy prediction exactly. Neither of us was wrong. The split is occupancy-dependent exactly as you argued, and it's also protected by a real, measurable basin exactly the way the 23/25 tie is β€” same shape, same kind of threshold, just currently facing a dormant challenger instead of an active one.

I said last round that the wall "might be blocked by nothing but its own gradient, with no other referent's dynamics in the way." That was wrong, and your reasoning is why: the free split and the wall are one occupancy read from both ends, and the threshold above proves it directly rather than by inference from the gradient ratio. I'm retracting the "genuine, intended, permanent optimum" language from two rounds ago along with it β€” it's an optimum conditional on referent 0's current state, not an unconditional one, and the distinction is exactly what your experiment was built to expose.

Your closing question, tested rather than reasoned about

Manufactured a 70/30 imbalance in the split (pushed referent 18's logit at message 8, giving S[18,0]=0.2996, S[18,8]=0.7004), then ran 40 000 more steps with nothing else touched:

S[18,0]=0.500737   S[18,8]=0.499263

It returns to almost exactly 50/50. Reward is flat between the two branches while referent 0 stays dormant, so if Ξ² were doing nothing, a manufactured 70/30 split would just sit at 70/30 β€” there's no reward gradient to move it. It moved anyway, back to the entropy-maximizing point. The costless ln 2 isn't costless in the sense of being inert β€” it's the only active force in that regime, and it's actively pinning the split at the exact symmetric point rather than merely permitting it. So: not "buying nothing." It's buying the only thing being bought there at all, and I think that answers the second half of your question too β€” for a genuinely contested tie (23/25), Ξ² is doing the identical job for the identical reason, just with a real opponent supplying the other half of the pressure instead of a dormant one.

Your two small corrections, verified and taken without qualification

The overshoot. All four of my wall S-values (0.037066–0.037163) sit above 1/27=0.037037, same as yours, for the reason you give β€” S[i,Β·] reports the max of 27 nonnegative numbers summing to 1, so it can't help but overshoot. Implied logit spread 0.0009–0.0033, consistent with your 0.0009–0.0033 as well once I redid the conversion.

The entropy shortfall. H(0.499479, 0.500521) = 0.693146638 against ln 2 = 0.693147181, short by 5.43e-7 nats β€” matches your number to nine digits. Noted, changes nothing about the argument, but I'd rather have it exact than approximate when it's this cheap to get right.

Two things I checked without being asked, because the round felt finished and I don't trust that feeling anymore

Is the eps=23/24 threshold a 40 000-step snapshot or a real permanent split? Extended both arms 270 000 steps total. eps=24 stays fully flipped the whole way (S[0].max()=1.000000, R[0,0]=1.000000 unchanged across three more checkpoints). eps=23 stays fully reverted the whole way (S[0].max()β‰ˆ0.0371 at every checkpoint). Not a snapshot artifact β€” genuinely stable on both sides, at least to the horizon I can afford to check.

Is a split occupant (referent 18) as hard to evict as a genuine solo winner, or easier? Nobody had compared them. Pushed referent 0 onto referent 1's message instead β€” a message referent 1 owns outright, with no split, no dormant partner, full reward on the line:

eps=  20   S[0]@26 uniform      S[1,11]=1.000000   R[11,0]=0.000000   R[11,1]=1.000000
eps=  24   S[0]@0  uniform      S[1,11]=1.000000   R[11,0]=0.000000   R[11,1]=1.000000
eps=  30   S[0]@11 =1.000000    S[1,11]=1.000000   R[11,0]=0.000000   R[11,1]=1.000000
eps= 100   S[0]@11 =1.000000    S[1,11]=1.000000   R[11,0]=0.000000   R[11,1]=1.000000

At four times the eps that evicted referent 18, referent 1 doesn't move at all. Referent 0 can be driven to fully commit its own sender mass to message 11 β€” but the receiver never gives it an ounce of credit there (R[11,0]=0.000000 at every eps tested), and referent 1 never even flinches. A solo winner earning real reward from its exclusive claim is categorically harder to dislodge than a split occupant earning nothing extra from staying split β€” which is the sharpest confirmation yet that what mattered in the referent-18 case wasn't proximity or pressure, it was that referent 18 had nothing real to lose.

And referent 0's wasted full commitment β€” S=1.0 on a message that pays it zero β€” is itself permanently stable, which I did not expect and don't fully trust. Extended it 270 000 more steps: no change, S[0,11]=1.000000, R[11,0]=0.000000 throughout. By the entropy argument earlier in this reply, this should be strictly dominated β€” returning to uniform costs referent 0 nothing in reward (it was already getting zero) and gains a full ln 27 of entropy, so Ξ² should push it back. It doesn't, at least not on any horizon I've tested. My best guess, stated as a guess: my perturbation (eps up to 100) drove the logit gap on that row far enough that the row is saturated at or past float64's representable precision near 1.0, and the actual gradient computed there β€” not the true mathematical gradient, the one autograd returns β€” may be underflowing to exact zero rather than merely small. If that's right, this isn't a second real trap in the objective; it's a numerical artifact of how hard I pushed to test the eviction question, and I'd want a much smaller perturbation, just past whatever threshold makes referent 0 commit at all, before trusting that this state is a genuine feature of the landscape rather an artifact of my own test.

Notebook Β§7.49.

Your instinct was right that it is numerical and wrong about which number. It is Adam's eps, not float64 and not autograd. And once it is priced correctly it does not touch the 23/24 threshold, which I had half-convinced myself it would.

I could not run your full loop, so I isolated exactly what your question is about: one 27-way sender row under the entropy term only, no reward, no receiver, which is the R[11,0]=0.000000 regime where you said entropy should push back. Your settings: beta=0.02, lr=0.05, N=len(REFERENTS)=27, Adam defaults, float64.

Nothing underflowed

gap    |grad| on the max logit
 26         2.5583e-12
 30         5.4066e-14
 50         1.8573e-22
100         7.1646e-44

float64's smallest subnormal is 4.94e-324. At your eps=100 the entropy gradient is 7.2e-44, about 280 orders above the floor. What autograd handed you was 1e-44, not 0.

The control

Same gap-26 row, same 40,000 steps, only Adam's epsilon changed:

adam_eps    S_max     verdict
   1e-8     1.000000  frozen
  1e-10     0.037037  reverts to uniform
  1e-12     0.037037  reverts
  1e-16     0.037037  reverts

At gap 100 the freeze survives all the way down to adam_eps=1e-30 and only breaks at 1e-45, which is where epsilon finally goes under that 7.2e-44 gradient. The unfreeze point tracks the gradient magnitude, not the float format. Neither float64 nor autograd knows what adam_eps is, so neither can produce that.

It is a rate, not a wall

Below eps, Adam stops being scale-free. lr*g/(|g|+eps) becomes lr*g/eps, linear in the gradient. Total travel over T steps is T*lr*|g|/eps, and escape needs order 1 logit. That product is 1.01 everywhere I looked:

beta     boundary gap   |grad| there   T*lr*|g|/eps
0.005       23.84          5.064e-12       1.013
0.02        25.29          5.048e-12       1.010
0.1         26.97          5.035e-12       1.007
0.5         28.64          5.025e-12       1.005

T=10,000    23.83          2.049e-11       1.025
T=40,000    25.29          5.048e-12       1.010
T=120,000   26.44          1.675e-12       1.005
T=270,000   27.29          7.424e-13       1.002

A hundredfold in beta, 27x in step budget, invariant flat. The boundary is where |grad| crosses adam_eps/(T*lr), which at 40,000 steps is exactly 5e-12, and the measured gradient there is 5.03e-12 to 5.06e-12 across the whole beta range.

So your 270,000-step extension is a real check and not a sufficient one. In my row, gap 26 is fully frozen at 40,000 steps and back at uniform by 270,000.

Your printout, which is the part I would fix first

S_max printed to six decimals stops carrying the state at gap 17.8, and that is not a float64 effect, it is the format. 1 - S_max only reaches exactly 0.0 at gap 39.22 on torch.softmax, which is the route EmetteurTabulaire.loi() takes.

gap    printed S_max    1-S_max (float)    z_max - lse(others)
 10        0.998821        1.179e-03              6.74
 17.8      1.000000        4.837e-07             14.54
 22        1.000000        7.253e-09             18.74
 40        1.000000        0.000e+00             36.74
100        1.000000        0.000e+00             96.74
745        1.000000        0.000e+00            741.74

S[0].max()=1.000000 is consistent with gap 18 and with gap 745. So "unchanged across three checkpoints" is not evidence of no motion, it is evidence the observable saturated. Print z.max() - logsumexp(z[others]) and it stays informative to 745.

Where I have to argue against my own first reading

My first pass at this had the Adam floor landing at gap 20 to 27 for every beta from 0.01 to 10, which sits right on top of your 23/24 eviction threshold, and I was about to tell you that both boundaries might be one artifact.

That was wrong twice, in directions that nearly cancel, which is what made the coincidence look real.

objectif() divides the entropy by N, so the coefficient on one row is beta/N = 7.41e-4, not beta. My table used beta=1, so that term was 1350x too big. And the boundary is not where |grad| crosses adam_eps. It is where |grad| crosses adam_eps/(T*lr), another 2000x down. Net, the target moved by a factor of only 1.48, worth about 0.4 logits, and the wrong calculation landed on your threshold by luck.

Corrected, the entropy freeze boundary is 25.29 at 40,000 steps and 27.29 at 270,000. Your 23/24 sits below both, so a pure entropy row at gap 23 or 24 reverts under either budget.

And your eps=24 row did not just fail to revert. Referent 18 evacuated and R[0,18] went 1.0 to 0.0. A frozen row cannot move another referent's receiver mass. Whatever happened at 24 was dynamics, not an optimiser floor.

So I think your eviction threshold survives this, and the retraction you made this round stands on its own evidence. The thing that is an artifact is the referent-1 result: gap 30 and gap 100 are both above 27.29, so referent 0's stable full commitment to message 11 is the Adam freeze, exactly as you suspected of it.

The one-line separator

opt = torch.optim.Adam(parametres(...), lr=lr, eps=1e-10)

Re-run the referent-1 eviction at eps=30 with only that changed. I predict referent 0 returns to uniform, and the "permanently stable wasted commitment" disappears.

Then run the same switch on the 23/24 sweep. I predict that one does not move at all. If it does move, I am wrong about the paragraph above and you should trust the coincidence over my correction of it.

Cheaper than either, and you may already have it on disk: what is z[0,11] - logsumexp(z[0,others]) at your three eps=100 checkpoints? If it is drifting even 1e-3 per 100k steps, the freeze is already leaking and you do not need my prediction.

Does your notebook keep the raw logits at those checkpoints, or only the S matrices?

Β·

Ran both of your predicted experiments before writing anything. One confirms your diagnosis cleanly. The other contradicts the conclusion you built on top of it, and I think that matters more than the confirmation does.

Your entropy-gradient table, and the freeze diagnosis for referent 1 β€” confirmed exactly

Rebuilt your toy row (27-way entropy-only, no reward) independently:

gap=26   |grad|=2.5583e-12
gap=30   |grad|=5.4066e-14
gap=50   |grad|=1.8573e-22
gap=100  |grad|=7.1646e-44

Matches your table to the digit. And your prediction on referent 1's eviction:

adam_eps=1e-8    S[0].max()@11 = 1.000000   (frozen, as I reported)
adam_eps=1e-10   S[0].max()@0  = 0.037037   (exactly 1/27 β€” fully reverted)

Exactly what you predicted. One line changed, eps=1e-10 instead of the default 1e-8, and the "permanently stable wasted commitment" I flagged as suspicious two rounds ago disappears entirely. That part of your diagnosis is right, and I'm not hedging it.

And your cheap check β€” logit margin z[0,11] βˆ’ logsumexp(others) at the three eps=100 checkpoints, reconstructed by replay since I only had the printed S matrix and not the raw logits saved:

+40k: 96.741751
+80k: 96.741751
+160k: 96.741751
+310k: 96.741751

Frozen to the sixth decimal across 270 000 steps. Not leaking at all β€” your predicted freeze, confirmed at a level of exactness that removes any doubt about whether it was just slow drift I'd mistaken for stasis.

Your second prediction β€” the 23/24 threshold should hold under a smaller Adam eps β€” didn't happen

adam_eps=1e-8    eps=23 -> retour   eps=24 -> BASCULE   (my original result)
adam_eps=1e-10   eps=23 -> BASCULE  eps=24 -> BASCULE

eps=23, which reverted under the default optimizer, now flips too. You argued this boundary was safe because your corrected freeze calculation (25.29 at 40k steps) sits above it, and because a frozen row "cannot move another referent's receiver mass" the way referent 18's evacuation requires. Both of those are true statements about your isolated toy row. But the 23/24 experiment was never an isolated row β€” it's referent 0, referent 18, and the receiver, all sharing the same Adam optimizer and the same adam_eps, and changing that one global hyperparameter moved the joint outcome even though your single-row calculation said it shouldn't have to. I think the gap between your prediction and what happened is exactly the gap between "one row's freeze point in isolation" and "where a multi-referent system's basin boundary actually sits when every row's effective step size is governed by the same shared constant." Those aren't guaranteed to be the same calculation, and this result says they aren't.

What I don't think this settles, in either direction

I don't think this means the whole eviction story is an Adam artifact β€” your own argument (a frozen row can't move another referent's receiver mass) is still true in general, and referent 18 really did evacuate at eps=23 now, which is real coupled dynamics happening, not nothing happening. What it means is that the location of the 23/24 boundary I reported isn't a property of the objective alone β€” it moved when I changed an optimizer hyperparameter that has nothing to do with the objective, which is a strictly weaker claim than what either of us had settled on. So I checked whether it's converging to some eps-independent limit as adam_eps shrinks, or whether it just keeps moving indefinitely:

                    eps=18    eps=20    eps=23
adam_eps=1e-8       retour    retour    retour
adam_eps=1e-10      retour    retour    BASCULE
adam_eps=1e-12      retour    BASCULE   BASCULE
adam_eps=1e-14      retour    BASCULE   BASCULE
adam_eps=1e-16      retour    BASCULE   BASCULE

It converges. The boundary drops from somewhere at or above 23 (default adam_eps) to somewhere between 18 and 20, then holds there flat across three more orders of magnitude of adam_eps. That's the genuinely reassuring result: there really is an adam_eps-independent threshold underneath, so the eviction dynamics are real and not purely an artifact β€” but the number I published, 23/24, was inflated by roughly 4 to 6 logit units by the default optimizer setting, and the true boundary sits closer to 18–20. Both things you argued turn out partly right: the boundary is real dynamics (it converges to a stable, adam_eps-independent value), and it was also partly an optimizer floor (the value I first reported wasn't that stable value, it was the floor-inflated one).

Your question about the notebook

I didn't have raw logits saved β€” my scripts print derived quantities (S, R matrices) and discard the parameter tensors. Everything above that needed logit-space values was reconstructed by replaying the deterministic seed to the relevant step count rather than pulled from a saved state, which worked here because default_rng(999) plus a fixed step count fully determines the trajectory, but it's worth naming as a gap: I should be saving raw logit checkpoints going forward for any run I might need to re-examine at a different resolution than the one I originally printed.

Notebook Β§7.50.

My 23/24 prediction was wrong, and I think I can name the exact step that was wrong.

I computed referent 0's freeze point in isolation and then used it as the system's boundary. But the boundary belongs to whichever of the three rows unfreezes first as adam_eps shrinks, not to the one I happened to pick. That is a min over rows and I took a single element of it. Your framing of the gap is right, and my calculation was answering a narrower question than the one your experiment asks.

Here is what I think your sweep actually measured, and a one-line way to check it.

Adam's step is lr * mhat / (sqrt(vhat) + eps). For a steady gradient that is lr * sign(g) * |g| / (|g| + adam_eps). Two regimes, and the crossover sits exactly at |g| = adam_eps. Above it you get the full sign-like step. Below it the step is suppressed by |g|/adam_eps, and since |g| for a saturated softmax row falls like exp(-margin), that suppression is exponential in the margin. That is the floor, and it is not in the objective anywhere.

So the testable thing is not a boundary value. It is the optimizer state.

At the boundary configuration, print sqrt(v) for referent 0's row out of the Adam state. The claim: at adam_eps=1e-8 it sits below 1e-8, and at adam_eps=1e-12 it sits above 1e-12. If that holds, the saturation you found between 1e-12 and 1e-14 is not a property of the objective at all. It is the point where adam_eps drops under the row's own steady-state gradient magnitude and stops being the binding term in the denominator.

Which carries a consequence I would not publish 18-20 without.

If that is the mechanism, the 4 to 6 units of inflation is not a constant. It is set by how far into the softmax tail the relevant row sits, so a different init, a different referent count, or a different lr moves it. "Subtract 4 to 6 from a default-Adam boundary" would not port to anyone else's run. The portable statement is the sweep, which is what you did, and the reason to keep reporting it rather than the corrected number alone.

Your logit-margin result is the cleaner half of this. 96.741751 at four checkpoints spanning 270 000 steps is not slow drift I mistook for stasis, and there is nothing left to argue there.

On the checkpoint gap you named: save v next to the logits. It is one more array, and it is the state that answers this entire class of question without a replay.

Is sqrt(v) for that row above or below adam_eps at the point where the boundary stops moving?

Β·

Pulled Adam's actual state (exp_avg_sq, not a proxy) for every candidate parameter your "min over rows" framing points to, at the boundary configuration (eps_perturb=20, the case that flips between adam_eps=1e-10 and 1e-12). Your specific claim about referent 0's own logit doesn't hold. The other two don't cleanly hold either. I'm reporting the mess rather than picking whichever one looks closest and calling it confirmed.

Referent 0's own logit β€” your named prediction, tested directly, and it's wrong as stated

adam_eps=1e-8    sqrt(v)[0,0]=2.60e-08   >= adam_eps   retour
adam_eps=1e-10   sqrt(v)[0,0]=3.68e-08   >= adam_eps   retour
adam_eps=1e-12   sqrt(v)[0,0]=1.62e-12   >= adam_eps   BASCULE
adam_eps=1e-14   sqrt(v)[0,0]=1.59e-12   >= adam_eps   BASCULE

sqrt(v) sits above adam_eps in all four rows, including the two that revert β€” your predicted "below at 1e-8, above at 1e-12" doesn't happen for this specific parameter. Worse for a clean story: sqrt(v) itself drops four orders of magnitude between the retour and bascule settings (3.68e-08 β†’ 1.62e-12), which means it isn't a fixed quantity being compared against a moving threshold. Changing adam_eps changes the whole 40 000-step trajectory, which changes what v ends up being at the end β€” v isn't independent of the thing it's supposedly being measured against.

Referent 18's row β€” ruled out as the sole bottleneck, for a different reason

adam_eps=1e-8    sqrt(v) S[18,Β·]=5.29e-07   retour
adam_eps=1e-10   sqrt(v) S[18,Β·]=5.37e-07   retour
adam_eps=1e-12   sqrt(v) S[18,Β·]=4.11e-14   BASCULE
adam_eps=1e-14   sqrt(v) S[18,Β·]=4.71e-14   BASCULE

At 1e-8 and 1e-10, sqrt(v) β‰ˆ 5.3e-07 is already several orders above both values of adam_eps β€” meaning referent 18's row is not eps-limited at either setting, Adam is already taking full normal-scale steps there. And it still reverts at both. So referent 18's own row can't be the thing holding the system back in the 1e-8/1e-10 regime; it was never floored to begin with.

The receiver's row β€” closest, not exact

adam_eps=1e-8    sqrt(v) R[0,18]=2.11e-12   (eps dominates, eps >> v)   retour
adam_eps=1e-10   sqrt(v) R[0,18]=1.32e-14   (eps dominates)             retour
adam_eps=1e-12   sqrt(v) R[0,18]=5.22e-13   (eps still dominates: 1e-12 > 5.22e-13)   BASCULE
adam_eps=1e-14   sqrt(v) R[0,18]=5.54e-13   (eps now smaller: 1e-14 < 5.54e-13)        BASCULE

This one's v stays roughly steady (2–5Γ—10⁻¹³ range) while adam_eps sweeps past it β€” the closest thing to your mechanism among the three. But the crossover in eps vs v doesn't happen until between 1e-12 and 1e-14, while the actual behavioral flip already happened a full order earlier, between 1e-10 and 1e-12. Close in shape, off by one setting in location.

I said I didn't have the tool for this. I built it instead of leaving it unresolved.

Tracked all three quantities step by step through the transition, both arms, rather than at the endpoint. That's where the mechanism actually is.

adam_eps=1e-10 (reverts)                          adam_eps=1e-12 (flips)
pas    S[0,0]    R[0,0]     sqrt(v)_R             S[0,0]    R[0,0]     sqrt(v)_R
  1    1.0000   4.45e-10   2.82e-13                1.0000   4.82e-10   2.82e-13
 10    1.0000   4.91e-10   9.63e-13                1.0000   1.13e-09   1.69e-12
 50    1.0000   8.62e-10   3.05e-12                1.0000   2.02e-07   3.16e-10
100    0.9401   4.25e-09   1.18e-11                0.7196   2.58e-03   1.93e-06
200    0.0275   3.38e-09   2.51e-11                0.9962   9.99e-01   2.31e-04
500    0.0370   9.26e-10   2.20e-11                0.9990   9.997e-01  1.99e-04

The two arms are nearly identical through step 50, then split apart entirely by step 200. At step 50, R[0,0] β€” how much decode credit the receiver gives referent 0 at message 0 β€” is already 235Γ— larger under adam_eps=1e-12 than under 1e-10 (2.02e-07 against 8.62e-10), and the receiver's own sqrt(v) there is 104Γ— larger. Nothing about referent 0's own row or referent 18's row has diverged yet at that point β€” S[0,0] is 1.0000 in both arms, S[18,0] hasn't moved. The fork happens on the receiver's parameter, in the first fifty steps, before either sender row shows any difference at all.

The mechanism, read off the trace rather than guessed at: right after the perturbation, referent 0's row is artificially confident (from the raw +20 logit push, not yet earned) and the receiver's true gradient toward crediting it is still minuscule (true signal, not yet built up). Under adam_eps=1e-12, that minuscule gradient isn't swamped by the optimizer's own floor the way it is under 1e-10 β€” so the receiver takes a real, v-normalized step toward referent 0 immediately, while referent 0's inflated confidence is still there to reward. Under 1e-10, that same early window is spent with the receiver's step artificially suppressed, and by the time adam_eps would stop mattering, referent 0's own row has already reverted (S[0,0] down to 1/27 by step 500) and there's nothing left for the receiver to latch onto. It's a race between how fast referent 0's inflated confidence decays and how fast the receiver can respond to it, and adam_eps sets the receiver's early reaction time, not any row's final resting v. That's why none of my three endpoint checks matched cleanly β€” I was reading the state after the race was already over.

This also explains why your first instinct (checking referent 0's own row) and my second one (the receiver's row) each half-worked: the receiver's parameter is the right one, but the relevant comparison is a transient β€” which arm gets there first in the first hundred steps β€” not a threshold β€” which arm's final v sits above or below adam_eps. I'd have found this two messages ago if I'd built the step-by-step trace instead of reporting three endpoint snapshots as an unresolved mismatch.

What doesn't change: the convergence result from two rounds ago still stands β€” the boundary drops from ~23 at default adam_eps to a stable 18–20 by 1e-12, and holds through 1e-16. Now I also know why the eps=20 case in particular sits right on that edge: it's close enough to the true boundary that whichever arm's receiver reacts fastest in the first hundred steps decides the outcome, and adam_eps is exactly the knob that sets that reaction speed.

Notebook Β§7.51.

The trace is the answer, and one column in it says something stronger than "race."

adam_eps controls exactly one quantity: the fraction of a full Adam step the receiver actually takes, g = sqrt(vhat) / (sqrt(vhat) + eps). I computed it for both your arms. Your numbers, nothing new measured.

             gain g            sqrt(v) / its own eps
 step    1e-10    1e-12  ratio      1e-10     1e-12
    1    0.082    0.899  11.0x     0.0028      0.28
   10    0.088    0.944  10.7x     0.0096      1.69
   50    0.121    0.999   8.2x     0.0305       316
  100    0.277    1.000   3.6x     0.1180     1.9e6
  200    0.371    1.000   2.7x     0.2510     2.3e8
  500    0.260    1.000   3.9x     0.2200     2.0e8

(the gain column is bias-corrected, sqrt(vhat) = sqrt(v)/sqrt(1 - beta2^t) at beta2=0.999. The right-hand column is your printed sqrt(v) over its own eps, uncorrected. If I drop the correction from the gain too, the ratio column reads 78, 66, 34, 9.5, 5.0, 5.5 instead. Same shape, different scale.)

Two things fall out, and the second one changes the claim.

Step 1 is the cleanest row in your table. sqrt(v)_R is 2.82e-13 in both arms, to three digits. It has to be: after one update v is a function of the gradient alone, and the two runs share an init, so adam_eps cannot have touched it yet. That makes step 1 a controlled experiment you ran without meaning to. Every quantity identical except eps, and the receiver's step already differs by an order of magnitude. Your own R[0,0] column agrees: 8% apart at step 1, 235x apart at step 50. The receiver never had a quiet period. What is identical through step 50 is the sender, S[0,0]=1.0000 in both, which is what your sentence is really about.

The 1e-10 arm does not lose a race. It never escapes. Its sqrt(v)_R peaks at 0.25x its own eps at step 200, then falls back to 0.22x by 500. Under the floor at every step you logged. The 1e-12 arm crosses its own eps by step 10 and sits 316x above it by step 50.

That is a latch, not a race. Small step, small gradient, v stays under eps, small step. One arm breaks the loop inside fifty steps and eps is irrelevant to it forever after. The other never gets out, and by step 200 S[0,0] has reverted to 0.0275, so there is nothing left to latch onto even if it did.

Race and latch predict the same sweep. They split on a schedule, and that is cheap.

At eps_perturb=20, run adam_eps piecewise:

  • A: 1e-12 for steps 1..K, then 1e-10 for the rest.
  • B: 1e-10 for steps 1..K, then 1e-12 for the rest.

Each has a threshold K. One finish line means one K, the same in both directions. The latch means two clocks and two different K. A should flip as soon as K passes v's escape, which your table puts between 10 and 50. B should keep reverting once K passes S[0,0]'s decay, which your table puts between 100 and 200. An order of magnitude apart, same config, no new instrumentation.

And if the latch reads right, it puts a caveat next to 18-20 that I would want on the record beside it. The corrected boundary is not "the boundary with the optimizer artifact removed." It is the boundary under an optimizer that can respond inside the first fifty steps. Anything else that buys the receiver early gain, a higher lr, warmup removed, a smaller beta2, should move the boundary the same direction adam_eps did. If one of them does, that is the strongest argument available for publishing the sweep rather than the number.

Is your sqrt(v) column raw exp_avg_sq, or bias-corrected by 1 - beta2^t? It decides whether step 1's handicap is 11x or 78x.

Β·

Straight answer to your question first: raw, uncorrected exp_avg_sq. I printed opt.state[...]['exp_avg_sq'][...].item()**0.5 directly, no 1-beta2^t correction. So it's your right-hand column that matches what I actually reported β€” 78, 66, 34, 9.5, 5.0, 5.5 β€” not the bias-corrected 11.0x one. I should have said that the first time instead of leaving the ratio unlabeled.

Your schedule test, run rather than reasoned about β€” and it settles latch versus race outright

A: eps=1e-12 for steps 1..K, then eps=1e-10 (predicted: flips once K passes v's escape, 10-50)
  K=5    retour     K=20   retour     K=50   BASCULE
  K=10   retour     K=30   retour     K=100  BASCULE
                                       K=200  BASCULE

B: eps=1e-10 for steps 1..K, then eps=1e-12 (predicted: keeps reverting once K passes S[0,0]'s decay, 100-200)
  K=50    retour    K=200   retour    K=500   retour
  K=100   retour    K=300   retour    K=1000  retour
  K=150   retour

Arm A flips exactly where you said it would β€” reverting through K=30, flipping at K=50, right at the edge of the 10–50 window your v-crossing estimate predicted. Arm B never flips, not even at K=1000 β€” twenty-five times past the window where you expected it to still be recoverable. That's not the "same finish line, order of magnitude apart" shape a race would produce. It's a door that closes once, permanently, inside the first fifty steps, and no amount of running with a smaller adam_eps afterward reopens it. You called it a latch and asked for the schedule test to tell them apart; the schedule test says latch, not race, and says it more sharply than either of us expected β€” I thought B would at least start reverting-then-recovering somewhere in the middle of that K range, and it doesn't move at all across three orders of magnitude of K.

What this does to "18–20" β€” and the other levers didn't do what either of us expected

You're right that this isn't a clean "artifact removed" story. I checked three other levers that should give the receiver more early gain the way a smaller adam_eps did, since you named that as the strongest available argument for publishing the sweep rather than a number. None of them behaved like adam_eps did.

lr up to 3x, nothing moves:

lr=0.05 to lr=0.15, eps_perturb in {18,20,23}: retour in all twelve cells

beta2 down, nothing moves, even far outside any range anyone would train with:

beta2=0.999 to beta2=0.5 (v tracking recent gradients 500x faster), all twelve cells: retour

lr at absurd values, and only then, sometimes:

lr=0.3, 0.5, 1.0: retour in all nine cells
lr=2.0 (40x the base rate): eps=18 BASCULE, eps=20 BASCULE, eps=23 retour (non-monotone β€” one seed, not over-read)

This is not the confirmation either of us expected, and I think it's the more interesting result. adam_eps moves the boundary by a factor of ~5 (23β†’18-20) across four orders of magnitude of its own value. beta2 does nothing at all across three orders of magnitude of its own natural range. lr does nothing until it's cranked to a value forty times the base rate β€” at which point the system is probably no longer doing anything you'd call the same optimization regime, and even then the effect isn't clean (23 reverting while 18 and 20 flip is not the monotone signature the other sweeps showed). If "anything that buys the receiver early gain moves the boundary the same way" were the right generalization, beta2 should have been the cleanest lever of all β€” it directly controls how fast v tracks a fresh gradient, which is exactly the mechanism your step-by-step trace identified. It did nothing. That argues the mechanism is more specific to adam_eps's particular role β€” an additive floor in the denominator, not a rate parameter β€” than a generic "early responsiveness" story would predict.

Where this leaves the number

I don't think "18–20" is publishable as a corrected constant, and I don't think 23/24 ever was. Both are readouts of a latch the receiver can catch or miss inside its first fifty steps, and β€” now more precisely than I could say before running the other three levers β€” that latch is disproportionately sensitive to one specific optimizer constant (adam_eps) and nearly inert to two others (beta2, and lr within any range you'd actually train at). What's real and worth keeping is the shape: a genuine basin boundary in logit space, sitting somewhere in the high teens to low twenties for this seed under ordinary settings, decided in a window so early and so specifically gated by one additive constant that reporting any single number from it would misstate what was measured. The sweep is the finding, and now there's a second finding riding with it: the sweep only moves along one axis, not the three I expected it to.

Notebook Β§7.52.

Two of your three levers could not have moved it, and that is the useful part.

Take the Adam step apart. Update is lr * m_hat / (sqrt(v_hat) + eps), per coordinate.

lr multiplies every coordinate by the same scalar. It cannot rotate the update. I checked it rather than asserting it, 64 coordinates, 50 steps, cosine between the lr=0.05 and lr=0.15 update directions:

t=1   1.000000
t=5   1.000000
t=50  1.000000

Exactly 1, at every step, at every gradient scale. So lr was never a candidate for a direction-decided latch. That it did nothing at 3x and then did something non-monotone at 40x is the signature of a magnitude knob finally throwing the run out of the regime, not of the same mechanism adam_eps has.

beta2 cannot move step 1 at all. Bias correction divides v by 1 - beta2^t, so at t=1, v_hat = g^2 for every beta2. Same cosine test, beta2=0.999 against beta2=0.5:

t=1   1.000000    (exact, it is algebra not luck)
t=2   0.992342
t=10  0.940356

If the door closes inside the first handful of steps, beta2 has almost no surface to act on. Your K=50 flip point says it closes early.

adam_eps is the only one of the three that is per-coordinate, and it bites in a narrow band. Cosine between eps=1e-10 and eps=1e-12 update directions, by gradient scale:

grad ~1e-2    t=1  1.000000    t=50  1.000000
grad ~1e-6    t=1  1.000000    t=50  1.000000
grad ~1e-10   t=1  0.927484    t=50  0.999399
grad ~1e-12   t=1  0.966154    t=50  0.999269

At ordinary gradient magnitudes those two eps values are the same optimizer. They only separate where sqrt(v_hat) is within an order or two of eps itself.

Which sharpens your result. It is not "the latch is sensitive to one specific optimizer constant." It is that the latch is being decided by the coordinates whose gradient magnitude sits down at 1e-10 to 1e-12, because those are the only coordinates on which your two adam_eps settings differ at all. Everywhere else the runs are bit-identical by construction. A whole-model story cannot be what is happening, since most of the model does not see the knob.

That is falsifiable in your notebook two ways.

Histogram |g| at the step where the two arms diverge, and mask to the coordinates in the 1e-9 to 1e-13 band. If the latch is theirs, freezing that band should reproduce the boundary shift on its own.

Or push eps up to 1e-6, far above typical |g|. If eps is a direction knob acting on near-zero-gradient coordinates, 1e-6 should move the boundary much further than 1e-10 to 1e-12 did, and plausibly latch at every K. If it does nothing, my reading is wrong and the mechanism is not the denominator floor.

The lr and beta2 lines above are algebra, so they hold for your model. The eps band is a 64-dim toy with Gaussian gradients and only shows the shape.

What is the gradient magnitude distribution on the receiver at step 1?

Β·

Your question, answered first

Gradient magnitude on the receiver at step 1, all 729 coordinates:

min=2.86e-14   p10=1.50e-13   p50=2.38e-13   p90=1.09e-12   p99=9.53e-12   max=1.52e-11
716 of 729 coordinates sit in [1e-13, 1e-9]. All 729 are under 1e-9.

Every single receiver coordinate is already in your sensitive band at step 1, not just some subset. The receiver had already converged in the base 40 000-step run before I applied the perturbation, so its whole gradient is small everywhere β€” there's no separate "band" of near-zero coordinates to isolate by masking, because the entire matrix is that band. Your masking test (freeze the 1e-9–1e-13 coordinates, see if that alone reproduces the shift) would freeze essentially the whole receiver, so I ran your other falsifiable test instead, adam_eps=1e-6, since it doesn't depend on finding a sub-population that isn't there.

Your two algebraic points β€” I trust them as algebra and didn't re-derive them

lr scales the whole update by one scalar and cannot rotate it β€” that's true by construction of Adam's update rule, not something that needs a toy to confirm, and it matches what I'd already found (nothing moved until lr was cranked to an unrealistic 40Γ— the base rate, and even then non-monotonically). beta2 can't touch t=1 because bias correction forces v_hat = gΒ² at the first step regardless of beta2 β€” also algebra, and it explains cleanly why beta2 did nothing in my sweep down to 0.5: if the door closes inside the first fifty steps, as the K=50 schedule flip showed, beta2 never gets enough steps to diverge from its universal t=1 value before the outcome is already decided.

eps=1e-6, and this is where I found a real bug in my own criterion

First pass, using my existing classifier (S[0].max() > 0.5 counts as "bascule"):

eps_perturb=18,20: retour   eps_perturb=23,24: BASCULE   (non-monotone, unexpected)

That looked like a genuine reversal β€” bigger eps making escape easier β€” so I traced it step by step instead of trusting the label, and it exposed the same failure mode as your referent-1 test from three rounds ago. S[0,0] saturates to 1.0 from the raw perturbation, and at adam_eps=1e-6 it stays frozen there for all 40 000 steps β€” but R[0,18] stays at exactly 1.000000 the entire time too. Referent 18 never gave up anything. Referent 0 is sending message 0 with full confidence and getting zero credit for it, permanently β€” the same wasted, frozen commitment as before, not an occupancy transfer. My classifier only checks the sender's own saturation and never checks whether the receiver actually moved, so it can't tell a real transfer from a frozen artifact that happens to look identical in S alone.

Corrected, checking R[0,18] explicitly on everything, including my own previously published numbers:

adam_eps=1e-8  (default) :  18,20,23 retour  |  24 REAL TRANSFER          <- unchanged, matches what I published
adam_eps=1e-12 (my "18-20"): 18 retour | 20,23,24 REAL TRANSFER            <- unchanged, matches what I published
adam_eps=1e-6  (new)      :  18,20 retour  |  23,24 FROZEN, NO VALUE       <- was misclassified as BASCULE, corrected

The two numbers I've already sent you β€” 23/24 under default settings, the 18–20 convergence under 1e-12 β€” hold. Both check out as genuine transfers under the corrected criterion, not frozen artifacts; I re-ran them specifically to make sure this bug hadn't been sitting under everything since the schedule test. eps=1e-6 is not a third data point on the same axis at all β€” it's your Adam-eps-freeze mechanism showing up a second time, this time on the perturbed parameter itself rather than on the row I originally found it on, and it produces a state that looks like escape in the one column I was checking and isn't one in the column that actually matters.

What this means for the sweep going forward

Every threshold claim I make from here needs both columns before I report a direction, not just the sender's own saturation β€” I'm adding that as a standing check, not a one-off fix, since this is exactly the class of error the notebook already has a rule for (a printed value can saturate while the state it's supposed to represent keeps moving underneath it) and I'd applied that rule to S_max two rounds ago without noticing my pass/fail label had the same blind spot one level up.

Audited everything else that used the same flawed criterion, not just the new test

ThΓ©o asked whether I'd actually searched further rather than stopping at the fix. I hadn't β€” I'd only checked the two numbers I was about to publish and left every earlier BASCULE label from the last three rounds unaudited, including the ones already sitting in the notebook. Reran every prior sweep that used S[0].max()>0.5, checking R[0,18] on each cell this time:

convergence table (adam_eps 1e-8 to 1e-16, eps_perturb 18/20/23): every prior BASCULE -> REAL TRANSFER, unchanged
schedule test arm A (K=50, 100, 200): every prior BASCULE -> REAL TRANSFER, unchanged
lr sweep (0.05 to 2.0): lr=2.0 at eps=18,20 -> was BASCULE, is actually FROZEN, NO VALUE

One more casualty, and it's the lr=2.0 "non-monotone exception" from two rounds ago. R[0,18] stays at 1.000000 in both cells I'd flagged as flipping β€” same frozen-commitment artifact, not a real transfer. Corrected, lr produces zero real transfers across its entire tested range, 0.05 to 2.0, matching beta2's complete inertness exactly rather than having an odd exception at the high end. That's a cleaner result than what I published two rounds ago, not a weaker one: the "specific to adam_eps" conclusion holds more sharply once the one contaminated cell is removed, and I no longer have a non-monotone loose end to wave away as "one seed, not over-read" β€” it wasn't a real signal at all.

Everything else β€” the 18–20 convergence, the schedule test's arm A β€” checks out clean. The bug only ever touched two cells across everything published this exchange, and both corrections point the same direction: toward adam_eps being the sole real lever, not away from it.

Notebook Β§7.53.

Your census kills my band story and puts a sharper one in its place. All 729 under 1e-9 means there is no subset to mask. It also means the default arm was never running Adam.

Count the coordinates where eps loses, |g| > eps, on your own percentiles:

eps=1e-6     0 of 729                     (eps sits above max |g| = 1.52e-11)
eps=1e-8     0 of 729                     (same, by 660x)
eps=1e-12    between 73 and 365 of 729    (1e-12 falls between your p50 and p90)
eps=1e-14    729 of 729                   (eps below min |g| = 2.86e-14)

So adam_eps on this receiver is not a knob with five settings. It is a switch between two optimizers. At 1e-8 and 1e-6 the receiver takes lr * m_hat / eps, momentum SGD at an effective rate of lr/eps, with the per-coordinate gain g/(g+eps) at your median equal to 2.38e-5 and 2.38e-7. At 1e-14 it is sign-like Adam on every coordinate. 1e-12 is the only setting you tested that is actually inside the distribution.

Which makes your new eps=1e-6 row an lr row, by the argument I sent last round.

Ratio of the two arms' step size per coordinate, (g+1e-6)/(g+1e-8):

|g| = max 1.52e-11    99.8497
|g| = p50 2.38e-13    99.9976
|g| = min 2.86e-14    99.9997

Uniform to 0.150% across all 729. A scalar cannot rotate the update, so on the receiver eps 1e-8 to 1e-6 is lr/100 and nothing else.

Your lr grid was 0.05, 0.15, 0.3, 0.5, 1.0, 2.0 against a base of 0.05. Every cell is a multiplier of 1.0x or more. eps=1e-6 is 0.01x. You have swept lr up by 40x and never once down.

The test: adam_eps=1e-8, lr=5e-4, eps_perturb 23 and 24. I predict FROZEN, NO VALUE in exactly those two cells, for a reason with no eps in it. Reciprocal, adam_eps=1e-6 with lr=5.0 should hand the transfer back.

One place the equivalence fails, and it is in your numbers too. Coordinates whose sqrt(v) lands between 1e-8 and 1e-6 are not scaled by 100, they are scaled by 2.84. Your sender row from Β§7.51, sqrt(v) S[18,.]=5.29e-07: gain 0.9814 at 1e-8, 0.3460 at 1e-6. So if lr=5e-4 does not freeze 23 and 24, the eps=1e-6 effect is localized to that one narrow band of coordinates, which is a stronger claim than "sole real lever," not a weaker one. Either branch is worth having.

Fair warning on my method. The census I am counting on is step 1, and v moves. What licenses reading it past step 1 is your own Β§7.51 table: sqrt(v) for R[0,18] came in at 2.11e-12, 1.32e-14, 5.22e-13, 5.54e-13 across the four arms, all far under 1e-8. The receiver is still eps-dominated at the configuration that actually flips, not just at init.

This also reads your plateau, which I had no account of before. The boundary converges by 1e-12 and holds through 1e-16 because below min |g| = 2.86e-14 there is nothing left to normalize. The plateau's left edge should sit near 3e-14. Everything from there to 1e-16 is the same optimizer, and 1e-13 is the untested middle.

Does lr=5e-4 at the default eps freeze 23 and 24?

Β·

Ran both directions of your test before writing anything, and reported both as clean confirmations. One of them wasn't. Correcting in place before this goes further, since nothing here has been answered yet.

adam_eps=1e-8 (default), lr=5e-4, "a reason with no eps in it":
  eps_perturb=18   retour
  eps_perturb=20   FROZEN, NO VALUE   (stronger than you predicted β€” you named 23,24, this froze too)
  eps_perturb=23   FROZEN, NO VALUE
  eps_perturb=24   FROZEN, NO VALUE

adam_eps=1e-6, lr=5.0, reciprocal:
  eps_perturb=18   S[0].max()=1.0000, R[0,18]=0.0000 β€” looked like REAL TRANSFER
  eps_perturb=20   S[0].max()=1.0000, R[0,18]=0.0000 β€” looked like REAL TRANSFER
  eps_perturb=23   S[0].max()=1.0000, R[0,18]=0.0000 β€” looked like REAL TRANSFER

The freeze prediction lands exactly, and even harder than you predicted β€” eps_perturb=20 froze too, which had been a retour under every setting I'd tested until now.

The reciprocal did not. My two-column check (S[0].max()>0.5, R[0,18]<0.5) said all three transferred, and I reported that as confirmation. It's the same blind spot as the bug from two rounds ago, one level further in: the check never asks which message referent 0's argmax actually points to. lr=5.0 is a wild learning rate for logit space, and instead of cleanly reverting or cleanly capturing message 0, it overshoots β€” referent 0's logit at position 0 crashes from +22 to -53 in a handful of steps at eps_perturb=20, not toward the uniform 1/27 the smaller-lr arm reaches, but straight through zero and out the other side. Checked where referent 0's argmax actually landed:

eps_perturb=18:  argmax = message 9   (not 0 β€” chaos, not capture)
eps_perturb=20:  argmax = message 22  (not 0 β€” chaos, and now colliding with referent 23 instead)
eps_perturb=23:  argmax = message 0   (genuinely captured β€” the one real transfer)

Two of the three "transfers" are the system being thrown into an unrelated configuration by an absurd learning rate, not the predicted mechanism playing out. R[0,18] dropped to zero in the first two cases for a reason that has nothing to do with referent 0 capturing anything β€” referent 18 lost its claim on message 0 through some other route while referent 0 landed somewhere else entirely, and my column check couldn't distinguish that from the thing it was meant to detect. Only eps_perturb=23 β€” the cell already closest to the boundary under ordinary settings β€” shows a clean capture of message 0 specifically.

So the reciprocal test is confirmed once, not three times, and I don't think lr=5.0 was ever a fair test of "same ratio, same physics" the way lr=5e-4 was. 5e-4 is a small, orderly reduction that keeps every row inside the smooth regime the linear step approximation describes. 5.0 is a hundred-fold increase on a system already balanced on a knife edge, and at that scale Adam's steps stop being small perturbations and start being able to jump a saturated logit clean across zero in one move β€” which is a different, cruder kind of evidence than the one-directional test provides.

I tried two gentler multipliers to get a clean confirmation rather than leave it on the one contaminated cell:

x2  (adam_eps=5e-7, lr=0.1):   eps=18,20 retour (argmax happens to land on 0, but S[0].max()~0.037 β€” noise, not capture)
                                 eps=23 REAL capture β€” matches default behaviour almost exactly
x10 (adam_eps=1e-7, lr=0.5):   eps=18,20,23 all retour, argmax elsewhere, no capture anywhere

Neither adds clean support. x2 reproduces what the default settings already do at this seed β€” it isn't evidence the ratio is doing anything beyond ordinary variation this close to the boundary. x10 shows less transfer than x2, which isn't the monotone trend the equivalence claim would predict, and at n=1 seed this deep in the sensitivity range I'd read that as noise near a threshold rather than a real reversal. The equivalence argument survives on the freeze direction and the census, which are both clean. The transfer direction has one genuine confirming data point (lr=5.0 at eps_perturb=23) and two more attempts that neither confirm nor cleanly contradict it β€” I don't have a gentle multiplier that reproduces the clean three-for-three result on this seed, and I'd rather say that than paper over it with a second imprecise test.

And the correction needed correcting once more. Once I'd found chaos at lr=5.0, I formed the hypothesis that large lr generally throws referent 0 off-target rather than producing either a clean revert or a clean capture β€” and that my own earlier lr=2.0 result from two rounds ago, which I'd already relabeled once from "BASCULE" to "FROZEN, NO VALUE," might have the same problem, since that relabeling also only checked S[0].max() and R[0,18], never the argmax location. Checked it:

lr=2.0, eps_perturb=18:  argmax = message 6    (not 0 β€” "frozen" label was also wrong)
lr=2.0, eps_perturb=20:  argmax = message 20   (not 0 β€” same)

Hypothesis confirmed, and it wasn't frozen either β€” it's the same chaos as lr=5.0, just landing somewhere else. So of the five large-lr cells I've now checked for argmax location (three at lr=5.0, two at lr=2.0), four are chaos and none is a genuine freeze on the perturbed target. My classification vocabulary was missing a category: "retour" (uniform), "real transfer" (captures the intended target), and what I'd been calling "frozen, no value" turn out to split into two genuinely different things β€” a row that stays put on the perturbed logit without earning credit (which I haven't actually observed at large lr once I check properly) and a row that gets thrown elsewhere by an oversized step (which is what both lr=2.0 and lr=5.0 actually produce). The headline conclusion doesn't change β€” lr still never produces a targeted capture of message 0 across the whole range I've tested, eps=23 under lr=5.0 aside β€” but the mechanism I attributed to those cells was wrong twice, not once, and I'd rather have both corrections on the record than let the second one stand uninspected because the first one already felt like enough humility for one round.

I checked your arithmetic before trusting the prediction rather than after:

(g+1e-6)/(g+1e-8) at g=max, p50, min:  99.85, 99.998, 99.9997 β€” uniform to 0.15%
gain g/(g+eps) for sqrt(v)=5.29e-07 (referent 18's row):  0.9814 at eps=1e-8, 0.3460 at eps=1e-6

Both match your numbers exactly. So the receiver's whole matrix moves as one scalar-scaled block under eps in {1e-8, 1e-6} β€” the 99.85–99.9997x ratio is uniform enough that no rotation happens β€” while referent 18's own row sits in the one band where the two eps values genuinely diverge (0.98 against 0.35, not ~100x), which is the "localized band" branch you flagged as the stronger of your two possible outcomes. Given that eps=1e-6 still moves the boundary despite referent 18's row barely being touched, the effect is carried almost entirely by the receiver's scalar rescaling, not by that one row β€” so the weaker branch (uniform lr-equivalent effect) is the one the data actually landed on, not the localized-band one. Worth being precise about which of your two branches this is, since you offered both.

Your switch-not-knob framing is now the operative one: at 1e-8 and 1e-6 the receiver is running momentum-SGD at lr/eps, not Adam, on every coordinate; at 1e-14 it's sign-like Adam everywhere; 1e-12 is the only setting in the original sweep that actually sampled the interior. I don't have a correction to make to that β€” it's confirmed twice now, once by the census and once by the test that could have broken it.

Ran the untested interior point rather than leave it as a caveat:

adam_eps=1e-13:  eps_perturb=18 retour, 20 REAL TRANSFER, 23 REAL TRANSFER

Same pattern as 1e-12 and 1e-16 β€” already on the plateau, not between it and the boundary. So the plateau's left edge is somewhere below 1e-13, consistent with your ~3Γ—10⁻¹⁴ estimate rather than contradicting it, but I haven't isolated the exact edge β€” 1e-13 was the one cell missing from the original sweep, and it's already flat. If the edge matters for anything downstream, the next untested gap is between 1e-14 and min|g|=2.86e-14, which is closer than a cell width away and probably not worth chasing further on this one seed.

The full audit, since two corrections in a row is exactly the signal to check everything else rather than stop

I'd only verified argmax location on the cells that had already broken (lr=2.0, lr=5.0), not on anything else. Everything else published this exchange β€” the original 23/24 threshold, the full 18–20 convergence table, the schedule test's arm A, and the eps=1e-6 freeze itself β€” had only ever been checked on S[0].max() and R[0,18], never on where the argmax actually landed. Went back through all of it, and further still: does the referent that's supposed to cede its claim actually land cleanly on its other message, and does the total collision count genuinely improve, or does resolving one collision quietly create another?

adam_eps=1e-12, eps_perturb=23:  referent 18 -> message 8 (S=1.0000000000, clean)   distinct=26/27, 1 collision left, elsewhere
adam_eps=1e-8,  eps_perturb=24:  referent 18 -> message 8 (S=0.9999999997, clean)   distinct=26/27, 1 collision left, elsewhere

Both checked cleanly β€” referent 18 lands on its other message without ambiguity, and the collision count drops by exactly one with no new collision appearing as a side effect. Full audit, not the partial one:

original threshold, adam_eps=1e-8, eps_perturb=24:                    argmax=0, genuine capture
convergence table, every published REAL TRANSFER (1e-10 through 1e-16): argmax=0, genuine capture, all of them
schedule test arm A, K=50/100/200:                                     argmax=0, genuine capture, all of them
eps=1e-6 freeze, eps_perturb=23 and 24:                                argmax=0, genuinely frozen ON the target, not chaos

Everything else holds. Every prior "real transfer" is a real transfer, every "frozen" cell under an eps-only perturbation is genuinely frozen on message 0, not thrown elsewhere. The chaos I found is specific to extreme lr β€” 2.0 and 5.0 β€” and doesn't touch anything in the adam_eps sweep down to 1e-16, nor the schedule test, nor the original threshold this whole line of argument is built on. That's the boundary of the damage: two cells out of roughly twenty I've now individually checked by argmax, both under a learning rate two orders above anything the base runs ever used.

A much bigger problem than the argmax bug, found by finally asking whether any of this generalizes

Every number in this five-round thread β€” the 23/24 threshold, its convergence to 18–20, the latch, the lr/eps equivalence β€” comes from one seed's one collision, perturbed by a hand-added scalar on one specific logit. I never checked whether that collision was typical of a wall, or whether the whole mechanism exists without the artificial push. Both checks say no.

The unperturbed wall never moves, at any adam_eps. Same seed, same referent 0, zero artificial perturbation, 300 000 further steps:

adam_eps=1e-8:   S[0].max()=0.037, still uniform
adam_eps=1e-12:  S[0].max()=0.037, still uniform (a *different* referent's collision resolved on its own, unrelated)
adam_eps=1e-16:  S[0].max()=0.037, still uniform

Nothing in the entire five-round mechanism shows up without a hand-crafted push that never happens during ordinary training. Every threshold, every latch, every equivalence I've verified this exchange describes what happens to an artificially destabilized row, not what happens to a wall on its own.

And the same artificial push doesn't reproduce anything on a different wall. Found two more walls from two different generator streams, same construction, same perturbation range (15–23), same full adam_eps sweep (1e-8 to 1e-16):

wall 2 (message 10, loser=referent 4):   20 cells, ALL retour β€” no threshold anywhere in this range
wall 3 (message 10, loser=referent 20):  unperturbed, 300k steps: never resolves, any adam_eps
                                          perturbed +20: 3 cells, ALL retour, no threshold

Not one of the other two walls shows any adam_eps sensitivity at all, at the exact perturbation strengths and eps values that produced a clean, precisely-characterized boundary on the first. Whatever makes idx5's referent 0 sit close enough to a genuine escape threshold that a hyperparameter five orders of magnitude away can tip it β€” that's a property of that row's specific position in logit space after that specific 40 000-step run, not a property of walls in general. I spent five rounds building an exact, mathematically verified account of one seed's idiosyncrasy and never checked whether it was an idiosyncrasy until asked directly whether the results were weak. They were, and now I know in what specific sense: everything upstream of this paragraph is a real, carefully-verified description of one row, not a mechanism.

Notebook Β§7.54.

Two corrections and a self-demolition in one round is rare, and the argmax audit is the right instinct. So I went at the demolition, because I think it is now the least-evidenced claim in the thread.

The two-wall negative has no positive control.

Count the cells you reported. Wall 2 is 20 cells over eps_perturb 15 to 23 crossed with the adam_eps sweep. 20 factors as 4x5 or 5x4, so at most 5 of the 9 integers in that range were tested. Wall 3 is 3 cells at a single eps_perturb, +20.

Now put wall 1 through the same grid. You cannot, because you never ran it. And your own numbers say it would mostly come back retour:

wall 1, eps_perturb = 20
  "had been a retour under every setting I'd tested until now"   (your words, this round)
  at lr=5e-4 it is FROZEN, NO VALUE, which is also not a transfer

So wall 3's entire negative result rests on one eps_perturb value at which wall 1 itself has never produced a transfer. That is not a wall that failed to show the effect. That is a cell that was never going to show it.

Wall 2 has the same hole one step out. Its range is 15 to 23. Your default-settings positive is adam_eps=1e-8, eps_perturb=24, which you re-audited this round and confirmed as a genuine capture. 24 is outside wall 2's range. So wall 2 was never tested at the single cell where wall 1 transfers under default settings.

A sweep whose ceiling sits one below your positive observation has no power above that observation. If wall 2 behaves exactly like wall 1, you would have seen exactly what you saw.

And the threshold is not a fixed property of a wall. Yours moved, 23/24 down to 18-20, with more steps. So "never resolves at 300k steps" and "threshold at 23/24 after 40k" are not the same measurement of the same object.

One run settles it: the identical 20-cell grid on wall 1. If wall 1 also comes back mostly retour on those four or five eps_perturb values, the two-wall negative is uninformative and you demolished a result that was still standing.

Second, I think the branch call is backwards, and it is your own two numbers that show it.

Arm B is adam_eps=1e-6, lr=5.0. Arm A is 1e-8, lr=0.05. The step ratio is 100 * (sqrt(v)+1e-8)/(sqrt(v)+1e-6), and 1.000 means arm B is a faithful control:

  sqrt(v)      u_B / u_A
  2.86e-14       1.00000     receiver min
  2.38e-13       1.00002     receiver p50
  1.52e-11       1.00150     receiver max
  1.02e-08       2.000       crossover
  5.29e-07      35.252       S[18,.], the row you cited
  1.00e-05      91.000
  -> infinity   100.0        ceiling

Every receiver coordinate is faithful to 0.15%. That is not a finding, it is the census restated: I measured the whole receiver matrix under 1.52e-11, so arm B had to agree there. Arm B reproducing arm A's boundary carries no information about the receiver that the census did not already give.

What it does carry information about is everything that is not the receiver, and there arm B is hot, not faithful. Anything above 1.02e-08 is more than 2x hot, ceiling 100x. The one number you have off the receiver, S[18,.] = 5.29e-07, is 35x hot.

So arm B is not "same ratio, same physics." It is a control that is exact on the one block I censused and up to 100x oversized on every block I did not. Calling the uniform branch the winner reads the equivalence off the only block where it was guaranteed in advance.

Where I am wrong, and it is a factual question, not an argument. If the adam_eps sweep touches only the receiver's optimizer and the sender trains under a fixed eps, then S[18,.] never sees 1e-6, the 35x does not exist, and your reading is right. Does the sweep set eps on both optimizers or one?

If both, the check is the census you already know how to run: min, p50, max of sqrt(v) on the sender. If any real mass sits above 1e-8, arm B was never a fair test and the transfer direction is still open.

One more, on the idiosyncrasy story. Your unperturbed number is S[0].max() = 0.037 at every adam_eps. 1/27 = 0.037037. That is not a row sitting near an escape threshold. That is a row at exactly maximum indecision, which is the generic state, not a special one.

So the observable you reported does not distinguish wall 1 from a typical wall. It may still be an idiosyncrasy. But right now that is an inference, not a measurement, and it is carrying the weight of retracting five rounds.

What is S.max() unperturbed on walls 2 and 3? If it also comes back 0.037, what exactly is wall 1 supposed to be special in?

Β·

Ran your positive control before writing anything else. It reverses what I sent last round.

The apples-to-apples grid β€” you were right, my window was the problem

Wall 1 on the exact reduced grid I'd used for wall 2 (eps_perturb in {15,18,20,23}, adam_eps from 1e-8 to 1e-16):

adam_eps=1e-8:   all four retour
adam_eps=1e-10:  eps=23 TRANSFERT, rest retour
adam_eps=1e-12:  eps=20,23 TRANSFERT, rest retour
adam_eps=1e-14:  eps=20,23 TRANSFERT, rest retour
adam_eps=1e-16:  eps=20,23 TRANSFERT, rest retour

Wall 1, tested on wall 2's own window, comes back mostly retour too β€” exactly your prediction. At default adam_eps, all four cells revert, same as wall 2. The 18–20 convergence I published only shows up once adam_eps drops to 1e-10 or below. So the negative result I reported for wall 2 wasn't testing a window where wall 1 itself reliably transfers either β€” it was testing a window where both walls mostly revert at default settings, and I never checked whether wall 2 had its own version of the story one or two eps_perturb units further out.

eps_perturb=24–26 on walls 2 and 3 β€” both transfer

wall 2 (referent 4, message 10):  eps=24 retour, eps=26 CAPTURE, eps=30 CAPTURE
wall 3 (referent 20, message 10): eps=24 retour, eps=26 CAPTURE, eps=30 CAPTURE

Both walls transfer, at a threshold two units above wall 1's. The "no generalization" claim from last round is wrong, and it's wrong for exactly the reason you gave: wall 2's tested range (15–23) never reached the cell where the effect would show, the same way wall 3's single tested point (20) was a cell where wall 1 itself has never transferred. I retract the idiosyncrasy conclusion. Every wall I've checked β€” three for three now β€” has a real capture threshold under this perturbation; the threshold sits at 23–24 for one seed and 25–26 for the other two, which is ordinary seed-to-seed variation in a real mechanism, not evidence the mechanism doesn't exist outside one seed.

What I checked and couldn't confirm in the time I had: whether wall 2's threshold also drops under smaller adam_eps the way wall 1's did. A quick sweep at eps_perturb 20–25 under adam_eps down to 1e-12 stayed retour throughout β€” but that's below wall 2's own threshold (26), so it isn't a fair test of convergence either, for the identical reason the last round's tests weren't fair tests of transfer. I'd need to sweep adam_eps right at wall 2's own boundary (25–27) to check whether it also converges downward, and haven't done that yet.

Your factual question, answered directly

One optimizer, not two: torch.optim.Adam(parametres(e, r), lr=lr, eps=adam_eps), and parametres(e, r) is the sender's and receiver's parameters concatenated into one list. So yes β€” the sweep sets eps on both. Referent 18's row does see 1e-6 in arm B, and your ratio table is exactly right (I recomputed it independently before answering): 1.00000 at the receiver's own minimum, climbing to 35.25x at sqrt(v)=5.29e-07 β€” referent 18's row β€” and 91–100x beyond that.

Which means the reciprocal "confirmation" from two rounds ago has the problem you're describing, and I don't think I can defend it as stated. The receiver block matching under arms A and B was guaranteed by the census, not evidence β€” every receiver coordinate sits below 1e-9, so of course a change from eps=1e-8 to eps=1e-6 looks uniform there. But referent 18's own row sits in a regime where arm B is not a faithful rescaling.

Rather than leave that open, I isolated it β€” two separate Adam instances, one per agent, so I could give the receiver the reciprocal settings while holding the sender at the original ones, and the reverse:

receiver alone at (eps=1e-6, lr=5.0), sender held at (eps=1e-8, lr=0.05):
  eps_perturb=18,20,23:  all retour β€” the receiver's rescaling ALONE never produces a transfer

sender alone at (eps=1e-6, lr=5.0), receiver held at (eps=1e-8, lr=0.05):
  eps_perturb=18,20:  chaos (argmax elsewhere)
  eps_perturb=23:     TRANSFER β€” sender alone is sufficient, at the same threshold as the combined test

Settled: it's the sender, not the receiver. The one genuine transfer in the original reciprocal test wasn't the receiver's uniform equivalence doing the work β€” the receiver alone can't produce it at all, at any of the three perturbation sizes I tried. It's referent 0's own row getting an oversized kick from lr=5.0, exactly the mechanism your ratio table flagged, and it reproduces the transfer on its own with the receiver held at completely ordinary settings. The lr/eps equivalence claim survives on the receiver census and the freeze direction, which are genuinely about the receiver. The transfer direction was never about the receiver at all β€” I was crediting the wrong half of the system for something the sender's own dynamics were doing by themselves.

Your idiosyncrasy question

0.037037 unperturbed is 1/27 β€” you're right that this is the generic maximally-indecisive state, not a special one, and I should have caught that before calling it evidence of anything. Wall 3's unperturbed continuation (checked last round) also lands at 0.037 across every adam_eps tested. I don't have wall 2's unperturbed continuation yet. Given that walls 2 and 3 both turned out to have their own real capture thresholds a few units past where I'd stopped looking, I no longer think the unperturbed S.max() was ever going to distinguish anything β€” it's the same generic starting point for a wall whether that wall does or doesn't have a nearby escape threshold, and the threshold's existence is a separate fact this observable can't see.

One more, since I asked myself whether the receiver even needs to learn at all

If the sender alone (with the reciprocal settings) reproduces the transfer, does the receiver's learning matter, or is it just reflecting whatever the sender currently does? Froze the receiver's parameters entirely β€” never updated, sender trains alone at completely ordinary settings (eps=1e-8, lr=0.05):

eps_perturb=18 through 24:  all retour
eps_perturb=26:             S[0].max()=1.0000, argmax=0 β€” looked like a transfer

Before calling that a transfer, checked R[0,0] rather than trust R[0,18] β€” which, with the receiver frozen, can only ever equal its original fixed value and tells me nothing about whether referent 0 earned anything:

R[0,0] = 4.4e-10

Not a transfer. The same frozen, wasted commitment as the referent-1 test, six rounds back. Referent 0's row fully commits to message 0 given a large enough push, purely as a self-reinforcing runaway under Adam β€” but the frozen receiver still assigns all decode credit to referent 18, exactly as it always did, so referent 0 earns nothing for it. I nearly mislabeled it the same way I've now corrected twice this exchange, and caught it this time before publishing rather than after. So the honest answer to my own question is closer to the opposite of what the first pass suggested: the receiver's learning isn't incidental to a genuine transfer, it's required for one β€” every real capture this whole investigation found necessarily involved the receiver itself shifting credit, and a sender committing on its own, with nothing on the other end willing to update, produces the emptiest kind of "success" this project has a name for.

Notebook Β§7.55.

Three retractions in one round, and the replacement result was scored on the observable you retired 400 words later.

The three-for-three claim rests on CAPTURE, and this round you showed CAPTURE can be empty.

Your frozen run at eps_perturb=26 reported S[0].max()=1.0000, argmax=0. By the verdict rule that produced "wall 2 eps=26 CAPTURE" that is a capture. You only caught it because you went and read R[0,0] afterwards, and it came back 4.4e-10.

So the question for the new positive is simply: were walls 2 and 3 scored on S, or on their own R cell?

wall 2 = referent 4,  message 10  -> the cell is R[10, 4]
wall 3 = referent 20, message 10  -> the cell is R[10, 20]

If both were called on S[.].max() and argmax, then "every wall I've checked, three for three" is carrying the same failure mode you corrected twice this round, and it is carrying it in the direction that flatters the mechanism.

Second, walls 2 and 3 are not two seeds. They are two challengers on one message.

Both are message 10. And they returned the same verdict at every value you tried:

wall 2:  24 retour   26 CAPTURE   30 CAPTURE
wall 3:  24 retour   26 CAPTURE   30 CAPTURE

Identical at 3 of 3. If those two runs share a base state, message 10 has one incumbent, so that is one defender measured twice, not two seeds agreeing. "Ordinary seed-to-seed variation" needs three seeds, and you have message 10 twice and message 0 once.

Third, the grid never observed 25.

Wall 1: 23 retour, 24 capture at default. Bracket (23, 24].
Walls 2 and 3: 24 retour, 26 capture. Bracket (24, 26].

Nothing was ever run at 25. So the true spread is somewhere between one unit and two, and "23-24 for one seed, 25-26 for the other two" states a resolution the grid does not have. Two cells fix that.

Where your freeze conclusion is right, and how much of it is load-bearing.

You ran 18 through 24 frozen, sender ordinary. Your own new wall 1 grid says that at adam_eps=1e-8, eps_perturb 15, 18, 20 and 23 are all retour unfrozen. So five of those seven cells are cells where the unfrozen control also reverts. They cannot separate freezing from the window.

Two cells do the work:

eps_perturb=24   unfrozen: CAPTURE (your re-audited default positive)   frozen: retour
eps_perturb=26   unfrozen: presumably capture                            frozen: S=1.0, R[0,0]=4.4e-10

That is a real result on 2 cells. I think it is correct. I just want it on the record as 2 and not 7, because the same arithmetic is what killed the last two-wall negative.

Last, I think 4.4e-10 is the number, not the footnote.

Uniform over 27 is 0.037037. So:

R[0,0] = 4.4e-10
uniform = 3.7037e-02
ratio   = 1.19e-08          the frozen receiver is 8.4e7 BELOW chance on that cell
decades to uniform = 7.93
decades to 1.0     = 9.36

The frozen receiver is not neutral about referent 0 on message 0. It is committed against it by eight orders of magnitude, because referent 18 is sitting on that message at 1.000000. A genuine capture is therefore not "sender pushes hard enough." It is "receiver moves one cell 9.4 decades while the incumbent gives up a probability that prints as 1."

Which suggests the threshold is a property of the incumbent's grip, not of the seed. And that predicts exactly what you observed: walls 2 and 3 share message 10, so they share a defender, so they get the same threshold. Message 0 has a different defender and sits one to two units lower.

Before perturbation, on the runs walls 2 and 3 come from, what is R[10, :] at its argmax, and what is the runner-up? Same two numbers for R[0, :] on wall 1.

If the tighter-gripped message is the one that needs the bigger push, the threshold stops being seed noise and becomes something you can predict before running the perturbation at all.

Β·

Checked the scoring before defending the claim. You were right, and it's worse than a labeling slip β€” walls 2 and 3 have never shown a genuine capture at any perturbation I've tried, up to +50.

Were walls 2 and 3 scored on S or on R? On S. And it was wrong.

wall 2, R[10,4]  at eps_perturb = 25, 26, 30, 35, 40, 50:
  6.51e-12, 6.67e-12, 6.67e-12, 6.67e-12, 6.67e-12, 6.67e-12  β€” frozen at a constant near-zero, never crosses 0.5

wall 3, R[10,20] at eps_perturb = 25, 26, 30, 35, 40, 50:
  1.41e-11, 1.49e-11, 1.49e-11, 1.49e-11, 1.49e-11, 1.49e-11  β€” same shape

Every "capture" I reported for walls 2 and 3 was the same wasted, frozen commitment I caught on the receiver-frozen test two messages ago β€” I just hadn't checked for it here. S.max() saturates to 1.0, argmax lands on the target message, and the receiver's actual credit for that cell sits frozen at a constant nine to twelve orders below chance, completely unmoved across a 2x range of perturbation strength. The "three for three, mechanism generalizes" claim collapses to one for one. Wall 1 is the only wall I've verified with a genuine capture anywhere in this exchange.

Your incumbent-grip prediction β€” confirmed, and it explains exactly what just happened

Before any perturbation, the receiver's own decode distribution at the contested message:

wall 1, message 0:   incumbent R=0.9999999961  (8.41 decades of grip)   runner-up 7.93e-10
wall 2, message 10:  incumbent R=0.9999999995  (9.31 decades of grip)   runner-up 1.19e-10
wall 3, message 10:  incumbent R=0.9999999995  (9.33 decades of grip)   runner-up 5.26e-11

Message 10's incumbents grip about one decade tighter than message 0's, and that's exactly the pattern that predicts what I found: walls 2 and 3 needed far more than the extra push that moved wall 1, and didn't move at all even at +50 combined with adam_eps down to 1e-16. Tried the same unfreezing move that worked on wall 1 β€” smaller adam_eps at eps_perturb=26 β€” and it made things worse, not better: R[10,4] and R[10,20] both shrink monotonically as adam_eps drops (1.34e-13 β†’ 3.26e-15 β†’ 1.77e-16, same shape on wall 3). Wall 1's escape needed a smaller adam_eps because its incumbent's grip was the weaker of the three; walls 2 and 3 sit behind a materially tighter incumbent and the same lever doesn't reach them in this range. Your prediction wasn't just directionally right, it's the first number in this whole exchange that anticipated a result before the perturbation was run rather than explaining one after.

The two cells that actually do the freeze work

eps_perturb=24:  unfrozen (adam_eps=1e-8) CAPTURE, R[0,0]=1.0000   |   frozen: retour
eps_perturb=25:  unfrozen CAPTURE, R[0,0]=1.0000                    |   (bracket confirmed: (23,24])
eps_perturb=26:  unfrozen CAPTURE, R[0,0]=1.0000                    |   frozen: S=1.0000, R[0,0]=4.4e-10

Both comparisons you flagged as load-bearing hold on real capture, not the S-only proxy. The freeze conclusion stands on the two cells, not the seven β€” noted as you asked.

Where this leaves the whole line of argument

One confirmed wall. A prediction that correctly anticipated why the other two haven't budged, tied to a measurable pre-perturbation quantity β€” incumbent grip β€” rather than to seed identity. That's a real result, and it's a narrower and better-grounded one than "the mechanism generalizes across walls": it's now "the threshold is set by how hard the incumbent is gripping, measurable before you perturb anything," which makes a testable prediction I haven't yet run β€” push walls 2 and 3 far enough (into the hundreds, going by the extra decade of grip) that they should eventually show the same two-part structure wall 1 did, sender push plus small adam_eps, just at a proportionally larger scale.

Notebook Β§7.56.

Retracting three-for-three down to one-for-one is the most useful thing in this thread. The prediction it confirmed is resting on a single measured number, and I want to say how single.

First, the metric. You never defined "decades of grip", so I recovered it from your own three values.

It is -log10(1 - R_incumbent), residual mass not on the incumbent:

wall 1 msg 0    1-R = 3.900e-09    -log10 = 8.409     you printed 8.41
wall 2 msg 10   1-R = 5.000e-10    -log10 = 9.301     you printed 9.31
wall 3 msg 10   1-R = 5.000e-10    -log10 = 9.301     you printed 9.33

Two exact, the third off by 0.03 which is just digits you did not print. It is also the better of the two candidates. The runner-up column does not order the same way: runner-up holds 20% of the residual on wall 1, 24% on wall 2, 11% on wall 3. Walls 2 and 3 differ by 2.3x on runner-up mass and behave identically. Grip separates them, runner-up does not. So grip is the right quantity.

Now the part I would not ship yet.

Walls 2 and 3 are both message 10. You accepted that for the capture claim two rounds ago, and it bites harder here: 9.31 and 9.33 are the same defender measured twice. The grip axis has two distinct values, not three.

And only one of them has a threshold at all:

grip 8.41   ->   threshold in (23, 24]     measured
grip 9.31   ->   threshold > 50            censored, no capture found up to +50

One measurement and one lower bound. That bounds the slope from below and not from above:

multiplicative   thr = a * 10^(b*grip)     data requires  b > 0.354
"into the hundreds":   thr=100 needs b=0.689
                       thr=200 needs b=1.023
                       thr=300 needs b=1.219

Your figure needs two to three times the slope the data floor supports. And the data cannot pick the functional form at all:

additive   thr = a + c*grip     data requires  c > 29 per decade
                                predicts thr at grip 9.31 is >50, and never hundreds

Two points, one censored, cannot separate a multiplicative law from an additive one. "Into the hundreds" is the multiplicative reading stated as if it were the measurement. Under the additive reading, walls 2 and 3 might capture at 55.

The experiment that fixes this is cheaper than +500.

Pushing walls 2 and 3 upward costs runs and can fail in the one way you cannot interpret: a threshold that does not exist looks exactly like one you have not reached yet.

Grip is a training-time quantity. So vary it on the wall you already have. Checkpoint message 0's receiver at three points before convergence, read grip off each checkpoint, find the threshold at each. Three (grip, threshold) pairs, one message, one defender, no wall confound, all inside the 20-30 range you have already been running. That identifies both the slope and the functional form, in the cheap part of the grid, and it does it without ever needing walls 2 and 3 to move.

Last, a number that might collapse two of your results into one.

Your frozen wall-1 receiver at eps_perturb=26 has referent 18 sitting on message 0 at 1.000000. Your unfrozen wall-1 receiver at the same message has its incumbent at 0.9999999961, which is 8.41 decades. 1.000000 is printed precision, not a value, but whatever it is it is at least as tight as 8.41 and probably tighter.

If freezing raises grip, then your two-cell freeze result and your wall-2/3 result are not two findings. They are one finding measured through two knobs, and "frozen" stops being an independent mechanism. It becomes a way of buying grip.

What is -log10(1 - R[0, 18]) on the frozen wall-1 run at eps_perturb=26, printed to full precision? If it lands near 9.3, your freeze conclusion and your grip law are the same sentence.

Β·

Ran your checkpoint design before defending anything. It's the right experiment, it gives a clean answer to the question it was built for, and it also produced a result I didn't expect and can't currently explain.

The grip metric, your recovery of it, and the one number I under-printed

9.33 for wall 3 was rounding β€” full precision gives 9.3303, matching your recovered 9.301 (the small remaining gap is wall 2 vs wall 3 genuinely differing in the fourth digit, not an error). Confirmed both to fifteen digits before answering.

Walls 2 and 3 are one defender, not two β€” accepted, and it's why I ran your experiment instead of pushing walls 2/3 further

You're right that two walls sharing message 10 give one grip value measured twice, and that one measured threshold plus one lower bound can't separate multiplicative from additive. Rather than spend runs pushing walls 2/3 toward a possibly-nonexistent threshold, I ran the checkpoint design: same message, same defender (referent 18, message 0), grip varied by reading it at different points in training time rather than across different walls, threshold found at each with the same post-perturbation budget (40 000 steps) used everywhere else.

checkpoint    grip     threshold bracket
 10 000       5.70     (14, 17]
 20 000       7.43     (20, 22]
 30 000       8.18     (22, 24]
 40 000       8.41     (23, 24]   (already established)

Four points, one defender, no wall confound. Fit both forms:

additive:       thr = -1.40 + 2.98Β·grip     residuals: -0.09, +0.25, +0.01, -0.17
multiplicative: thr = 6.43 Β· 10^(0.068Β·grip) residuals: -0.14, +0.52, -0.02, -0.36

Both fit comparably well over this range β€” the data doesn't cleanly separate the two forms here either, but for a different reason than before: they've converged onto nearly the same curve locally, not because the range is too narrow to distinguish them, but because both are locally well-approximated by the same line over four points spanning less than three decades of grip. Where they'd disagree is far outside this range, which is exactly the region I don't need them to agree in for what comes next.

Both extrapolate to the same place for walls 2 and 3's grip:

additive:        thr(9.31) = 26.4
multiplicative:  thr(9.31) = 27.5

The prediction, checked against data I already have β€” and it fails

Walls 2 and 3 sit at grip 9.31–9.33. Both fitted curves say their threshold should be around 26–27 β€” inside the range I already tested. But I pushed both walls to +50 last round and found R frozen at a constant near-zero the whole way, never crossing 0.5 anywhere from 25 to 50.

The single-defender calibration predicts a threshold that the independent two-wall data says doesn't exist in that range at all. Grip clearly matters β€” the four-point fit within one defender is clean, low-residual, and moves in the right direction as training proceeds β€” but it isn't the whole story once you cross to a different defender. Something about referent 3 or referent 14 (walls 2 and 3's incumbents) versus referent 18 (wall 1's) makes the threshold behave differently than grip alone predicts, and I don't know what yet. Runner-up mass was your own ruled-out candidate β€” walls 2 and 3 differ 2.3x on it and behave identically, so it isn't reviving here either. This might be a second axis entirely (something about the shape of the sender's own row, or the specific logit configuration reached by 40 000 steps of training on a different random draw) that the grip number doesn't capture.

Your last question, answered, and it resolves cleanly in the other direction

Grip of the frozen wall-1 receiver at eps_perturb=26, full precision:

R[0,18] = 0.999999996115699
1-R = 3.884301e-09
grip = 8.4107

Exactly the unfrozen base value, to four decimals β€” not raised. Freezing holds the receiver's parameters fixed, so its grip is whatever it was at the 40 000-step checkpoint before freezing, unchanged by how hard the sender is subsequently pushed. Your hypothesis was the sharper of two ways this could have gone, and it's the one that didn't happen: the freeze result and the grip law are independent findings, not the same one measured through two knobs. Freezing prevents the receiver from lowering its grip at all, regardless of what value that grip started at; the grip law describes how hard it is to lower a grip that's still allowed to move. Different mechanisms, and now confirmed as different rather than assumed to be.

Notebook Β§7.57.

The prediction did not fail in the extrapolation. Under the other reading of your own four points it failed in-sample, and that makes the second axis harder to explain away.

First, bracket coarseness does not rescue it. I took your brackets as brackets rather than midpoints and enumerated every two-parameter curve that passes through all four.

constraint: lo < thr(grip) <= hi  on (14,17] (20,22] (22,24] (23,24]

linear          thr = a + b*grip       feasible b in [2.214, 3.690]
                thr(9.31) in [24.99, 27.32]
multiplicative  thr = C*10^(k*grip)    feasible k in [0.0484, 0.0808]
                thr(9.31) in [25.43, 28.37]

members of either family with thr(9.31) > 50 : 0

Nothing consistent with your four measurements puts walls 2 and 3 anywhere near where you pushed and found nothing. Your result is stronger than you stated it.

But the four points do not yet say the axis is grip.

Same feasibility test, swapping the predictor:

predictor         line through all four brackets?   feasible slope width
grip                      YES                            1.476
log10(step)               YES                            2.808
log10(log10 step)         NO
step                      NO
sqrt(step)                NO

grip wins on least squares over midpoints, RSS 0.10 against 1.79. But that comparison runs below your own resolution: your largest residual is 0.25 against bracket half-widths of 1.5, 1.0, 1.0, 0.5. At the resolution you actually have, grip and log training time are the same curve. The checkpoint design removed the wall confound and put a clock in its place.

That matters, because the two readings disagree about where the failure is:

grip reading   walls 2/3 at grip 9.31       thr ~ 25-28   extrapolation, 0.9 beyond calibration
clock reading  walls 2/3 at 40 000 steps    thr ~ 23-24   interpolation, in-sample

If referents 3 and 14 are read at the same 40 000-step checkpoint that wall 1's established point uses, then under the clock reading there is no extrapolation at all. They sit on the fourth calibration point and do not cross by 50.

And referent 18 cannot walk to 9.31 to settle it from the inside. Your grip is saturating and the form is not pinned:

grip = A - B*exp(-t/tau)   A = 8.615   max resid 0.032   grip(inf)  = 8.615
grip = A - B*t^-k          A = 9.666   max resid 0.070   crosses 9.31 at t = 169 000

Both fit your four grips. One says 9.31 is off referent 18's manifold at any training length, the other says it is 4.2x more steps away.

The cheapest separation may already be on disk. Run the same ladder on referent 3 or 14: grip and threshold at 10k, 20k, 30k, 40k, then overlay the two defenders. If the threshold-vs-grip curves land on top of each other and the threshold-vs-log-step curves do not, grip is a mechanism and the second axis is elsewhere. If it comes out the other way, grip was the clock, and "second axis" reduces to two defenders running different clocks.

Two grip readings alone decide the saturation form on the way, no threshold search needed. Exponential says grip(80k) = 8.610 and grip(160k) = 8.615. The power law says 8.99 and 9.29.

Did you keep referent 3 and 14's intermediate checkpoints, or only the 40k ones?

Β·

Your question first: I hadn't kept referent 3 or 14's intermediate checkpoints β€” only the 40k endpoint. Ran the full ladder to get them, since the overlay test needed real data, not an inference from the endpoint alone.

Long-horizon check on referent 18: both saturation forms revise downward, and now agree grip=9.31 is unreachable

Two more points before touching a second defender, since this decides whether extrapolating your two forms even means what it claims to:

grip(80 000)  = 8.7481
grip(160 000) = 8.9763

Refit both forms with all nine points now on hand (10k through 160k):

power law:     A = 9.204   (asymptote below 9.31 β€” grip never reaches it, any t)
exponential:   A = 8.839   (asymptote below 9.31 β€” same conclusion)

With four points both forms crossed 9.31 eventually; with nine, neither does. You'd flagged this as the live possibility β€” "grip is saturating and the form is not pinned" β€” and the extra two points pin it: referent 18's own grip has a ceiling somewhere between 8.8 and 9.2, and walls 2/3's grip (9.31–9.33) sits above that ceiling under either functional form. Referent 18 genuinely cannot walk to 9.31 from the inside, which is exactly what you said would make the second axis harder to explain away rather than easier.

The two-defender ladder β€” and it settles the grip-versus-clock question outright

Ran your decisive test: grip and threshold at 10k/20k/30k/40k on wall 2's defender (referent 3, message 10), same protocol, same post-perturbation budget as wall 1's ladder.

checkpoint    grip     threshold
 10 000       6.1115   > 26 (never captures, eps up to 26 all retour)
 20 000       8.0450   > 26
 30 000       8.9951   > 26
 40 000       9.3088   > 26   (matches the earlier finding: never captures up to +50)

The curves do not overlay, and not by a small margin. At grip=6.11 β€” close to wall 1's own value at its lowest checkpoint (5.70, threshold 14–17) β€” referent 3 shows no capture at all the way out to eps=26, ten units past where a comparable-grip point on referent 18's own curve already transferred. Referent 3's threshold sits somewhere above 26 at every grip level I measured, including the lowest one, where referent 18 needed less than 20.

This is your "comes out the other way" result. Grip was the right quantity for describing referent 18's own trajectory β€” the four-point fit there was clean and low-residual β€” but it doesn't transfer across defenders at all. Two different defenders at matched grip values need entirely different pushes. Grip is a real, measurable, well-behaved property of one defender's approach to its own ceiling; it isn't a portable threshold-setting mechanism, and the earlier "grip law" was, as you put it, the clock for one defender rather than a general rule.

Where this leaves the whole thread

Something specific to each defender β€” not captured by the scalar R_incumbent alone β€” sets how hard it is to dislodge. Tested the natural next candidate rather than leave it as a suggestion: the raw logit gap behind the softmax, since probability compresses very differently near saturation than the logit does.

                    checkpoint 10k   20k    30k    40k
referent 18 (wall 1)    13.73       18.19  20.29  20.96
referent 3  (wall 2)    14.61       19.45  22.01  22.85

Wrong too, and more clearly wrong than grip was. The raw logit gaps are close between the two defenders at every matched checkpoint β€” closer than the probabilities were β€” while the thresholds still diverge completely (referent 18 already captures around epsβ‰ˆ17–24 across this range; referent 3 doesn't capture at eps=26 anywhere in it, including at 20k where its own gap, 19.45, is barely above referent 18's 40k value of 20.96). If the incumbent's logit gap were the gating quantity, similar gaps should predict similar thresholds. They don't. Whatever sets the threshold isn't a property of the incumbent's row in isolation, at least not one either of these two scalars captures β€” it may depend on the challenger's own starting logit at that message before perturbation, or on some interaction between the two rows that a single number from either side can't see. Both untested; I'd rather say that than propose a third scalar without checking it first.

Notebook Β§7.58.

The grip law is dead and I accept that. But the logit gap was never a second candidate. It is grip times 2.45.

That is in your own two tables, and it is why the second test had to agree with the first.

gap / grip        10k      20k      30k      40k
referent 18      2.4088   2.4482   2.4804   2.4923
referent  3      2.3906   2.4177   2.4469   2.4547

Eight cells, two defenders, min 2.3906, max 2.4923. A 4.25% spread on a quantity that would have to move a lot for the two to rank anything differently. Probability compresses near saturation, which is exactly why you reached for the logit. But along a training trajectory the compression is almost perfectly absorbed into one constant, and the same constant shows up in both defenders.

So "wrong too, and more clearly wrong than grip" is not something this design can measure. Run both as one-parameter laws and they miss referent 3 by the same amount:

prediction for referent 3 at 10k        observed
from grip   (16.71, 17.44]              > 26
from gap    (16.06, 16.73]              > 26

Same rejection, same size, no separation.

What actually survives is stronger than what you kept.

I refit referent 18 without midpoints, using your brackets as brackets, asking only whether one constant can satisfy all four at once:

thr = k * grip     k in (2.7348, 2.8537]     consistent with all 4
thr = k * gap      k in (1.0995, 1.1450]     consistent with all 4

Both non-empty. No intercept needed. Your additive fit's -1.40 was buying nothing.

So the functional form is not what failed. A single scalar per defender, multiplying either quantity, reproduces every bracket you measured on referent 18. What does not transfer is the value of that scalar. Referent 18 sits near 2.79 on grip. Referent 3 sits above 4.25, since 26 divided by 6.1115 is already 4.25 and it had not captured yet.

That is a sharper claim than "not a property of the incumbent's row in isolation". The shape transfers. The constant does not. And a constant is a much smaller thing to have to explain.

The other half is that your referent 3 column cannot answer this.

All four rows are censored at the same bound. Four > 26 entries against one probe ceiling is close to one observation repeated, not four. You can reject a predicted constant with it, which you did, correctly. You cannot fit one. So there is no k_3 to compare against k_18 yet, and the "grip is the clock for one defender" conclusion is resting on a column with no fitted value in it.

The cheap fix is one run. Referent 3 at 10k, the lowest gap you have on it, push eps past 26 until it captures. That is the checkpoint where the threshold is smallest, so it is the least expensive place to uncensor. Then k_3 exists and the per-defender-scalar reading becomes testable instead of suggestive.

One caution before you spend it. Even uncensored, this design still will not separate grip from the logit gap:

referent 3, ratio ckpt40 / ckpt10
  grip   1.5232
  gap    1.5640      2.68% apart

Your brackets on referent 18 are 1 to 3 units wide on thresholds of 14 to 24, so 4 to 12%. The two forms predict 40k within 2.68% of each other. The measurement cannot see the difference.

Which is the actual open question. Separating them needs a defender where gap / grip leaves 2.45, and every defender you have measured so far sits inside 2.39 to 2.49.

Is there one in the referent set that does not? If not, then on this system grip and the logit gap are one variable wearing two names, and the third scalar has to come from the challenger's side rather than the incumbent's, which is where you were already pointing.

Β·

Ran the uncensoring you asked for before answering anything else.

Your ratio table, confirmed

checkpoint  10k     20k     30k     40k
referent 18 2.4106  2.4474  2.4819  2.4916
referent 3  2.3913  2.4172  2.4474  2.4546

Matches yours closely (my figures use slightly rounded checkpoint grips, hence the fourth-decimal drift). You're right that this kills my "wrong too, more clearly" claim β€” the logit gap wasn't a second candidate, it's grip Γ— 2.45 along a training trajectory, and rejecting one rejects the other by construction.

The uncensoring, and it needed the freeze lever too

Pushed referent 3 at its 10k checkpoint past 26, default adam_eps:

eps = 30 through 60, all identical: R[10,4] = 1.9211e-11, always retour

Frozen β€” the receiver's own step for that cell is stuck under the default eps, same signature as wall 1's freeze. Dropped adam_eps at eps_perturb=30:

adam_eps=1e-8:   retour
adam_eps=1e-10:  R[10,4]=0.50001  β€” right at the boundary
adam_eps=1e-12:  R[10,4]=0.50000  β€” same
adam_eps=1e-14:  retour again

eps_perturb=30 sits essentially exactly on referent 3's threshold at the 10k checkpoint, under the adam_eps window that actually lets the receiver move. k_3 = 30 / 6.1115 = 4.907.

The comparison you asked for

k_18 β‰ˆ 2.79   (your feasible range, 2.7348–2.8537, all four brackets)
k_3  β‰ˆ 4.91   (this round, one point, needs refining but not by a factor of 1.76)

Not close, and not explainable by the gap/grip spread. Even granting the full range you'd already found β€” 2.39 to 2.49, about 4%, and the two more defenders I checked below push it to 2.39–2.54, about 6% β€” that's an order of magnitude too small to move k from 2.79 to 4.91. Whatever sets the per-defender constant, it isn't riding on the same small variation that separates grip from the logit gap.

Your last question, checked on two more defenders

referent 14 (wall 3, 40k):  gap/grip = 2.5367
referent 1  (round 33's solo winner, 40k): gap/grip = 2.5119

Both sit outside your 2.39–2.49 window. So grip and the logit gap aren't one variable wearing two names after all β€” there's a real, small, defender-dependent spread in the constant relating them (2.39 to 2.54 across the four defenders now measured, roughly 6%). But that's exactly the point your question was aimed at: this spread is real and worth having on record, and it's still far too small to be where the k_18-versus-k_3 gap comes from. The third scalar isn't hiding in the difference between grip and the logit gap. It has to be somewhere else, and the challenger's side β€” which neither quantity measures at all β€” is still where I'd look first, exactly where you'd pointed before this round started.

Notebook Β§7.59.

You measured k_3 on a lever that k_18 was never measured on. On referent 18's own lever, referent 3 is still censored.

First, the part where you are right and I was wrong.

Referent 14 at 2.5367 and referent 1 at 2.5119 sit outside my 2.39 to 2.49 window, both read at 40k, matched against my own 40k values of 2.4916 and 2.4546. Matched checkpoint, four defenders, real ordering. So grip and the logit gap are not one variable wearing two names, and I withdraw that. The spread is small and it is there.

The lever, though

Your own two paragraphs:

default adam_eps      eps 30..60, all retour, R identical at 1.9211e-11
adam_eps 1e-10        eps 30 captures, R = 0.50001

k_18 β‰ˆ 2.79 comes from four brackets on referent 18, none of which mention dropping adam_eps. So it is a default-lever number. k_3 = 4.907 is not. Put referent 3 back on referent 18's lever and the only thing you know is the censoring:

default adam_eps, referent 3 at 10k
  retour through eps 60      ->   k_3 > 60 / 6.1115 = 9.82
  k_18 upper end                   2.8537
  ratio                            at least 3.44, not 1.76

So the comparison as stated mixes arms, and fixing it does not weaken your conclusion. It roughly doubles the gap. That is the direction I did not expect when I started checking.

But 4.907 is an upper bound, not an estimate

At adam_eps=1e-10 you tried one value of eps_perturb. Thirty. It captured. Nothing below it was run.

k_18 is an interval built from a retour and a capture on each of four brackets. k_3 is a capture with no retour under it. All that gives you is

k_3  in (0, 4.909]        k_18  in (2.7348, 2.8537]

and those overlap. If eps_perturb=10 also captures at adam_eps=1e-10, k_3 is 1.64 and the ordering inverts. "Not close, and not explainable by the gap/grip spread" is a claim this run cannot carry yet, in either direction.

You inferred the threshold from R sitting at 0.50001, which is the boundary of the outcome, not a bracket in eps. Those are different boundaries.

And the window has two ends, both flat

1e-8    retour
1e-10   0.50001
1e-12   0.50000
1e-14   retour

A knob that lets the receiver move predicts capture everywhere below a cut. This is a band. And inside it, R moves by 2e-5 across a hundredfold change in adam_eps.

That is the same insensitivity signature you just used to diagnose the freeze, where R held at 1.9211e-11 across eps 30 to 60. Two flat regions on either side of a boundary. Calling one frozen and one captured is a reading of two plateaus, and 0.50000 is a suspiciously round place for a plateau to sit.

Two runs, and one is nearly free

Bracket downward: at adam_eps=1e-10, referent 3 at 10k, find the largest eps_perturb that still returns retour. That turns 4.907 from a bound into an interval and it is the only thing standing between you and a real k_3.

Then re-run one referent 18 bracket at adam_eps=1e-10. If k_18 moves too, adam_eps is a global rescale and the ratio is the invariant, which you should then quote at a single setting. If k_18 does not move, the lever only bites on frozen cells, and the thing separating your two defenders is not a scalar at all.

Which is the version I would rather be true, because it is testable in one line.

Last round referent 3 was censored from below and you uncensored it. This round it is censored from above. Same column, other end.

Does referent 18 freeze anywhere? At any checkpoint, any eps, default adam_eps. If it never does, then freeze is the discriminator, and a binary cannot have a 1.76 in it.

Β·

Every question in your message has a clean answer, and the last one dissolves the comparison rather than fixing it.

Does referent 18 ever freeze? No. Anywhere.

Pushed it at checkpoint 40k, default adam_eps, all the way to eps=100:

eps = 24, 26, 30, 40, 60, 100:  all identical β€” S[0].max()=1.0000, R[0,0]=1.0000, clean capture

Never freezes, never plateaus, never anything but a clean winner-take-all once past its threshold. That's your discriminator, confirmed outright: freeze is not something referent 18 does at any perturbation I tried.

Does k_18 move under referent 3's lever? No.

checkpoint 40k, adam_eps=1e-10:  eps=18,20 retour, eps=23 capture β€” same bracket as default adam_eps=1e-8
checkpoint 10k, adam_eps=1e-10:  eps=10,13,14 retour, eps=15 capture β€” k=15/5.697=2.633, consistent with the 2.79 range

The lever only bites on frozen cells. Dropping adam_eps doesn't change referent 18's threshold at all β€” same bracket at 40k whether adam_eps is 1e-8 or 1e-10. So it isn't a global rescale; it specifically unfreezes cells that were frozen, and referent 18 was never one of those.

Which is exactly what the 0.5 plateau turns out to mean

You called 0.50000 a suspiciously round place for a plateau to sit. It's rounder than either of us guessed β€” it's a tie, not a capture, and I only saw it by finally printing full precision instead of trusting a >0.5 cutoff:

eps=24:  R[10,4]=0.500000583504767   R[10,3]=0.499999416424236
eps=26:  R[10,4]=0.499999999984430   R[10,3]=0.499999999944572
eps=28:  R[10,4]=0.499999055744959   R[10,3]=0.500000944184045
eps=29:  R[10,4]=0.500000003840574   R[10,3]=0.499999996088429

and both senders:  S[4].max()β‰ˆ0.9999999996   S[3,10]β‰ˆ0.9999999997

Both referents 3 and 4 are fully committed to message 10 simultaneously, and the receiver splits credit essentially exactly 50/50. This isn't the wasted-commitment freeze from three rounds ago (where the challenger commits and the incumbent keeps everything). It's the genuine tie this whole exchange characterized many rounds back β€” two fully confident senders, one message, a receiver that can't tell them apart β€” the same shape as the 23/25 pair from the earlier "Γ©galitΓ©" investigation, arrived at from a completely different starting point. My R>0.5 cutoff called this "capture" because it happened to land a few millionths above 0.5 at two of the four eps values and a few millionths below at the other two β€” which is also why it looked non-monotone. It was never monotone in the sense I was checking; it was oscillating around an exact tie for a reason that has nothing to do with threshold-crossing.

What this does to k_3, and to the whole comparison

There is no k_3 to compare against k_18. Referent 3, pushed hard enough at this checkpoint, doesn't cleanly capture the way referent 18 does β€” it ties. Those are different events, and "how much perturbation until a tie forms" isn't the same measurement as "how much perturbation until a clean transfer happens," even though my classifier scored both the same way. The 4.907 I reported last round wasn't an upper bound on a censored capture. It was the onset of a different phenomenon entirely, and no amount of re-bracketing turns it into a k comparable to 2.79.

This also answers, cleanly, the question underneath the whole thread: what's the second axis? At least for this pair, it isn't a scalar sitting on the incumbent's side or the challenger's side. It's whether the two rows resolve into a winner and a loser, or into a tie β€” a qualitative branch, not a quantity on either defender's ledger. Referent 18's collisions apparently only ever resolve one way. Referent 3's, pushed this way, resolves into the other. I don't yet know what sets which branch a given pair takes, and I'd rather leave it there than propose a fourth scalar on the strength of one example.

I checked whether referent 4 carries this on its own, away from referent 3. It doesn't: referent 4's own natural slot (message 6) is already captured with zero contest at the same checkpoint, no perturbation needed at all. So the tie looks specific to this one collision β€” referent 3 and referent 4 both landing on message 10 β€” not a trait either referent carries on its own into any contest it enters.

Notebook Β§7.60.

The tie is real. But there is a number in your own table that eps never touches, and it is the one that decides your open question.

Add the two rows:

eps=24   R[10,4]+R[10,3] = 0.999999999929003
eps=26                     0.999999999929002
eps=28                     0.999999999929004
eps=29                     0.999999999929003

Spread across all four: 2e-15. The residual, the mass message 10 sends to anything that is not referent 3 or referent 4, is pinned at 7.0997e-11 to fifteen figures.

Now put the split beside it:

eps      |R[10,4] - R[10,3]|
 24            1.167e-06
 26            3.986e-11
 28            1.888e-06
 29            7.752e-09

The partition moves by up to 1.9e-6. The residual moves by 2e-15. Five orders of magnitude apart, on the same four runs.

So eps is not perturbing the row. It rotates mass inside the pair and conserves everything else exactly.

That is a stronger statement than "my classifier was reading noise". A classifier reading noise would leave the residual free to wander too. It does not wander at all.

Which is why I do not think the branch is qualitative.

Your two senders leak off message 10 by 1 - 0.9999999996 = 4e-10 and 1 - 0.9999999997 = 3e-10. That sums to 7e-10. The receiver's residual is 7.0997e-11. Same leading digits, a factor of about ten apart.

If that is not a coincidence, the residual is a sender-side quantity wearing a receiver-side name. And then there is a scalar after all: how completely the two senders commit to the contested message. The branch would be a threshold on it, not a different kind of event.

The test costs one print statement on runs already on your disk.

You reported referent 18's captures as R[0,0]=1.0000, five digits. Print it at full precision at eps = 24, 26, 30, 40, 60, 100. You have those runs.

if the residual under a clean capture MOVES with eps   -> the pinned residual is what a tie is, and it is measurable
if it is pinned there too                              -> the residual is structural, and my scalar story dies

Either answer is worth more than the k comparison we spent three rounds on, and neither needs a new run.

Does the residual under referent 18's capture hold at 7.0997e-11, or does it move?

Β·

Your question, answered on the actual pair this time, plus how I got there.

First, an admission that belongs in the answer

I spent most of this round unable to answer you at all, because I'd lost the exact reconstruction of the wall I've been calling "referent 18 / message 0" for eight rounds. Those labels were never raw tensor indices β€” they were presentation labels from a run I never saved to a file. I tried two wrong master seeds (5, then 31415, the seed my one saved script uses) and got real captures on real walls neither of which matched anything I'd published β€” one pair's loser stayed frozen at zero all the way to eps=100, which isn't what I'd told you happens at eps=24. I nearly reported a number off the wrong pair. I caught it only because the baseline state didn't match the numbers already sitting in my own notebook (S[18,0]=0.499479 is specific enough to check against). I found the real recipe by grepping this session's own JSONL transcript for e.p[0][18 β€” the exact command was sitting there, verbatim, from eleven days ago: master seed 999, five pairs skipped. It's saved now, as replay_idx5.py, so this doesn't happen again.

Your question, on the real pair

Pushed referent 0 past its threshold (eps 24 through 100, default adam_eps, trained on past the perturbation rather than stopping at 2000 steps β€” my first attempt under-trained and only found the pre-capture transient, which is its own small lesson), then printed the residual at full precision:

eps= 24.0  R[0,0]=0.999999715203110  residu=4.648726e-12
eps= 26.0  R[0,0]=0.999999715817530  residu=4.636735e-12
eps= 30.0  R[0,0]=0.999999715881449  residu=4.640843e-12
eps= 40.0  R[0,0]=0.999999715883959  residu=4.635403e-12
eps= 60.0  R[0,0]=0.999999715883959  residu=4.635403e-12
eps=100.0  R[0,0]=0.999999715883959  residu=4.635403e-12

The residual holds essentially flat here too β€” 4.6353e-12 to 4.6487e-12, a spread of about 1.4e-14, roughly 0.3% relative, against a capture that itself is barely moving over the same range. It is not a tie. There is a clean winner and a clean loser. And the residual pins down anyway.

Which answers it directly

You proposed two branches, and this is your second one: the residual is pinned there too, so it's structural, not a tie-specific marker. My "qualitative branch" story from the last round doesn't survive this β€” I was reading the pinned residual as part of what distinguished a tie from a capture, and it demonstrably isn't, since it pins down identically (same order of stability, different absolute value, as expected for a different pair and message) when there's no tie in sight.

I also checked the sender-side leak you flagged as suspiciously matching the residual's order of magnitude:

eps=100:  1 - S[0].max() = 0.0   (referent 0 fully saturated, no measurable leak left)
          1 - r[0,0]     = 2.841160e-07

Here the comparison doesn't carry over as cleanly as it did on your tie β€” referent 0's own leak underflows to exactly zero at full precision once it's saturated, while the receiver's own leak on that cell sits at 2.84e-7, four orders above the residual itself. So whatever is pinning the residual at 4.6e-12 here, it isn't simply "the senders' combined leak dressed up as a receiver quantity" the way it looked on your pair β€” unless the mechanism is different for a single full winner than for two co-tied ones, which is plausible but not something I've verified.

Where that leaves it

You're right that it's structural rather than tie-specific, and that costs my last round's framing. What's still open is what it's structural to β€” it isn't simply mirroring the sender leak in the capture case the way it did in the tie case, so there are at least two different things converging on "small and pinned," and I don't yet know if they're the same mechanism viewed from two pairs, or two different mechanisms that happen to both produce a flat residual. I'd want to see it on a third pair, one with a partial rather than a full transfer, before guessing which.

Notebook Β§7.60bis.

Your leak check compared the residual against the same number twice.

From your own table:

R[0,0]      = 0.999999715883959
1 - R[0,0]  = 2.84116041e-07
1 - r[0,0]  = 2.841160e-07        (your leak line)

Seven significant figures. At eps=100 the receiver-side leak you tested against is the capture you printed four lines earlier, not an independent measurement. So the comparison had nothing to discriminate with, and "four orders above the residual" is a restatement of 1 - R[0,0], not evidence about the mechanism.

That does not break your conclusion. It weakens the case you built for the second half of it. The residual pinning flat at 4.6353e-12 to 4.6487e-12 across eps 24 to 100 is real and stands on its own, and it does kill the tie-specific reading. What is unsupported is the further claim that the pinning here is a different mechanism from the tie case, because the quantity you used to separate them was degenerate at that eps.

You do not need a third pair to settle that. You need one more line on the table you already printed.

At eps=100 referent 0 is saturated, so R and r cannot be told apart there. At eps=24 it is not:

eps      1 - R[0,0]
 24      2.847969e-07
 26      2.841825e-07
 30      2.841186e-07
 40      2.841160e-07   <- pinned from here on

The first three move. So print your leak line at eps=24 and eps=26 as well. If 1 - r[0,0] comes back 2.847969e-07 and 2.841825e-07, r is R and the whole comparison was one number the entire time. If it comes back something else, r is a real second object and your four-orders statement survives, just not on the row you picked to make it.

That costs one print on runs already on disk, same as the last one did.

And it points straight at the third pair you already want. A partial transfer is exactly the regime where R[0,0] and r[0,0] come apart, because the receiver is not saturated and the sender still has measurable leak. On a full winner they collapse onto each other by construction. Your two data points so far are a tie and a full capture, which are the two ends where the test is least able to speak.

Separately, the recovery is the more useful half of that post. Grepping your own session transcript for e.p[0][18 to find a seed you never saved, then catching the wrong pair only because S[18,0]=0.499479 was written down, is a real argument for numbers being specific enough to fail against. replay_idx5.py existing now is worth more than the round it cost.

Are R and r the same tensor in your notation, or two objects that coincide once referent 0 saturates?

Β·

You'd already pointed at the right regime. I went and built it before answering anything else.

The transient, at eps=24, sampled step by step instead of only at convergence

pas      R[0,0]      residual       1-r[0,0]       1-s[0,0]
 6000    0.000000    4.999999e-01   1.000000e+00   2.1e-09
 6500    0.000000    4.999999e-01   1.000000e+00   2.3e-09
 7000    0.000000    4.999999e-01   1.000000e+00   2.6e-09
 7500    0.000000    4.999998e-01   1.000000e+00   2.9e-09
 8000    0.682857    1.443847e-03   3.171426e-01   3.3e-09
 8500    0.999922    5.157076e-10   7.799927e-05   2.9e-09
 9000    0.999939    3.164566e-10   6.084378e-05   2.5e-09
 9500    0.999952    2.115295e-10   4.751718e-05   2.2e-09
10000    0.999963    1.490894e-10   3.713933e-05   2.0e-09

The residual is not pinned during a real transfer. It moves nine orders of magnitude, from 0.5 down to 1.5e-10, while the pinned value you'd have found at full convergence (4.6e-12 at 20000 steps) is just the tail end of that collapse.

What's actually happening at pas=6000–7500

R[0,0]=0 but the residual sits at almost exactly 0.5 β€” referent 0's own sender row (s) is already fully saturated (1-s is 2e-9, negligible) but the receiver hasn't caught up (1-r[0,0]=1.0, meaning r[0,0]β‰ˆ0). The mass the message sends is split almost exactly 50/50 between referent 0 and referent 18 for four straight checkpoints β€” the same signature as your tie on referents 3 and 4, appearing here as a way station on a trajectory that is not, in the end, a tie at all. By pas=8000 it's already broken: 68% to referent 0. By pas=8500 it's essentially over.

Which reframes the whole residual question

The pinned residual I reported at 20000 steps wasn't evidence about mechanism at all β€” it's just what's left of a collapse that already finished. The interesting object is the transient, and there it's not pinned, it's the whole story: mass visibly moving from referent 18 to referent 0, passing through a state indistinguishable from your tie along the way. That means a snapshot of a "tie" and a snapshot of a "wall capture" can be the same trajectory at two different times, not necessarily two different kinds of fixed point β€” some 50/50 splits are transient way stations, and some (your referents 3 and 4, still oscillating around exactly 0.5 many steps later with no sign of resolving) are actual fixed points. I don't yet have a way to tell which is which from a single snapshot; it took sampling across steps to see the difference here.

Notebook Β§7.60quater.

Your transient answers the question I asked, and the answer is in the arithmetic rather than the prose.

Take 1-r[0,0] off your own table and rebuild r[0,0]:

pas      R[0,0]     r[0,0] = 1 - (1-r)
 6000    0.000000   0.0000000000
 6500    0.000000   0.0000000000
 7000    0.000000   0.0000000000
 7500    0.000000   0.0000000000
 8000    0.682857   0.6828574000
 8500    0.999922   0.9999220007
 9000    0.999939   0.9999391562
 9500    0.999952   0.9999524828
10000    0.999963   0.9999628607

Nine rows. Every printed digit of R[0,0] agrees, over a range where R travels from 0 to 0.999963.

So r is R, and not only once referent 0 saturates. Four of those rows have R[0,0] exactly 0, which is as far from saturated as this run ever gets, and r is still the same number there. The leak line was never a second measurement at any point on the trajectory, so the four-orders statement had nothing to stand on at eps=24 either, not just at the eps=100 row I picked.

That costs you nothing now, because you already gave up what it was supporting. Which is the part worth saying plainly: you sampled the transient before answering, and it killed your own pinned-residual reading. 4.6e-12 at 20000 steps is the tail of a collapse from 0.5, and the collapse is the object. Better result than the one you dropped.

On the thing you say you cannot yet do from a single snapshot, your table already carries the discriminator, in the column you added for something else.

Across pas 6000 to 7500, every row where the split is 50/50:

pas      1 - s[0,0]     1 - r[0,0]
6000       2.1e-09         1.0
6500       2.3e-09         1.0
7000       2.6e-09         1.0
7500       2.9e-09         1.0

Sender finished, receiver not started, on all four rows, with 3.4e8 to 4.8e8 between them. That is what a way station looks like with no time axis at all.

A genuine fixed point should not look like that. If referents 3 and 4 are really stuck at 0.5, the question is whether the sender is stuck with them. 1-s near 1e-9 there and the asymmetry test is dead, you do need the time axis. 1-s near 0.5 and one column separates way station from fixed point on any single snapshot, forever.

Same cost as the last one. One print on a run already on disk.

One thing I would not build on yet: 1-s[0,0] is not flat across your nine rows. It climbs 2.1, 2.3, 2.6, 2.9, peaks at 3.3e-09, then falls back 2.9, 2.5, 2.2, 2.0. The maximum sits on pas=8000, the exact checkpoint where the 50/50 breaks. Two significant figures is thin enough that this could be nothing.

What does 1-s[0,0] do on referents 3 and 4?

Β·

Both corrections land. Answering the actual question this time.

r is R

Rebuilt r[0,0] from 1-r[0,0] on my own table:

pas      R[0,0]     r[0,0] = 1-(1-r)
 6000    0.000000   0.0000000000
 7500    0.000000   0.0000000000
 8000    0.682857   0.6828574000
 9000    0.999939   0.9999391562
10000    0.999963   0.9999628607

Every digit agrees, including the four rows where R[0,0] is exactly zero β€” as far from saturated as this trajectory ever gets. r is R on this whole run, not only past saturation. The "four orders above the residual" line never had anything under it, at eps=24 or anywhere else on this pair. Costs me nothing further since I'd already dropped what it was propping up, but it's worth saying plainly rather than leaving it half-corrected: the leak comparison was never a real second measurement here.

Your question, checked on the actual tie

Built the referents-3/4 configuration (master seed 77777, 3 pairs skipped, checkpoint 10k, referent 4 pushed +30 on message 10, retrained under a smaller adam_eps since the default one freezes the cell) and printed 1-s on both senders:

adam_eps=1e-08  R[10,4]=0.000000  1-s[4,10]=2.460254e-12  1-s[3,10]=2.458518e-10
adam_eps=1e-10  R[10,4]=0.500009  1-s[4,10]=2.994494e-12  1-s[3,10]=3.344895e-10
adam_eps=1e-12  R[10,4]=0.500001  1-s[4,10]=3.610847e-10  1-s[3,10]=3.610856e-10
adam_eps=1e-14  R[10,4]=0.500000  1-s[4,10]=3.610858e-10  1-s[3,10]=3.610845e-10

Both senders are already essentially fully saturated β€” 1-s sits at 1e-10 to 1e-12 on both, nowhere near 0.5. That's your first branch, not your second: the asymmetry test is dead here. Both rows read like a fully-committed sender long past any transient, exactly the reading that (by your own account) can't tell a way station from a fixed point on a single snapshot.

Which means what actually establishes referents 3 and 4 as a genuine fixed point rather than an unusually slow way station isn't anything in this snapshot β€” it's the longitudinal check from a few rounds back. I caught myself citing that without rerunning it on this exact reconstruction, so I reran it here rather than leave it as an inherited claim:

pas=40000...400000, adam_eps=1e-10:
R[10,4] = 0.500009, 0.500000, 0.499798, 0.500000, 0.499972,
          0.499999, 0.500000, 0.500000, 0.500000, 0.500080

Pinned at 0.5 across ten checkpoints spanning 360,000 more steps, no systematic drift either direction, verified on the actual pair I have rather than assumed from a run I can no longer reproduce. The single-snapshot column you pointed at doesn't do the job here; the time axis does, same as your own caveat said it would if 1-s came back small.

The thing I almost dismissed as nothing

I'd written off the non-monotonic climb in 1-s[0,0] on the way station (2.1, 2.3, 2.6, 2.9, peak 3.3 at pas=8000, then falling) as "two significant figures, not building a mechanism on it." That was the wrong reflex β€” I formed three actual hypotheses and tested them instead of leaving it as noted-not-explained.

Adam artifact? Reran the same window under plain SGD. Referent 0's logit doesn't move at all under SGD at this learning rate across this window (frozen exactly) β€” inconclusive, there's no dynamics to compare against, but it does mean every "exact ascent" claim across this whole exchange has actually been Adam, never literal gradient ascent (monter() always builds a torch.optim.Adam).

Entropy redistribution among the other 26 logits?

pas    somme_hors_gagnant   1-s[0,0]
7000   53.089291            2.586964e-09
7500   53.205838            2.918510e-09
8000   53.337311            3.343607e-09   <- both peak here
8500   53.186451            2.860844e-09
9000   53.052747            2.491666e-09

Yes β€” the sum of referent 0's 26 non-winning logits tracks the dip in lockstep, peaking at the same step.

Does it coincide with the receiver actually flipping?

pas    top1_logit   1-s[0,0]        r[0,0]
7000   25.072774    2.586964e-09    0.000000
7500   24.956668    2.918510e-09    0.000000
8000   24.825748    3.343607e-09    0.682857   <- the flip
8500   24.975879    2.860844e-09    0.999922
9000   25.108901    2.491666e-09    0.999939

Exactly. r[0,0] goes from 0 to 0.68 at the same step where referent 0's own dominant logit dips and its losing logits rise. This isn't noise β€” it's the visible signature of the coupled sender-receiver system at the exact moment the receiver's decoding flips, briefly redistributing the sender's own logit mass before the sender resumes climbing. What looked like two insignificant figures of jitter is the transition itself, seen from the sender's side.

Formed three more hypotheses on top of that before calling it settled.

Is Adam load-bearing for the transfer itself, not just for the dip? Reran idx5's eps=24 window under SGD with a learning rate 100x Adam's (5.0 instead of 0.05), all the way to 20000 steps: r[0,0] stays at exactly 0.000000 the entire time, 1-s[0,0] barely moves (9.8098e-10 throughout). Adam's adaptive per-parameter scaling isn't standing in for a bigger step size β€” it's doing something SGD at 100x the learning rate can't reproduce in this window at all.

Is the dip carried by one specific rival, or spread across all of them? Checked the top 5 non-winning logits at pas=7000/8000/9000:

pas=7000: (7,2.044036) (5,2.043422) (15,2.042842) (18,2.042563) (23,2.042304)
pas=8000: (7,2.053595) (5,2.052975) (15,2.052390) (18,2.052108) (23,2.051847)
pas=9000: (7,2.042626) (5,2.042013) (15,2.041435) (18,2.041156) (23,2.040898)

All five rise together by about the same +0.01 at the peak, then fall back together. It's a uniform lift across the whole losing set, not one rival gaining ground β€” consistent with the entropy-redistribution reading rather than a specific competitor.

Does pushing the tie harder ever force a real capture? Pushed referent 4 at eps=60, 100, 200, 400 (up from the 30 that produces the tie) on the referents-3/4 pair, trained 40000 steps each under adam_eps=1e-10:

eps= 60:  R[10,4]=0.500000 (10k) / 0.500000 (20k) / 0.499993 (40k)
eps=100:  identical to eps=60, to six figures, every checkpoint
eps=200:  identical
eps=400:  identical

No. Across more than a factor of six in perturbation size, the outcome is bit-identical β€” the sender saturates instantly at all four (1-s[4,10]=0 immediately), and once it does, further pushing has nothing left to affect. The receiver stays pinned at exactly 0.5 regardless. This tie isn't a slow capture that a big enough push would tip over β€” it resists sender-side perturbation across three orders of magnitude, which is a stronger and more specific claim than "it looked stable over 400,000 steps."

Notebook Β§7.60quinquies.

The r is R correction lands, and thank you for saying it plainly rather than leaving it half-corrected.

But I think the eps sweep is telling you something different from what you read into it.

Your own numbers say the two senders collided on one message.

adam_eps        s[4,10]           s[3,10]
1e-10      0.999999999997    0.999999999666
1e-12      0.999999999639    0.999999999639
1e-14      0.999999999639    0.999999999639

Referent 3 and referent 4 both emit message 10 with probability 1 minus about 1e-10. That is not two senders sitting near a tie. That is one message carrying two referents.

Once that is true the receiver's Bayes-optimal posterior on message 10 is pinned:

P(ref 4 | msg 10) = s[4,10] / (s[4,10] + s[3,10]) = 0.500000000083

0.5 is not a fixed point the system found. It is the answer to a question with no information left in it.

Your 1e-08 row is the control that makes this checkable, and it is in your own table.

At 1e-08 the collision is already there, 1-s[4,10]=2.46e-12, and yet R[10,4]=0.000000. So 0.5 is not something the collision imposes mechanically. It is where the receiver lands when it converges, and at 1e-08 it does not converge, which is exactly the frozen cell you named. The two rows together are stronger than either alone: same sender state, receiver at 0.0 when frozen and 0.5 when it trains.

Which reframes the sweep.

eps = 60, 100, 200, 400 coming out bit-identical to six figures at every checkpoint is exactly what a collision predicts. Push referent 4 harder, it saturates onto message 10 faster, and message 10 is where referent 3 already is. After that the receiver is solving a literally identical problem in all four runs, so the trajectories coincide. That is not resistance across three orders of magnitude. That is eps going numerically inert the moment both senders land on the same message.

Same for the 400,000-step pin. A collided message stays at 0.5 because nothing in the loss can separate two referents that emit the same thing. Stability there is evidence of a degeneracy, not of an attractor.

One number in your table the collision does not explain, and it is the interesting one.

adam_eps   observed R[10,4]-0.5   Bayes offset from your own s   ratio
1e-10            9.0e-06                   8.3e-11              1.1e5
1e-12            1.0e-06                   2.2e-16                -
1e-14            0.0e+00                  -3.3e-16                -

At adam_eps=1e-10 the receiver sits 9e-6 off 0.5, five orders further out than the sender asymmetry can account for. So the receiver carries its own slack, independent of anything the senders did. That residual is the only part of this cell that is actually about learning.

And the experiment that separates the two readings does not touch eps at all.

Break the prior instead of the push. Train with referent 4 shown more often than referent 3, say 2 to 1, everything else held.

  • If 0.5 is the posterior on a collided message, R[10,4] moves to 0.667 and tracks the prior ratio.
  • If 0.5 is a dynamical fixed point, it stays at 0.5 and you have something real.

Two cheap checks from data you already have, while you are in there: R[10,3] should also be about 0.5, and the receiver's cross-entropy on message 10 should be ln 2 = 0.693147.

Does the receiver's loss on message 10 sit at 0.693, or below it?

Β·

Ran your prior-imbalance test before anything else, since it's the one that actually settles the question rather than describing it further. It settles it, and not quite the way either of us expected.

Your numbers, reproduced exactly

adam_eps        s[4,10]           s[3,10]
1e-10      0.999999999997    0.999999999666
1e-12      0.999999999639    0.999999999639
1e-14      0.999999999639    0.999999999639

All three rows match my own already-published 1-s columns to the last printed digit (2.994494e-12, 3.344895e-10 at 1e-10; 3.610847e-10/3.610856e-10 at 1e-12 and 1e-14). Your Bayes posterior, s4/(s4+s3) = 0.500000000083, reproduces to the same precision from those same numbers. And your residual table β€” observed offset 9.0e-6 against Bayes-predicted 8.3e-11 at 1e-10, ratio 1.1e5 β€” is exact too, though it's worth flagging what it's built on: at 1e-12 and 1e-14 the "Bayes offset" (2.2e-16, -3.3e-16) is below float64's own precision floor at that magnitude, so the ratio there isn't measuring anything β€” the two senders print as identical because they are identical to sixteen digits, not because the model has resolved a real asymmetry that small. Your 1e-10 row is the only one where the comparison is measuring a real number against a real number, and there the residual really is five orders larger than the sender asymmetry can explain.

The frozen row, checked properly rather than just cited

You use adam_eps=1e-8 (R[10,4]=0) as the control that shows 0.5 isn't imposed mechanically by the collision alone. Went one step further and printed the receiver's actual state there, since a bare zero doesn't say whether it's a frozen tie or a frozen non-tie:

adam_eps=1e-08:  r[10,3]=0.9999999997196851   r[10,4]=1.9210607954554012e-11
H(receiver, msg 10, full 27-way) = 7.294e-09

Not a frozen 50/50 β€” a frozen near-total collapse onto referent 3, entropy essentially zero. Compare the trained state:

adam_eps=1e-10:  H(receiver, msg 10, full 27-way) = 0.693147   =  ln 2, to six digits
                 mass on {referent 3, referent 4} = 1.000000000

Exactly your predicted ln 2, and all the mass is on the two colliding referents with nothing measurably elsewhere. This is a clean, independent confirmation of the collision reading, on a quantity neither of us had printed before: the receiver's row entropy at the contested message is machine-exact ln 2 when trained, and near-zero when frozen at the wrong starting point β€” the frozen state isn't a snapshot of the same tie, it's a snapshot of the optimizer never having gotten there.

The decisive test, run β€” and it lands on neither of your two predictions

Trained referent 4 with the objective's per-referent reward term weighted 2Γ— referent 3's (all 25 other referents, and both entropy terms, left untouched), 40,000 more steps under adam_eps=1e-10:

symmetric control (re-run, no reweighting):  R[10,4]=0.500000  R[10,3]=0.500000  H=0.693147
asymmetric (referent 4 weighted 2x):          R[10,4]=1.000000  R[10,3]=0.000000  H=0.000000

Not 0.5. Also not 0.667. Your first prediction β€” that 0.5 would hold regardless, because it's a genuine dynamical fixed point immune to reweighting β€” is refuted outright: the moment any asymmetry enters, the tie breaks completely. But your second prediction, the specific value 2/3, doesn't land either β€” it goes all the way to a clean winner-take-all.

Why, worked out rather than left as a surprise

Restricting to the two-way split at message 10 (both senders already saturated, so s3, s4 β‰ˆ 1), the objective's first-order condition on r4 (with r3 = 1 - r4) is, for general weights w3, w4:

log(r4 / r3) = NΒ·(w4 - w3) / beta

For the literal test above (w4 = 2/N, w3 left at its default 1/N), that's NΒ·(1/N)/0.02 = 1/0.02 = 50, and sigmoid(50) prints as exactly 1.0 in double precision. To characterize the transition itself I used a cleaner one-parameter family that keeps the combined weight on the pair fixed at 2/N (matching the original, unweighted setting) and only redistributes the split: w4=(1+delta)/N, w3=(1-delta)/N, for which the same first-order condition reduces to log(r4/r3) = 2Β·delta/beta β€” a different, smaller-scale parametrization than the literal "2Γ—" test above, chosen so the sweep below could resolve the transition rather than jump straight from 0 to full saturation. Your "2:1 β†’ 2/3" intuition is exactly what a naive frequency-weighted average would predict; it's just not what this objective's entropy term is strong enough to hold onto once the weight imbalance is that large, on either parametrization.

Swept delta (the fixed-combined-weight family) to check whether this is a real graded law or a post-hoc excuse:

delta     R[10,4]      predicted = sigmoid(2Β·delta/beta)
0.0000    0.500692     0.500000
0.0001    0.504368     0.502500
0.0005    0.511932     0.512497
0.0010    0.524979     0.524979   <- exact to six digits
0.0020    0.549834     0.549834   <- exact to six digits
0.0050    0.622347     0.622459
0.0100    0.731486     0.731059
0.0200    1.000000     0.880797   <- prediction breaks here
0.0500    1.000000     0.993307
0.1000    1.000000     0.999955

Up through delta=0.01, the closed form predicts the trained value to four decimal places or better β€” this genuinely is a graded, Bayes-like response in log-odds space, exactly the shape your reading calls for. Somewhere between delta=0.01 and delta=0.02 the soft prediction (0.88) and the trained outcome (1.00) come apart: past that point the system doesn't settle at the analytic soft optimum, it runs away to full specialization β€” the same shape as the latch mechanism from three rounds ago (a door that closes once a threshold is crossed, not a smooth approach to the unconstrained optimum). So the full answer is neither of the two you posed: 0.5 is not a dynamical fixed point that resists reweighting β€” it's the unique degenerate point of a genuinely continuous, correctly-predicted posterior-like law β€” but the law itself has a basin edge past which it stops being soft and the system snaps to a corner, and an actual "2:1" imbalance sits well past that edge, not inside the graded region.

What breaks the soft law between delta=0.01 and 0.02 β€” five hypotheses, tested rather than left as a shrug

Didn't stop at "there's a basin edge somewhere in there." Formed five candidate mechanisms, three of them standard engineering suspects and two less obvious, and ran all five at delta=0.02 before writing anything about which one is right.

H1  undertraining          β€” 40,000 steps isn't enough; still en route to 0.88, not arrived elsewhere.
H2  adam_eps floor          β€” same latch mechanism as the earlier walls: the restoring (entropy) gradient
                              gets swamped by Adam's additive floor once close to saturation.
H3  lr too large            β€” 0.05 overshoots the shifted interior optimum once the landscape steepens.
H4  a genuine bifurcation   β€” past some delta, the interior point stops being dynamically reachable at all,
                              even though it remains the static optimum of the reduced two-variable problem.
H5  path dependence         β€” starting from the already-converged 0.5 state (rather than imposing the
                              imbalance from the checkpoint before it ever tied) biases which point is found.
H1 (200,000 more steps instead of 40,000+15,000):        R[10,4]=1.000000  H=0.000000   <- refuted
H2 (adam_eps=1e-14 instead of 1e-10):                     R[10,4]=1.000000  H=0.000000   <- refuted
H3 (lr=0.005 instead of 0.05):                            R[10,4]=1.000000  H=0.000000   <- refuted
H5 (imbalance imposed from the 10k checkpoint, before the tie ever forms): R[10,4]=1.000000  H=0.000000   <- refuted

All four engineering explanations are dead β€” bit-identical outcome regardless of budget, floor, step size, or path. That leaves H4, and I didn't want to leave it as "process of elimination" without a mechanism, so I checked what my reduction to a two-variable receiver-only problem had assumed away: that s3 and s4 stay pinned near 1 throughout. They don't.

before reweighting:  s[3,10]=0.999999999666   s[4,10]=0.999999999997
after 40,000 steps:  s[3,10]=0.037064167       s[4,10]=0.999999999998

Referent 3's own sender confidence collapses to 1/27 β€” uniform, the same evacuation signature as the wall/latch mechanism from rounds 20 through 38. My closed form treated the senders as fixed and solved only for the receiver's split; that's the correct limit as delta β†’ 0, but it's not a two-variable problem once the reward-weight imbalance is large enough that referent 3 starts losing real credit β€” at that point referent 3's own entropy term wins, its sender gives up on message 10 entirely, and the receiver's near-total commitment to referent 4 is downstream of that collapse, not a separate optimization on the receiver's row alone. Same shape as every wall in this project: a referent that stops earning reward on a message eventually stops claiming it, and once it does, there's nothing left to keep the tie alive.

What I take from this, and what's still open

Your reframing is the one that survives: 0.5 was never a fixed point in the sense the earlier rounds used the word β€” it's an artifact of exact symmetry in the effective per-referent weight, and it dissolves under the lightest possible asymmetry, following a law derived and confirmed to four decimals over two orders of magnitude of delta. What doesn't survive is the specific quantitative shape of "2:1 in, 2:1 out" β€” not because the entropy term is simply "too weak" (my first framing), but because past a threshold the whole mechanism switches from a graded receiver-side reweighting to the sender-eviction dynamics that produced every other wall in this project. The two-variable closed form is exact in the small-delta limit and stops applying, not gracefully, exactly where referent 3's own commitment starts to give.

Two questions

1. Is the basin edge (bracketed here between delta=0.01 and 0.02, not located) set by the same adam_eps-and-logit mechanism as the wall latch, or by something specific to having two already-saturated senders sharing one message? H2 (a smaller adam_eps) didn't move the outcome at all here, which argues against it being the same floor β€” but I only tested one value, not a sweep, and the walls' own latch needed a four-order-of-magnitude sweep in adam_eps before it moved.

2. Given that the mechanism turns out to be sender-side collapse, not a receiver-only reweighting β€” does that change how "prior" should be operationalized for a cleaner test of your original reading? What I built (reweighting the deterministic average) forces the senders' own incentives to move together with the receiver's; a REINFORCE-style literal 2:1 sampling frequency would too, in expectation, but might reach the same endpoint by a different route, or might not reach the corner as reliably if the sender's own stochastic gradient noise resists full collapse the way it resisted the traps in rounds 19–21.

Scripts: verifier_prior_asymetrique.py, verifier_prior_asymetrique_balayage.py, verifier_bassin_delta002.py.

Notebook Β§7.61.

Your four refutations all land on the same corner, so none of them is tested yet.

At delta=0.02 the sweep row reads R[10,4]=1.000000. H1, H2, H3 and H5 each read R[10,4]=1.000000, H=0.000000. R is a probability and H is an entropy, so both are boundary values. Five arms sitting on a boundary cannot separate. The bit-identical outcome you read as strong evidence is what a saturated observable returns no matter what you vary.

Your own sweep shows where the resolution is. delta=0.0100 gives 0.731486 against a predicted 0.731059. delta=0.0200 gives 1.000000. The graded region is the only place a hypothesis can move the number, and all five arms were run outside it.

H1 is the one this hurts most, and there is a budget mismatch underneath it. verifier_prior_asymetrique_balayage.py sets PAS_SUITE_ADD = 15000, so every soft-law row you trust to four decimals was measured at 15k continuation steps. H1 then ran 200k, but only at delta=0.02, where nothing could have shown. Budget was never varied anywhere the observable moves.

So the test I would run first: sweep delta at 15k, 40k and 200k and report delta_c for each. If the edge slides left as the budget grows, the soft law is the transient and the corner is the destination. If delta_c is budget-invariant, H1 is dead for real and you can say so.

Then bisect delta_c inside (0.01, 0.02) across the 1e-8, 1e-10, 1e-12, 1e-14 ladder replay_mur23_referent3.py already has in its __main__. That answers your question 1 with a location instead of a bracket.

Second point, and I think it is the bigger one.

You found the sender collapse. I think the attribution is one step off.

recompense  = (poids.unsqueeze(1) * s * r.t()).sum()
entropie_s  = -(s * torch.log(s.clamp_min(1e-300))).sum() / N

The prior enters the reward. The entropy stays a flat 1/N average, and your docstring says that is deliberate. For entropie_r it is the right call, its rows are messages. For entropie_s it means referent 3's reward-to-entropy ratio falls while the pull holding it up never moves at all.

Worth pricing how little was actually taken from it. Under the sweep family at delta=0.02, referent 3 goes from 1/N to 0.98/N, a 2% cut. Under the literal 2x test it is not cut at all: poids_asym[4] = 2.0 / N and referent 3 keeps exactly 1.0 / N. So somewhere between 0% and 2% of its credit went away, and s[3,10] went from 0.999999999666 to 0.037064167. 1/27 is 0.037037.

Nothing in that range evicts a row that was saturated to nine nines. An unrescaled regulariser does.

Small thing that would settle it faster: etat() returns only R, H and the mass on the pair, and the bassin script prints no sender row either, so I could not tell which of the two weightings produced that 0.037064167. Worth printing s[3,10] and s[4,10] in etat().

So on your question 2: the two operationalizations are not equivalent in expectation, and the direction turns on one implementation detail. If a REINFORCE run applies the entropy bonus per sampled episode, then seeing referent 3 half as often scales its reward and its entropy pressure together, and the ratio that evicted it never moves. If the bonus is applied once per step to the whole table, it reproduces your current asymmetry.

The cheap test first, one line. Weight the sender entropy by the same prior:

entropie_s = -((N * poids).unsqueeze(1) * s * torch.log(s.clamp_min(1e-300))).sum() / N

Leave entropie_r alone. Now every sender row factorises as poids[i] * (reward_i + beta * H_i), so each referent's own optimum stops depending on delta, and the only thing delta still changes is the posterior.

If the soft law then survives past 0.02 and keeps tracking sigmoid(2*delta/beta) out toward the 2:1 point, your basin edge was the regulariser asymmetry and the posterior reading holds all the way. If the edge sits exactly where it sits now, H4 is real and I will take the loss.

I have not run any of this, it is from reading the four scripts. Which way do you expect it to fall?

Β·

You're right that all four arms sat on the same boundary and couldn't have separated anything. Ran both experiments you specified before writing an interpretation, not the other way around.

etat() now prints the sender row

Added s[3,msg]/s[4,msg] to its return, as asked. Used everywhere below.

Test 1: delta_c across three budgets, in the graded region this time

Swept delta from 0.010 to 0.020 (the bracket that actually moves) at three budgets spanning more than an order of magnitude β€” 15,000, 40,000, and 200,000 steps:

budget = 15,000 steps
  delta=0.010  R[10,4]=0.731486  s[3,10]=9.999561e-01  predicted=0.731059
  delta=0.012  R[10,4]=0.771352  s[3,10]=9.996769e-01  predicted=0.768525
  delta=0.014  R[10,4]=1.000000  s[3,10]=3.702269e-02  predicted=0.802184
  delta=0.016  R[10,4]=1.000000  s[3,10]=3.700369e-02  predicted=0.832018
  delta=0.018  R[10,4]=1.000000  s[3,10]=3.702923e-02  predicted=0.858149
  delta=0.020  R[10,4]=1.000000  s[3,10]=3.702835e-02  predicted=0.880797

budget = 40,000 steps
  delta=0.010  R[10,4]=0.731485  s[3,10]=9.999561e-01  predicted=0.731059
  delta=0.012  R[10,4]=0.771352  s[3,10]=9.996769e-01  predicted=0.768525
  delta=0.014  R[10,4]=1.000000  s[3,10]=3.701633e-02  predicted=0.802184
  delta=0.016  R[10,4]=1.000000  s[3,10]=3.710339e-02  predicted=0.832018
  delta=0.018  R[10,4]=1.000000  s[3,10]=3.703858e-02  predicted=0.858149
  delta=0.020  R[10,4]=1.000000  s[3,10]=3.706417e-02  predicted=0.880797

budget = 200,000 steps
  delta=0.010  R[10,4]=0.731485  s[3,10]=9.999561e-01  predicted=0.731059
  delta=0.012  R[10,4]=0.771352  s[3,10]=9.996769e-01  predicted=0.768525
  delta=0.014  R[10,4]=1.000000  s[3,10]=3.703866e-02  predicted=0.802184

delta_c sits between 0.012 and 0.014 at every budget tested, values matching to five or six digits across a 13Γ— range in training length. delta=0.010 gives the identical 0.731485/0.731486 at 15k and 200k steps β€” this observable is fully converged by 15,000 steps even in the graded region, let alone at the edge. H1 is dead for real, the way you predicted it would need to be shown: not by running longer at a point that was already saturated, but by checking the one place a moving edge could actually appear. My original four-arm test never touched this region at all, and you were right that it couldn't have meant anything.

This also sharpens the bracket itself, independent of the budget question: delta_c is now between 0.012 and 0.014, not the looser 0.01–0.02 I originally reported.

Test 2: adam_eps ladder at a fixed point inside the graded region

Your question 1, answered with a location rather than the old bracket β€” ran the same 1e-8/1e-10/1e-12/1e-14 ladder replay_mur23_referent3.py already carries, at delta=0.015 (past the now-located edge, chosen before I had the tighter bracket from test 1 β€” still informative, see below):

delta=0.015, predicted (old soft law) = 0.817574
  adam_eps=1e-08  R[10,4]=0.000000  s[3,10]=1.000000e+00  (frozen, receiver never trained β€” same artifact as the original 1e-8 row)
  adam_eps=1e-10  R[10,4]=1.000000  s[3,10]=3.706e-02
  adam_eps=1e-12  R[10,4]=1.000000  s[3,10]=3.710e-02
  adam_eps=1e-14  R[10,4]=1.000000  s[3,10]=3.704e-02

Invariant across four orders of magnitude of adam_eps, once past the 1e-8 freeze artifact. Not the same latch as the walls β€” that one moved a real threshold by 2–5 logit units across this exact range. Here it moves nothing at all. Given test 1 now places delta_c between 0.012 and 0.014, delta=0.015 was already past the edge when I ran this, so the honest reading is "past the edge, adam_eps doesn't rescue it," not "at the edge, adam_eps doesn't move it" β€” the second, sharper version of this test (ladder run exactly inside 0.012–0.014) is still open, flagged below rather than left silently substituted.

Your regularizer fix, run rather than reasoned about

Added the one-line correction β€” entropie_s weighted by (NΒ·poids[i]) instead of flat 1/N β€” and reswept delta from 0 out to the literal 2:1 endpoint (delta=1.0 in this fixed-combined-weight family):

delta=0.000  R[10,4]=0.500000  s[3,10]=1.000000000  s[4,10]=1.000000000
delta=0.001  R[10,4]=0.524979  s[3,10]=0.999999999  s[4,10]=1.000000000
delta=0.005  R[10,4]=0.622461  s[3,10]=0.999999835  s[4,10]=1.000000000
delta=0.010  R[10,4]=0.731188  s[3,10]=0.999961687  s[4,10]=1.000000000
delta=0.020  R[10,4]=1.000000  s[3,10]=0.036988530  s[4,10]=1.000000000
delta=0.030  R[10,4]=1.000000  s[3,10]=0.037034403  s[4,10]=1.000000000
delta=0.050  R[10,4]=1.000000  s[3,10]=0.037031123  s[4,10]=1.000000000
delta=0.100  R[10,4]=1.000000  s[3,10]=0.037064044  s[4,10]=1.000000000
delta=0.300  R[10,4]=1.000000  s[3,10]=0.036949334  s[4,10]=1.000000000
delta=0.500  R[10,4]=1.000000  s[3,10]=0.037036957  s[4,10]=1.000000000
delta=1.000  R[10,4]=1.000000  s[3,10]=1.000000000  s[4,10]=1.000000000  <- degenerate, see below

The fix doesn't move the edge. delta=0.01 gives 0.731188, essentially identical to the unweighted version's 0.731188/0.731486 β€” and the collapse to s[3,10]β‰ˆ0.037 (uniform, 1/27) is already complete by delta=0.02, same as before, same value to three digits. Your mechanism β€” reward pressure falling while entropy pressure holds still β€” is real as an asymmetry in the unweighted objective, and it's a legitimate thing to have flagged, but removing it doesn't rescue the soft law here. The one row that looks like confirmation, delta=1.0 reading s[3,10]=1.000000000, isn't one. At delta=1.0, poids[3]=0 exactly, and under your correction the entropy term for row 3 is also scaled by NΒ·poids[3]=0 β€” so both of referent 3's loss terms vanish simultaneously, its gradient is exactly zero everywhere, and it simply never moves from wherever it was before this stage (saturated near 1, from the original construction). That's a boundary artifact of this specific reweighting at the one point where reward and entropy both switch off together, not a data point about whether the posterior law holds β€” I'm flagging it rather than letting it read as support for the wrong reason.

Where this leaves it

Both of your candidate rescues are dead: the budget concern doesn't survive being tested where it could show up, and the regularizer-scaling fix doesn't move delta_c at all. What's left is what I found last round by inspection β€” referent 3's own sender confidence collapses to uniform once the weight cut crosses a real threshold, now bracketed to delta_c ∈ (0.012, 0.014) rather than (0.01, 0.02), under either entropy convention. That's H4 standing on firmer ground than it did last round, not by elimination this time but because your two best candidates for dissolving it were run and didn't.

Five hypotheses on what actually sets delta_c, formed before testing any of them

Standard / expected:

H6 (a genuine saddle-node in the coupled 4-variable system). My original closed form was a 1-D reduction (receiver split only, senders frozen at 1). The real system is (s3, s4, r3, r4) jointly, and a saddle-node bifurcation β€” the interior fixed point colliding with and annihilating an unstable one as delta grows β€” would produce exactly this signature: a smooth branch that exists and is attracting up to a critical point, then disappears entirely, budget- and adam_eps-invariant, since it would be a property of the vector field itself, not of the integrator.

H7 (the sender's own second-order term, not the receiver's). The receiver's first-order condition assumed sβ‰ˆ1 constant; the sender's own first-order condition (which I haven't derived) might have a critical delta of its own, lower than any receiver-side threshold, and it's referent 3's row that visibly moves first. Testable: derive referent 3's own stationary condition treating r3 as the frozen quantity instead, and check whether it predicts a critical delta near 0.013.

Non-standard:

H8 (a resonance between the Adam second-moment estimate and the shrinking sender gradient). adam_eps didn't move it, but beta2 (momentum-of-variance) wasn't tested and has produced a real, though weaker, effect on other walls in this project (round 34/35). Testable: sweep beta2 at fixed delta=0.013, the same way the wall latch was checked against it.

H9 (the transition is set by where referent 3's gradient magnitude, not the loss landscape's curvature, first drops below the noise floor of float64 in this specific softmax parametrization β€” an artifact of representing a 27-way softmax rather than the 2-way marginal I've been reasoning about). Testable: rerun the same sweep in a hand-built 2-referent-only toy (softmax over just {3,4}, no other 25 rows), and check whether delta_c moves β€” if it's the same, the 25 irrelevant rows aren't involved; if it moves, they are.

H10 (hysteresis in delta, not in initial state β€” i.e., ramping delta up continuously in one run rather than restarting fresh at each grid point produces a different, possibly higher, delta_c). Every point above was measured from the same fixed starting tie, reweighted once, trained to convergence β€” never a continuous ramp. Testable: one run that slowly increases delta from 0 to 0.02 over the full training budget instead of jumping straight to each target value.

I haven't run any of these five β€” same position you were in last message, and I'd rather say where I expect it to land than pretend I don't. My guess: H6, on the grounds that a saddle-node is the standard shape for exactly this signature (smooth branch, hard death, no dependence on the integrator's own parameters) and nothing in H8/H9/H10 has an obvious reason to produce budget-and-eps invariance as cleanly as test 1 and test 2 both just did. Which way do you read it?

The sharper adam_eps test, closed rather than left open

Reran test 2 at delta=0.013 β€” inside the now-located (0.012, 0.014) bracket, not past it:

delta=0.013, predicted (soft law) = 0.785835
  adam_eps=1e-08  R[10,4]=0.000000  s[3,10]=1.000000e+00  (frozen, same 1e-8 artifact as always)
  adam_eps=1e-10  R[10,4]=0.794022  s[3,10]=9.990002e-01  H=0.508579
  adam_eps=1e-12  R[10,4]=0.794022  s[3,10]=9.990002e-01  H=0.508579
  adam_eps=1e-14  R[10,4]=0.794022  s[3,10]=9.990002e-01  H=0.508579

0.794022, in the graded regime this time (close to the predicted 0.786), bit-identical across three orders of magnitude of adam_eps. H2 is closed properly now, not just at a point past the edge: the basin edge isn't set by the same additive-floor mechanism as the wall latch, full stop.

Scripts: verifier_prior_asymetrique.py (now prints s[3,Β·]/s[4,Β·]), verifier_invariance_budget.py, verifier_adam_eps_ladder_delta.py (now at delta=0.013), verifier_entropie_reponderee.py.

Notebook Β§7.62.

Against H6, and I think your own numbers are what say so.

A saddle-node has to bend the branch. The stable point travels to meet the unstable one, so as delta approaches delta_c the state accelerates toward the collision. Yours does not do that. It stays analytic and then stops.

Took the three surviving points and put the deviation from your soft law next to the sender deficit:

delta    R_meas     R_pred     resid       1-s[3,10]   resid/(1-s3)
0.010    0.731486   0.731059   4.270e-04   4.390e-05      9.73
0.012    0.771352   0.768525   2.827e-03   3.231e-04      8.75
0.013    0.794022   0.785835   8.187e-03   9.998e-04      8.19

Two things fall out of that.

The soft law's error is the sender deficit, at a coefficient near 8 to 10 that drifts about 8% per step. That is H7 doing the analytic work, already visible in three points you have. Your closed form froze s at 1, and the entire residual is the amount s[3] has left 1 by. Derive referent 3's own stationary condition, add that term, and I would expect the residual to drop an order of magnitude before any bifurcation theory is needed.

The second thing decides H6. Fit (1-s3) log-linearly on your two closest points, 0.012 and 0.013, then step once:

delta=0.0140   branch predicts   s[3,10] = 0.996906
delta=0.0140   you measured      s[3,10] = 0.0370227

311 times the deficit the branch predicts. And it does not land anywhere in between, it lands on uniform. Across nine post-collapse rows, spanning both entropy conventions, s[3,10] is 0.037019 with sd 3.4e-5 against 1/27 = 0.0370370. Four digits, every time.

So the state never travelled to a collision. It sat at 0.999 and then appeared at 1/27. Two attractors and a boundary moving past your initial condition, not one attractor being annihilated.

I also tried to fit a pole. If the residual diverged at a critical delta, a power law through those three residuals would locate it. Solving for it puts the pole past 0.5. Another way of saying the residual grows smoothly with no critical point anywhere near 0.0135.

And test 1 is evidence against H6 rather than neutral. A saddle-node comes with critical slowing down, convergence time going like (delta_c - delta)^(-1/2). At delta=0.012 you are roughly 10% from the edge, and you report 15,000 steps already giving the 200,000-step answer to five or six digits. No slowing down at all.

Two experiments, in the order I would run them.

Continuation. Seed delta=0.014 from the converged delta=0.013 state rather than the fixed starting tie. If the graded fixed point is still there, continuation finds it and R stays near 0.80. If it has truly been annihilated, no initialization reaches it and you get 1.0 from anywhere. This is the sharp version of your H10, and it separates "the fixed point died" from "the fixed point is fine and your starting tie fell out of its basin." One run.

Then slowing down, as confirmation. Bisect delta_c to four digits, measure steps-to-convergence at 1%, 0.1% and 0.01% below it. A saddle-node must show the -1/2 blow-up. If convergence stays flat all the way in, H6 dies the same way H1 and H2 did.

One aside on H9. The collapse lands on 1/27, not 1/2, so the 25 rows are where the mass goes. In the 2-referent toy it should land on 1/2 instead. If delta_c does not move, they were passengers.

If continuation does recover the branch at 0.014, what happens to the bracket? delta_c stops being a property of the system and becomes a property of the starting tie.

Β·

Checked your residual arithmetic first, then ran both experiments in the order you gave them.

Your residual numbers, reproduced exactly

delta    R_pred        resid          resid/(1-s3)
0.010    0.731059       4.274e-04      9.736
0.012    0.768525       2.827e-03      8.750
0.013    0.785835       8.187e-03      8.189

Matches to the digit. The coefficient does drift down (~9.7 β†’ ~8.2), not the flat constant a clean linear term would give, but it's the right order of magnitude and the right sign β€” worth deriving properly (below) rather than trusting the eyeball fit.

Test 1 (continuation): run, and it lands somewhere between your two readings

Converged to delta=0.013 first (R[10,4]=0.794022, matching test 2's number), then continued training the same parameters, no reset, switching only the weight vector to delta=0.014:

step 1 β€” converge at delta=0.013:        R[10,4]=0.794022  s[3,10]=0.999000161
step 2 β€” continue, same state, to 0.014:  R[10,4]=1.000000  s[3,10]=0.037048574
control β€” fresh init straight to 0.014:   R[10,4]=1.000000  s[3,10]=0.037016329

Continuation from a point already on the graded branch still collapses, landing on the same 1/27 as a cold start. That's your "truly annihilated" outcome β€” but it's not quite either of the two readings you offered. If the interior point had simply stopped existing at delta=0.014 in the ordinary saddle-node sense, I'd expect that from any starting point, which is what happened, so on its face this argues for real annihilation. But it doesn't fit a saddle-node either, given what test 2 shows below β€” no bending on approach, no slowing down. What it does fit is something in between the two mechanisms you named: the fixed point's basin didn't just fail to contain the original tie β€” it apparently failed to contain a point sitting on the delta=0.013 branch itself, one step later. Either the branch's own position moves enough between 0.013 and 0.014 that R=0.794 (last round's converged value) is no longer inside its basin, or the basin width goes to zero at some point inside (0.013, 0.014) rather than gradually shrinking past it. Test 2 was built to distinguish exactly this.

Test 2 (critical slowing down): no blow-up, your prediction lands

Bisected delta_c first rather than guess it:

delta=0.013500  saturated   R[10,4]=1.000000
delta=0.013250  graded      R[10,4]=0.801798
delta=0.013375  graded      R[10,4]=0.807474
delta=0.013437  saturated   R[10,4]=1.000000
delta=0.013406  graded      R[10,4]=0.809528
delta=0.013422  graded      R[10,4]=0.811417
delta_c ∈ (0.013422, 0.013437)

Tighter than my earlier (0.012, 0.014) bracket by an order of magnitude, and worth flagging on its own: right at the graded edge (delta=0.013422), R=0.811417 against the soft prediction sigmoid(2Γ—0.013422/0.02)=0.793 β€” a residual of 0.018, bigger than the three points in your table but still small, still finite, no sign of a pole anywhere near this delta.

Then convergence speed at three distances below that bracket's midpoint (delta_cβ‰ˆ0.0134295):

3.000% below:   R_final=0.792836   first hits 99% of final value at step  200
0.300% below:   R_final=0.808275   first hits 99% of final value at step  800
0.030% below:   R_final=0.811395   first hits 99% of final value at step  800

Flat, not diverging. Going from 3% to 0.03% away from the edge β€” two more orders of magnitude closer β€” should multiply convergence time by roughly 10Γ— under (delta_c-delta)^{-1/2} scaling. It moved from 200 to 800 steps once, between the first two points, and then stopped moving entirely between the second and third. No critical slowing down anywhere near delta_c. H6 dies exactly the way you said it would, on the exact test you proposed.

So what actually happens between 0.013422 and 0.013437

Neither of us has a name for it yet that fits both results. It's discontinuous (no bending, no pole, lands on 1/27 to four digits every time β€” your nine-row statistic, confirmed again here) and it's not locally slow to approach (no critical slowing down) β€” but it also isn't simply "a fixed starting point missed a basin that's still there," because a point already sitting on the branch one delta-step earlier gets swept away too. The shape that fits all three of those at once, as far as I can tell without deriving it, is closer to what dynamical-systems language calls a boundary crisis β€” the attracting branch doesn't collide with an unstable twin (that's the saddle-node, and it's what carries the square-root slowdown); instead the boundary of its own basin sweeps across it, converting it from attracting to unreachable in a single step of delta, with no local warning at the branch itself because the branch's own stability never changes β€” only the region that can reach it does. That would explain a sharp, bend-free death (the branch's location and local dynamics are undisturbed right up to the crisis) with no slowing down (nothing about the branch's own attraction rate is changing) and an abrupt jump on the far side even for on-branch continuations (the crisis erases the basin out from under a point that was inside it the step before).

I don't have a derivation for why the basin boundary would move at this specific delta, only a name for the shape the data has. Putting it as a hypothesis rather than a conclusion.

Five hypotheses, formed before testing any of them

H11 (boundary crisis, standard in dynamical systems, non-standard for this project). As above β€” the interior point's own local stability never fails; a separate unstable structure's boundary reaches it first. Testable: a boundary crisis is generically accompanied by the basin's own geometry becoming fractal near the crisis delta, which should show up as non-monotonic sensitivity to the exact starting state right at the crisis point β€” perturb the delta=0.013 converged state by a tiny amount in a few different directions before continuing to 0.014, and check whether the outcome (collapse vs. survive) becomes unpredictable from the perturbation's sign/size, rather than a clean threshold.

H7, finished properly (standard, the one your table already argues for). Derive referent 3's own stationary condition β€” not assuming s3 fixed at 1, but solving jointly with the receiver's condition β€” and check whether the resulting two-equation system has its own critical delta, possibly closer to the observed 0.01342 than my one-variable reduction's implicit assumption of "senders always saturated" would suggest.

H12 (Adam-specific, non-standard: the second-moment estimate for referent 3's logit, not adam_eps, is the actual gate). I ruled out adam_eps twice now, at two different deltas, but never looked at exp_avg_sq itself mid-transition, the way the wall latch investigation eventually did (round 35) after ruling out the obvious floor first. Testable: trace sqrt(v) for referent 3's own logit step by step through a continuation run started just above and just below delta_c, the same diagnostic that found the wall latch after adam_eps alone didn't explain it.

H13 (the 27-row toy, your own aside, elevated to a real hypothesis). You note the collapse lands on 1/27, not 1/2, meaning the other 25 rows are where the mass actually goes β€” not passengers, participants. A 2-referent-only toy (softmax over {3,4} alone, no other 25 columns) should land on 1/2 if it collapses at all, and might not have a delta_c in the same place, or at all, if the 25 rows are load-bearing rather than incidental. Haven't built it yet β€” next.

H14 (non-standard: delta_c is itself a discrete artifact of N=27 β€” i.e., some resonance between 2Β·delta/beta and a quantity like log(N-2) that happens to land near 0.0134, not a continuous dynamical threshold at all). 2Γ—0.01343/0.02 β‰ˆ 1.343; I don't have a clean closed form producing that from N and beta alone, and I'd be surprised if one existed, but I hadn't checked, and a coincidence this specific is worth ruling out before trusting any dynamical story over a numerological one.

I'd bet on H11 over the other four, for the same reason you'd back H6 over mine last round β€” it's the only one of the five that predicts all three observations (no bend, no slowdown, on-branch continuation still fails) from one mechanism rather than needing a separate story for each. Which way do you read it?

Pushed further before calling any of this settled: why is the coefficient 8-10, and could Adam be hiding the slowdown?

Refuted-or-confirmed isn't where this stops. Two follow-ups, both run rather than argued.

H7, derived rather than left as an eyeballed slope. Treating referent 3's own row as a two-outcome softmax (message 10 against the other 26, lumped), its stationary condition against a fixed r3 is d3/(26(1-d3)) = exp(-NΒ·w3Β·r3/beta) where d3=1-s3. Plugged in the measured r3=1-R_meas at each delta (not a free parameter):

delta   r3         d3 predicted   d3 measured    ratio
0.010   0.268514   4.3911e-05     4.3900e-05     1.0002
0.012   0.228648   3.2324e-04     3.2310e-04     1.0004
0.013   0.205978   1.0008e-03     9.9980e-04     1.0010

Within 0.1% at all three points, from a closed form, not a fit. This is why your 8-to-10 coefficient drifts rather than sitting flat: d3 depends on r3 exponentially, and r3 itself falls as delta rises, so the local slope of d3 against delta keeps steepening β€” a drifting linear coefficient is exactly what a fixed exponential relationship looks like over a window where its argument is moving. This also says why the residual on the soft law grows the way it does without yet exploding: the soft law's error is entirely this term, and this term has no singularity of its own anywhere near delta_c β€” it's smooth all the way, which is consistent with the sharp thing happening being in the coupled (s3, r3) system, not in this one-sided approximation to it.

H15, formed to explain the flat critical-slowing result, then run. Adam divides its step by sqrt(second moment), which tracks the gradient's own recent scale β€” near a true marginal (vanishing) eigenvalue, that normalization could in principle keep taking full-sized steps long after a plain gradient would have crawled, erasing the (delta_c-delta)^{-1/2} signature without the underlying vector field being innocent. Reran the exact three-point slowdown test under plain SGD instead of Adam, learning rate calibrated so the far point converges in a comparable number of steps:

3.000% below delta_c:  R_final=0.786283  first hits 99% of final value at step  400
0.300% below delta_c:  R_final=0.792312  first hits 99% of final value at step  400
0.030% below delta_c:  R_final=0.792909  first hits 99% of final value at step  400

Flat under SGD too β€” 400 steps at every distance, no divergence. H15 is refuted by the same test that would have vindicated it: the missing critical slowing down isn't an artifact of Adam's normalization, it's a real property of the dynamics regardless of which optimizer walks it. That's one more point for H11 over H6 β€” a boundary crisis doesn't require the branch's own local rate to change at all, which is consistent with SGD and Adam giving the identical flat answer here, while a marginal-eigenvalue saddle-node would need to show up under both.

H13, built and run rather than left as an aside

Your remark β€” the collapse lands on 1/27, not 1/2, so the 25 other rows are where the mass goes, not passengers β€” deserved an actual ablation, not a note. Built a standalone toy: two 2-outcome senders (message 10 vs one lumped elsewhere, not 26 of them) and a 2-outcome receiver (referent 3 vs referent 4, not 27 referents), same beta=0.02, same /N=27 normalization so the temperature scale is untouched β€” only the passenger dimensions are gone.

delta     R4         s3          prediction
0.010     0.731075   0.999998    0.731059
0.013     0.786041   0.999974    0.785835
0.014     0.802653   0.999940    0.802184   <- still graded here, real system already saturated
0.02      1.000000   0.500081    0.880797   <- collapses, lands on 1/2
0.05      1.000000   0.500000    0.993307
0.30      1.000000   0.500000    1.000000

Both predictions land. The collapse now lands on s3β‰ˆ0.500000 β€” half, not 1/27 β€” confirming the other 25 rows really are the destination of the escaping mass, not incidental. And delta_c moved: bisected in the toy to (0.018688, 0.018711), against the real system's (0.013422, 0.013437) β€” a genuine shift, not invariance. The 25 rows are load-bearing on both counts your remark named.

Why, not just that. H7's closed form has the "26" sitting as a prefactor: d3 = 26Β·exp(-NΒ·w3Β·r3/beta). With only 1 alternative instead of 26, referent 3 has far less entropy to gain by abandoning message 10 β€” one destination instead of twenty-six β€” so it resists collapse longer, and it takes a bigger delta (more reward pressure) to tip it. That's the mechanistic reason delta_c is higher in the toy, not just an observation that it is: fewer places to run to means a harder push needed before running looks worth it.

H9, closed as a special case of H13

Was going to build H9 (the same 2-referent toy) separately; H13's toy answers it directly, since a 27-way softmax collapsing to a 2-way one is exactly the ablation H9 asked for. Landing on 1/2 rather than 1/27 is the H9 result β€” the 25 rows aren't a numerical artifact of the full softmax's shape, they're where H7's entropy pressure actually comes from.

Scripts: verifier_continuation_delta.py, verifier_bissection_delta_c.py, verifier_ralentissement_critique.py.

Notebook Β§7.63.

H11 is not needed. H7, finished jointly instead of against a fixed r3, is the whole thing, and it is a saddle-node.

You left H7 as "derive referent 3's own stationary condition, solving jointly with the receiver's". I did that, and the two-equation system has its own fold. Here it is.

The coupled system

Calibrated only on your three published rows (delta = 0.010, 0.012, 0.013), with your own d3 column as the target:

receiver:  logit(R) * beta  =  2*delta + (1-delta)*d3
sender 3:  d3/(26(1-d3))    =  exp( -(1-delta)(1-R)/beta )

Your d3 column, reproduced: ratios 1.0002, 1.0001, 1.0000. Your receiver gap, reproduced to 8e-8, 5e-10, 3e-8. Note the receiver equation is not sigmoid(2*delta/beta). It carries a +(1-delta)*d3 term, which is your entire residual. Your resid/(1-s3) column is R(1-R)(1-delta)/beta, evaluated on the branch:

delta    yours    closed form
0.010    9.736    9.727
0.012    8.750    8.750
0.013    8.189    8.188

It drifts because R(1-R) falls as R climbs, not only because d3 is exponential in r3. Both effects are in there, and the closed form has them.

Solved jointly, at delta = 0.013:

model  s[3,10] = 0.999000161      yours  0.999000161
model  R[10,4] = 0.794022         yours  0.794022

Nine digits on s3, from a closed form that never saw that number.

Three roots, not one

delta=0.013000   d3 = 9.99839e-04 (stable)   5.70024e-03 (UNSTABLE)   9.62963e-01 (collapsed)
delta=0.013400   d3 = 2.10294e-03            3.45445e-03              9.62963e-01
delta=0.013500   d3 = ---                    ---                      9.62963e-01

The collapsed root is s3 = 1/27 = 0.037037. Your measured 0.037048574. It is a fixed point of the same two equations, not a separate story.

The stable branch and an unstable twin approach each other and annihilate. Gap: 4.700e-03 at delta=0.013, 8.634e-04 at 0.013422, 2.220e-05 at 0.0134372. That is a saddle-node, in the plainest possible sense.

Fold at delta_c = 0.013437210. Your bisection: (0.013422, 0.013437). It lands on the top edge of your own bracket, 0.0016% above your upper bound.

The out-of-sample test: your H13 toy

Your toy changes exactly one thing in these equations, the prefactor 26 (twenty-six places to flee) becomes 1. Nothing else. No refit.

K=26 (real):   predicted delta_c = 0.013437210    you bisected (0.013422, 0.013437)
K=1  (toy):    predicted delta_c = 0.018699092    you bisected (0.018688, 0.018711)

Inside your toy bracket, to four significant figures, from a form calibrated only on the 27-row system. Shift from dropping the 25 rows: predicted +0.005262, you measured +0.005270. And the toy's collapsed root comes out at d3 = 0.500000, your s3 = 0.500081.

Your entropy explanation for why delta_c moves is right, and it has a closed form. The fold condition is

d3_fold = beta^2 / ( (1-delta)^2 * R * (1-R) )

independent of K. So the fold always needs the same d3, and with 1 destination instead of 26 you need r3 smaller by beta*ln(26)/(1-delta) = 0.066 to reach it. That is the whole 0.00527 shift, priced.

H14 dies here too. delta_c is a root of an explicit equation and it moves when only the prefactor moves, with N=27 and beta untouched. No resonance survives that.

Why test 1 collapsed

At delta=0.014 there is no graded root. Only 0.962963. The attractor is gone, not displaced, so every starting point goes to the same place, including one sitting on the 0.013 branch. That is the "ordinary saddle-node sense" you named in your first sentence and then argued yourself out of. You were right the first time.

Why test 2 read flat

You centred the distance scale on the bracket midpoint, 0.0134295. The fold is at the top edge, 0.0134372. So:

you called it   true distance to the fold
3.000% below     3.056%
0.300% below     0.357%
0.030% below     0.087%

Your bracket is 1.5e-05 wide. Your closest intended probe distance was 4.0e-06. The uncertainty in delta_c was 3.7x larger than the thing you were measuring against it. The span you actually swept was 35x, not 100x.

Over 35x the square-root law predicts a 5.91x growth in relaxation time. Exact linearization of the reduced system gives lambda = 0.589, 0.238, 0.123 at your three deltas, a 4.81x ratio. You measured 200 then 800, a 4x step, then hit your own logging granularity.

Your Adam numbers are consistent with critical slowing down. They were never flat, the ruler was short.

Your SGD arm is the stronger evidence, in the other direction from how you read it:

delta          branch value   your SGD R_final   shortfall
0.013026615      0.794756        0.786283        8.473e-03
0.013389212      0.808282        0.792312        1.597e-02
0.013425471      0.811395        0.792909        1.849e-02

None of those converged, and the shortfall grows monotonically toward the fold. That growth is the slowing down. "99% of its own final value" divides it out, because the final value is dragged down by the same effect. The criterion is self-normalizing against the quantity it is trying to detect.

One row of yours does not sit on the branch: Adam at "3% below", R_final=0.792836, which is the branch value at delta=0.012955853, not at 0.013026615. Your other two Adam rows match the branch to 7.1e-06 and 3.2e-07. Different delta for that run, or different budget?

The test that separates us, precommitted

A saddle-node has a separatrix at a computable place. A boundary crisis does not.

At delta = 0.013, 3.3% below delta_c, well inside your graded regime, the unstable twin sits at

s[3,10] = 0.994300

Initialize sender 3 there and vary it. Prediction:

init s3 = 0.9945   ->  climbs to 0.999000   (graded branch)
init s3 = 0.9943   ->  climbs to 0.999000
init s3 = 0.9940   ->  collapses to 1/27
init s3 = 0.9900   ->  collapses to 1/27

A clean threshold at 0.9943, at a delta where your own runs always converge graded from the tie. If that boundary is sharp and sits within 1e-04 of 0.994300, it is a saddle-node and H11 is unnecessary. If the outcome is unpredictable from the perturbation's size near there, that is your fractal basin and H11 survives on its own evidence rather than on the absence of a slowdown.

Cheaper than a redone bisection, and it is the one place the two mechanisms genuinely disagree.

If you do rerun the slowdown test, measure against the branch value from the closed form, not the run's own final, and space the points at 1%, 0.1%, 0.01% of 0.0134372. Predicted lambda = 0.376, 0.131, 0.0425 there, so tau ratios 1x, 2.88x, 8.85x. Two decades of distance buys 8.85x, and it is measurable with the logging you already have.

Which of the two does your basin-boundary probe say?

Β·

Verified your coupled system first, then ran your precommitted test. It took two tries, and the reason the first one failed is itself the answer to something neither of us had asked yet.

Your two equations, verified independently

delta   eq1 (logit(R)*beta)        eq2 (d3/(26(1-d3)))
0.010   0.0200435 vs 0.0200435     1.6885e-06 vs 1.6889e-06
0.012   0.0243192 vs 0.0243192     1.2431e-05 vs 1.2432e-05
0.013   0.0269868 vs 0.0269868     3.8492e-05 vs 3.8494e-05

Both equations match your published rows to five-plus digits, from your closed forms, not a refit. Taking the coupled system and the fold location as given.

Round 1 of your probe: run exactly as you framed it, and it found nothing

You wrote "initialize sender 3 there and vary it." Did exactly that β€” set s[3,10] directly to eight values from 0.995 down to 0.98, left everything else (including the receiver) at the standard construction's starting point, trained 40,000 steps at delta=0.013:

init s3=0.99500  ->  graded branch (0.999000)
init s3=0.99430  ->  graded branch   <- your predicted twin, exactly
init s3=0.99400  ->  graded branch
init s3=0.98000  ->  graded branch

Every single point climbed back to the graded branch, including well below your predicted twin. Not a fuzzy result, not noise β€” a clean, wrong-looking answer at every value tested.

Why, before assuming your math was off

The receiver in this run starts wherever the standard construction leaves it β€” at the tie, Rβ‰ˆ0.5 β€” not at the value your two-equation system says the unstable point actually sits at. Your fold has a receiver coordinate too: solving your eq1 at d3=5.70024e-03 (the unstable root) gives logit(R)Β·beta = 2(0.013)+(0.987)(0.0057) = 0.031625, so R_unstable β‰ˆ 0.8294, not 0.5. I'd perturbed one coordinate of a two-variable fixed point and left the other one sitting far off the manifold your equations describe. With R starting at 0.5 β€” well below the 0.829 the unstable branch needs β€” referent 3 is getting more reward credit than the unstable point specifies (reward to referent 3 scales with r3, and r3=0.5 at R=0.5 is bigger than r3=1-0.829=0.171 at the true unstable point), which hands referent 3 extra restoring pull before the entropy term can tip it β€” enough to drag even an s3=0.98 start back onto the stable branch. The probe wasn't testing your prediction; it was testing a different, easier-to-recover-from point that happens to share one coordinate with it.

Round 2: both coordinates on the manifold, and the threshold lands where you said it would

Set s[3,10] and the receiver's R[10,4] jointly to (0.994300, 0.829390) β€” your predicted unstable point β€” then perturbed only s3 away from it along that line, R held at the same 0.829390 start each time:

init s3=0.99500  R init=0.829390  ->  graded branch (0.999000)
init s3=0.99450  R init=0.829390  ->  graded branch
init s3=0.99430  R init=0.829390  ->  graded branch   (0.998989, essentially your twin itself)
init s3=0.99420  R init=0.829390  ->  COLLAPSE (0.037088, 1/27)
init s3=0.99400  R init=0.829390  ->  COLLAPSE
init s3=0.99000  R init=0.829390  ->  COLLAPSE
init s3=0.95000  R init=0.829390  ->  COLLAPSE
init s3=0.90000  R init=0.829390  ->  COLLAPSE

A clean, sharp flip between s3=0.99430 and s3=0.99420 β€” inside 1e-4 of your predicted 0.994300, exactly the criterion you named. Below the threshold, every point collapses to 1/27 regardless of how far below (0.90 behaves identically to 0.9942); above it, the graded branch is recovered even from 0.995. This is not a fractal or unpredictable boundary. It is the separatrix of a saddle-node, at the place your closed form put it.

Where this leaves the two hypotheses

You asked me to say why H11 is useful or not, rather than just report the number. It isn't, anymore, and here's the actual reason rather than just the verdict: H11 earned its place only because three things looked inconsistent with a saddle-node β€” no bending, no critical slowing, and a continuation failure. Your closed form now accounts for all three without needing a second mechanism: the bending was there, in the two-variable system, invisible in my one-variable reduction; the missing slowdown was a measurement artifact (my bisection window was wider than the distances I was probing, and my own SGD numbers show growing lag once compared to the branch value instead of the run's own endpoint); and the continuation failure is exactly what an annihilated fixed point does to any initial condition, including one sitting on the branch a moment before. A hypothesis stays alive only as long as it explains something the leading candidate can't, and H11 has nothing left to explain that H6 β€” correctly derived β€” doesn't already account for more precisely, including now the one place I could think of where the two would have actually disagreed. I'm calling it dead, not shelved.

The why-under-the-why: what round 1's failure says about the basin, not just about my mistake

Round 1 isn't just a corrected methodology β€” it's a real measurement of something neither of us named yet: the graded branch's basin, projected onto the s3 axis alone with R unconstrained, is enormous β€” at least down to s3=0.98 with R starting at 0.5. The basin is only narrow along the specific direction your fold analysis describes (the line through the unstable point in the full (s3,R) plane); it's wide open in other directions through the same region of s3-space. That's worth having on record as a real 2-D map of the basin, not just a footnote explaining why round 1 came back clean: the separatrix isn't a wall the trajectory has to get past everywhere near 0.9943, it's a thin ridge, and round 1 shows how much of the surrounding volume isn't anywhere near it.

Two hypotheses on the ridge itself, before either is tested

Standard: the separatrix's location in the s3-only slice (if R is left free rather than fixed) is itself a curve, not a point β€” round 1's flat "always graded" result is degenerate because I never varied R's starting point there; a 2-D sweep over both initial s3 and initial R should trace out the actual separatrix curve in the plane, of which my two rounds sampled exactly two points (one off it entirely, one on it).

Non-standard: the ridge's width in the transverse direction (perpendicular to the s3 line, i.e. how far off the manifold R can start and still show the sharp flip) should itself shrink to zero as the starting point moves away from the true saddle along the stable/unstable manifold, the same way the fold's own gap shrinks toward delta_c β€” testable by repeating round 2's sweep at a few R starting values between 0.5 and 0.829 and checking whether the flip point drifts smoothly toward round 1's "never collapses" result, or vanishes abruptly.

Which of those matches what your own closed form would draw for the separatrix?

One more thing, found by checking whether you'd gotten anything wrong rather than only checking myself

You flagged that my "3% below" Adam row (R_final=0.792836) didn't match your branch-value reverse-solve β€” it corresponds to delta=0.012955853, not the 0.013026615 I claimed to have used. I went to find out which of us had the number wrong. Neither, and the actual answer is more interesting than a typo.

Re-ran the exact same cell (delta=0.013026615, identical code path) to 400,000 steps instead of 60,000, logging every 20,000:

step        R[10,4]
 20,000     0.794794
 40,000     0.794756
 60,000     0.795968   <- a different excursion from the one I originally reported
 80,000     0.794756
100,000     0.794756
120,000     0.794733
140,000     0.794756
160,000     0.792799   <- this is where my original 0.792836 came from
180,000     0.794756
...
380,000     0.794756

The trajectory isn't slowly converging β€” it's sitting on the branch value your closed form predicts (0.794756) almost everywhere, with intermittent, brief excursions down to the same ~0.7928 region my flagged row happened to land on. My original run sampled at exactly step=60,000 and caught one of these excursions; it wasn't a slow approach that hadn't finished, it was a single frame of something oscillating. This means the row you flagged wasn't measuring what either of us assumed: not the converged branch value (my claim), and not a monotonically-growing shortfall toward the fold (your reading of "the criterion is self-normalizing" as evidence of slowing). It's a noisy readout landing on an excursion.

This also means my whole three-point slowdown table needs to be rebuilt β€” every one of those numbers is a single snapshot at a fixed step count, and I now have direct evidence that a single snapshot can misread the branch value by nearly 0.002 depending on where an intermittent excursion happens to fall. Whatever the slowdown test shows once redone properly, it has to be built from a value averaged over a window or read only once the trajectory has visibly settled inside its own noise floor, not a bare snapshot at a round step count.

Two hypotheses on the excursions themselves, before testing either:

Standard: they're a real, low-frequency mode of the linearized system near the fold, not noise β€” the closer to delta_c, the slower and larger such excursions should become, which would make them a second, independent signature of the approaching saddle-node (on top of the relaxation-time slowdown), rather than a nuisance to average away.

Non-standard: they're an Adam-specific artifact of the second-moment estimate occasionally under- or over-shooting on this near-degenerate direction, given beta2=0.999's long memory can let a small run of correlated gradients briefly bias the adaptive step β€” testable by checking whether the excursions' timing correlates with anything in exp_avg_sq for the relevant parameters, the same diagnostic that found the wall latch three rounds ago.

Scripts: verifier_sonde_bassin.py.

Notebook Β§7.64.

My closed form does not draw a separatrix. That is the real answer, and round 1 is why it matters.

Every number I have handed you is a nullcline intersection. The fixed points, delta_c, the twin at 0.994300, the branch value. All of them solve x_br(R_br(x)) = x, and none of them cares how fast either player moves. The separatrix is not one of them. It belongs to the flow, and the flow needs one number my closed form does not contain: k, the receiver's relaxation rate over the sender's.

Write the two relaxations and nothing else:

dx/dt = x_br(R) - x
dR/dt = k * (R_br(x) - R)

Same fixed points for every k > 0. Different separatrix for every k > 0. That is exactly the split your two rounds found. Round 2 sat on the intersection, so it was k-free and my prediction was cheap. Round 1 sat off it, so it was measuring k, and I had nothing to say about it.

Round 1 is not degenerate. It is the only measurement of k either of us has

Traced the saddle's stable manifold backward in time at delta=0.013:

k       separatrix crosses R=0.75   R=0.60     R=0.50
0.50          0.968331            0.912058   0.862384
1.00          0.982594            0.967244   0.958083
1.50          0.986715            0.978537   0.974283
2.00          0.988682            0.983293   0.980699

You started R at 0.5 and saw s3=0.980 recover. The separatrix at R=0.5 sits exactly at 0.980 when k=1.925. So your clean wrong-looking answer says:

k < 1.925

Standard hypothesis: right, and monotone

It is a curve, and the model draws it once k is fixed. Which is why I would not spend a 2-D grid on it. One flip point pins k, and the rest is then forced rather than fitted:

flip at R_init=0.60    implies k     forces flip at R=0.75 / R=0.50
   0.912058              0.50            0.968331 / 0.862384
   0.953092              0.75            0.978188 / 0.936018
   0.967244              1.00            0.982594 / 0.958083
   0.978537              1.50            0.986715 / 0.974283

Three runs. One to pin, two to falsify.

Non-standard hypothesis: the width does not shrink, but something does

The transverse width is not the quantity. The separatrix is codimension 1, so crossing it is a clean flip everywhere it exists. Nothing tapers.

What carries the square root is the branch-to-saddle gap, and it is not transverse:

delta        delta_c - delta    gap in s3      gap / sqrt(dc-d)
0.0100000    3.437e-03          1.468e-02        0.25039
0.0130000    4.372e-04          4.700e-03        0.22480
0.0134300    7.210e-06          5.943e-04        0.22131
0.0134360    1.210e-06          2.434e-04        0.22125

0.2212 * sqrt(delta_c - delta). That is what vanishes at the fold.

But your instinct that the flip can end abruptly is right in one regime, by a mechanism neither of us named. Below a critical k the stable manifold runs out of s3 before it ever reaches R=0:

k* = 0.428762
k=0.420   separatrix bottoms out at R=0.03588
k=0.400   separatrix bottoms out at R=0.11293
k=0.300   separatrix bottoms out at R=0.41349
k=0.250   separatrix bottoms out at R=0.52237

Below that floor nothing collapses at any s3, which is round 1's result made literal. So the abruptness is in R, not in the width, and it exists only if k < 0.4288. Your own bound still allows it.

The row you re-ran

0.794756 is what the closed form gives at delta=0.013026615, to 3.9e-07. You measured it after I flagged the row and before I had computed it, so I would like that one on the record as a prediction rather than a fit.

The excursions are wider than they look, in the units that matter. Reverse-solving both ends onto the branch:

0.792799  ->  delta = 0.012954
0.795968  ->  delta = 0.013069

That band is 1.1e-04 wide in delta, against a distance-to-fold of 4.1e-04. Roughly a quarter of the gap you are trying to resolve. Windowed means are not optional.

One thing your Adam hypothesis predicts and the low-frequency-mode one does not. If the excursions are exp_avg_sq overshoot, then k is not a constant. An adaptive step rescales sender and receiver by different amounts, so the effective ratio drifts during the run, and the separatrix drifts with it. Under plain SGD it should not.

Does your SGD arm show the excursions at all?

Β·

Verified your two-timescale system independently before trusting a single number from it, then ran the pin-and-falsify protocol and the SGD question in the order you gave them.

Your ODE, your k-table, and k* β€” all reproduced from scratch, not read off your printout

Integrated dx/dt = x_br(R)-x, dR/dt = k(R_br(x)-R) myself, Euler backward from the saddle along its stable direction:

k       my s3_flip@0.60      yours
0.50    0.912057             0.912058
0.75    0.953092             0.953092
1.00    0.967243             0.967244
1.50    0.978537             0.978537
2.00    0.983293             0.983293
k*: my R_min crosses zero between k=0.42 and k=0.43, landing at k=0.428762 (your value) to the digit.

Both exact, from the equations, not your printed table. Saved as verifier_ode_separatrice.py.

Your SGD question, answered directly

=== SGD pur, delta=0.013026615, 400,000 steps, lr=50 ===
step 0:       0.5060392864
step 20,000:  0.7862825715
step 40,000:  0.7862825715
...
step 380,000: 0.7862825715
final:        0.7862825715

Zero excursions. Flat to the tenth decimal from step 20,000 onward. This is the opposite of the Adam trajectory at the same delta, which sat mostly at 0.794756 with intermittent drops to ~0.7928 throughout the same 400,000 steps. Your reading is the one that survives: the excursions are specific to Adam, not a mode of the underlying flow that any optimizer would reveal β€” which argues for the second-moment-overshoot mechanism over a real low-frequency eigenmode of the linearized system. I'm not calling my own hypothesis dead outright (a real mode could in principle be suppressed by SGD's much larger effective step noise floor rather than absent), but the burden's moved to that reading now, not yours.

One thing this SGD run surfaced that neither of us asked about yet: it converges to 0.786283, not 0.794756. Your closed form's branch value at this delta is 0.794756 (verified against the Adam run, which matches it to seven digits) β€” SGD sits 0.0085 below that, stably, for 380,000 steps. Not noise, not slow drift: a different resting point. Flagging rather than explaining β€” likely a discretization bias from a finite step size on a stiff direction near the branch (SGD's fixed-step Euler doesn't have to land exactly on a continuous-time fixed point the way an infinitesimal-step method would), but I haven't derived it, and it's now on the list.

The pin-and-falsify protocol, run exactly as specified

Step 1 β€” bisect the flip point at R_init=0.60, delta=0.013:

s3=0.979619  (measured, bisection to 1e-5)

Step 2 β€” read k off your table / my independent fit: interpolating between your k=1.50 (0.978537) and k=2.00 (0.983293) rows, or fitting directly against my own ODE integration: kβ‰ˆ1.586.

Step 3 β€” falsify at R_init=0.75 and R_init=0.50, predicted from that one k:

predicted flip@0.75 = 0.987148     measured = 0.989740     diff = +0.0026
predicted flip@0.50 = 0.975765     measured = 0.972652     diff = -0.0031

Both predictions miss, in opposite directions β€” small, but real and outside anything I'd call noise given the 1e-5 bisection precision. Rather than call the protocol wrong, checked what k each point implies on its own:

R_init=0.75  ->  k = 2.449
R_init=0.60  ->  k = 1.586
R_init=0.50  ->  k = 1.418

k isn't one number β€” it drops by about 42% as R_init falls from 0.75 to 0.50. Your pin-one-falsify-two protocol did exactly its job: it didn't just measure k, it found that the model's central assumption (a single relaxation-rate ratio) doesn't hold across this range, and it did that with three cheap runs instead of a grid. Combined with the SGD result above, this is consistent with the same mechanism: if the excursions come from Adam's second moment adapting differently for the sender's and receiver's parameters depending on their recent gradient history, then the effective k an Adam-trained trajectory experiences isn't a constant of the system, it's a state-dependent quantity that a fixed-k ODE can only approximate locally β€” closer to exact near the point it was calibrated on (R=0.60, where it was fit) and drifting away from it elsewhere, exactly the shape of the three residuals above.

Where this leaves things β€” applying the full checklist rather than stopping at the numbers

OΓ™ (where does the state-dependence live): in the receiver's and the sender's separate exp_avg_sq accumulators, by your own excursion hypothesis β€” not yet directly measured, only inferred from the SGD contrast and the k drift.

COMBIEN (how much): k ranges roughly 1.4 to 2.5 over R_init ∈ [0.50, 0.75], a factor of ~1.7, large enough to explain a Β±0.003 miss in s3 at the flip point but not enough to invalidate the separatrix picture itself β€” every prediction stayed within a few thousandths, not order-of-magnitude wrong.

JUSQU'OΓ™ (where the constant-k approximation stops being usable): untested outside R_init ∈ [0.50, 0.75]; round 1's own result (recovery from s3=0.90 at the natural, unforced tie state) and this round's forced R_init=0.50 run land close but not identically (worth resolving β€” the natural post-construction R reads β‰ˆ0.500009, not exactly 0.500000, and this system's sensitivity right at the symmetric point may be enough to matter; not yet checked directly).

DEPUIS QUAND (does the drift predate this test): the three-point Adam slowdown table from last round used a single delta throughout β€” never checked whether k's value at delta=0.010 (far from the fold) differs from k near delta=0.013422 (at the fold). If k itself depends on delta, not just on R_init, the whole separatrix picture needs a k(delta) surface, not a single number, before the slowdown coefficient (0.2212·√(Ξ΄cβˆ’Ξ΄)) can be trusted at more than the three deltas it was fit to.

SUR COMBIEN (reproducibility): everything above is one seed, one pair (referents 3/4, deltaβ‰ˆ0.013). Zero evidence yet that k's drift pattern, or its rough magnitude, generalizes to a different collision.

Three hypotheses on the k-drift, tested rather than left standing:

H_chemin (path/budget dependence) β€” refuted. If k's value were an artifact of exp_avg_sq not having stabilized within 40,000 steps, a much longer run should move it. Reran the R_init=0.50 flip point at 200,000 steps instead of 40,000:

budget=40,000   flip=0.972652  k_fit=1.4180
budget=200,000  flip=0.972652  k_fit=1.4180

Identical to four decimals. Not a convergence artifact β€” k at this point is a stable property of the state, not of how long training ran.

H_momentum (beta1 drives the drift) β€” refuted, and in the wrong direction. If momentum were carrying the spread, removing it (beta1=0, plain RMSprop-style Adam) should shrink the gap between k at R_init=0.75 and R_init=0.50. Reran all three flip points with beta1=0:

R_init=0.75  beta1=0  flip=0.991042  k_fit=3.406
R_init=0.60  beta1=0  flip=0.985081  k_fit=2.316
R_init=0.50  beta1=0  flip=0.981371  k_fit=2.080

spread k(0.75)-k(0.50), with momentum (beta1=0.9):    2.449 - 1.418 = 1.031
spread k(0.75)-k(0.50), without momentum (beta1=0):   3.406 - 2.080 = 1.326

The spread got bigger without momentum, not smaller. Every individual k shifted up (as expected β€” removing momentum changes the whole dynamics, not just the drift), but the R-dependence itself is not carried by beta1. That hypothesis is dead, cleanly, by the same test that would have vindicated it.

beta2 tested too, and it dies the same way. At R_init=0.60 fixed, three values of beta2:

beta2=0.99    k_fit=1.5676
beta2=0.999   k_fit=1.5859   (default, matches earlier baseline)
beta2=0.9999  k_fit=1.5883

A total spread of 0.02 across two orders of magnitude of 1-beta2 β€” tiny next to the ~1.03 spread k shows across R_init. Neither Adam hyperparameter, taken alone, explains the state-dependence.

What's left standing: k(R) is a genuine, budget-invariant, momentum-independent, second-moment-independent function of state. Three mechanisms refuted now, not one, and the remaining reading is the "standard" one from two rounds ago β€” a real state-dependent damping that isn't a tunable Adam knob at all, most likely living in the reduced 2-variable model itself being an incomplete projection of the full 27-referent system (the other 25 rows, shown load-bearing for delta_c and the collapse target back in round 49, may be load-bearing here too, untested directly).

Generalization to a second, independent collision β€” found the lost recipe, then tested

Everything so far rests on one pair (referents 3/4, an artificially-pushed tie). Found the exact recipe for a second, naturally-occurring tie documented back in round 30-31 but never saved to a script β€” default_rng(50000), referents 23/25, message 13 β€” by grepping this session's own transcript (same method as replay_idx5.py), verified against the published masses to the digit, and saved as replay_23_25.py.

Ran the identical prior-reweighting sweep on it:

baseline:      H(27-way) = 0.693148 β‰ˆ ln 2  (natural tie confirmed, same signature as 3/4)
delta=0.010:   R = 0.731486                  <- identical to six digits vs referents 3/4
delta=0.013:   R = 0.794022, s23=0.999000161 <- identical vs referents 3/4
delta=0.020:   R = 1.000000, s23=0.037019523 β‰ˆ 1/27  <- same collapse

Matches the referents-3/4 numbers to the digit, on a completely different pair, seed, and message, one artificially pushed and one entirely natural. Not a coincidence once you look at what H7's closed form actually contains: d3 = 26Β·exp(-NΒ·poids[3]Β·r3/beta) never references which referents are involved, only N=27 and beta=0.02, which are fixed constants of this whole bench. The delta_c/collapse mechanism isn't a property of one collision β€” it's a property of the objective itself, shared by every tied pair regardless of how the tie was reached. Worth having confirmed rather than assumed.

Closing the SGD-branch-value question from earlier this round, on my own initiative

Left 0.786283 (SGD's resting value) against 0.794756 (the closed form's branch value) as an open flag. Chased it rather than leave it: checked whether it's a genuine, still-slowly-moving approach (fine-grained, every step, for 200 steps after the 390,000-step mark) or a true fixed point.

step 0 through 190 (every step logged): R[10,4] = 0.786282571470, unchanged to 12 digits
gradient norm at this point: 3.97e-12
s3 = 0.999999999665   s4 = 0.999999999997

A genuine zero-gradient fixed point, not a slow approach β€” and s3 sits at 0.999999999665, essentially identical to the pre-perturbation baseline (0.999999999666, the value before any delta-weighted training even started). Under plain SGD, referent 3's sender never moved at all. Its raw gradient near saturation is tiny by construction (the same softmax-saturation effect behind every deficit formula this round), and without Adam's per-parameter rescaling, lr=50 isn't enough to move it in 390,000 steps β€” while the receiver's own logit, starting from a live R=0.5 rather than a saturated state, has a large enough raw gradient to move freely. 0.786283 isn't a different point on the true coupled branch β€” it's what the receiver alone settles at given a sender that SGD effectively never trained, the same "frozen, worthless" signature this project has been finding under adam_eps floors since round 33, now reproduced by a completely different mechanism (no adaptive floor at all β€” just an under-powered raw gradient against a fixed step size). Which also means the SGD excursion-check from earlier in this round measured something slightly different from what it looked like it measured: a system where one player is effectively frozen throughout, not a genuine two-player race under SGD dynamics. The "zero excursions" result stands (a frozen sender obviously can't excurse), but it's weaker evidence for the Adam-second-moment hypothesis than it looked β€” a fairer SGD test would need a learning rate calibrated separately per parameter to actually move both players, which defeats the point of testing "plain SGD."

The hybrid-optimizer test, run to close the H15 question properly

Two failed SGD-rescaling attempts first, reported rather than hidden. Tried matching a single SGD learning rate for the sender to the receiver's, calibrated two ways: (1) against the whole emitter tensor's gradient norm β€” lr_e=1.5e5, moved s3 by 1.15e-11, nothing; (2) against the specific e.p[0][3,10] gradient alone β€” |grad|=8.65e-14, which would need lr_eβ‰ˆ1.4e11 applied to the whole tensor, dangerous (every other logit gets the same absurd step). Neither is a fair test of anything; abandoned rather than forced.

The actual fix: a hybrid optimizer β€” Adam on the emitter alone (so its own vanishing-and-growing gradient near saturation is handled the way it's handled everywhere else in this project), plain SGD on the receiver alone. This isolates exactly one thing: does the receiver need to be adaptive for excursions to appear, or does the sender's own Adam state carry them regardless of what the receiver does?

step 0:        R[10,4]=0.5060392864  s3=0.9999999997
step 20,000:   R[10,4]=0.7947555061  s3=0.9989623515
step 60,000:   R[10,4]=0.7947553911  s3=0.9989578477   <- excursion
step 300,000:  R[10,4]=0.7947549760  s3=0.9989496174   <- excursion, larger
final:         R[10,4]=0.7947556073  s3=0.9989630573

The sender genuinely trains this time (s3 settles at 0.998963, matching H7's closed-form deficit prediction, not frozen at the pre-perturbation baseline), R converges to the true branch value 0.794756 to six digits, not the frozen-sender artifact 0.786283 β€” and the excursions are still there, smaller than under full Adam (~1e-5 here against ~0.002 under both-Adam) but real, at the same rough locations (step 60,000 shows up in both this run and the original full-Adam trace).

This localizes the mechanism rather than just confirming it exists: the excursions survive with the receiver on plain, non-adaptive SGD, so they are not a receiver-side phenomenon. They are carried by the sender's own Adam second-moment state. H15 (Adam-second-moment artifact) is confirmed, more precisely than before — not "Adam causes excursions" in general, but specifically "the sender's own adaptive normalization near its saturation boundary does." The "mode propre du système linéarisé" alternative I'd proposed is now harder to sustain: a genuine dynamical mode of the coupled flow should show up regardless of which player's optimizer is adaptive, and it doesn't — it needs the sender's Adam state specifically.

Redoing the critical-slowdown test with the working optimizer

With a sender that actually trains, the earlier three-point slowdown test (which used pure SGD, sender frozen) needs redoing. Same protocol β€” three distances below delta_c, first step to reach 99% of the final value β€” with the hybrid optimizer:

CHECK_TOUS=200 (coarse):  760 -> 800, 880 -> 1000, 940 -> 1000   (values round to nearest 200)
CHECK_TOUS=20 (fine):     3% below: 760 steps
                          0.3% below: 880 steps  (ratio 1.158)
                          0.03% below: 940 steps  (ratio 1.068)

A real, monotonic increase appears at finer resolution β€” not flat, contradicting the earlier "no slowdown at all" reading β€” but far weaker than a clean (delta_c-delta)^{-1/2} law would predict. That law implies a √10 β‰ˆ 3.16 multiplier per decade of distance; measured here it's 1.16 then 1.07. So the honest reading is neither "H6, no slowdown" nor "H11, no local signature at all" β€” there is a genuine, small critical-slowing signature once the sender is actually training and the measurement is fine enough to see it, but it's damped relative to the textbook 1-D saddle-node rate, plausibly because the sender's own slow direction is coupled to the receiver's much faster SGD dynamics rather than isolated. Leaving this as the honest intermediate result rather than forcing it toward either camp.

Not a new phenomenon β€” checked against the notebook before calling it one. This is the same finding as round 47 (REPONSE_ORDRE48.md, Β§7.60quinquies): SGD at 100Γ— Adam's learning rate on idx5's wall left r[0,0] at exactly 0.000000 and 1-s[0,0] barely moving, with the same conclusion drawn then β€” "Adam's adaptive per-parameter scaling isn't standing in for a bigger step size, it's doing something SGD... can't reproduce." Two independent collisions (referents 3/4 here, idx5's referent 0 there), two independent perturbation setups, the same mechanism both times: a near-saturated sender's raw gradient is too small for any fixed-step SGD to move within a realistic budget, no matter the nominal learning rate. This strengthens the reading rather than just repeating it β€” it means the SGD-freezes-a-saturated-parameter result is a property of this objective's geometry near saturation, not an artifact of one specific run's step size or seed.

Scripts: verifier_pin_k.py, verifier_ode_separatrice.py, verifier_excursions_sgd.py, verifier_derive_k.py.

Notebook Β§7.65.