Skip to content

Commit 253ff64

Browse files
wormeymanclaude
andcommitted
Replicate the volcanism contrast out of sample: it does not hold
#403 read the frequency 0.5 arm as halving the #84 residual on the three known regions - 41 of 1248 against 21 of 1359, z = +2.9 - and ended with "raise n before slicing again". This is that, done the same day: eight fresh 256x256 regions, disjoint from all 23 Vulcanus cliff regions captured before and all more than 1000 tiles from the origin, at the default and the frequency 0.5 arm. Each was checked on the engine for cliffs in BOTH arms before capture. 16 runs, 2 seconds each. Out of sample the default arm is the better one and the gap is inside noise: default 124 of 4545 = 2.73%, frequency 0.5 138 of 4064 = 3.40%, z = -1.80. So "the residual moves with the field" was a small-n reading and is withdrawn. Volcanism is not a lever the residual responds to, and it cannot decide the elevation-side / placement-side question it was captured to decide. The sweep's rows stay frozen as counts; the comment that read them as a finding now says what replaced it. What survived is the spread between REGIONS at one setting. In the frequency 0.5 arm, [-2200,-1500] carries 74 of the arm's 138 errors - 24 wrong and 47 surplus of 795 cells, 9.3% - while [1800,3400] has 1 of 200. 47 surplus is the ore rule's signature, not the connection pass's. That concentration is a lead the diffuse in-sample numbers never offered, and it is frozen in the new test. Also in this change: the runner grew a mode argument (`sweep` / `oos`) and keeps the oracle's error so a failed run reports its cause rather than a missing dump (CodeRabbit's one finding on #403). The sweep fixture was regenerated to prove determinism - byte-identical data, only its _comment moved - and its provenance says so. No engine source changed; engine.wasm is byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EadT1PyhSjDbjJwvLfo3zr
1 parent a8bd84c commit 253ff64

6 files changed

Lines changed: 53921 additions & 37 deletions

File tree

crates/fmw-noise/src/fixtures.rs

Lines changed: 184 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4538,12 +4538,16 @@ fn vulcanus_cliffs_track_the_volcanism_sliders() {
45384538
//
45394539
// R1 is 277 / 4 / 2 / 2 in every arm, by the control above.
45404540
//
4541-
// What it says, and what it cannot: the residual MOVES with the elevation
4542-
// input - the non-R1 error rate spans 1.5% to 3.3% across arms - so it is
4543-
// not a fixed placement-side defect that the elevation leaves alone. But
4544-
// it is not monotonic in frequency either (default is the worst arm, not
4545-
// frequency 2), and with 14 to 41 residual events per arm the resolution
4546-
// is about two sigma. It localises the residual to "depends on the field",
4541+
// What it looked like, and what it turned out to be: on these three
4542+
// regions the non-R1 error rate spans 1.5% to 3.3% across arms, with
4543+
// frequency 0.5 and size 3 each about 2.9 sigma below the default - which
4544+
// read as "the residual moves with the field". It does NOT replicate.
4545+
// `the_volcanism_contrast_out_of_sample` below re-measures the same
4546+
// contrast on eight fresh regions and gets z = -1.80, the other direction.
4547+
// With 14 to 41 residual events per arm these rows cannot carry a claim
4548+
// about the lever at all; they stand as frozen counts, not as a finding.
4549+
// What IS real is the per-region spread - see the out-of-sample test.
4550+
// It does not localise the residual to a term or to "depends on the field";
45474551
// not to a term. `unscored` is game cliffs on the region boundary, every
45484552
// one of them - see the volcanism sweep section (2026-09-07) of
45494553
// `docs/noise/vulcanus-cliffs-NOTES.md`.
@@ -4588,6 +4592,180 @@ fn vulcanus_cliffs_track_the_volcanism_sliders() {
45884592
}
45894593
}
45904594

4595+
/// Two-proportion z: `a` errors of `n_a` against `b` errors of `n_b`, pooled.
4596+
fn two_proportion_z(a: usize, n_a: usize, b: usize, n_b: usize) -> f64 {
4597+
#[allow(clippy::cast_precision_loss)]
4598+
let (a, n_a, b, n_b) = (a as f64, n_a as f64, b as f64, n_b as f64);
4599+
let p = (a + b) / (n_a + n_b);
4600+
(a / n_a - b / n_b) / (p * (1.0 - p) * (1.0 / n_a + 1.0 / n_b)).sqrt()
4601+
}
4602+
4603+
/// The sweep's headline contrast, OUT OF SAMPLE (#84).
4604+
///
4605+
/// [`vulcanus_cliffs_track_the_volcanism_sliders`] found the frequency 0.5 arm
4606+
/// halving the residual rate against the default - 41 of 1248 against 21 of
4607+
/// 1359, about 2.9 sigma - on the three regions every earlier #84 number was
4608+
/// measured on. Eight fresh regions, disjoint from all 23 captured before and
4609+
/// all outside the starting area, at the same two arms. The regions and the
4610+
/// engine-side pre-check that each contains cliffs in BOTH arms are in
4611+
/// `scripts/probes/vulcanus-cliff-volcanism/capture.ts`.
4612+
///
4613+
/// Asserted before any count is read: the read-back agrees with the arm, and
4614+
/// every region's game cliffs MOVED between the arms - the override reached
4615+
/// the generator in every one of the eight.
4616+
#[test]
4617+
fn the_volcanism_contrast_out_of_sample() {
4618+
let fixture = load_captured_at(
4619+
"test/fixtures/oracle-vulcanus-cliff-volcanism-oos.seed123456.json",
4620+
"2.1.17",
4621+
);
4622+
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
4623+
let seed0 = fixture.get("seed").as_f64() as u32;
4624+
let arms = fixture.get("arms").as_array();
4625+
assert_eq!(arms.len(), 2, "default and frequency 0.5");
4626+
assert_eq!(arms[0].get("label").as_str(), "default");
4627+
assert_eq!(arms[1].get("label").as_str(), "frequency 0.5");
4628+
4629+
let cell = |c: &Json| -> (u64, u64, String) {
4630+
(
4631+
c.get("x").as_f64().to_bits(),
4632+
c.get("y").as_f64().to_bits(),
4633+
c.get("orientation").as_str().to_owned(),
4634+
)
4635+
};
4636+
let game_cells = |case: &Json| -> Vec<(u64, u64, String)> {
4637+
case.get("cliffs")
4638+
.as_array()
4639+
.iter()
4640+
.filter(|c| c.get("name").as_str() == "cliff-vulcanus")
4641+
.map(cell)
4642+
.collect()
4643+
};
4644+
4645+
let mut per_arm: Vec<(String, Vec<SweepRow>, SweepRow)> = Vec::new();
4646+
for arm in arms {
4647+
let label = arm.get("label").as_str().to_owned();
4648+
let frequency = arm.get("frequency").as_f64();
4649+
let size = arm.get("size").as_f64();
4650+
let reported = arm.get("reported");
4651+
assert_eq!(reported.get("frequency").as_f64(), frequency, "{label}");
4652+
assert_eq!(reported.get("size").as_f64(), size, "{label}");
4653+
4654+
let mut ctx = crate::eval::ctx::EvalCtx::new(seed0);
4655+
ctx.vulcanus_volcanism_frequency = frequency;
4656+
ctx.vulcanus_volcanism_size = size;
4657+
let cases = arm.get("cases").as_array();
4658+
assert_eq!(cases.len(), 8, "{label}: eight regions");
4659+
let rows: Vec<SweepRow> = cases
4660+
.iter()
4661+
.map(|case| sweep_score(case.get("region"), case.get("cliffs").as_array(), &ctx))
4662+
.collect();
4663+
let total = rows.iter().fold(SweepRow::ZERO, |acc, r| acc.add(*r));
4664+
for (i, r) in rows.iter().enumerate() {
4665+
eprintln!("{label:>14} region {i}: {r:?}");
4666+
}
4667+
eprintln!("{label:>14} total: {total:?}");
4668+
per_arm.push((label, rows, total));
4669+
}
4670+
4671+
// Every region moved on the game side, or the lever never reached it.
4672+
let default_cases = arms[0].get("cases").as_array();
4673+
let moved_cases = arms[1].get("cases").as_array();
4674+
for (i, (a, b)) in default_cases.iter().zip(moved_cases).enumerate() {
4675+
assert_eq!(
4676+
a.get("region").get("x0").as_f64(),
4677+
b.get("region").get("x0").as_f64()
4678+
);
4679+
assert_ne!(
4680+
game_cells(a),
4681+
game_cells(b),
4682+
"region {i}: must move under frequency 0.5"
4683+
);
4684+
}
4685+
4686+
// The frozen rows, measured 2026-09-07. Read a moved number, do not adjust
4687+
// it. Regions in the order `OUT_OF_SAMPLE` lists them.
4688+
let row = |matched, wrong, surplus, missing, unscored| SweepRow {
4689+
matched,
4690+
wrong,
4691+
surplus,
4692+
missing,
4693+
unscored,
4694+
};
4695+
let expected: [(&str, [SweepRow; 8]); 2] = [
4696+
(
4697+
"default",
4698+
[
4699+
row(309, 6, 5, 0, 12),
4700+
row(208, 4, 6, 3, 11),
4701+
row(763, 1, 4, 0, 13),
4702+
row(462, 5, 6, 0, 24),
4703+
row(703, 1, 1, 0, 8),
4704+
row(518, 7, 13, 0, 15),
4705+
row(834, 11, 16, 0, 12),
4706+
row(697, 13, 22, 0, 11),
4707+
],
4708+
),
4709+
(
4710+
"frequency 0.5",
4711+
[
4712+
row(612, 3, 8, 5, 13),
4713+
row(768, 24, 47, 3, 27),
4714+
row(199, 1, 0, 0, 6),
4715+
row(774, 2, 4, 1, 24),
4716+
row(439, 2, 3, 0, 11),
4717+
row(255, 1, 0, 1, 18),
4718+
row(641, 8, 11, 0, 0),
4719+
row(319, 6, 8, 0, 2),
4720+
],
4721+
),
4722+
];
4723+
for ((label, rows, total), (want_label, want)) in per_arm.iter().zip(expected) {
4724+
assert_eq!(label, want_label);
4725+
assert_eq!(rows.as_slice(), want.as_slice(), "{label}: per-region rows");
4726+
assert_eq!(
4727+
*total,
4728+
want.iter().fold(SweepRow::ZERO, |acc, r| acc.add(*r)),
4729+
"{label}: totals"
4730+
);
4731+
}
4732+
4733+
// THE CONTRAST DOES NOT REPLICATE. In sample it was 41 of 1248 against 21
4734+
// of 1359, z = +2.9 in favour of frequency 0.5. Out of sample, on eight
4735+
// regions and 3.5x the cells, the default arm is the BETTER one and the
4736+
// gap is inside noise. So the sweep's "the residual moves with the field"
4737+
// was a small-n reading - `docs/noise/vulcanus-cliffs-NOTES.md` records
4738+
// both the claim and its refutation - and volcanism is not a lever the
4739+
// residual responds to.
4740+
let errors = |r: &SweepRow| r.wrong + r.surplus + r.missing;
4741+
let comparable = |r: &SweepRow| r.matched + r.wrong + r.missing;
4742+
let (d, m) = (&per_arm[0].2, &per_arm[1].2);
4743+
assert_eq!((errors(d), comparable(d)), (124, 4545), "default: 2.73%");
4744+
assert_eq!(
4745+
(errors(m), comparable(m)),
4746+
(138, 4064),
4747+
"frequency 0.5: 3.40%"
4748+
);
4749+
let z = two_proportion_z(errors(d), comparable(d), errors(m), comparable(m));
4750+
assert!(
4751+
(-1.85..-1.75).contains(&z),
4752+
"z = {z:.3}: measured -1.80, and the SIGN is the finding"
4753+
);
4754+
4755+
// What is real is the spread BETWEEN regions, at one slider setting: the
4756+
// frequency 0.5 arm's `[-2200,-1500]` carries 74 of that arm's 138 errors
4757+
// on its own - 24 wrong and 47 surplus of 795 comparable cells, 9.3% - while
4758+
// `[1800,3400]` in the same arm has 1 of 200. A residual that concentrated
4759+
// is a lead the diffuse in-sample numbers never offered.
4760+
let worst = &per_arm[1].1[1];
4761+
assert_eq!(
4762+
(worst.wrong, worst.surplus),
4763+
(24, 47),
4764+
"the concentrated region"
4765+
);
4766+
assert_eq!(errors(worst), 74);
4767+
}
4768+
45914769
/// The Vulcanus cliff fields against the game's own samples at the game's own
45924770
/// lattice - 12,675 corners across three regions.
45934771
///

docs/noise/vulcanus-cliffs-NOTES.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4714,3 +4714,56 @@ measurement is more regions per arm, not more arms - the capture is 2 seconds
47144714
per region now, so eight fresh regions at two arms (default, frequency 0.5) is
47154715
under a minute and would put the 2.9 sigma either side of 4. Raise n before
47164716
slicing again, per the section above this one.
4717+
4718+
## RAISING N on the volcanism contrast: it does NOT replicate (2026-09-07, #84)
4719+
4720+
The section above ended with "raise n before slicing again", so this is that,
4721+
done the same day and before the finding could be cited anywhere else. The
4722+
sweep's headline was frequency 0.5 halving the residual rate on the three known
4723+
regions: 41 of 1248 against 21 of 1359, z = +2.9. **Out of sample, on eight
4724+
fresh regions and 3.5x the cells, the default arm is the better one and the gap
4725+
is inside noise:**
4726+
4727+
| arm | R2 + R3 (in sample) | rate | eight fresh regions | rate |
4728+
| ------------- | ------------------- | --------: | ------------------- | --------: |
4729+
| default | 41 of 1248 | 3.29% | 124 of 4545 | **2.73%** |
4730+
| frequency 0.5 | 21 of 1359 | **1.55%** | 138 of 4064 | 3.40% |
4731+
| z | | **+2.9** | | **-1.80** |
4732+
4733+
Fixture: `oracle-vulcanus-cliff-volcanism-oos.seed123456.json`, eight 256x256
4734+
regions disjoint from all 23 captured before, all more than 1000 tiles from the
4735+
origin, each checked on the engine for cliffs in BOTH arms before capture (the
4736+
table is in `capture.ts`). Test: `the_volcanism_contrast_out_of_sample`. The
4737+
read-back guard held on all sixteen runs and every region moved between arms.
4738+
4739+
**So the sweep's "the residual moves with the field" was a small-n reading and
4740+
is withdrawn.** Volcanism is not a lever the residual responds to. That also
4741+
means the "scales or flat" framing the sweep was built to decide has no answer
4742+
from this lever: neither elevation-side nor placement-side is favoured by it.
4743+
4744+
**What IS real is the spread between REGIONS at one slider setting**, which
4745+
the three in-sample regions were too few to show. Per region, frequency 0.5 arm,
4746+
errors of comparable cells:
4747+
4748+
| region | wrong | surplus | missing | comparable | rate |
4749+
| --------------- | ----: | ------: | ------: | ---------: | ----: |
4750+
| `[-2200,-1500]` | 24 | 47 | 3 | 795 | 9.3% |
4751+
| `[2600,800]` | 6 | 8 | 0 | 325 | 4.3% |
4752+
| `[1400,-3200]` | 8 | 11 | 0 | 649 | 2.9% |
4753+
| `[2000,-1200]` | 3 | 8 | 5 | 620 | 2.6% |
4754+
| `[4000,-600]` | 2 | 3 | 0 | 441 | 1.1% |
4755+
| `[600,2200]` | 2 | 4 | 1 | 777 | 0.9% |
4756+
| `[-2800,400]` | 1 | 0 | 1 | 257 | 0.8% |
4757+
| `[1800,3400]` | 1 | 0 | 0 | 200 | 0.5% |
4758+
4759+
One region carries 74 of the arm's 138 errors, and 47 of them are SURPLUS -
4760+
cells the port places and the game does not - which is the ore rule's
4761+
signature from the earlier sections (`cliff_removal_probability`, the destroy
4762+
stage) far more than the connection pass's. At the default the same region is
4763+
13 errors of 215. A residual this concentrated is a lead the diffuse in-sample
4764+
numbers never offered: one region, one arm, 795 cells, and the port over-places
4765+
by 6%.
4766+
4767+
Cost of the whole exercise: 28 seconds of capture. The cost of NOT doing it
4768+
would have been a #84 comment, a memory and a CLAUDE.md line all citing a 2.9
4769+
sigma that does not exist.

0 commit comments

Comments
 (0)