Skip to content

feat: a calibration probe plan — two currents, each with a cold cable and a warm one - #36

Merged
zebraengine merged 1 commit into
mainfrom
feat/probe-plan
Sep 15, 2026
Merged

zebraengine merged 1 commit into
mainfrom
feat/probe-plan

Conversation

@zebraengine

@zebraengine zebraengine commented Sep 15, 2026 •

Copy link
Copy Markdown
Owner

Problem

The forecast backtest (#35), once its sampling bias was controlled, left one error standing: heat soak from the charge before — a step-down after a hot full-rate run reads 2.8–3.6 °C optimistic from the sensor. It also showed why the recorded history cannot resolve it: the controller chose every run's current on the strength of the model, so hot days and low currents arrive together, and an ambient effect, a current-law error and a warm cable all wear the same signature. The fix is a designed experiment, not another fit: the same probe current with a cold cable and with a warm one.

Code touched

contrib/derate_amp_control.py

  • --probe-amps takes a list; new --probe-cable (any | cold | warm, comma-separated), --probe-plan-interval-days (7), --probe-replicates (2), --probe-cold-gap-h (4), --probe-warm-min (30). The plan is currents × cable conditions.
  • cold: starts within the session's first 3 min, only if ≥ 4 h passed since the last charging tick. warm: starts after ≥ 30 min at ≥ 90 % of normal_amps, uncapped, as a mid-session step-down. any: whenever due (the original behavior; the default).
  • Selection: least-replicated condition first (ties in plan order). A condition that could still be met later in this session (warm, before its 30 min) makes the daemon wait rather than spend the slot on an easier one — unless the slot is overdue by 2× the interval, when anything eligible goes. One probe per session. Weekly until each condition has 2 completions, then monthly, cycling. Cadence anchored at the probe's session start (anchored at completion, a session exactly one interval later was a few minutes short of due at its first tick — the only minutes a cold probe can start in).
  • State gains: the active probe's current and condition, a log of completed probes (last 50), and the session bookkeeping the conditions are judged on (last charging tick, this session's start, the gap before it, how long at full rate). Old state files load with defaults; a hold in progress from before carries no current and continues at the daemon's single one.
  • Action.probe and event_for put {"amps", "cable"} on the probe's amp_capped event.
  • Precedence unchanged: a thermal cap below the probe current still abandons it; a thermal cap this tick still outranks starting one; the probe still outranks restoring while it holds; completion still restores to the sustainable current.

deploy/install-derate-amp-control.sh: passes the new flags through. contrib/backtest_forecast.py: probe runs identified from the controller's events (with the pre-plan probe recognized by its reason text) instead of by current — which had been tagging 24 A foldback runs as probes — and grouped per condition (probe 32A cold …). Docs: amp-control.md "A probe plan"; thermal-model.md cross-reference.

Risk

  • Behavior you will see: once a week for ~2 months, one session runs 40 min at 32 or 40 A instead of 48; for the warm conditions that is a step-down 30 min into a session that was charging at full rate. That is the experiment's cost, ~1/3 slower for those 40 min, in a season where full rate is never near the limit.
  • A plan whose next-needed condition is warm waits through a session shorter than 30 min and starts nothing; the slot is not consumed and the next session retries. If sessions were consistently under 30 min the plan would stall on warm until the 2× overdue fallback took a cold one instead. Your sessions run 1–3 h.

Nothing else changes for an install without --probe-cable: the single-current, any configuration is byte-for-byte the previous behavior, and the default remains off.

Verification

  • uv run pytest: 163 passed (9 new). The plan is exercised by a tick-by-tick simulation of 85 daily sessions through decide() — every condition visited least-replicated-first (32 cold → 32 warm → 40 cold → 40 warm, twice), cold probes at minute 0 and warm at minute 30, then monthly cycling from the top; cold refused after a 2 h gap, on a fresh state file's first session, and on a daemon upgraded mid-session; warm refused under a thermal cap; the wait-for-the-needy-condition rule and its overdue fallback; one per session; the single-current any configuration keeps its monthly cadence; CLI parsing and rejection; old state file and mid-probe upgrade; event detail. Backtest tags a probe run from the event and groups it.
  • Dry run of the new daemon on the mini-PC against the live /api/thermal with the deployed flags plus --probe-amps 32,40 --probe-cable cold,warm and a scratch state file: parses, decides, exits 0, touches nothing.

Deploy (needs sudo; the installer rewrites the unit and reloads):

sudo bash ~/wallmonitor/deploy/install-derate-amp-control.sh --tesla-ble http://10.69.2.213 \
  --wallmonitor http://127.0.0.1:8480 --probe-amps 32,40 --probe-cable cold,warm \
  --extra-args '--forecast-confidence-k 4'

The existing state file carries over. Its Sep 14 probe (32A any, outside the plan) anchors the cadence, so the first plan probe is due Sep 21 on the first eligible session — and the daemon needs one session after deploy to learn when the last charge ended before it can call a session cold-cable, which that week provides.

🤖 Generated with Claude Code

… and a warm one

The forecast backtest, once its sampling bias was controlled, left one
error standing: heat soak from the charge before. It also showed why the
history cannot resolve it - the controller chose every run's current on
the strength of the model, so hot days and low currents arrive together
and an ambient effect, a current-law error and a warm cable all wear the
same signature. The fix is a designed experiment, not another fit.

The probe becomes a plan: the product of --probe-amps (now a list) and
--probe-cable. "cold" is a probe started in a session's first three
minutes after --probe-cold-gap-h (4) without charging; "warm" is a
mid-session step-down after --probe-warm-min (30) at full rate, uncapped;
"any" is the original behavior and the default, so a single-current
install is unchanged. The least-replicated condition goes first, and if it
can still be met later in the session the daemon waits for it rather than
spending the slot on an easier one, unless the slot is overdue by a whole
interval. One probe per session. Weekly (--probe-plan-interval-days) until
each condition has --probe-replicates (2), then monthly as before, cycling.
The cadence is anchored at the probe's session start: anchored at its
completion, a session exactly one interval later fell a few minutes short
of due at its first tick - the only minutes a cold probe can start in.

Each probe's start is recorded in its amp_capped event (detail.probe), and
the backtest groups probe runs by condition from that instead of guessing
from the current, which had been mis-tagging foldback runs as probes.

Deployed as 32,40 x cold,warm this is eight probes over about two months -
in a cooling season, also a fixed-current sweep across 15 C of ambient
that the summer's controller-chosen history could not provide.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@zebraengine
zebraengine merged commit c6f3793 into main Sep 15, 2026
1 check passed
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