You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(pv90): clamp the synthesised p90 against the array ceiling, not against capped_data
The previous commit's clamp used capped_data, which is
min(ceiling_slot, max(observed_slot, raw_value)) - not the array's physical
ceiling. It collapses to the raw forecast itself whenever the forecast sits
below the ceiling, and capped_p50 then equals capped_data, so the ratio came
out at exactly 1.0 and erased best_day_scaling altogether.
That is worse than the bug it was meant to fix. The create_pv10 path is reached
only by Forecast.solar and Open-Meteo - exactly the users with no real Solcast
p90, and the ones the docs now tell that calibration provides the 90% data.
With calibration not enabled (fewer than 3 valid history days, so a fresh
install or a system that was down) every adjustment is forced to 1.0 and the
p90 would have equalled the p50 exactly, every slot, every day: the scenario
still simulated, still costing planning time, measuring nothing on the axis it
exists for, silently.
Both the planner series and the published pv_estimate90 now clamp against
ceiling_slot, the physical ceiling. The two expressions are the same clamp
algebraically, so they still agree slot for slot as the comment above them
requires. The division is guarded against a zero capped_p50.
test_pv_calibration_capped_data_clamp gains a second scenario where the ceiling
does NOT bind (max_kwh 20.0 against the same 3 kW forecast) and asserts the p90
carries its full 1.3x upside while staying under the array limit. Scenario A
alone could not pin this: with the ceiling binding, a p90 collapsed onto the p50
is indistinguishable from a correctly clamped one. Verified in three states -
no clamp fails the over-ceiling assertion, the capped_data clamp fails the new
non-binding assertion, and the ceiling_slot clamp passes both.
Weight-0 plan identity gate re-confirmed byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
print("ERROR: pv_forecast_minute90 peaks at {} kWh/slot, below the p50 peak of {} kWh/slot - the clamp must hold the upside case AT the ceiling, not below it".format(round(max_slot_p90, 4), round(max_slot_p50, 4)))
3425
3440
failed=True
3426
3441
3442
+
# ---- Scenario B: the array ceiling does NOT bind (max_kwh 20.0 against the same 3 kW forecast) ----
3443
+
# Scenario A alone cannot pin the clamp: there every slot sits AT the ceiling, so a p90 that has
3444
+
# been collapsed onto the p50 is indistinguishable from a correctly clamped one. Here the ceiling
3445
+
# is 8x above the forecast and must not bite at all - the p90 has to carry the full
3446
+
# best_day_scaling upside. Clamping against capped_data instead of the physical ceiling fails
3447
+
# this: capped_data collapses to the raw forecast whenever the forecast is below the ceiling, so
3448
+
# the ratio comes out at exactly 1.0 and the upside is erased on every normally-sized system.
"ERROR: with the array ceiling {} kWh/slot well above the forecast, pv_forecast_minute90 peaks at only {} kWh/slot - expected {} ({}x the {} kWh/slot forecast). The upside has been clamped away by something other than the array limit".format(
print("ERROR: pv_forecast_minute90 peaks at {} kWh/slot, above the array ceiling of {} kWh/slot".format(round(max_slot_p90_open, 4), round(ceiling_open, 4)))
0 commit comments