mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
133 lines
5 KiB
Python
133 lines
5 KiB
Python
"""The ModellingOrchestrator aligns the Optimiser's objective with the
|
|
Scenario's goal (ADR-0062): Reducing CO2 emissions maximises the carbon
|
|
reduction the budget buys, Energy Savings maximises the annual bill saving,
|
|
and Increasing EPC keeps its SAP target semantics. End-to-end through
|
|
``run_modelling`` (no database) with the real calculator, against the
|
|
uninsulated solid-brick 001431 dwelling where the SAP-optimal and
|
|
carbon-optimal packages diverge at a £16,000 budget.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
|
|
import pytest
|
|
|
|
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
|
from domain.fuel_rates.fuel import Fuel
|
|
from domain.fuel_rates.fuel_rates import FuelRate, FuelRates
|
|
from domain.modelling.measure_type import MeasureType
|
|
from domain.modelling.plan import Plan
|
|
from domain.modelling.scenario import Scenario
|
|
from harness.console import run_modelling
|
|
from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository
|
|
from repositories.fuel_rates.fuel_rates_static_file_repository import (
|
|
FuelRatesStaticFileRepository,
|
|
)
|
|
from tests.domain.modelling._elmhurst_recommendation import (
|
|
parse_recommendation_summary,
|
|
)
|
|
|
|
|
|
def _solid_brick_dwelling() -> EpcPropertyData:
|
|
return parse_recommendation_summary("solid_brick_ewi_001431_before.pdf")
|
|
|
|
|
|
def _scenario(goal: str, *, budget: float) -> Scenario:
|
|
return Scenario(
|
|
id=999, goal=goal, goal_value="", budget=budget, is_default=True
|
|
)
|
|
|
|
|
|
def test_reducing_co2_scenario_buys_carbon_not_sap() -> None:
|
|
# Arrange — at £16,000 the SAP objective buys the wall + floor + £3,200
|
|
# gas boiler package (~2,069 kg CO2/yr, SAP 72.9). The carbon objective
|
|
# swaps the boiler for electric storage heaters (~1,098 kg/yr) — a lower
|
|
# SAP, but ~970 kg/yr less carbon on the low-carbon grid.
|
|
epc = _solid_brick_dwelling()
|
|
|
|
# Act — the same dwelling and budget under each goal.
|
|
sap_led: Plan = run_modelling(
|
|
epc,
|
|
scenario=_scenario("Valuation Improvement", budget=16000.0),
|
|
print_table=False,
|
|
)
|
|
carbon_led: Plan = run_modelling(
|
|
epc,
|
|
scenario=_scenario("Reducing CO2 emissions", budget=16000.0),
|
|
print_table=False,
|
|
)
|
|
|
|
# Assert — the goal changes the outcome in the goal's favour: the carbon
|
|
# plan cuts materially more CO2 than the SAP plan buys with the same
|
|
# money, and the gas boiler that wins on SAP-per-£ is rejected.
|
|
assert (
|
|
carbon_led.post_retrofit.co2_kg_per_yr
|
|
< sap_led.post_retrofit.co2_kg_per_yr - 500.0
|
|
)
|
|
selected = {measure.measure_type for measure in carbon_led.measures}
|
|
assert MeasureType.GAS_BOILER_UPGRADE not in selected
|
|
|
|
|
|
def test_a_goal_aligned_scenario_without_a_budget_fails_loudly() -> None:
|
|
# Arrange — 'reduce as much as possible within this budget' is undefined
|
|
# without a budget: unconstrained it would recommend every beneficial
|
|
# measure. A budget-less goal-aligned Scenario is a misconfiguration and
|
|
# must fail visibly, not produce a maximal plan.
|
|
epc = _solid_brick_dwelling()
|
|
budgetless = Scenario(
|
|
id=999,
|
|
goal="Reducing CO2 emissions",
|
|
goal_value="",
|
|
budget=None,
|
|
is_default=True,
|
|
)
|
|
|
|
# Act / Assert
|
|
with pytest.raises(ValueError, match="budget"):
|
|
run_modelling(epc, scenario=budgetless, print_table=False)
|
|
|
|
|
|
class _FixedFuelRates(FuelRatesRepository):
|
|
def __init__(self, rates: FuelRates) -> None:
|
|
self._rates = rates
|
|
|
|
def get_current(self) -> FuelRates:
|
|
return self._rates
|
|
|
|
|
|
def _cheap_electricity_snapshot() -> FuelRates:
|
|
"""The committed snapshot with electricity at 1p/kWh — a world where any
|
|
electric heating out-bills gas, while SAP's internal price book (which the
|
|
calculator rates against) is unmoved."""
|
|
base = FuelRatesStaticFileRepository().get_current()
|
|
rates = dict(base.rates)
|
|
rates[Fuel.ELECTRICITY] = FuelRate(
|
|
unit_rate_p_per_kwh=1.0,
|
|
standing_charge_p_per_day=rates[Fuel.ELECTRICITY].standing_charge_p_per_day,
|
|
)
|
|
return dataclasses.replace(base, rates=rates)
|
|
|
|
|
|
def test_energy_savings_scenario_prices_packages_at_the_live_fuel_rates() -> None:
|
|
# Arrange — SAP is itself a cost metric, but it prices energy from its
|
|
# internal tariff book. The Energy Savings goal must price at the *live*
|
|
# Fuel Rates snapshot: with 1p/kWh electricity, electric heating slashes
|
|
# the bill even though SAP still scores the gas boiler package higher.
|
|
epc = _solid_brick_dwelling()
|
|
|
|
# Act
|
|
plan: Plan = run_modelling(
|
|
epc,
|
|
scenario=_scenario("Energy Savings", budget=16000.0),
|
|
fuel_rates=_FixedFuelRates(_cheap_electricity_snapshot()),
|
|
print_table=False,
|
|
)
|
|
|
|
# Assert — the bill objective abandons the boiler for electric heating.
|
|
selected = {measure.measure_type for measure in plan.measures}
|
|
assert MeasureType.GAS_BOILER_UPGRADE not in selected
|
|
assert selected & {
|
|
MeasureType.AIR_SOURCE_HEAT_PUMP,
|
|
MeasureType.HIGH_HEAT_RETENTION_STORAGE_HEATERS,
|
|
}
|