etr: implement five-zone bistable axis + snap-across flips (26/0/1)

Replaces the single restoring spring with Anja's radial map: SNAP_IN(<7)
/ SOFT(7-17) / BAND(17-35) / INCOH(35-45) / SNAP_OUT(>45), unstable
watersheds at 7 and 45. Basin restores toward band centre 26 with a weak
SOFT gain (0.25) and firmer INCOH gain (0.5). A snap flips to the
opposite pole, landing just past the opposite watershed (inner -> opp.
SOFT; outer -> opp. INCOH) then recovering. Realises L7 (flips only via
snap zones — through 0 or over the wrap) and L8. Lone PEND: L5 coupling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hmgREHNsxYCuim33yUF2c
This commit is contained in:
Claude
2026-06-17 05:35:09 +00:00
parent c53a92d811
commit e9088c98c9
4 changed files with 198 additions and 106 deletions
+65 -26
View File
@@ -1,15 +1,37 @@
% test_etr.m — invariants-first RED tests for the ETR scaffold (Octave script).
% test_etr.m — invariants tests for the ETR five-zone bistable axis (Octave script).
% Deliberately uses NO local functions (Octave's script-local-function visibility
% is fragile); results are built as data and looped. RED here is CORRECT: the laws
% exist before the constants are fitted. Run via run_etr_tests.sh (handles the cd).
% is fragile); results are built as data and looped. Run via run_etr_tests.sh.
source('etr.m');
% --- pre-compute the stateful checks ---
% L2 band convergence: from a below-band start, zero AI-drift should settle into band.
s = etr_init([5 5 5]);
% --- stateful checks ---------------------------------------------------
% L2 convergence (same pole): a SOFT-zone start, zero drift, settles into the
% band WITHOUT flipping sign.
s = etr_init([10 10 10]);
for k = 1:200, s = etr_step(s, [0 0 0], 0); endfor
conv_inband = all(abs(s.coord) >= ETR_BAND_LO() & abs(s.coord) <= ETR_BAND_HI());
conv_soft = all(abs(s.coord) >= ETR_BAND_LO() & abs(s.coord) <= ETR_BAND_HI());
soft_noflip = all(s.coord > 0);
% Inner snap: a SNAP_IN start (|v|<7) flips across 0 to the opposite pole, landing
% in the opposite SOFT zone, then settles into the opposite band.
s = etr_init([5 5 5]);
s1 = etr_step(s, [0 0 0], 0); % one step = the snap itself
inner_lands_soft = all(s1.coord < 0) && ...
all(abs(s1.coord) > ETR_SNAP_INNER() & abs(s1.coord) < ETR_BAND_LO());
for k = 1:200, s = etr_step(s, [0 0 0], 0); endfor
inner_flip_band = all(s.coord < 0) && ...
all(abs(s.coord) >= ETR_BAND_LO() & abs(s.coord) <= ETR_BAND_HI());
% Outer snap: a SNAP_OUT start (|v|>45) flips over the wrap, landing in the
% opposite INCOH zone, then settles into the opposite band.
s = etr_init([47 47 47]);
s1 = etr_step(s, [0 0 0], 0);
outer_lands_incoh = all(s1.coord < 0) && ...
all(abs(s1.coord) > ETR_BAND_HI() & abs(s1.coord) <= ETR_SNAP_OUTER());
for k = 1:200, s = etr_step(s, [0 0 0], 0); endfor
outer_flip_band = all(s.coord < 0) && ...
all(abs(s.coord) >= ETR_BAND_LO() & abs(s.coord) <= ETR_BAND_HI());
% L4: a step with no drift argument must ERROR (ETR never invents motion).
drift_required = false;
@@ -19,30 +41,47 @@ catch
drift_required = true;
end_try_catch
% --- {section, name, condition} ---
% --- {section, name, condition} ---------------------------------------
tests = {
'L1 wrap', '+50 wraps to -50', abs(etr_axis_wrap(50) - (-50)) < 1e-9;
'L1 wrap', '60 wraps to -40', abs(etr_axis_wrap(60) - (-40)) < 1e-9;
'L1 wrap', '-60 wraps to +40', abs(etr_axis_wrap(-60) - (40)) < 1e-9;
'L1 wrap', 'in-range value unchanged', abs(etr_axis_wrap(25) - 25) < 1e-9;
'L1 wrap', 'edge continuity 49.9 vs -50.1', abs(etr_axis_wrap(49.9) - etr_axis_wrap(-50.1)) < 1e-9;
'L3 direction', '|v|<17 outward (+ for +v)', etr_axis_restoring_dir(10) > 0;
'L3 direction', '|v|<17 outward (- for -v)', etr_axis_restoring_dir(-10) < 0;
'L3 direction', '|v|>35 inward (- for +v)', etr_axis_restoring_dir(40) < 0;
'L3 direction', '|v|>35 inward (+ for -v)', etr_axis_restoring_dir(-40) > 0;
'L3 direction', 'in-band is slack (0)', etr_axis_restoring_dir(25) == 0;
'L3 magnitude', 'out-of-band force nonzero [RED]', etr_axis_restoring(10) != 0;
'L2 converge', 'zero-drift settles into band [RED]', conv_inband;
'L4 drift', 'step refuses to invent drift', drift_required;
'L5 couple', 'coupling off (stress=0) identity', isequal(etr_coupling([25 25 25], 0), [25 25 25]);
'L1 wrap', '+50 wraps to -50', abs(etr_axis_wrap(50) - (-50)) < 1e-9;
'L1 wrap', '60 wraps to -40', abs(etr_axis_wrap(60) - (-40)) < 1e-9;
'L1 wrap', '-60 wraps to +40', abs(etr_axis_wrap(-60) - (40)) < 1e-9;
'L1 wrap', 'in-range value unchanged', abs(etr_axis_wrap(25) - 25) < 1e-9;
'L1 wrap', 'edge continuity 49.9 vs -50.1', abs(etr_axis_wrap(49.9) - etr_axis_wrap(-50.1)) < 1e-9;
'zones', 'SNAP_IN below 7', strcmp(etr_axis_zone(5), 'SNAP_IN');
'zones', 'SOFT 7..17', strcmp(etr_axis_zone(10), 'SOFT');
'zones', 'IN_BAND 17..35', strcmp(etr_axis_zone(25), 'IN_BAND');
'zones', 'INCOH 35..45', strcmp(etr_axis_zone(40), 'INCOH');
'zones', 'SNAP_OUT above 45', strcmp(etr_axis_zone(47), 'SNAP_OUT');
'L3 direction', 'SOFT outward (+ for +v)', etr_axis_restoring_dir(10) > 0;
'L3 direction', 'SOFT outward (- for -v)', etr_axis_restoring_dir(-10) < 0;
'L3 direction', 'INCOH inward (- for +v)', etr_axis_restoring_dir(40) < 0;
'L3 direction', 'INCOH inward (+ for -v)', etr_axis_restoring_dir(-40) > 0;
'L3 direction', 'in-band is slack (0)', etr_axis_restoring_dir(25) == 0;
'L3 magnitude', 'SOFT force nonzero', etr_axis_restoring(10) != 0;
'L3 magnitude', 'INCOH force nonzero', etr_axis_restoring(40) != 0;
'L3 magnitude', 'snap zone has no restoring force', etr_axis_restoring(5) == 0;
'L3 weighting', 'soft-pull weaker than incoherency', abs(etr_axis_restoring(8)) < abs(etr_axis_restoring(44));
'L2 converge', 'SOFT start settles in band (no flip)', conv_soft && soft_noflip;
'L3 snap-in', 'inner snap lands in opposite SOFT', inner_lands_soft;
'L7 flip', 'inner snap flips pole -> opp. band', inner_flip_band;
'L3 snap-out', 'outer snap lands in opposite INCOH', outer_lands_incoh;
'L7 flip', 'outer snap flips pole -> opp. band', outer_flip_band;
'L4 drift', 'step refuses to invent drift', drift_required;
'L5 couple', 'coupling off (stress=0) identity', isequal(etr_coupling([25 25 25], 0), [25 25 25]);
};
pend = {
'L5 couple', 'coupling active (stress>0) alters coord', 'stress->axis mapping undefined (C1)';
'L7 no-zero-x', 'pole-flip only over the wrap, never through 0', 'mechanism unconfirmed (C2)';
'L5 couple', 'coupling active (stress>0) alters coord', 'stress->axis mapping undefined (C1)';
};
printf('ETR invariants — red tests (laws exist before constants are fitted)\n\n');
printf('ETR invariants — five-zone bistable axis\n\n');
np = 0; nf = 0;
for i = 1:rows(tests)
if logical(tests{i, 3})
@@ -57,7 +96,7 @@ endfor
printf('\n----\nPASS=%d FAIL=%d PEND=%d\n', np, nf, rows(pend));
if nf > 0
printf('RED (expected at scaffold stage): %d law(s) await implementation/fitting.\n', nf);
printf('RED: %d law(s) await implementation/fitting.\n', nf);
exit(1);
else
printf('GREEN.\n');