Summary
On my installation the promotion gate returned NO_GO for a candidate that was significantly better than the serving champion on the shared paired evidence — 7.86 vs 9.64 kWh daily energy MAE, with the 95 % confidence interval of the difference entirely below zero. It was rejected solely on the critical-slice checks, three of which had too few independent days to be evaluated at all.
The evaluation record is stored in full in ai_model_evaluations_v2, so this is not a reconstruction. Everything below is copied from that row.
I also want to raise a design question: with min_independent_eligible_days = 7 and critical_slice_min_independent_days = 3 across 6 weather regimes per cycle, I do not see how all slices can ever be adequately powered.
Environment
|
|
| Home Assistant |
core-2026.8.2 (HAOS) |
| SFML at evaluation time |
V44.0.2 |
| SFML now |
V44.2.0 (installed 2026-08-17 09:21) |
| Plant |
15.30 kWp, Kostal Piko, 3 strings (89°/45°, 269°/44°, 179°/37°) |
| Database |
154 MB, in continuous use since January 2026 |
| Model type |
tiny_lstm |
Timeline
| Date |
Event |
| 2026-07-28 23:33 |
Champion trained, R² 0.930 — still the serving model today |
| 2026-07-29 23:32 |
Candidate 1 71028c55 created, R² 0.857 |
| from 2026-07-30 |
Every night: AI model training skipped: candidate shadow evaluation pending for 71028c55… |
| 2026-08-02 03:02 |
Candidate 2 4c44d872, R² 0.838 |
| 2026-08-03 15:46 |
Candidate 3 768c5a52, R² 0.849 (manual retrain_ai_model call) |
| 2026-08-11 |
contract_version moved model_candidate_shadow_v1 → v2, evidence counter reset |
| 2026-08-16 23:30 |
Evaluation runs, 7/7 independent days reached → NO_GO |
| 2026-08-17 00:25 |
Queue advances to candidate 3, counter back to 0 |
| 2026-08-17 09:21 |
Update to V44.2.0; terminal event no_go written for candidate 1 |
Net effect: the serving model has not changed since 2026-07-28, and system_status reports Last ML training was 20 days ago.
The evaluation record
From ai_model_evaluations_v2, evaluation 5f691d22-266a-482c-a1a7-449e6e7af7b2, contract cycle_aware_model_promotion_v2:
decision: NO_GO
reason_codes: ["CRITICAL_SLICE_REGRESSION", "CRITICAL_SLICE_UNDERPOWERED"]
eligible_hour_count: 198
eligible_independent_day_count: 7
daily_energy_mae: candidate 7.856 champion 9.636
candidate_minus_champion_daily_energy_mae_ci_95: [-3.057, -0.526]
cycle_mean_daily_energy_deltas: post_midnight -1.743 pre_sunrise -1.818
Per-day paired deltas (negative = candidate better): −0.929, −3.252, −2.650, +0.040, −1.800, +0.675, −4.545. The candidate was better on 5 of 7 days and never worse by more than 0.7 kWh.
The critical slices
allowed_daily_energy_regression is 0.0 for all twelve slices, min_independent_days is 3:
| slice |
days |
adequately powered |
delta (kWh) |
| post_midnight · bright_mixed |
1/3 |
false |
+0.0730 |
| post_midnight · dry_cloud_edge |
3/3 |
true |
−1.1560 |
| post_midnight · low_ghi_overcast |
3/3 |
true |
−0.0200 |
| post_midnight · low_sun_uncertain |
2/3 |
false |
+0.0305 |
| post_midnight · mixed_uncertain |
7/3 |
true |
−0.4051 |
| post_midnight · stable_good |
6/3 |
true |
−0.9880 |
| pre_sunrise · bright_mixed |
2/3 |
false |
+0.0430 |
| pre_sunrise · dry_cloud_edge |
3/3 |
true |
−0.5280 |
| pre_sunrise · low_ghi_overcast |
3/3 |
true |
−0.0047 |
| pre_sunrise · low_sun_uncertain |
3/3 |
true |
+0.0204 |
| pre_sunrise · mixed_uncertain |
7/3 |
true |
−0.7953 |
| pre_sunrise · stable_good |
6/3 |
true |
−0.9235 |
Four slices show a positive delta between 20 and 73 Wh. Three of those four are flagged as not adequately powered, so the same slices trigger both reason codes. A model that is on average 1.8 kWh per day better is blocked by differences that are two orders of magnitude smaller and, in three cases, rest on one or two days of data.
The design question
Seven independent days have to be distributed over 6 weather regimes per cycle, and each slice needs at least 3 of them. Seven days can supply 3+ days to at most two regimes. At least four of the six regimes per cycle will therefore be underpowered in any evaluation window, unless the weather is unusually monotonous for a week.
If CRITICAL_SLICE_UNDERPOWERED is blocking on its own, the gate looks structurally unpassable at the current settings. If it is only advisory and CRITICAL_SLICE_REGRESSION did the blocking, then a 20 Wh difference against a 0.0 allowance is doing the same job — with the regression measured on one or two days in three of the four affected slices.
Would it be worth considering one of these:
- ignoring slices that are not adequately powered instead of counting them as regressions,
- a tolerance for
allowed_daily_energy_regression that scales with the noise of the slice rather than a hard 0.0,
- or letting the overall result outweigh slice regressions when the global confidence interval is entirely on the candidate's side.
Secondary observation
The governance has never activated a model on this installation:
ai_active_model_pointer: 0 rows
ai_model_promotion_events: 0 rows
ai_model_evaluations: 0 rows (ai_model_evaluations_v2: 1 row)
and sensor.solar_forecast_ml_ai_metrics reports:
serving_source: legacy_fallback
bootstrap_active: true
So the serving R² 0.930 model is being used through the legacy fallback path rather than through the governance pointer. I mention it because it may be related to the bootstrap issues discussed in the forum and addressed in V44.0.4.
Relation to #185
This is not the same failure as #185. There the candidate never moves and reset_ai_model does not clear it. Here the chain works: shadow runs accumulate, the evaluation runs, a decision is recorded, a terminal event is written and the queue advances. My problem is the content of the decision, not a stuck state.
Reproducing the readout
import sqlite3, json
con = sqlite3.connect("file:/config/solar_forecast_ml/solar_forecast.db?mode=ro", uri=True)
c = con.cursor()
c.execute("SELECT decision, reason_codes_json, decision_json FROM ai_model_evaluations_v2")
dec, reasons, dj = c.fetchone()
d = json.loads(dj)
print(dec, reasons)
print(d["daily_energy_mae"], d["candidate_minus_champion_daily_energy_mae_ci_95"])
for s in d["critical_slice_results"]:
print(s["name"], s["eligible_independent_day_count"], s["adequately_powered"],
round(s["candidate_minus_champion_daily_energy_loss"], 4))
Note on versions
The evaluation above ran under V44.0.2, i.e. before the promotion gate rework in V44.0.4. I have been on V44.2.0 since 2026-08-17, and the next candidate is currently at 1 of 7 days, so the first decision under the reworked gate is due around 2026-08-24. I am reporting this now because the recorded decision documents the slice arithmetic clearly, and because the thresholds in the stored evaluation_config_json may well be unchanged. I will follow up here with the outcome of the next evaluation.
Happy to supply the complete evaluation_config_json and decision_json, the shadow run tables, or anything else that helps.
Summary
On my installation the promotion gate returned
NO_GOfor a candidate that was significantly better than the serving champion on the shared paired evidence — 7.86 vs 9.64 kWh daily energy MAE, with the 95 % confidence interval of the difference entirely below zero. It was rejected solely on the critical-slice checks, three of which had too few independent days to be evaluated at all.The evaluation record is stored in full in
ai_model_evaluations_v2, so this is not a reconstruction. Everything below is copied from that row.I also want to raise a design question: with
min_independent_eligible_days = 7andcritical_slice_min_independent_days = 3across 6 weather regimes per cycle, I do not see how all slices can ever be adequately powered.Environment
Timeline
71028c55created, R² 0.857AI model training skipped: candidate shadow evaluation pending for 71028c55…4c44d872, R² 0.838768c5a52, R² 0.849 (manualretrain_ai_modelcall)contract_versionmovedmodel_candidate_shadow_v1→v2, evidence counter resetNO_GOno_gowritten for candidate 1Net effect: the serving model has not changed since 2026-07-28, and
system_statusreportsLast ML training was 20 days ago.The evaluation record
From
ai_model_evaluations_v2, evaluation5f691d22-266a-482c-a1a7-449e6e7af7b2, contractcycle_aware_model_promotion_v2:Per-day paired deltas (negative = candidate better): −0.929, −3.252, −2.650, +0.040, −1.800, +0.675, −4.545. The candidate was better on 5 of 7 days and never worse by more than 0.7 kWh.
The critical slices
allowed_daily_energy_regressionis0.0for all twelve slices,min_independent_daysis3:Four slices show a positive delta between 20 and 73 Wh. Three of those four are flagged as not adequately powered, so the same slices trigger both reason codes. A model that is on average 1.8 kWh per day better is blocked by differences that are two orders of magnitude smaller and, in three cases, rest on one or two days of data.
The design question
Seven independent days have to be distributed over 6 weather regimes per cycle, and each slice needs at least 3 of them. Seven days can supply 3+ days to at most two regimes. At least four of the six regimes per cycle will therefore be underpowered in any evaluation window, unless the weather is unusually monotonous for a week.
If
CRITICAL_SLICE_UNDERPOWEREDis blocking on its own, the gate looks structurally unpassable at the current settings. If it is only advisory andCRITICAL_SLICE_REGRESSIONdid the blocking, then a 20 Wh difference against a0.0allowance is doing the same job — with the regression measured on one or two days in three of the four affected slices.Would it be worth considering one of these:
allowed_daily_energy_regressionthat scales with the noise of the slice rather than a hard0.0,Secondary observation
The governance has never activated a model on this installation:
and
sensor.solar_forecast_ml_ai_metricsreports:So the serving R² 0.930 model is being used through the legacy fallback path rather than through the governance pointer. I mention it because it may be related to the bootstrap issues discussed in the forum and addressed in V44.0.4.
Relation to #185
This is not the same failure as #185. There the candidate never moves and
reset_ai_modeldoes not clear it. Here the chain works: shadow runs accumulate, the evaluation runs, a decision is recorded, a terminal event is written and the queue advances. My problem is the content of the decision, not a stuck state.Reproducing the readout
Note on versions
The evaluation above ran under V44.0.2, i.e. before the promotion gate rework in V44.0.4. I have been on V44.2.0 since 2026-08-17, and the next candidate is currently at 1 of 7 days, so the first decision under the reworked gate is due around 2026-08-24. I am reporting this now because the recorded decision documents the slice arithmetic clearly, and because the thresholds in the stored
evaluation_config_jsonmay well be unchanged. I will follow up here with the outcome of the next evaluation.Happy to supply the complete
evaluation_config_jsonanddecision_json, the shadow run tables, or anything else that helps.