"""Economic peak policy. Planning assumptions NEVER become metering facts. Monthly marginal cost is convex. A rest-month scenario is a peak expected after this horizon under a comparable control policy, not a configured limit, free allowance, or a promise of savings. With insufficient evidence use full tariff. """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta from math import isclose from .domain import number, utc, month_key, ZURICH VERIFIED_SOURCES = frozenset({'meter_month_register','verified_month_history','verified_new_month'}) ESTIMATE_SOURCES = frozenset({'power_history_estimate','sampled_power_estimate','counter_interval_estimate','operator_estimate','new_month'}) def valid_month(value): if not isinstance(value,str) or len(value)!=7: raise ValueError('Calendar month YYYY-MM required') if datetime.strptime(value,'%Y-%m').strftime('%Y-%m') != value: raise ValueError('Invalid calendar month') return value def basis_record(value, month, at, allow_estimates=False): """Strict provenance; missing stays missing. Caller decides explicit opt-in.""" valid_month(month) if not isinstance(value,dict) or set(value)-{'kw','quality','source','observedAt','notes','coverage','meterId'}: raise ValueError('Invalid peak-basis schema') kw=number(value.get('kw'),'peak basis kW',0,1e6) quality=value.get('quality') source=value.get('source') if quality=='verified': if source not in VERIFIED_SOURCES:raise ValueError('Not a verified peak source') elif quality=='estimated': if not allow_estimates or source not in ESTIMATE_SOURCES: raise ValueError('Estimated planning basis not permitted or source invalid') elif quality=='new_month': if source!='new_month' or kw!=0 or month<=month_key(at): raise ValueError('Zero future-month state is not a past measured peak') else:raise ValueError('Peak quality must be explicit') stamp=utc(value.get('observedAt')) if stamp>utc(at):raise ValueError('Peak evidence from the future') if quality!='new_month' and month>month_key(at): raise ValueError('Future peak is an outlook, not historical evidence') notes=value.get('notes','') if not isinstance(notes,str) or len(notes)>500:raise ValueError('Invalid peak notes') coverage=value.get('coverage') if coverage is not None:number(coverage,'peak coverage',0,1) mid=value.get('meterId') if mid is not None and (not isinstance(mid,str) or not 1<=len(mid)<=160):raise ValueError('Invalid meter identity') return {**value,'kw':kw,'quality':quality,'source':source,'observedAt':stamp.isoformat()} @dataclass(frozen=True) class PeakScenario: future_peak_kw: float probability: float @dataclass(frozen=True) class RestMonthOutlook: month: str scenarios: tuple[PeakScenario, ...] issued_at: datetime future_from: datetime history_until: datetime control_policy_id: str method: str evidence_id: str reliance: float = 0.5 def validate(self, at, horizon_end): valid_month(self.month) at=utc(at);end=utc(horizon_end) if utc(self.issued_at)>at or utc(self.history_until)>utc(self.issued_at): raise ValueError('Rest-month outlook contains future information') if at-utc(self.issued_at)>timedelta(days=2):raise ValueError('Stale rest-month outlook') if utc(self.future_from)=end:return None good={} for r in daily_records: if r.get('controlPolicyId')!=control_policy_id or r.get('complete') is not True:continue if r.get('quality') not in ('verified','estimated'):continue day=datetime.strptime(r['day'],'%Y-%m-%d').replace(tzinfo=ZURICH) finished=utc(day+timedelta(days=1)) observed=utc(r['observedAt']) if finished>utc(at) or observed>utc(at) or observedtimedelta(days=90):continue kw=number(r['peakKw'],'historical daily peak',0,1e6) if r['day'] in good and good[r['day']]!=kw:raise ValueError('Conflicting daily peak evidence') good[r['day']]=kw if len(good)len(longest):longest=segment segment=[] segment.append(day) if len(segment)>len(longest):longest=segment if len(longest)