Repository navigation
feat(octopus): soften the PV10 dispatch hedge once the car is charging in it - #5896
Merged
Merged
Conversation
…g in it
In PV10, every Intelligent dispatch minute more than 30 minutes ahead was
assumed gone and paid at rate_max, even while the car was charging in the
dispatch. That made any plan that used the rest of the dispatch about 56p
worse on a real evening, far beyond the 0.1p/kWh bird-in-hand credit.
Once the run is confirmed (io_confirmed_credit set, so the car is charging
in it), a dispatch that goes away is seen within one Predbat cycle. So a
confirmed minute keeps its dispatch, with the battery still held for the
car, and the house and battery imports pay RUN_EVERY minutes of each half
hour at the worst-case rate. The car's own import pays nothing extra,
because the car stops when its dispatch does.
Both engines are changed; kernel parity revision 19.
The bird-in-hand planner test p4 ("credit does not outweigh losses")
assumed the old hedge. With the run confirmed, the lossy battery now fills
and holds through the dispatch rather than cycling, which is the intended
result, so p4 now checks that instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
springfall2008
commented
Oct 31, 2026
springfall2008
left a comment
Owner
Author
There was a problem hiding this comment.
Automated comment from the triage bot.
Reviewed at head 1d5e92a: found no issues — 0 inline, 0 in body.
What I checked:
- Semantics (prediction.py:767-776, 1430-1432): the softening is gated on
dispatch_gone(io_adjusted ∩ PV10 ∩ minute > 30), so nominal/PV90 are untouched;dispatch_riskis reset every loop iteration; the premium correctly excludes the car's share viadiff - car_amount_premiumin bothcar_energy_reported_loadmodes (in bypass mode the car's energy is not indiffat all), andcar_gone_kwhstays 0 on confirmed minutes so the old refund path cannot fight the new premium (they never both fire). - Kernel parity: prediction_kernel.cpp:845-853 and 1460-1463 mirror prediction.py exactly — same minute mapping (
k * PREDICT_STEP + minutes_now, prediction_kernel.py:701-702), same operation order(max(c->rate_max, import_rate) - import_rate) * PK_RUN_EVERY / 30.0, same placement inside the import branch, anddispatch_risk != 0.0matchesif dispatch_risk:. A stale committed binary degrades to the load-time "stale binary" warning plus Python fallback (prediction_kernel.py:359-364) rather than silent wrong numbers, and the kernel-binaries job rebuilds. - Credit-map coupling:
io_confirmed_creditcovers the contiguousio_adjustedrun fromminutes_now(plan.py:628-635); its values are always strictly positive (divisor 1+n), keys align with the prediction loop'sminute_absolute, and a gap in the run falls back to the original full hedge (conservative direction). Confirmed state rides the same dispatch-check decision the existing credit already used, as the docstring documents. - Tests: the h1/h2/h3 expected values match the code's arithmetic exactly (
stored10 * (LOW_RATE + (rate_max - LOW_RATE) * RUN_EVERY / 30)); h3's pre-change failure story is consistent with the fixture (car_charging_from_battery True, reported load, dispatch slots without an "average" key so the slot rate is 0 — pre-change PV10 met the car from the grid while nominal met it from the battery, post-change both take the normal path); the h-suite runs both engines; everything the new tests mutate is restored (skew_SNAPSHOT_ATTRS+STATE_FIELDS, includingprediction_kernel_enable,now_utc_real,minutes_now). Module already registered in TEST_REGISTRY. - Docs/goldens: car-charging.md updated in both the PV10 section and the confirmation section; no new setting, so no CONFIG_ITEMS entry is needed; no committed
coverage/casesdump containsio_confirmed_credit, so no golden shifts.
One calibration observation I considered and did not raise as a finding: the premium is aggregated as 5/30 of every half hour of the run, while the true exposure after a cancellation is a one-off ~5-minute sliver before the replan, so the modelled premium over the run is larger than a single certain cancellation's cost by roughly the run's half-hour count. That is a deliberate score calibration (uniform across plans, 6x closer than #5304's full hedge, and the PR's Effect table shows it prices the motivating evening within 6.9p), not a defect, so I left it to your judgement.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
In the PV10 scenario, Predbat assumes every Intelligent dispatch minute more than 30 minutes ahead is gone, and charges it at
rate_max(#5304). This still happens after the car has been confirmed charging in the dispatch (bird-in-hand, #5635).On a real evening (dispatch 19:00–05:30 at 6.6p, firm off-peak from 23:30), a plan that charges the battery with the car from 19:05 scored 56.3p worse than one that waits for 23:30. The 0.1p/kWh bird-in-hand credit can't come close to that. So the credit only ever moved charging into the first half hour, which the hedge already leaves alone.
Change
Once the run is confirmed (
io_confirmed_creditis set for the minute, meaning a car is charging in an uncancelled dispatch), a dispatch that disappears is noticed within one Predbat cycle. So for a confirmed minute in PV10:dispatch_goneis false and the battery is still held for the car(max(rate_max, rate) - rate) * RUN_EVERY / 30, i.e. 5 minutes of every half hour at the worst-case rateUnconfirmed dispatches keep the full hedge. Nothing changes unless a car is charging in an
io_adjustedrun.Both engines are changed. Kernel parity revision is now 19, with no ABI change: the kernel already receives
io_credit. The binaries will be rebuilt by thekernel-binariesjob.Effect
The same 19:05 evening, scored by hand (lower is better):
On that evening the plan itself doesn't change. The car hold already stops the battery discharging during the dispatch, and the firm window fills it, so waiting is still slightly cheaper.
The change matters when the battery would otherwise power the house through the dispatch and refill later. The bird-in-hand lossy test case is one: it now fills and holds the battery through the dispatch instead of cycling it through its losses.
Tests
run_confirmed_hedge_testsintest_iog_bird_in_hand, on both engines, all against exact expected costs:RUN_EVERY/30of the worst-case premium.kernel_paritypasses against a locally rebuilt library withPREDBAT_KERNEL_REQUIRED=1. Its dispatch-car case runs PV10 with a confirmed credit and the car's load reported, so it exercises both the premium and the car exclusion../run_all --quickpasses. pre-commit passes.Review notes
io_confirmed_credit, so behaviour is unchanged unless a car is charging in a dispatch.io_confirmed_credit_update(). It avoids a new kernel input, at the cost of tying the two together.🤖 Generated with Claude Code