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): keep the p90 series in step with its p50, clamp the synthesised p90 to the array ceiling, document the weight
Fixes the five findings from the final whole-branch review. pv_metric90_weight
stays at its default of 0.0, so the feature still ships inert; every fix here
protects the expert who opts in.
1. pv_forecast_minute90 could go permanently stale. The old guard only fired on
an EMPTY p90, so a caller that reassigns pv_forecast_minute directly - most
importantly annual.py, which reuses ONE PredBat instance across every sampled
day of a year - pinned every later day's "upside" to the first day's solar,
turning pv90 into a severe downside case. annual.py now assigns the p90 on
every sampled day, and plan.py's guard (now refresh_pv_forecast_minute90())
re-derives the p90 whenever it fails to cover the plan horizon or has been
left behind by a p50 that moved without it. A real p90 that moves with its
p50 is never touched. Production is unaffected: fetch.py always builds all
three series together over one shared minute range.
2. The calibration-synthesised p90 escaped the array-ceiling clamp its published
sibling keeps. best_day_scaling has no floor at 1.0 (1.3 by default with
calibration off, up to 2.0 with it on), so every Open-Meteo and Forecast.solar
user's planner p90 could exceed what the panels can physically produce - and
disagreed with the clamped pv_estimate90 for the same slot. It is now scaled
per slot by min(best_day_scaling, capped_data / capped_p50), mirroring the
published series exactly.
3. pv_metric90_weight was undocumented, leaving load_scaling90's text pointing at
a setting no doc mentioned. Documented in customisation.md alongside its
siblings, and the two apps-yaml.md passages updated.
4. Three of the four weight-0 skip gates were untested - the charge_min_max,
export and levels gates could all be deleted with the suite green, imposing
~50% extra simulation cost on every user at the default weight. The weight-0
test now patches all four launch functions; each gate was sabotaged in turn
and confirmed to fail.
5. The random kernel parity sweep had swapped scenario coverage rather than
adding it (~75/~75 nominal/pv10 became 57/58/35). It now loops all three
scenarios per seed - 150/150/150 - leaving every previously generated
configuration unchanged, for 0.62s -> 1.25s.
Weight-0 plan identity re-confirmed: both debug cases produce byte-identical
output to before these changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
self.log("SolarAPI: PV Calibration: Created pv_estimate10/pv_estimate90 data using worst day scaling factor {} and best day scaling factor {}".format(dp2(worst_day_scaling), dp2(best_day_scaling)))
1232
+
# Use the best day scaling factor to create pv_estimate90, clamped to this slot's
1233
+
# array ceiling. best_day_scaling has no floor at 1.0 and reaches 2.0 (1.3 when
1234
+
# calibration is disabled), so without the clamp the planner's upside case would
1235
+
# predict more solar than the panels can physically produce - and would disagree
1236
+
# with the published pv_estimate90 for the very same slot, which is clamped.
"SolarAPI: PV Calibration: Created pv_estimate10/pv_estimate90 data using worst day scaling factor {} and best day scaling factor {} ({} minutes held at the array ceiling)".format(
0 commit comments