The orchestrator threads the dwelling design heat loss into ASHP sizing 🟥

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-03 10:31:57 +00:00
parent b25e7bb7f9
commit cb91954bb4
2 changed files with 61 additions and 0 deletions

View file

@ -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

View file

@ -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