Skip to content

Name the steps inside the balancer's four longest methods - #641

Draft
tomquist wants to merge 2 commits into
developfrom
claude/astramet-code-refactor-6lnniy
Draft

Name the steps inside the balancer's four longest methods#641
tomquist wants to merge 2 commits into
developfrom
claude/astramet-code-refactor-6lnniy

Conversation

@tomquist

@tomquist tomquist commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Why

LoadBalancer is the one part of the CT002 stack #639 deliberately left alone. The reason was
parity, not that it didn't need the work: balancer.h mirrors the Python
private method names one for one, so restructuring one side means
restructuring both, and that was too much to carry alongside 122 other files.

The file's problem is not its size — a control loop is genuinely large — but
that its longest methods each interleave several decisions that have nothing
to say to one another. Reading _pace_reading meant holding a clock, a
four-branch learning state machine and a clamp in your head at once, and the
only way to see which of the three a line belonged to was to know already.

What this actually trades

Be clear about the cost: the file gets longer. Extraction buys shorter
methods by paying for signatures, and here every signature is paid three times
— Python, .cpp, and the .h declaration.

balancer.py before after
total lines 2698 2766
of which code 1578 1604

What the +68 buys:

before after
_compute_auto_target 126 88
_pace_reading 114 62
compute_target 85 57
_compute_efficiency_deprioritized 92 76
longest method in the file 126 88

Why there is no smaller version of this file

Worth recording, since it is the obvious follow-up question and the answer is
measured rather than assumed:

  • balancer.py is 1605 lines of code. The 2766 figure includes 870 lines
    of comments and docstrings (31% of the file) and 291 blanks.
  • No dead code: every private method has a live caller (AST pass over
    src/ and tests/).
  • No dead configurability: all 24 BalancerConfig knobs are reachable
    from user config, so no branch exists to serve an option nobody can set.
  • No duplication left to collapse: a structural scan (identifiers and
    constants normalised away, 4-statement windows) finds one cross-function
    match, and it is a false positive — runs of self._x = 0.0 in __init__ /
    _reset_window / clear. The triplicated share formula this PR removed was
    the real one.

So the file is not reducible by deletion. It is decomposable: splitting it
into ~8 mirrored modules would take the largest file to ~680 lines and
LoadBalancer from 65 methods to ~28 — but it raises the total by ~7%, and it
is a separate change, not this one.

One correction to an earlier version of this description, which claimed the
probe subsystem was an obvious candidate to lift out on its own: it is not a
clean seam.
_reject_probe rewrites self._priority, _commit_probe
writes _last_rotation, three of them drive fade_weight and the saturation
tracker, and _compute_probe_target calls _emit. Extracted alone it would
be a set of methods that all reach back into the class. It is the probe and
efficiency-rotation machinery together (23 methods) or neither.

What moved (commit 1)

  • _track_saturation — the "is this battery following its commands"
    score, out of compute_target. That method is the balancer's public entry
    point and was spending a third of itself on an aside, comment included.
  • _pace_cap — the ramp-cap state machine (reversal / grow / stall
    escape), split from _pace_reading. The two halves answer different
    questions: what movement has this battery earned versus what do we send
    it now
    . Only the first is a state machine; only the second touches the
    clock.
  • _residual_share — the grid-imbalance slice, out of
    _compute_auto_target. This is where the grid-tracking and pool-balancing
    terms are held apart so only the first is clamped against the grid direction
    (issue Uneven battery charging behavior in version 2.2.1 compared to 2.1.2 #523); it was the densest arithmetic in the file.
  • _probe_active_set_change and _log_role_changes — the tail of the
    efficiency pass, which ended in two unrelated loops over set differences.

What was deleted (commit 2)

Commit 1 made every method shorter and the file longer, which is only half a
refactor. Three things it should have removed:

  • A needless abstraction. _efficiency_weights was a five-line body
    behind eleven lines of signature and docstring. Inlined.
  • A dead guard. faded_adjustments and consumer_id and faded_adjustments.get(...) == 0.0 guards an empty dict that .get already
    handles — an empty mapping returns None, which is not 0.0. Five lines to
    one.
  • A triplicated formula. The weight-proportional share — sum the weights,
    divide, fall back to an even split — was written out three times in Python
    and three times in C++, each with its own spelling of the fallback. It is
    now weighted_share, one free function per stack, used by _fair_share,
    _balance_correction and _concentration_pool_balanced. The C++ helper is
    a template over the three container shapes those sites walk, with three
    one-line key adapters, rather than three copies of the loop.

Parity

Every extraction is mirrored into esphome/components/ct002/balancer.{h,cpp}
under the same name, with the Mirrors balancer.py <name> comment the file
uses. _log_role_changes has no counterpart: that is pre-existing and
deliberate — balancer.{h,cpp} carries no ESPHome includes at all (so two
host build paths can compile it), and the firmware's only balancer logging is
the set_steer_log_sink seam.

Verification

  • uv run ruff format . && uv run ruff check . && uv run mypy src/ — clean.
  • uv run pytest1741 passed, 8 skipped (5 need Docker, 3 are
    Python-only paths).
  • tests/components/ct002/163 passed, including the 45 cross-stack
    balancer parity tests, which build the bare-g++ harness and compare the
    C++ numerically against Python.
  • 146 C++ host gtests across all 8 binaries (40 in host_balancer_test).
  • g++ -std=c++17 -Wall -Wextra -Wshadow -Werror -O2 on balancer.cpp — clean.
  • CI's 14 ct002-esphome-compile targets pass, which is the ESP32 coverage I
    could not run locally (PlatformIO's tool-cmake extracts truncated through
    my sandbox's proxy).

Steering evaluation. Locally the result is byte-identical to the
pre-change baseline, and stayed so after commit 2's expression rewrites — the
same md5 (b024199f0ce47bab4316a7552b84cf5d) for base, commit 1 and commit 2,
33 scenarios × 5 seeds. The baseline was taken from a separate git worktree
at develop with PYTHONPATH overriding the editable install, so it cannot
have picked up the working tree.

The equivalence of the shared weighted_share was also checked directly
rather than only through the simulator: 400,000 random cases × 3 call sites,
each original expression against the helper, compared at the bit level —
zero mismatches, including zero-weight, tiny-weight and negative-total cases.
The equal-split fallback is reachable in only one of the three sites; the
other two are guarded by non-empty preconditions, so folding max(1, n) into
the helper cannot change them.

Note for reviewers: the steering eval is not reproducible in CI

The first CI run on this head reported mixed_cadence/eff moving
(band_crossings_per_h +8%, grid_p2p_w −6%). Re-running the identical
commit produced no movement at all.
From the two job logs:

attempt 1 attempt 2
head settle 43.4, band 25.6, p2p 108.3 settle 43.9, band 23.6, p2p 115.4
base settle 43.9, band 23.6, p2p 115.4 settle 43.9, band 23.6, p2p 115.4

Base is identical across both attempts and matches every local run; only the
head run moved, and only once. The uploaded artifact changed size (25420 →
25356 bytes) on identical inputs.

I could not reproduce the non-determinism locally: 15+ runs, Python 3.11 and
3.13, os.cpu_count() forced to 1–5, fixed PYTHONHASHSEED — all identical,
all matching CI's base. run_scenario reseeds per task
(random.seed(seed)), so the harness is deterministic by construction on my
machine. Something about CI's environment defeats that, and I have not
identified what; it is a pre-existing harness issue rather than something
this PR introduces, and worth its own investigation before the eval is trusted
as a gate on a chaotic scenario.

Checklist

  • Base branch is develop, not main
  • uv run ruff format . && uv run ruff check . && uv run mypy src/ && uv run pytest passes
  • Python ↔ ESPHome parity held — every extraction mirrored under the same name; host gtests, cross-stack parity suite and CI's 14 compile targets all green
  • web/ changes — none
  • User-visible change — none; refactor only, so no CHANGELOG.md entry

🤖 Generated with Claude Code

https://claude.ai/code/session_01MprqHXqPgjAXwWAwWFEevx

`LoadBalancer` carried its control loop in a handful of very long methods,
each mixing several decisions that have nothing to say to one another. Six
of those steps are now methods with names and contracts:

- `_track_saturation` — the "is this battery following its commands" score,
  lifted out of `compute_target`, which is the public entry point and was
  spending a third of itself on an aside.
- `_pace_cap` — the ramp-cap state machine (reversal / grow / stall escape),
  split from `_pace_reading`, which now just measures the poll interval,
  applies the cap and records what was sent. The two halves answer different
  questions: what has this battery earned, and what do we send it now.
- `_efficiency_weights` and `_residual_share` — the share weights and the
  grid-imbalance slice, out of `_compute_auto_target`. `_residual_share` is
  where the tracking and balancing terms are kept apart, which was the
  densest arithmetic in the file and the least self-evident.
- `_probe_active_set_change` and `_log_role_changes` — the tail of the
  efficiency pass, which ended in two unrelated loops over set differences.

Pure code motion: no condition, constant or order of operations changed.
The four-branch residual selection is now a single positive condition
guarding one assignment, which is the same predicate written the short way.

Every extraction is mirrored into `esphome/components/ct002/balancer.{h,cpp}`
under the same names, as the parity rule requires; `_log_role_changes` has no
counterpart because the firmware balancer has no logging beyond the steer
sink, which is a pre-existing deliberate divergence.

Verified behaviour-identical: the steering evaluation over 33 scenarios x 5
seeds produces a byte-identical JSON to the pre-change baseline (same md5),
so no metric moved at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MprqHXqPgjAXwWAwWFEevx
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Steering evaluation (base vs head)

Overall: 0 improved, 0 regressed, 15 unchanged across 15 metrics — mean 0% (unchanged).

Priority: priority-weighted 0% (unchanged) — ✅ no do-no-harm guardrail regressions.

Lower is better for every metric. See src/astrameter/simulator/eval_metrics.py for definitions.

Metrics are the per-scenario mean of 5 seeds.

Aggregate — mean across 33 scenarios

Metric Base Head Δ
settle_mean_s 38.9 38.9 =
settle_p95_s 67.6 67.6 =
unsettled_events 0.7 0.7 =
overshoot_mean_w 50.5 50.5 =
overshoot_max_w 119.6 119.6 =
band_crossings_per_h 290.8 290.8 =
grid_p2p_w 266.4 266.4 =
grid_rms_w 238.4 238.4 =
steady_rms_w 114.0 114.0 =
mean_abs_grid_w 96.3 96.3 =
share_imbalance_w 60.7 60.7 =
avoidable_import_wh 33.6 33.6 =
avoidable_export_wh 25.9 25.9 =
cost_regret_ct 0.76 0.76 =
battery_travel_w_per_h 23102.7 23102.7 =

📊 Interactive grid-power charts (zoom / hover / toggle series) are in the self-contained steering-eval-report.html report — see the link below (it opens directly in the browser).

What do these metrics mean?
Metric Meaning
settle_mean_s Mean seconds after a load/PV step for grid power to return inside the ±25 W settle band and hold for 10 s (reaction speed).
settle_p95_s 95th-percentile settle time — the slow tail of reactions.
unsettled_events Number of disturbance events that never settled within the 10-minute measurement window.
overshoot_mean_w Mean overshoot (W): how far grid power swings past zero to the opposite sign after an event.
overshoot_max_w Worst-case overshoot (W) across all events.
band_crossings_per_h Sign flips per hour across the ±20 W hysteresis band — oscillation / hunting frequency.
grid_p2p_w Sustained peak-to-peak grid swing (95th - 5th percentile) over the whole run — oscillation amplitude. Non-zero whenever the loop keeps hunting, including continuous oscillation the step-response metrics (settle/overshoot) miss.
grid_rms_w RMS grid power (W) over the whole run, transients included — the L2 tracking error: how cleanly the loop held zero, penalising big excursions (overshoot, swings) far harder than a small steady offset. Pairs with battery_travel_w_per_h as the control-effort term.
steady_rms_w RMS grid power (W) during steady state (excluding the 120 s after each event) — residual jitter when nothing is changing.
mean_abs_grid_w Mean absolute grid power (W) over the whole run — overall tracking accuracy.
share_imbalance_w Time-weighted watts misallocated between batteries sharing a phase (sum of each battery's deviation from the even fair share) — 0 when the pool splits load evenly, higher when one battery is left lopsided (issue #523). 0 for scenarios with at most one battery per phase.
avoidable_import_wh Energy imported from the grid (Wh) the battery could have supplied (it had charge and discharge headroom) — missed self-consumption.
avoidable_export_wh Energy exported to the grid (Wh) an AC-chargeable battery could have absorbed (it had room and charge headroom) — missed charging.
cost_regret_ct Money north-star: electricity bill (eurocents, import @ 30 ct/kWh, export @ 8 ct/kWh) over what a perfect-foresight optimal battery would have paid on the same load. Ungameable (both grid directions cost); 0 = matched the optimum. The single number that says how much the controller left on the table.
battery_travel_w_per_h Total absolute change in battery setpoints per hour (W/h) — control effort / actuator wear; lower is smoother.
Per-scenario tables (33 scenarios)
b2500_pair_dc_floor — settle 103.9→103.9s, overshoot 102.1→102.1W, RMS 155.8→155.8W
Metric Base Head Δ
settle_mean_s 103.9 103.9 =
settle_p95_s 247.5 247.5 =
unsettled_events 4.4 4.4 =
overshoot_mean_w 49.9 49.9 =
overshoot_max_w 102.1 102.1 =
band_crossings_per_h 17.4 17.4 =
grid_p2p_w 666.3 666.3 =
grid_rms_w 316.5 316.5 =
steady_rms_w 155.8 155.8 =
mean_abs_grid_w 136.5 136.5 =
share_imbalance_w 55.1 55.1 =
avoidable_import_wh 37.6 37.6 =
avoidable_export_wh 0.0 0.0 =
cost_regret_ct 0.71 0.71 =
battery_travel_w_per_h 16916.8 16916.8 =
full_battery_low_pace — settle 0.0→0.0s, overshoot 0.0→0.0W, RMS 22.5→22.5W
Metric Base Head Δ
settle_mean_s 0.0 0.0 =
settle_p95_s 0.0 0.0 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 0.0 0.0 =
overshoot_max_w 0.0 0.0 =
band_crossings_per_h 0.0 0.0 =
grid_p2p_w 31.0 31.0 =
grid_rms_w 22.5 22.5 =
steady_rms_w 22.5 22.5 =
mean_abs_grid_w 20.3 20.3 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 0.0 0.0 =
avoidable_export_wh 5.1 5.1 =
cost_regret_ct 0.0 0.0 =
battery_travel_w_per_h 7668.0 7668.0 =
mixed_cadence/eff — settle 43.9→43.9s, overshoot 148.2→148.2W, RMS 21.2→21.2W
Metric Base Head Δ
settle_mean_s 43.9 43.9 =
settle_p95_s 63.0 63.0 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 63.0 63.0 =
overshoot_max_w 148.2 148.2 =
band_crossings_per_h 23.6 23.6 =
grid_p2p_w 115.4 115.4 =
grid_rms_w 247.3 247.3 =
steady_rms_w 21.2 21.2 =
mean_abs_grid_w 59.3 59.3 =
share_imbalance_w 209.1 209.1 =
avoidable_import_wh 33.6 33.6 =
avoidable_export_wh 25.8 25.8 =
cost_regret_ct 0.8 0.8 =
battery_travel_w_per_h 30252.6 30252.6 =
mixed_cadence/fair — settle 43.2→43.2s, overshoot 43.6→43.6W, RMS 13.1→13.1W
Metric Base Head Δ
settle_mean_s 43.2 43.2 =
settle_p95_s 60.9 60.9 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 21.2 21.2 =
overshoot_max_w 43.6 43.6 =
band_crossings_per_h 16.8 16.8 =
grid_p2p_w 70.4 70.4 =
grid_rms_w 244.0 244.0 =
steady_rms_w 13.1 13.1 =
mean_abs_grid_w 56.0 56.0 =
share_imbalance_w 89.4 89.4 =
avoidable_import_wh 30.9 30.9 =
avoidable_export_wh 25.0 25.0 =
cost_regret_ct 0.73 0.73 =
battery_travel_w_per_h 26948.8 26948.8 =
mixed_cadence_solar/eff — settle 46.6→46.6s, overshoot 1077.6→1077.6W, RMS 51.6→51.6W
Metric Base Head Δ
settle_mean_s 46.6 46.6 =
settle_p95_s 84.8 84.8 =
unsettled_events 2.4 2.4 =
overshoot_mean_w 133.1 133.1 =
overshoot_max_w 1077.6 1077.6 =
band_crossings_per_h 39.2 39.2 =
grid_p2p_w 144.1 144.1 =
grid_rms_w 266.8 266.8 =
steady_rms_w 51.6 51.6 =
mean_abs_grid_w 74.2 74.2 =
share_imbalance_w 135.0 135.0 =
avoidable_import_wh 52.6 52.6 =
avoidable_export_wh 58.7 58.7 =
cost_regret_ct 1.11 1.11 =
battery_travel_w_per_h 42784.4 42784.4 =
mixed_cadence_solar/fair — settle 51.1→51.1s, overshoot 65.5→65.5W, RMS 22.6→22.6W
Metric Base Head Δ
settle_mean_s 51.1 51.1 =
settle_p95_s 109.3 109.3 =
unsettled_events 1.8 1.8 =
overshoot_mean_w 26.8 26.8 =
overshoot_max_w 65.5 65.5 =
band_crossings_per_h 29.9 29.9 =
grid_p2p_w 92.6 92.6 =
grid_rms_w 252.9 252.9 =
steady_rms_w 22.6 22.6 =
mean_abs_grid_w 66.9 66.9 =
share_imbalance_w 102.6 102.6 =
avoidable_import_wh 47.8 47.8 =
avoidable_export_wh 52.6 52.6 =
cost_regret_ct 1.01 1.01 =
battery_travel_w_per_h 32090.4 32090.4 =
mixed_venus_b2500/eff — settle 108.6→108.6s, overshoot 319.9→319.9W, RMS 28.6→28.6W
Metric Base Head Δ
settle_mean_s 108.6 108.6 =
settle_p95_s 227.0 227.0 =
unsettled_events 3.2 3.2 =
overshoot_mean_w 158.3 158.3 =
overshoot_max_w 319.9 319.9 =
band_crossings_per_h 108.8 108.8 =
grid_p2p_w 101.3 101.3 =
grid_rms_w 198.6 198.6 =
steady_rms_w 28.6 28.6 =
mean_abs_grid_w 50.1 50.1 =
share_imbalance_w 270.9 270.9 =
avoidable_import_wh 52.9 52.9 =
avoidable_export_wh 22.1 22.1 =
cost_regret_ct 1.41 1.41 =
battery_travel_w_per_h 46399.0 46399.0 =
mixed_venus_b2500/fair — settle 128.0→128.0s, overshoot 319.0→319.0W, RMS 37.6→37.6W
Metric Base Head Δ
settle_mean_s 128.0 128.0 =
settle_p95_s 242.9 242.9 =
unsettled_events 3.4 3.4 =
overshoot_mean_w 178.3 178.3 =
overshoot_max_w 319.0 319.0 =
band_crossings_per_h 115.7 115.7 =
grid_p2p_w 96.0 96.0 =
grid_rms_w 194.3 194.3 =
steady_rms_w 37.6 37.6 =
mean_abs_grid_w 58.5 58.5 =
share_imbalance_w 144.0 144.0 =
avoidable_import_wh 66.7 66.7 =
avoidable_export_wh 21.1 21.1 =
cost_regret_ct 1.83 1.83 =
battery_travel_w_per_h 41798.0 41798.0 =
phase_imbalance — settle 60.0→60.0s, overshoot 163.7→163.7W, RMS 30.3→30.3W
Metric Base Head Δ
settle_mean_s 60.0 60.0 =
settle_p95_s 135.1 135.1 =
unsettled_events 1.0 1.0 =
overshoot_mean_w 85.2 85.2 =
overshoot_max_w 163.7 163.7 =
band_crossings_per_h 67.2 67.2 =
grid_p2p_w 41.8 41.8 =
grid_rms_w 191.6 191.6 =
steady_rms_w 30.3 30.3 =
mean_abs_grid_w 38.2 38.2 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 24.0 24.0 =
avoidable_export_wh 14.2 14.2 =
cost_regret_ct 0.61 0.61 =
battery_travel_w_per_h 18784.2 18784.2 =
single_venus_d_solar — settle 23.7→23.7s, overshoot 83.2→83.2W, RMS 16.0→16.0W
Metric Base Head Δ
settle_mean_s 23.7 23.7 =
settle_p95_s 27.9 27.9 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 72.0 72.0 =
overshoot_max_w 83.2 83.2 =
band_crossings_per_h 19.7 19.7 =
grid_p2p_w 33.4 33.4 =
grid_rms_w 103.0 103.0 =
steady_rms_w 16.0 16.0 =
mean_abs_grid_w 18.4 18.4 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 12.5 12.5 =
avoidable_export_wh 15.1 15.1 =
cost_regret_ct 0.26 0.26 =
battery_travel_w_per_h 7657.8 7657.8 =
single_venus_d_steps — settle 25.2→25.2s, overshoot 86.6→86.6W, RMS 14.6→14.6W
Metric Base Head Δ
settle_mean_s 25.2 25.2 =
settle_p95_s 31.4 31.4 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 70.6 70.6 =
overshoot_max_w 86.6 86.6 =
band_crossings_per_h 19.0 19.0 =
grid_p2p_w 24.7 24.7 =
grid_rms_w 256.5 256.5 =
steady_rms_w 14.6 14.6 =
mean_abs_grid_w 57.4 57.4 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 30.6 30.6 =
avoidable_export_wh 26.7 26.7 =
cost_regret_ct 0.71 0.71 =
battery_travel_w_per_h 19146.0 19146.0 =
single_venus_d_washer — settle 0.0→0.0s, overshoot 0.0→0.0W, RMS 59.9→59.9W
Metric Base Head Δ
settle_mean_s 0.0 0.0 =
settle_p95_s 0.0 0.0 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 0.0 0.0 =
overshoot_max_w 0.0 0.0 =
band_crossings_per_h 320.4 320.4 =
grid_p2p_w 188.8 188.8 =
grid_rms_w 59.9 59.9 =
steady_rms_w 59.9 59.9 =
mean_abs_grid_w 40.9 40.9 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 12.1 12.1 =
avoidable_export_wh 8.3 8.3 =
cost_regret_ct 0.3 0.3 =
battery_travel_w_per_h 21397.6 21397.6 =
single_venus_drain — settle 0.0→0.0s, overshoot 0.0→0.0W, RMS 907.3→907.3W
Metric Base Head Δ
settle_mean_s 0.0 0.0 =
settle_p95_s 0.0 0.0 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 0.0 0.0 =
overshoot_max_w 0.0 0.0 =
band_crossings_per_h 68.9 68.9 =
grid_p2p_w 1597.5 1597.5 =
grid_rms_w 907.3 907.3 =
steady_rms_w 907.3 907.3 =
mean_abs_grid_w 645.3 645.3 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 14.4 14.4 =
avoidable_export_wh 10.2 10.2 =
cost_regret_ct 0.21 0.21 =
battery_travel_w_per_h 4043.2 4043.2 =
single_venus_fill — settle 360.0→360.0s, overshoot 0.0→0.0W, RMS 953.6→953.6W
Metric Base Head Δ
settle_mean_s 360.0 360.0 =
settle_p95_s 600.0 600.0 =
unsettled_events 4.0 4.0 =
overshoot_mean_w 0.0 0.0 =
overshoot_max_w 0.0 0.0 =
band_crossings_per_h 18.7 18.7 =
grid_p2p_w 1715.6 1715.6 =
grid_rms_w 978.7 978.7 =
steady_rms_w 953.6 953.6 =
mean_abs_grid_w 662.5 662.5 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 4.1 4.1 =
avoidable_export_wh 6.7 6.7 =
cost_regret_ct 0.12 0.12 =
battery_travel_w_per_h 3504.8 3504.8 =
single_venus_noisy — settle 0.0→0.0s, overshoot 0.0→0.0W, RMS 94.0→94.0W
Metric Base Head Δ
settle_mean_s 0.0 0.0 =
settle_p95_s 0.0 0.0 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 0.0 0.0 =
overshoot_max_w 0.0 0.0 =
band_crossings_per_h 1456.6 1456.6 =
grid_p2p_w 294.2 294.2 =
grid_rms_w 94.0 94.0 =
steady_rms_w 94.0 94.0 =
mean_abs_grid_w 78.9 78.9 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 43.5 43.5 =
avoidable_export_wh 35.4 35.4 =
cost_regret_ct 1.02 1.02 =
battery_travel_w_per_h 20892.8 20892.8 =
single_venus_pv — settle 0.0→0.0s, overshoot 0.0→0.0W, RMS 59.2→59.2W
Metric Base Head Δ
settle_mean_s 0.0 0.0 =
settle_p95_s 0.0 0.0 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 0.0 0.0 =
overshoot_max_w 0.0 0.0 =
band_crossings_per_h 42.4 42.4 =
grid_p2p_w 44.1 44.1 =
grid_rms_w 59.2 59.2 =
steady_rms_w 59.2 59.2 =
mean_abs_grid_w 17.3 17.3 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 8.6 8.6 =
avoidable_export_wh 17.3 17.3 =
cost_regret_ct 0.12 0.12 =
battery_travel_w_per_h 7132.6 7132.6 =
single_venus_solar — settle 26.0→26.0s, overshoot 93.0→93.0W, RMS 17.8→17.8W
Metric Base Head Δ
settle_mean_s 26.0 26.0 =
settle_p95_s 31.3 31.3 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 78.7 78.7 =
overshoot_max_w 93.0 93.0 =
band_crossings_per_h 29.6 29.6 =
grid_p2p_w 42.7 42.7 =
grid_rms_w 104.0 104.0 =
steady_rms_w 17.8 17.8 =
mean_abs_grid_w 20.6 20.6 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 13.9 13.9 =
avoidable_export_wh 17.0 17.0 =
cost_regret_ct 0.28 0.28 =
battery_travel_w_per_h 7467.0 7467.0 =
single_venus_solar_slow — settle 34.0→34.0s, overshoot 66.2→66.2W, RMS 22.7→22.7W
Metric Base Head Δ
settle_mean_s 34.0 34.0 =
settle_p95_s 42.3 42.3 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 31.6 31.6 =
overshoot_max_w 66.2 66.2 =
band_crossings_per_h 8.8 8.8 =
grid_p2p_w 61.1 61.1 =
grid_rms_w 127.6 127.6 =
steady_rms_w 22.7 22.7 =
mean_abs_grid_w 30.7 30.7 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 19.6 19.6 =
avoidable_export_wh 26.4 26.4 =
cost_regret_ct 0.38 0.38 =
battery_travel_w_per_h 6465.2 6465.2 =
single_venus_steps — settle 25.2→25.2s, overshoot 86.6→86.6W, RMS 14.6→14.6W
Metric Base Head Δ
settle_mean_s 25.2 25.2 =
settle_p95_s 31.4 31.4 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 70.6 70.6 =
overshoot_max_w 86.6 86.6 =
band_crossings_per_h 19.0 19.0 =
grid_p2p_w 24.7 24.7 =
grid_rms_w 256.5 256.5 =
steady_rms_w 14.6 14.6 =
mean_abs_grid_w 57.4 57.4 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 30.6 30.6 =
avoidable_export_wh 26.7 26.7 =
cost_regret_ct 0.71 0.71 =
battery_travel_w_per_h 19146.0 19146.0 =
single_venus_steps_slow — settle 41.1→41.1s, overshoot 101.9→101.9W, RMS 14.7→14.7W
Metric Base Head Δ
settle_mean_s 41.1 41.1 =
settle_p95_s 59.3 59.3 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 36.6 36.6 =
overshoot_max_w 101.9 101.9 =
band_crossings_per_h 10.6 10.6 =
grid_p2p_w 78.7 78.7 =
grid_rms_w 323.1 323.1 =
steady_rms_w 14.7 14.7 =
mean_abs_grid_w 84.8 84.8 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 44.7 44.7 =
avoidable_export_wh 40.1 40.1 =
cost_regret_ct 1.02 1.02 =
battery_travel_w_per_h 17760.8 17760.8 =
single_venus_trace — settle 0.0→0.0s, overshoot 0.0→0.0W, RMS 274.3→274.3W
Metric Base Head Δ
settle_mean_s 0.0 0.0 =
settle_p95_s 0.0 0.0 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 0.0 0.0 =
overshoot_max_w 0.0 0.0 =
band_crossings_per_h 275.6 275.6 =
grid_p2p_w 712.1 712.1 =
grid_rms_w 274.0 274.0 =
steady_rms_w 274.3 274.3 =
mean_abs_grid_w 119.7 119.7 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 74.8 74.8 =
avoidable_export_wh 44.3 44.3 =
cost_regret_ct 1.11 1.11 =
battery_travel_w_per_h 37922.8 37922.8 =
single_venus_washer — settle 0.0→0.0s, overshoot 0.0→0.0W, RMS 59.9→59.9W
Metric Base Head Δ
settle_mean_s 0.0 0.0 =
settle_p95_s 0.0 0.0 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 0.0 0.0 =
overshoot_max_w 0.0 0.0 =
band_crossings_per_h 320.4 320.4 =
grid_p2p_w 188.8 188.8 =
grid_rms_w 59.9 59.9 =
steady_rms_w 59.9 59.9 =
mean_abs_grid_w 40.9 40.9 =
share_imbalance_w 0.0 0.0 =
avoidable_import_wh 12.1 12.1 =
avoidable_export_wh 8.3 8.3 =
cost_regret_ct 0.3 0.3 =
battery_travel_w_per_h 21397.6 21397.6 =
two_venus/eff — settle 17.2→17.2s, overshoot 124.1→124.1W, RMS 14.3→14.3W
Metric Base Head Δ
settle_mean_s 17.2 17.2 =
settle_p95_s 21.7 21.7 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 79.3 79.3 =
overshoot_max_w 124.1 124.1 =
band_crossings_per_h 28.8 28.8 =
grid_p2p_w 24.2 24.2 =
grid_rms_w 210.7 210.7 =
steady_rms_w 14.3 14.3 =
mean_abs_grid_w 39.7 39.7 =
share_imbalance_w 169.4 169.4 =
avoidable_import_wh 22.8 22.8 =
avoidable_export_wh 16.9 16.9 =
cost_regret_ct 0.55 0.55 =
battery_travel_w_per_h 21763.0 21763.0 =
two_venus/fair — settle 17.5→17.5s, overshoot 122.4→122.4W, RMS 14.2→14.2W
Metric Base Head Δ
settle_mean_s 17.5 17.5 =
settle_p95_s 23.6 23.6 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 97.1 97.1 =
overshoot_max_w 122.4 122.4 =
band_crossings_per_h 17.6 17.6 =
grid_p2p_w 22.4 22.4 =
grid_rms_w 207.6 207.6 =
steady_rms_w 14.2 14.2 =
mean_abs_grid_w 38.6 38.6 =
share_imbalance_w 27.2 27.2 =
avoidable_import_wh 21.7 21.7 =
avoidable_export_wh 16.9 16.9 =
cost_regret_ct 0.51 0.51 =
battery_travel_w_per_h 19226.6 19226.6 =
two_venus_noisy/eff — settle 0.0→0.0s, overshoot 0.0→0.0W, RMS 94.2→94.2W
Metric Base Head Δ
settle_mean_s 0.0 0.0 =
settle_p95_s 0.0 0.0 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 0.0 0.0 =
overshoot_max_w 0.0 0.0 =
band_crossings_per_h 2894.0 2894.0 =
grid_p2p_w 292.5 292.5 =
grid_rms_w 94.3 94.3 =
steady_rms_w 94.2 94.2 =
mean_abs_grid_w 79.4 79.4 =
share_imbalance_w 23.7 23.7 =
avoidable_import_wh 45.5 45.5 =
avoidable_export_wh 33.8 33.8 =
cost_regret_ct 1.09 1.09 =
battery_travel_w_per_h 21265.2 21265.2 =
two_venus_noisy/fair — settle 0.0→0.0s, overshoot 0.0→0.0W, RMS 94.0→94.0W
Metric Base Head Δ
settle_mean_s 0.0 0.0 =
settle_p95_s 0.0 0.0 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 0.0 0.0 =
overshoot_max_w 0.0 0.0 =
band_crossings_per_h 2903.2 2903.2 =
grid_p2p_w 292.3 292.3 =
grid_rms_w 94.1 94.1 =
steady_rms_w 94.0 94.0 =
mean_abs_grid_w 79.3 79.3 =
share_imbalance_w 24.1 24.1 =
avoidable_import_wh 45.4 45.4 =
avoidable_export_wh 33.9 33.9 =
cost_regret_ct 1.09 1.09 =
battery_travel_w_per_h 21041.6 21041.6 =
two_venus_slow/fair — settle 41.4→41.4s, overshoot 19.6→19.6W, RMS 14.1→14.1W
Metric Base Head Δ
settle_mean_s 41.4 41.4 =
settle_p95_s 52.7 52.7 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 14.8 14.8 =
overshoot_max_w 19.6 19.6 =
band_crossings_per_h 10.6 10.6 =
grid_p2p_w 73.9 73.9 =
grid_rms_w 305.3 305.3 =
steady_rms_w 14.1 14.1 =
mean_abs_grid_w 80.0 80.0 =
share_imbalance_w 24.3 24.3 =
avoidable_import_wh 41.6 41.6 =
avoidable_export_wh 38.3 38.3 =
cost_regret_ct 0.94 0.94 =
battery_travel_w_per_h 17596.2 17596.2 =
two_venus_solar/eff — settle 25.9→25.9s, overshoot 534.9→534.9W, RMS 20.6→20.6W
Metric Base Head Δ
settle_mean_s 25.9 25.9 =
settle_p95_s 45.7 45.7 =
unsettled_events 1.6 1.6 =
overshoot_mean_w 124.1 124.1 =
overshoot_max_w 534.9 534.9 =
band_crossings_per_h 51.2 51.2 =
grid_p2p_w 58.9 58.9 =
grid_rms_w 218.4 218.4 =
steady_rms_w 20.6 20.6 =
mean_abs_grid_w 48.9 48.9 =
share_imbalance_w 71.4 71.4 =
avoidable_import_wh 36.3 36.3 =
avoidable_export_wh 37.0 37.0 =
cost_regret_ct 0.79 0.79 =
battery_travel_w_per_h 27471.0 27471.0 =
two_venus_solar/fair — settle 25.3→25.3s, overshoot 143.8→143.8W, RMS 20.4→20.4W
Metric Base Head Δ
settle_mean_s 25.3 25.3 =
settle_p95_s 48.6 48.6 =
unsettled_events 1.4 1.4 =
overshoot_mean_w 100.4 100.4 =
overshoot_max_w 143.8 143.8 =
band_crossings_per_h 34.4 34.4 =
grid_p2p_w 59.4 59.4 =
grid_rms_w 214.1 214.1 =
steady_rms_w 20.4 20.4 =
mean_abs_grid_w 47.2 47.2 =
share_imbalance_w 30.6 30.6 =
avoidable_import_wh 34.2 34.2 =
avoidable_export_wh 36.6 36.6 =
cost_regret_ct 0.73 0.73 =
battery_travel_w_per_h 24313.2 24313.2 =
two_venus_trace/eff — settle 0.0→0.0s, overshoot 0.0→0.0W, RMS 285.0→285.0W
Metric Base Head Δ
settle_mean_s 0.0 0.0 =
settle_p95_s 0.0 0.0 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 0.0 0.0 =
overshoot_max_w 0.0 0.0 =
band_crossings_per_h 294.0 294.0 =
grid_p2p_w 784.0 784.0 =
grid_rms_w 283.9 283.9 =
steady_rms_w 285.0 285.0 =
mean_abs_grid_w 125.7 125.7 =
share_imbalance_w 397.5 397.5 =
avoidable_import_wh 74.7 74.7 =
avoidable_export_wh 51.0 51.0 =
cost_regret_ct 1.84 1.84 =
battery_travel_w_per_h 56639.6 56639.6 =
two_venus_trace/fair — settle 0.0→0.0s, overshoot 0.0→0.0W, RMS 284.8→284.8W
Metric Base Head Δ
settle_mean_s 0.0 0.0 =
settle_p95_s 0.0 0.0 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 0.0 0.0 =
overshoot_max_w 0.0 0.0 =
band_crossings_per_h 288.0 288.0 =
grid_p2p_w 772.4 772.4 =
grid_rms_w 283.7 283.7 =
steady_rms_w 284.8 284.8 =
mean_abs_grid_w 125.1 125.1 =
share_imbalance_w 31.1 31.1 =
avoidable_import_wh 74.3 74.3 =
avoidable_export_wh 50.8 50.8 =
cost_regret_ct 1.82 1.82 =
battery_travel_w_per_h 54480.2 54480.2 =
venus_d_plus_c/eff — settle 17.2→17.2s, overshoot 122.0→122.0W, RMS 14.3→14.3W
Metric Base Head Δ
settle_mean_s 17.2 17.2 =
settle_p95_s 21.7 21.7 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 79.2 79.2 =
overshoot_max_w 122.0 122.0 =
band_crossings_per_h 28.4 28.4 =
grid_p2p_w 24.2 24.2 =
grid_rms_w 210.7 210.7 =
steady_rms_w 14.3 14.3 =
mean_abs_grid_w 39.8 39.8 =
share_imbalance_w 169.9 169.9 =
avoidable_import_wh 22.9 22.9 =
avoidable_export_wh 16.9 16.9 =
cost_regret_ct 0.55 0.55 =
battery_travel_w_per_h 21789.8 21789.8 =
venus_d_plus_c/fair — settle 17.5→17.5s, overshoot 122.4→122.4W, RMS 14.2→14.2W
Metric Base Head Δ
settle_mean_s 17.5 17.5 =
settle_p95_s 23.6 23.6 =
unsettled_events 0.0 0.0 =
overshoot_mean_w 97.1 97.1 =
overshoot_max_w 122.4 122.4 =
band_crossings_per_h 17.6 17.6 =
grid_p2p_w 22.4 22.4 =
grid_rms_w 207.6 207.6 =
steady_rms_w 14.2 14.2 =
mean_abs_grid_w 38.6 38.6 =
share_imbalance_w 27.2 27.2 =
avoidable_import_wh 21.7 21.7 =
avoidable_export_wh 16.9 16.9 =
cost_regret_ct 0.51 0.51 =
battery_travel_w_per_h 19226.6 19226.6 =

📊 Open the interactive reportsteering-eval-report.html, a single self-contained file (opens in-browser; download it if your browser blocks inline scripts).

…ormula

The previous commit made every method shorter and the file longer, which is
only half a refactor. Three things it should have removed:

- `_efficiency_weights` was a five-line body behind eleven lines of signature
  and docstring — an abstraction that cost more than it held. Inlined.
- `faded_adjustments and consumer_id and faded_adjustments.get(...) == 0.0`
  guards an empty dict that `.get` already handles: an empty mapping returns
  `None`, which is not `0.0`. Five lines to one.
- The weight-proportional share formula — sum the weights, divide, fall back
  to an even split — was written out three times in Python and three times in
  C++, each with its own spelling of the same fallback. It is now
  `weighted_share`, one free function per stack, used by `_fair_share`,
  `_balance_correction` and `_concentration_pool_balanced`.

The C++ helper is a template over the three container shapes those sites walk
(a report map, a vector of ids, a vector of id pointers) with three one-line
key adapters, rather than three copies of the loop.

Behaviour is unchanged and checked the same way: the steering evaluation over
33 scenarios x 5 seeds still produces a JSON byte-identical to the pre-change
baseline (md5 b024199f0ce47bab4316a7552b84cf5d). The equal-split fallbacks are
reachable in only one of the three sites; the other two are guarded by
non-empty preconditions, so folding `max(1, n)` into the shared helper cannot
change them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MprqHXqPgjAXwWAwWFEevx
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