mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Sibling migration to the sap10_calculator move — `domain.ml` now lives
at the root-level layout (`domain/sap10_ml/`) matching the pattern
already used by `domain.addresses`, `domain.tasks`, `domain.postcode`,
and `domain.sap10_calculator`.
Changes:
- `git mv packages/domain/src/domain/ml → domain/sap10_ml` (19 files;
history preserved).
- Subpackage rename: `domain.ml` → `domain.sap10_ml`. 32 references
rewritten across .py and .md files: 11 internal + 21 external
(datatypes/epc/domain/mapper.py, 14 files in domain/sap10_calculator,
2 backend tests, 2 ADRs, 1 README, 1 design doc).
- Path-string updates: `pytest.ini` testpath
`packages/domain/src/domain/ml/tests` → `domain/sap10_ml/tests` so
ML tests stay in the default auto-discovered sweep. `CONTEXT.md`
also updated.
`packages/domain/src/domain/` is now empty — the workspace `domain/`
tree has been fully migrated. Together with the `domain/__init__.py`
deletions from the sap10_calculator commit (29ac35cc), `domain` is
now a single root-level namespace package with subpackages
{addresses, sap10_calculator, sap10_ml, tasks} + the standalone
`postcode.py` module.
Verified:
- Focused sweep (backend mapper-chain + sap10_calculator worksheet
e2e + golden fixtures): 99 passed / 19 failed — identical baseline.
- Wider sweep (all sap10_calculator + sap10_ml): 1654 passed / 20
failed (same pre-existing failures).
- domain/sap10_ml/tests: 210/210 PASSED at new path.
- Pyright net-zero: heat_transmission.py 13, cert_to_inputs.py 35,
mapper.py 33, rdsap_uvalues.py 1 (all unchanged from baseline).
Note: `packages/domain/pyproject.toml` still declares
`packages = ["src/domain"]` for the hatchling wheel — that target
directory is now empty and the wheel build is effectively a no-op.
Retiring the workspace package or repointing the wheel is a follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
108 lines
4.1 KiB
Python
108 lines
4.1 KiB
Python
"""SAP10 §20.1 cost reconstruction: predicted total fuel cost + ECF.
|
|
|
|
ECF = 0.42 * total_cost / (TFA + 45) (SAP rating relationship)
|
|
|
|
Total cost (gbp/yr) = (space_kwh * space_fuel_price + dhw_kwh * dhw_fuel_price
|
|
+ lighting_kwh * elec_price) / 100 [pence -> pounds]
|
|
|
|
Standing charges are deliberately omitted at this slice -- they add a
|
|
fuel-mix-conditional offset the tree-based model can learn (ADR-0008,
|
|
"+ Lighting" scope).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from math import log10
|
|
from typing import Final, Optional
|
|
|
|
from domain.sap10_ml.sap_efficiencies import fuel_unit_price_p_per_kwh
|
|
|
|
|
|
# SAP10 deflator applied to total cost before the rating equation (Table 32).
|
|
_DEFLATOR: float = 0.42
|
|
|
|
# Electricity standard tariff fuel code (Table 32) — used for lighting + PV credit.
|
|
_ELECTRICITY_STANDARD_CODE: int = 30
|
|
|
|
# Annual PV yield (kWh / kWp / yr) by SAP10.2 region. Derived from Table 6e
|
|
# climate data integrated over an average roof (South-facing, 30 deg pitch,
|
|
# average overshading). Southern England ~ 900, Scotland ~ 700, Highland ~ 600.
|
|
_PV_YIELD_BY_REGION: Final[dict[int, float]] = {
|
|
1: 920, 2: 950, 3: 970, 4: 960, 5: 920, 6: 880, 7: 850, 8: 830,
|
|
9: 820, 10: 820, 11: 830, 12: 880, 13: 850, 14: 770, 15: 740,
|
|
16: 700, 17: 650, 18: 700, 19: 690, 20: 680, 21: 870, 22: 870,
|
|
}
|
|
_PV_YIELD_UK_AVG: Final[float] = 850.0
|
|
|
|
|
|
def predicted_pv_generation_kwh(
|
|
pv_total_peak_power_kw: Optional[float],
|
|
region_code: Optional[str],
|
|
) -> float:
|
|
"""Annual PV generation (kWh/yr) for the dwelling's PV array(s).
|
|
|
|
Linear in peak power; uses a SAP-region yield factor with South-facing,
|
|
30 deg pitch, average overshading assumptions (SAP10.2 Table 6e).
|
|
"""
|
|
if pv_total_peak_power_kw is None or pv_total_peak_power_kw <= 0:
|
|
return 0.0
|
|
yield_factor = _PV_YIELD_UK_AVG
|
|
if region_code is not None:
|
|
try:
|
|
yield_factor = _PV_YIELD_BY_REGION.get(int(region_code), _PV_YIELD_UK_AVG)
|
|
except (TypeError, ValueError):
|
|
pass
|
|
return pv_total_peak_power_kw * yield_factor
|
|
|
|
|
|
def predicted_total_fuel_cost_gbp(
|
|
predicted_space_heating_kwh: float,
|
|
predicted_hot_water_kwh: float,
|
|
predicted_lighting_kwh: float,
|
|
main_fuel_code: Optional[int],
|
|
water_heating_fuel_code: Optional[int],
|
|
predicted_pv_kwh: float = 0.0,
|
|
) -> float:
|
|
"""Annual regulated fuel cost (gbp/yr).
|
|
|
|
Skips standing charges; sums delivered kWh * unit price across the
|
|
three included end uses (lighting always at standard electricity).
|
|
|
|
Slice 17a: subtracts predicted_pv_kwh * standard electricity price as
|
|
a flat PV credit. SAP10.2 splits PV between self-consumption and
|
|
export with separate rates; both are 13.19 p/kWh in Table 32 so a
|
|
single rate is fine at this fidelity.
|
|
"""
|
|
space_p_per_kwh = fuel_unit_price_p_per_kwh(main_fuel_code)
|
|
dhw_p_per_kwh = fuel_unit_price_p_per_kwh(water_heating_fuel_code)
|
|
light_p_per_kwh = fuel_unit_price_p_per_kwh(_ELECTRICITY_STANDARD_CODE)
|
|
pv_p_per_kwh = fuel_unit_price_p_per_kwh(_ELECTRICITY_STANDARD_CODE)
|
|
total_pence = (
|
|
predicted_space_heating_kwh * space_p_per_kwh
|
|
+ predicted_hot_water_kwh * dhw_p_per_kwh
|
|
+ predicted_lighting_kwh * light_p_per_kwh
|
|
- predicted_pv_kwh * pv_p_per_kwh
|
|
)
|
|
return total_pence / 100.0
|
|
|
|
|
|
def predicted_ecf(
|
|
predicted_total_cost_gbp: float,
|
|
total_floor_area_m2: Optional[float],
|
|
) -> float:
|
|
"""SAP rating Energy Cost Factor: 0.42 * total_cost / (TFA + 45)."""
|
|
if total_floor_area_m2 is None or total_floor_area_m2 <= 0:
|
|
return 0.0
|
|
return _DEFLATOR * predicted_total_cost_gbp / (total_floor_area_m2 + 45.0)
|
|
|
|
|
|
def predicted_log10_ecf(predicted_ecf_value: float) -> float:
|
|
"""log10(ECF). Returns 0.0 for non-positive input so the feature is
|
|
finite for the (rare) all-PV property.
|
|
|
|
The SAP rating formula uses log10(ECF) for ECF >= 3.5 (low-SAP region);
|
|
in the high-SAP linear region the model can still use log10_ecf as a
|
|
monotone proxy for SAP."""
|
|
if predicted_ecf_value <= 0:
|
|
return 0.0
|
|
return log10(predicted_ecf_value)
|