Model/domain/baseline/performance.py
Khalim Conn-Kowlessar 76717dfc3a feat(baseline): BaselineOrchestrator + BaselinePerformance aggregate (#1135)
Stage 2 of First Run. Establishes each Property's Baseline Performance
from persisted source data and writes it back — reads only from repos,
never a Fetcher or HTTP (ADR-0003), so it is byte-identical whether
Ingestion ran milliseconds ago or last week.

Domain (`domain/baseline/`):
- `Performance` VO — the four rated quantities: SAP / EPC Band / CO2 /
  Primary Energy Intensity. `lodged_performance(epc)` reads them off the
  EPC's recorded fields (PEUI = `energy_consumption_current`).
- `BaselinePerformance` (ADR-0004) — the paired `lodged` + `effective`
  Performance + `rebaseline_reason`, plus the no-derivation part of the
  energy block (`space_heating_kwh` / `water_heating_kwh`, off the RHI,
  deterministic per ADR-0006). Both halves always populated.
- `Rebaseliner` port + `StubRebaseliner`: the re-score-on-override seam
  (ADR-0011). SAP10 certs pass through (effective == lodged, reason
  "none"); a pre-SAP10 cert raises `RebaselineNotImplemented` rather
  than fabricating a plausible-but-wrong "none" — ML rebaselining is not
  wired yet. Mirrors the repo's strict-raise culture.

Persistence: new `BaselineRepository` port + `BaselinePostgresRepository`
+ flat-column `baseline_performance` SQLModel (one row per Property). Per
ADR-0004's amendment this is a standalone table, NOT columns on the
retiring `property_details_epc`. Production migration is FE-owned
(Drizzle) — docs/migrations/baseline-performance-table.md.

Docs (grill-with-docs): corrected CONTEXT.md Lodged/Effective Performance
to Primary Energy Intensity (the term collided with its own _Avoid_ entry
under "heat demand") + fixed stale RHI field names; amended ADR-0004
Consequences for the standalone-table decision.

Fuel split + bills (rest of EPC Energy Derivation) deferred to a
follow-up — they need a Fuel Rates source (Ofgem-cap ETL) that does not
exist yet.

TDD, one test -> one impl: 7 tests (lodged read, rebaseliner pass-through
+ raise, orchestrator establish-and-persist + pre-SAP10 raise, Postgres
round-trip + absent). pyright strict clean; AAA layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:21:34 +00:00

53 lines
1.8 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, TypeVar
from datatypes.epc.domain.epc import Epc
from datatypes.epc.domain.epc_property_data import EpcPropertyData
_T = TypeVar("_T")
@dataclass(frozen=True)
class Performance:
"""One half of a Baseline Performance — a single set of SAP10 figures.
The four quantities a Property is rated on (CONTEXT.md: Lodged / Effective
Performance): SAP score, EPC Band, carbon emissions, and Primary Energy
Intensity. Used for both the Lodged half (off the gov register) and the
Effective half (what the modelling pipeline scored against).
"""
sap_score: int
epc_band: Epc
co2_emissions: float
primary_energy_intensity: int
def _require(value: Optional[_T], field: str) -> _T:
if value is None:
raise ValueError(
f"EPC is missing recorded performance field {field!r}; "
"cannot establish Lodged Performance"
)
return value
def lodged_performance(epc: EpcPropertyData) -> Performance:
"""The Lodged Performance recorded on an EPC — what the gov register says.
Reads the four rated quantities straight off the EPC's recorded fields
(CONTEXT.md: Primary Energy Intensity is recorded as `energy_consumption_current`).
Unmodified by modelling.
"""
return Performance(
sap_score=_require(epc.energy_rating_current, "energy_rating_current"),
epc_band=_require(
epc.current_energy_efficiency_band, "current_energy_efficiency_band"
),
co2_emissions=_require(epc.co2_emissions_current, "co2_emissions_current"),
primary_energy_intensity=_require(
epc.energy_consumption_current, "energy_consumption_current"
),
)