mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-08-03 05:18:22 +00:00
feat(overrides): GREEN — thread Boiler Efficiency Band to the overlay call site
ResolvedPropertyOverride now carries original_spreadsheet_description (populated by both postgres readers). overlays_from builds the main-heating overlay directly via _main_heating_overlay, resolving the band: an explicit boiler_efficiency_band row wins, else parse-on-read from the heating description (ships before FE). Band applied after the ADR-0067 fuel re-point. 28 overlay tests green, pyright clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e91e714221
commit
ce803ecedc
3 changed files with 73 additions and 12 deletions
|
|
@ -44,13 +44,20 @@ from domain.epc.property_overlays.main_heating_system_overlay import (
|
|||
main_heating_overlay_for,
|
||||
natural_fuel_for,
|
||||
)
|
||||
from domain.epc.property_overrides.boiler_efficiency_band import BoilerEfficiencyBand
|
||||
from domain.epc.property_overrides.boiler_efficiency_band_guard import (
|
||||
boiler_efficiency_band_guard,
|
||||
)
|
||||
from domain.epc.property_overlays.water_heating_overlay import (
|
||||
water_heating_overlay_for,
|
||||
)
|
||||
from domain.epc.property_overlays.roof_type_overlay import roof_overlay_for
|
||||
from domain.epc.property_overlays.wall_type_overlay import wall_overlay_for
|
||||
from domain.modelling.simulation import EpcSimulation
|
||||
from repositories.property.property_overrides_reader import ResolvedPropertyOverrides
|
||||
from repositories.property.property_overrides_reader import (
|
||||
ResolvedPropertyOverride,
|
||||
ResolvedPropertyOverrides,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -87,23 +94,70 @@ def overlays_from(overrides: ResolvedPropertyOverrides) -> list[EpcSimulation]:
|
|||
overrides.rows, key=lambda row: row.override_component in _APPLY_LAST
|
||||
)
|
||||
for row in ordered_rows:
|
||||
mapper = _COMPONENT_OVERLAYS.get(row.override_component)
|
||||
if mapper is None:
|
||||
continue
|
||||
value = row.override_value
|
||||
if row.override_component == "main_heating_system":
|
||||
# A fuel-agnostic gas-boiler archetype re-points to the oil / solid-fuel
|
||||
# member matching this property's `main_fuel`, so an oil/solid boiler
|
||||
# scores its own code — not the gas default the LLM assigned (ADR-0067).
|
||||
value = resolve_boiler_archetype(
|
||||
value, _override_value(overrides, "main_fuel")
|
||||
)
|
||||
overlay = mapper(value, row.building_part)
|
||||
# The main-heating overlay carries two joins the generic mapper can't:
|
||||
# the fuel re-point (ADR-0067) and the Boiler Efficiency Band slot
|
||||
# (ADR-0068), so it is built directly rather than via _COMPONENT_OVERLAYS.
|
||||
overlay = _main_heating_overlay(overrides, row)
|
||||
else:
|
||||
mapper = _COMPONENT_OVERLAYS.get(row.override_component)
|
||||
if mapper is None:
|
||||
# `boiler_efficiency_band` rows carry no standalone overlay — they
|
||||
# are consumed by the main_heating_system row above — so they, and
|
||||
# any other unmapped component, produce nothing here.
|
||||
continue
|
||||
overlay = mapper(row.override_value, row.building_part)
|
||||
if overlay is not None:
|
||||
overlays.append(overlay)
|
||||
return overlays
|
||||
|
||||
|
||||
def _main_heating_overlay(
|
||||
overrides: ResolvedPropertyOverrides, row: ResolvedPropertyOverride
|
||||
) -> Optional[EpcSimulation]:
|
||||
"""The main-heating overlay for one row, with its two resolution-time joins:
|
||||
the fuel re-point (a fuel-agnostic gas-boiler archetype → the oil/solid-fuel
|
||||
member matching the property's `main_fuel`, ADR-0067) and the SEDBUK Boiler
|
||||
Efficiency Band → `(winter, summer)` efficiency slot (ADR-0068)."""
|
||||
value = resolve_boiler_archetype(
|
||||
row.override_value, _override_value(overrides, "main_fuel")
|
||||
)
|
||||
band = _boiler_efficiency_band_for(overrides, row)
|
||||
return main_heating_overlay_for(value, row.building_part, band)
|
||||
|
||||
|
||||
def _boiler_efficiency_band_for(
|
||||
overrides: ResolvedPropertyOverrides, row: ResolvedPropertyOverride
|
||||
) -> Optional[BoilerEfficiencyBand]:
|
||||
"""The SEDBUK band for a main-heating row: an explicit `boiler_efficiency_band`
|
||||
override row for the same building part (the FE classifier's structured output)
|
||||
wins; absent one, the band is parsed on-read from the row's
|
||||
`original_spreadsheet_description` (`Boiler: C rated Combi`), so the band ships
|
||||
before the FE-owned band override row exists. `None` when no band is present."""
|
||||
for other in overrides.rows:
|
||||
if (
|
||||
other.override_component == "boiler_efficiency_band"
|
||||
and other.building_part == row.building_part
|
||||
):
|
||||
band = _band_from_override_value(other.override_value)
|
||||
if band is not None:
|
||||
return band
|
||||
if row.original_spreadsheet_description is not None:
|
||||
return boiler_efficiency_band_guard(row.original_spreadsheet_description)
|
||||
return None
|
||||
|
||||
|
||||
def _band_from_override_value(value: str) -> Optional[BoilerEfficiencyBand]:
|
||||
"""A stored `boiler_efficiency_band` override value → the enum member, or
|
||||
`None` for an unrecognised value or the `UNKNOWN` sentinel (never persisted,
|
||||
but treated as absent if it ever appears)."""
|
||||
try:
|
||||
band = BoilerEfficiencyBand(value)
|
||||
except ValueError:
|
||||
return None
|
||||
return None if band is BoilerEfficiencyBand.UNKNOWN else band
|
||||
|
||||
|
||||
# Coarse fuel family per RdSAP `main_fuel` code (main_fuel_overlay._FUEL_CODES),
|
||||
# for the plausibility check. The natural fuel a solid-fuel archetype drags
|
||||
# (house coal) is a *default* across the ambiguous solid family, so a same-family
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ def _resolve_overrides(session: Session, property_id: int) -> ResolvedPropertyOv
|
|||
override_component=row.override_component,
|
||||
building_part=row.building_part,
|
||||
override_value=row.override_value,
|
||||
original_spreadsheet_description=row.original_spreadsheet_description,
|
||||
)
|
||||
for row in rows
|
||||
)
|
||||
|
|
@ -65,6 +66,7 @@ def _resolve_overrides_many(
|
|||
override_component=row.override_component,
|
||||
building_part=row.building_part,
|
||||
override_value=row.override_value,
|
||||
original_spreadsheet_description=row.original_spreadsheet_description,
|
||||
)
|
||||
)
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@ class ResolvedPropertyOverride:
|
|||
override_component: str
|
||||
building_part: int
|
||||
override_value: str
|
||||
# The raw landlord cell the value resolved from. Carried so the modelling can
|
||||
# parse the SEDBUK Boiler Efficiency Band (`Boiler: C rated Combi`) on-read
|
||||
# (ADR-0068) — the band ships before the FE-owned boiler_efficiency_band
|
||||
# override row exists. Optional so in-memory/test snapshots may omit it.
|
||||
original_spreadsheet_description: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue