46 lines
2.1 KiB
Python
46 lines
2.1 KiB
Python
"""Forecast family fault isolation; no IO, fabricated samples or model switching."""
|
|
from collections.abc import Mapping
|
|
from math import isfinite
|
|
from numbers import Real
|
|
|
|
|
|
def collect_predictions(data, config, models, enabled):
|
|
"""Return complete finite PV/load series independently for each requested model.
|
|
|
|
A model with insufficient history must not prevent other valid families being
|
|
published. It is omitted, not filled with zeros or replaced by another model.
|
|
Global raw-telemetry checks still run BEFORE this helper in run_forecast.
|
|
"""
|
|
expected = tuple(data['df_fut'].index)
|
|
if not expected or len(set(expected)) != len(expected):
|
|
raise ValueError('Nonempty unique future interval index required')
|
|
expected_keys = set(expected)
|
|
predictions, errors = {}, {}
|
|
requested = 0
|
|
for variant, model in models.items():
|
|
predictions[variant] = {}
|
|
if not enabled(config, variant):
|
|
continue
|
|
requested += 1
|
|
try:
|
|
values = model.predict(data)
|
|
if not isinstance(values, Mapping) or set(values) != expected_keys:
|
|
raise ValueError('Missing, extra or non-matching forecast timestamps')
|
|
cleaned = {}
|
|
for at in expected:
|
|
value = values[at]
|
|
if isinstance(value, bool) or not isinstance(value, Real):
|
|
raise ValueError('Non-numeric forecast power')
|
|
value = float(value)
|
|
if not isfinite(value) or value < 0:
|
|
raise ValueError('Invalid nonnegative forecast power')
|
|
cleaned[at] = value
|
|
predictions[variant] = cleaned
|
|
except Exception as error:
|
|
# Expected data failures and unexpected model failures are visible,
|
|
# but model exception text may contain filesystem paths. Do not leak it.
|
|
errors[variant] = {'status': 'unavailable', 'errorType': type(error).__name__}
|
|
if requested and all(not p for p in predictions.values()):
|
|
raise ValueError('All requested PV/load models failed; no forecasts published')
|
|
return predictions, errors
|