Skip to content

fix(fetch): make the cloud/load divergence model actually modulate - #4871

Open
chalfontchubby wants to merge 1 commit into
mainfrom
fix/cloud-modulation-precedence
Open

fix(fetch): make the cloud/load divergence model actually modulate#4871
chalfontchubby wants to merge 1 commit into
mainfrom
fix/cloud-modulation-precedence

Conversation

@chalfontchubby

Copy link
Copy Markdown
Collaborator

Written by Claude, posted on behalf of @chalfontchubby.

Fixes #4870

step_data_history()'s divergence model picks its phase with

cloud_on = (int((minute + self.minutes_now) / 5) + 1 if flip else 0) % 2

A conditional expression binds looser than +, so that is ((int(...) + 1) if flip else 0) % 2 — a constant 0 whenever flip is False. Every such iteration takes the else branch and subtracts a cloud_diff that never becomes non-zero, so the series comes back untouched.

flip=True is passed by exactly one caller, pv_forecast_minute10_step. So switch.predbat_metric_cloud_enable only ever perturbed the PV10 scenario, never the central PV forecast, and metric_load_divergence did nothing whatsoever. flip only means anything as an antiphase to a modulated base, which gives away the intent: it is the offset that should be conditional, not the whole expression.

This changes plans on the defaults

Both settings default on, so this is a behaviour change for everyone rather than a quiet fix — flagging it prominently because it wants your judgement, not just a review of the diff.

baseline movement
predbat_debug_agile1 trivial — five charge targets by ~4Wh
predbat_debug_pre_saving1 real — a charge window shifts two hours later, its limit drops 5.02 → 3.02kWh, an export target moves 10 → 15%
random benchmark scenarios all 20 move

Baselines regenerated. runtime_s in random_results.json is left at the previous values so the diff shows only genuine changes rather than 20 lines of timing noise.

Testing

New cloud_modulation test module asserting the model actually modulates without flip, that flip produces antiphase, that raises and lowers alternate step by step, and that the total is roughly preserved. Confirmed it fails without the fix:

ERROR: cloud_factor made no difference without flip - the divergence model is inert
       for the central PV and load forecasts
ERROR: expected both series to raise some steps, got 0 plain and 144 flipped

There was no such coverage before — every reference to cloud_factor under apps/predbat/tests/ was a caller replicating the plan setup, so nothing asserted either knob had an effect and the suite passed either way.

test_pv90's clamp check compared the nominal and PV10 load totals against the pure load_scaling ratio. Those arrays take different divergence factors (PV10 is +0.5) and the model does not redistribute exactly — what it adds to a step it can only take back off the next one if that step is large enough — so the ratio no longer holds once divergence is live. Disabled there via metric_load_divergence_enable, since that test is about the clamp, not divergence.

./run_all --quick and ./run_all --test debug_cases both pass.

Notes

Unchanged since a278997 (#1655), a file split, so it likely predates that. No lint rule catches this class — all 918 rules in ruff 0.11.4 under --select ALL are silent on it, and the nearest, RUF021, covers only and/or chaining.

The total-drift behaviour noted above contradicts the loop's own "keeps the same total" comment. Left alone here as pre-existing and separable, but worth knowing it is now live for the load arrays for the first time.

step_data_history()'s divergence model picks the phase with

    cloud_on = (int((minute + self.minutes_now) / 5) + 1 if flip else 0) % 2

A conditional expression binds looser than "+", so that parses as
"((int(...) + 1) if flip else 0) % 2" - a constant 0 whenever flip is False.
Every such iteration takes the else branch and subtracts a cloud_diff that never
becomes non-zero, so nothing is modulated at all.

flip=True is passed by exactly one caller, pv_forecast_minute10_step. So
metric_cloud_enable only ever perturbed the PV10 scenario, never the central PV
forecast, and metric_load_divergence - a documented, user-facing setting - did
nothing whatsoever. flip only means anything as an antiphase to a modulated
base, which is what gives away the intent: it is the offset that is conditional,
not the whole expression.

This changes plans for everyone running the defaults, since both settings
default on. The two golden debug cases move: agile1 only trivially (five charge
targets by ~4Wh), but pre_saving1 genuinely - a charge window shifts two hours
later, its limit drops 5.02 -> 3.02kWh and an export target moves 10 -> 15%.
All twenty random benchmark scenarios move too. Baselines regenerated;
runtime_s left at the previous values so the diff shows only real changes.

test_pv90's clamp check compared the nominal and PV10 load totals against the
pure load_scaling ratio. Those arrays take different divergence factors (PV10 is
+0.5), and the model does not redistribute exactly - what it adds to a step it
can only take back off the next one if that step is large enough - so the ratio
no longer holds once divergence is live. Disabled via
metric_load_divergence_enable there, since that test is about the clamp.

Unchanged since a278997 (#1655), so it likely predates that file split. No lint
rule catches it: checked all 918 rules in ruff 0.11.4 with --select ALL and
nothing flags ternary/arithmetic precedence - RUF021 covers only and/or.

Fixes #4870

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

Cloud/load divergence model is inert: operator precedence makes it a no-op on every array except PV10

1 participant