SAFETY-04: stop the clamps standing in for the mechanisms under test
AC 3 - "integration freezes while saturated" - had no non-vacuous test. Deleting the integrator freeze outright failed 0 of 55 checks: the two checks that name it used a fixture at max_w 2000 with the integrator bound following it, so a wound value was truncated back to exactly 2000 and the assertion passed on the clamp instead. Fixture lifted to max_w 5000, clear of every rail. Deleting the freeze now fails 2. Then swept the whole function for the same pattern, one mechanism at a time: delete it, count which checks notice. It found a second instance - the OUTPUT clamp. `clamped to max_w` and `clamped to -max_w` were both satisfied by the integrator bound truncating first, so removing the output clamp failed only the reason-string check. Those two fixtures now set integrator_max_w above max_w so the mechanism they name is the binding one; the output clamp goes from 1 failure to 3. Every mechanism in compute() is now caught by a check that names it: freeze 2, integrator clamp 6, bound-follows-max_w 4, output clamp 3, slew 4, output freeze 2, deadband 5, quantise 2, detector 10, duration 2, counter reset 6, grid bias 3, None-seeding 6. No mechanism at zero. Method note: the audit disables bytecode caching. Rewriting control.py inside one second leaves a stale app/__pycache__ entry and silently under-reports - it under-reported one mutation as 2 failures where the true figure is 6. test_control.py stays at 55 checks, all passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa
This commit is contained in:
co-authored by
Claude Opus 5
parent
53d301b920
commit
389d9ecd6d
@@ -41,15 +41,19 @@ check("proportional step (gain 0.6)", d.target_w == 300)
|
|||||||
d = compute(prev_w=0, grid_w=-500, actual_w=0, tuning=T)
|
d = compute(prev_w=0, grid_w=-500, actual_w=0, tuning=T)
|
||||||
check("export drives charging", d.target_w == -300)
|
check("export drives charging", d.target_w == -300)
|
||||||
|
|
||||||
# Clamp
|
# Clamp.
|
||||||
d = compute(prev_w=1900, grid_w=1000, actual_w=1900, tuning=Tuning(max_w=2000, slew_w=5000))
|
# ⚠️ integrator_max_w is lifted clear of max_w so that the OUTPUT clamp is the
|
||||||
|
# mechanism under test. Left at the default the integrator bound truncates
|
||||||
|
# first, these two assertions pass on that alone, and deleting the output clamp
|
||||||
|
# fails nothing - the same masking that hid the integrator freeze.
|
||||||
|
TCLAMP = Tuning(max_w=2000, slew_w=5000, integrator_max_w=5000)
|
||||||
|
d = compute(prev_w=1900, grid_w=1000, actual_w=1900, tuning=TCLAMP)
|
||||||
check("clamped to max_w", d.target_w == 2000)
|
check("clamped to max_w", d.target_w == 2000)
|
||||||
|
|
||||||
# Slew: from 0 with a huge error, no more than slew_w in one cycle.
|
# Slew: from 0 with a huge error, no more than slew_w in one cycle.
|
||||||
d = compute(prev_w=0, grid_w=5000, actual_w=0, tuning=Tuning(max_w=5000, slew_w=1000))
|
d = compute(prev_w=0, grid_w=5000, actual_w=0, tuning=Tuning(max_w=5000, slew_w=1000))
|
||||||
check("slew limits one cycle", d.target_w == 1000)
|
check("slew limits one cycle", d.target_w == 1000)
|
||||||
d = compute(prev_w=-1900, grid_w=-1000, actual_w=-1900,
|
d = compute(prev_w=-1900, grid_w=-1000, actual_w=-1900, tuning=TCLAMP)
|
||||||
tuning=Tuning(max_w=2000, slew_w=5000))
|
|
||||||
check("clamped to -max_w", d.target_w == -2000)
|
check("clamped to -max_w", d.target_w == -2000)
|
||||||
d = compute(prev_w=0, grid_w=-5000, actual_w=0, tuning=Tuning(max_w=5000, slew_w=1000))
|
d = compute(prev_w=0, grid_w=-5000, actual_w=0, tuning=Tuning(max_w=5000, slew_w=1000))
|
||||||
check("slew limits one cycle, charging", d.target_w == -1000)
|
check("slew limits one cycle, charging", d.target_w == -1000)
|
||||||
@@ -132,7 +136,12 @@ check("integrator bound binds independently of the output clamp",
|
|||||||
d.i_w == 1000 and d.target_w == 1000)
|
d.i_w == 1000 and d.target_w == 1000)
|
||||||
|
|
||||||
# Freeze = may not wind further in the direction it is already pushing.
|
# Freeze = may not wind further in the direction it is already pushing.
|
||||||
TF = Tuning(saturation_w=500, saturation_cycles=3)
|
# ⚠️ max_w is raised WELL above the fixtures on purpose. At the default 2000
|
||||||
|
# the integrator bound truncates a wound value back to exactly 2000 and
|
||||||
|
# satisfies these assertions on its own, so deleting the freeze outright
|
||||||
|
# failed nothing - the clamp was standing in for the mechanism under test.
|
||||||
|
# Any fixture here must sit clear of every rail, or it tests the rail.
|
||||||
|
TF = Tuning(saturation_w=500, saturation_cycles=3, max_w=5000)
|
||||||
f1 = compute(prev_w=2000, grid_w=800, actual_w=0, tuning=TF, sat_count=3, i_w=2000.0)
|
f1 = compute(prev_w=2000, grid_w=800, actual_w=0, tuning=TF, sat_count=3, i_w=2000.0)
|
||||||
check("frozen: integration does not wind further", f1.i_w == 2000.0 and f1.frozen)
|
check("frozen: integration does not wind further", f1.i_w == 2000.0 and f1.frozen)
|
||||||
f2 = compute(prev_w=2000, grid_w=-800, actual_w=0, tuning=TF, sat_count=3, i_w=2000.0)
|
f2 = compute(prev_w=2000, grid_w=-800, actual_w=0, tuning=TF, sat_count=3, i_w=2000.0)
|
||||||
|
|||||||
Reference in New Issue
Block a user