Files
Enelix-EMS/services/netplan-v4/netplan_v4/history_timing.py
T

59 lines
3.0 KiB
Python

"""Explicit, retrospective equal-endpoint estimates for sparse state publication.
Never changes a source timestamp, real-time freshness limit, meter evidence or
actuator lease. A repeated value after a bounded gap supports a MODEL estimate,
not proof that the physical signal was constant between the observations.
"""
from math import isfinite
import re
def validate_policy(policy, sources):
if policy is None:
return None
if (not isinstance(policy, dict) or set(policy) != {'version', 'method', 'sources'}
or type(policy['version']) is not int or policy['version'] != 1
or policy['method'] != 'equal_endpoint_v1'
or not isinstance(policy['sources'], dict) or not policy['sources']):
raise ValueError('Explicit historical timing policy required')
defined = {s['key']: s for s in sources}
for key, rule in policy['sources'].items():
if key not in defined or defined[key]['role'] not in ('pv', 'physical_storage', 'solar_scale'):
raise ValueError('Timing estimates limited to explicit power/scale sources')
if not isinstance(rule, dict) or set(rule) != {'maxSpanSeconds', 'evidenceId'}:
raise ValueError('Timing bound and evidence reference required')
if type(rule['maxSpanSeconds']) is not int or not defined[key]['maxAgeSeconds'] <= rule['maxSpanSeconds'] <= 180:
raise ValueError('Historical endpoint span outside reviewed bounds')
if not isinstance(rule['evidenceId'], str) or not re.fullmatch(r'[A-Za-z0-9_-]{8,100}', rule['evidenceId']):
raise ValueError('Invalid publication evidence reference')
return policy
def endpoint_bridges(series, first_observed, blocks, capture_gaps, policy, source_specs):
"""Index strictly consecutive, equal original timestamps; never invent endpoints.
Only the stale tail of a normal hold is filled. A missing/invalid capture or a
source conflict forbids bridging. The right endpoint must have been observed;
its availability is retained by the caller for causal training and replay.
"""
result = {key: {} for key in series}
if policy is None:
return result
for key, rule in policy['sources'].items():
if key not in series:
continue # e.g. an unused DC channel in an AC-terminal formula
values = series[key]
times = sorted(values)
for left, right in zip(times, times[1:]):
value, next_value = values[left], values[right]
if (value is None or next_value is None or type(value) not in (int, float)
or not isfinite(value) or value != next_value):
continue
if not source_specs[key]['maxAgeSeconds'] < right-left <= rule['maxSpanSeconds']:
continue
if any(start < right and end > left for start, end in (*blocks[key], *capture_gaps)):
continue
result[key][left] = {'end': right, 'availableAt': first_observed[key][right],
'spanSeconds': right-left}
return result