SAFETY-04: clamp the integrator, not only the output

The loop was in velocity form: the accumulator WAS the commanded power, so
"clamp the integrator" and "clamp the output" were the same line of code and
could not be set apart. This makes the accumulator an explicit carried value
(`i_w`), bounds it with its own `integrator_max_w`, and keeps the output clamp
where it was. Killing either mechanism now still leaves the other holding -
which is the point of the ticket, and what the new regression test asserts.

Freeze semantics: while saturated the integrator may unwind but not wind
further. A strict freeze would strand the command at whatever it reached,
because the condition that releases it is the inverter tracking again, and not
tracking is exactly what saturation means.

The integrator is re-seeded from the arbiter's actual output whenever the loop
did not get what it asked for, so entering any failsafe (all of which resolve
to 0 W) zeroes it, and the first cycle after release does not dump the stale
period as power.

test_control.py: 24 -> 33 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:
glenn schrooyen
2026-08-24 21:44:05 +02:00
co-authored by Claude Opus 5
parent 5184a2cfc2
commit 37bac79ad8
5 changed files with 143 additions and 3 deletions
+2
View File
@@ -48,6 +48,7 @@ options:
step_w: 10
saturation_w: 500
saturation_cycles: 3
integrator_max_w: 3000
heartbeat_s: 10
stale_input_s: 15
auto_start: false
@@ -88,6 +89,7 @@ schema:
step_w: int(1,100)
saturation_w: int(100,2000)
saturation_cycles: int(1,10)
integrator_max_w: int(100,15000)
heartbeat_s: int(2,25)
stale_input_s: int(5,120)
auto_start: bool