Skip to content

feat: a settle-on-target amp controller and a closed-loop backtest to judge it (experimental, not wired in) - #38

Merged
zebraengine merged 1 commit into
mainfrom
feat/settle-amp-control
Oct 13, 2026
Merged

zebraengine merged 1 commit into
mainfrom
feat/settle-amp-control

Conversation

@zebraengine

@zebraengine zebraengine commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Problem

On 2026-10-02 (session 101) the amp controller moved 49 -> 40 -> 46 -> 44 -> 46 -> 48 -> 45 -> 41 A in eighteen minutes. The quick-reversal back-off then pinned the session at 41 A when about 44 A was sustainable. Three things in the deployed decision produce this:

  • Step-ups are decided on the plateau at the present current, never the target's.
  • Caps trust a two-minute-old trajectory fit.
  • A 2-3 A step stays inside predict()'s 10% window band, so the next fit mixes two currents.

The existing backtest cannot judge a replacement either: it is open-loop, so once a candidate commands a different current from the one the car actually drew, the recorded samples no longer describe what would have happened.

Code touched

  • contrib/settle_amp_control.py (new): a candidate replacement for the derate decision. It tracks one number per session, the effective ambient (plateau minus the model's rise), fused from the ambient prior and one reading per steady-current stretch. It targets the largest whole amp whose plateau stays a soak margin under the trip. It front-loads at full rate while the handle is cold and makes one planned drop. After that it steps down on whichever is higher of the fused estimate and the live stretch's own reading, with a handle-driven cut near the trip that ignores the estimate.
  • contrib/backtest_settle_amp_control.py (new): closes the loop. It replays a session's real samples through the real predict() until the simulated command diverges from what the car drew, then hands over to a two-time-constant plant fitted to that session. Each session is rerun against plants 1-2 C hotter than the fit.
  • tests/test_settle_amp_control.py (new): 9 tests, including the two failed drafts described under Verification.
  • Left alone: contrib/derate_amp_control.py, the daemon, the installer and the docs. Nothing imports the new controller.

Risk

  • None to a running install: both files are standalone in contrib/ and nothing is wired in. The deployed controller behaves exactly as before.
  • The risk is in reading the backtest as proof. Known limits:
    • The plant is fitted to the day it is judged on.
    • The forecast still under-reads the plateau until the cable has soaked, which is why hot days take several 1 A steps down rather than one move.
    • 09-06 is worse than the deployed controller: 12 changes and 6 reversals against 10 and 4.

Verification

  • uv run pytest: 176 passed (9 new). Ruff: no findings on the three new files.
  • Closed-loop backtest over the whole history, 82 sessions with a usable plant (15 short and 4 poor fits skipped), fitted plant, deployed vs settle at a 1.5 C soak margin:
deployed settle
Trips 0 0
Worst peak 64.5 C 63.7 C
Amp changes 432 288
Delivered 3542 Ah 3561 Ah
  • On the 54 sessions where full rate is not sustainable:
deployed settle
Median changes 7 5
Mean reversals 3.1 0.9
Sessions with no reversal 3 31
  • No trips for either controller on any of the four hotter plants.
  • A 2.0 C margin delivers less than the deployed controller and is no safer.
  • Two drafts failed this backtest before this one passed, and both are now pinned by tests: counting every tick of a long stretch as a new reading (held 48 A into a 64.3 C peak on 09-30), and averaging the live reading with earlier, cooler ones (seven trips on hot days across the history).

Deploy: nothing to deploy.

… judge it (experimental, not wired in)

2026-10-02 (session 101): the amp controller moved 49 -> 40 -> 46 -> 44 ->
46 -> 48 -> 45 -> 41 A in eighteen minutes, then the quick-reversal back-off
pinned the session at 41 A when ~44 A was sustainable. Step-ups are decided
on the plateau at the present current, never the target's; caps trust a
two-minute-old trajectory fit; and a 2-3 A step stays inside predict()'s 10%
window band, so the next fit mixes two currents.

contrib/settle_amp_control.py is a candidate replacement for the derate
decision. It tracks one number per session - the effective ambient, plateau
minus the model's rise - fused from the ambient prior and one reading per
steady-current stretch, and targets the largest whole amp whose plateau
stays a soak margin under the trip. It front-loads at full rate while the
handle is cold, makes one planned drop, and afterwards steps down on
whichever is higher of the fused estimate and the live stretch's own
reading, with a handle-driven cut near the trip that ignores the estimate.
Not wired into the daemon.

contrib/backtest_settle_amp_control.py closes the loop the existing
backtest leaves open: it replays a session's real samples through the real
predict() until the simulated command diverges from what the car drew, then
hands over to a two-time-constant plant fitted to that session, and reruns
each session against plants 1-2 C hotter than the fit.

Whole history, 82 sessions with a usable plant (15 short, 4 poor fits
skipped), fitted plant, deployed vs settle at a 1.5 C soak margin: trips
0 / 0, worst peak 64.5 / 63.7 C, amp changes 432 / 288, 3542 / 3561 Ah. On
the 54 sessions where full rate is not sustainable: median changes 7 / 5,
mean reversals 3.1 / 0.9, sessions with no reversal 3 / 31. No trips for
either on any of the four hotter plants. A 2.0 C margin delivers less than
the deployed controller and is no safer.

Two drafts failed this backtest before this one passed, both pinned by
tests: counting every tick of a long stretch as a new reading (held 48 A
into a 64.3 C peak on 09-30), and averaging the live reading with earlier,
cooler ones (seven trips on hot days across the history).

Known limits: the plant is fitted to the day it is judged on; the forecast
still under-reads the plateau until the cable has soaked, which is why hot
days take several 1 A steps down rather than one move; 09-06 is worse than
the deployed controller (12 changes, 6 reversals against 10 and 4).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@zebraengine
zebraengine merged commit a2552db into main Oct 13, 2026
1 check passed
@zebraengine zebraengine mentioned this pull request Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant