fix(ml): resolve NaN training loss on startup and preserve database zero entries - #4597
Open
rholligan wants to merge 6 commits into
Open
fix(ml): resolve NaN training loss on startup and preserve database zero entries#4597rholligan wants to merge 6 commits into
rholligan wants to merge 6 commits into
Conversation
…ero entries - Sanitize WindowedFeatures and chunk conversions against NaN, Inf, and None - Use nanmean/nanstd and sanitize scales before np.maximum in feature/target normalization - Add gradient clipping and finite parameter checks in Adam optimizer - Preserve valid 0.0 values in database save/load roundtrips - Fix duplicate training execution and curriculum pass initialization tracking - Add robust unit tests for NaN resilience, zero preservation, and 90-day curriculum
Contributor
There was a problem hiding this comment.
Pull request overview
This PR targets robustness and correctness in Predbat’s ML load forecaster by preventing NaN/Inf contamination during training (especially on startup with imperfect historical data) and by ensuring persisted history round-trips don’t silently drop legitimate 0.0 readings.
Changes:
- Hardened
LoadPredictornormalisation/training math against NaN/Inf and added safety measures (gradient clipping + finite-value clamps). - Changed ML history persistence to use
NaNas the “missing” sentinel so0.0values survive save/load, and removed redundant duplicate curriculum training invocation. - Expanded ML tests to cover NaN/Inf robustness, DB zero preservation, and longer curriculum intermediate-pass behaviour; plus a few cross-platform test runner improvements.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/predbat/load_predictor.py | Adds NaN/Inf sanitisation in normalisation paths and training safety clamps to prevent NaN loss at startup. |
| apps/predbat/load_ml_component.py | Changes DB save/load encoding to preserve explicit zeros and refactors curriculum training invocation. |
| apps/predbat/tests/test_load_ml.py | Adds regression tests for NaN/Inf robustness, DB zero preservation, and 90-day curriculum intermediate passes. |
| apps/predbat/unit_test.py | Forces UTF-8 stdout/stderr in the test runner for cross-platform output stability. |
| apps/predbat/tests/test_sunsynk_api.py | Adds a SIGALRM-less fallback hard timeout implementation for platforms without signal.SIGALRM. |
| apps/predbat/tests/test_plan_why_reason.py | Opens source files with explicit UTF-8 encoding for portability. |
| apps/predbat/tests/test_ml_training_perf.py | Makes resource usage optional so the perf harness can run on platforms without it. |
| apps/predbat/tests/test_download.py | Normalises test output strings and makes the SHA1 fixture use binary mode for consistent LF handling. |
| .cspell/custom-dictionary-workspace.txt | Adds nanmean/nanstd to the spellchecker dictionary. |
Suppressed comments (2)
apps/predbat/load_ml_component.py:840
- dict_to_array() writes arr[idx] = float(value) without guarding against None/non-numeric values. If any history dict contains None (or an unparsable value), save_database_history() will raise TypeError/ValueError and skip persisting the database entirely. Given the ML pipeline explicitly tolerates None/NaN in inputs, the DB save path should treat these as missing and leave the default NaN sentinel in place.
for minute, value in data_dict.items():
# Only persist historical data (non-negative integer keys)
if isinstance(minute, int) and minute >= 0:
idx = minute // PREDICT_STEP
if 0 <= idx < max_steps:
apps/predbat/load_ml_component.py:906
- array_to_dict() decides whether to preserve 0.0 values based on whether the loaded array contains any NaNs. This heuristic fails when a NaN-sparse file happens to have no NaNs (fully-populated history) and will revert to the legacy "0.0 means missing" logic, dropping valid zeros. Use the explicit metadata flag (and keep a fallback heuristic for older files) to choose decoding logic.
has_nans = np.isnan(arr).any()
for i in range(len(arr)):
val = float(arr[i])
if has_nans:
if not np.isnan(val) and np.isfinite(val):
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Author
|
Addressed all automated review findings in commit
|
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.
Summary
Fixes ML load forecaster startup training aborting with
huber_loss=nanat Epoch 1 when historical database records contain missing elements (NaN/None) or valid0.0readings.Why
np.maximum(np.nan, min_std)preservesNaNin standard deviations, causing division by zero/NaN in normalization and corrupting forward passes.dict_to_arrayinitialized arrays with0.0, causingarray_to_dictto drop valid0.0load/PV readings on reload._do_training()triggered duplicate consecutive curriculum passes, and intermediate passes failed to retain model initialization state.What Changed
apps/predbat/load_predictor.py: Hardened feature/target normalization againstNaN/Inf, added gradient clipping ([-10.0, 10.0]), finite weight checks, and preserved curriculum initialization state.apps/predbat/load_ml_component.py: Initialized history arrays withnp.nanto preserve0.0data points, and removed redundant duplicate training call in_do_training().apps/predbat/tests/test_load_ml.py: Added_test_nan_inf_robustness,_test_database_zero_preservation, and_test_curriculum_90day_intermediate_passes.apps/predbat/unit_test.py: Reconfigured console stdout/stderr to UTF-8 for cross-platform test runner stability.Test Plan
_test_nan_inf_robustness,_test_database_zero_preservation, and_test_curriculum_90day_intermediate_passesintests/test_load_ml.py(32/32 tests passed)../run_all --quicksuite passes.pre-commit(black, ruff, cspell, markdownlint) clean.✦ Developed with Google Antigravity