fix(history): model sparse publication without relaxing live freshness
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user