diff --git a/orchestration/modelling_orchestrator.py b/orchestration/modelling_orchestrator.py index e3180a4d2..ae43ca85c 100644 --- a/orchestration/modelling_orchestrator.py +++ b/orchestration/modelling_orchestrator.py @@ -264,6 +264,7 @@ def _candidate_recommendations( planning_restrictions: PlanningRestrictions, solar_potential: Optional[SolarPotential], considered_measures: Optional[frozenset[MeasureType]], + design_heat_loss_kw: Optional[float] = None, ) -> list[Recommendation]: """Run the applicable Recommendation Generators; keep the ones that apply. Solid-wall insulation, glazing, heating and solar are additionally gated by diff --git a/tests/orchestration/test_modelling_heat_pump_sizing_threading.py b/tests/orchestration/test_modelling_heat_pump_sizing_threading.py new file mode 100644 index 000000000..0b46bb8a6 --- /dev/null +++ b/tests/orchestration/test_modelling_heat_pump_sizing_threading.py @@ -0,0 +1,60 @@ +"""The ModellingOrchestrator threads the dwelling's SAP design heat loss into +the heating Recommendation Generator so the ASHP is sized to the dwelling +(ADR-0049), mirroring how planning_restrictions and the solar potential are +threaded. Tests the candidate-wiring seam directly; the end-to-end run-through- +repos path is covered by the DB integration tests. +""" + +from datatypes.epc.domain.epc_property_data import EpcPropertyData +from domain.geospatial.planning_restrictions import PlanningRestrictions +from domain.modelling.measure_type import MeasureType +from domain.modelling.product import Product +from domain.modelling.recommendation import Recommendation +from orchestration.modelling_orchestrator import ( + _candidate_recommendations, # pyright: ignore[reportPrivateUsage] +) +from repositories.product.product_repository import ProductRepository +from tests.domain.modelling._elmhurst_recommendation import ( + parse_recommendation_summary, +) + + +class _StubProducts(ProductRepository): + def get(self, measure_type: str) -> Product: + return Product( + measure_type=measure_type, + unit_cost_per_m2=0.0, + contingency_rate=0.15, + id=909, + ) + + +def _ashp_eligible_house() -> EpcPropertyData: + epc = parse_recommendation_summary( + "ashp_from_system_boiler_with_cylinder_001431_before.pdf" + ) + epc.property_type = "House" + return epc + + +def test_candidate_recommendations_sizes_the_ashp_to_the_threaded_design_heat_loss() -> ( + None +): + # Arrange — an ASHP-eligible house and a high (17.5 kW) design heat loss. + epc = _ashp_eligible_house() + + # Act + recommendations: list[Recommendation] = _candidate_recommendations( + epc, _StubProducts(), PlanningRestrictions(), None, None, design_heat_loss_kw=17.5 + ) + + # Assert — the ASHP is sized to the load (capped 12 kW aroTHERM plus 110281), + # not the fixed 5 kW / floor-area-proxy unit. + heating = next(r for r in recommendations if r.surface == "Heating & Hot Water") + ashp = next( + o + for o in heating.options + if o.measure_type == MeasureType.AIR_SOURCE_HEAT_PUMP + ) + assert ashp.overlay.heating is not None + assert ashp.overlay.heating.main_heating_index_number == 110281