mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Ventilation-once test pairs the wall with glazing, not a competing wall 🟪
Review feedback: cavity and internal wall insulation are competing options, so a package selecting both read as nonsense even in a synthetic fixture. The behaviour under test (a dependency triggered in both phases injects once) now uses an airtightness pair that genuinely coexists — cavity wall in phase 1, double glazing re-entering in phase 2 — with the same numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
38206c227b
commit
877d0043cb
1 changed files with 25 additions and 35 deletions
|
|
@ -47,6 +47,7 @@ _ROOF_OVERLAY = EpcSimulation(
|
|||
}
|
||||
)
|
||||
_HEATING_OVERLAY = EpcSimulation(heating=HeatingOverlay(sap_main_heating_code=201))
|
||||
_GLAZING_OVERLAY = EpcSimulation(glazing=GlazingOverlay(glazing_type=2))
|
||||
_BOILER_OVERLAY = EpcSimulation(heating=HeatingOverlay(sap_main_heating_code=201))
|
||||
_ASHP_OVERLAY = EpcSimulation(
|
||||
heating=HeatingOverlay(main_heating_index_number=13000)
|
||||
|
|
@ -177,52 +178,44 @@ def test_fabric_spend_comes_out_of_the_shared_budget_before_phase_two() -> None:
|
|||
assert abs(package.score.sap_continuous - 65.0) <= 1e-9
|
||||
|
||||
|
||||
_IWI_OVERLAY = EpcSimulation(
|
||||
building_parts={
|
||||
BuildingPartIdentifier.MAIN: BuildingPartOverlay(wall_insulation_type=3)
|
||||
}
|
||||
)
|
||||
_VENT_OVERLAY = EpcSimulation(
|
||||
ventilation=VentilationOverlay(mechanical_ventilation_kind="EXTRACT_OR_PIV_OUTSIDE")
|
||||
)
|
||||
|
||||
|
||||
class _TwoWallScorer:
|
||||
"""A stub with two wall treatments (cavity type=2, internal type=3): the
|
||||
internal wall is worthless raw but +4 once the cavity is done, and every
|
||||
ventilation overlay present costs −1 — so both phases trigger the same
|
||||
forced ventilation dependency and a double injection is visible in the
|
||||
package score."""
|
||||
class _AirtightnessScorer:
|
||||
"""A stub where tightening the envelope demands ventilation: the cavity
|
||||
wall is +5 SAP, the new double glazing is worthless on the raw dwelling
|
||||
but +4 once the wall is insulated, and every ventilation overlay present
|
||||
costs −1 — so a double injection is visible in the package score."""
|
||||
|
||||
def score(
|
||||
self, baseline: EpcPropertyData, simulations: Sequence[EpcSimulation]
|
||||
) -> Score:
|
||||
cavity = any(
|
||||
part.wall_insulation_type == 2
|
||||
for sim in simulations
|
||||
for part in sim.building_parts.values()
|
||||
)
|
||||
internal = any(
|
||||
part.wall_insulation_type == 3
|
||||
wall = any(
|
||||
part.wall_insulation_type is not None
|
||||
for sim in simulations
|
||||
for part in sim.building_parts.values()
|
||||
)
|
||||
glazing = any(sim.glazing is not None for sim in simulations)
|
||||
vents = sum(1 for sim in simulations if sim.ventilation is not None)
|
||||
sap = 60.0
|
||||
sap += 5.0 if cavity else 0.0
|
||||
sap += (4.0 if cavity else 0.0) if internal else 0.0
|
||||
sap += 5.0 if wall else 0.0
|
||||
sap += (4.0 if wall else 0.0) if glazing else 0.0
|
||||
sap -= float(vents)
|
||||
return Score(
|
||||
sap_continuous=sap, co2_kg_per_yr=0.0, primary_energy_kwh_per_yr=0.0
|
||||
)
|
||||
|
||||
|
||||
def _wall_ventilation_dependency(*, cost: float) -> MeasureDependency:
|
||||
def _airtightness_ventilation_dependency(*, cost: float) -> MeasureDependency:
|
||||
"""A forced 'airtightness requires ventilation' edge: both the wall and
|
||||
the sealed new glazing trigger the same mechanical ventilation."""
|
||||
return MeasureDependency(
|
||||
triggers=frozenset(
|
||||
{
|
||||
MeasureType.CAVITY_WALL_INSULATION,
|
||||
MeasureType.INTERNAL_WALL_INSULATION,
|
||||
MeasureType.DOUBLE_GLAZING,
|
||||
}
|
||||
),
|
||||
required=ScoredOption(
|
||||
|
|
@ -238,17 +231,17 @@ def _wall_ventilation_dependency(*, cost: float) -> MeasureDependency:
|
|||
|
||||
|
||||
def test_ventilation_dependency_is_injected_once_across_both_phases() -> None:
|
||||
# Arrange — the cavity wall (phase 1) and the internal wall (picked in
|
||||
# phase 2 on its post-cavity worth) both trigger the same forced
|
||||
# ventilation. It must land in the package exactly once — phase 2 sees the
|
||||
# phase-1 dwelling as already ventilated.
|
||||
# Arrange — the cavity wall (phase 1) and the double glazing (skipped in
|
||||
# phase 1 on merit, picked in phase 2 on its post-fabric worth) both
|
||||
# trigger the same forced ventilation. It must land in the package exactly
|
||||
# once — phase 2 sees the phase-1 dwelling as already ventilated.
|
||||
groups: list[list[ScoredOption]] = [
|
||||
[_scored("cavity_wall_insulation", gain=5.0, cost=1000.0, overlay=_WALL_OVERLAY)],
|
||||
[_scored("internal_wall_insulation", gain=0.0, cost=2000.0, overlay=_IWI_OVERLAY)],
|
||||
[_scored("double_glazing", gain=0.0, cost=500.0, overlay=_GLAZING_OVERLAY)],
|
||||
]
|
||||
scorer = _TwoWallScorer()
|
||||
scorer = _AirtightnessScorer()
|
||||
|
||||
# Act — target 68: phase 1 gives 60 + 5 − 1 = 64; the internal wall's
|
||||
# Act — target 68: phase 1 gives 60 + 5 − 1 = 64; the glazing's
|
||||
# post-fabric +4 closes it, but only if ventilation is not double-counted.
|
||||
package: OptimisedPackage = optimise_package_fabric_first(
|
||||
groups=groups,
|
||||
|
|
@ -256,11 +249,11 @@ def test_ventilation_dependency_is_injected_once_across_both_phases() -> None:
|
|||
baseline_epc=build_epc(),
|
||||
budget=10000.0,
|
||||
target_sap=68.0,
|
||||
dependencies=[_wall_ventilation_dependency(cost=300.0)],
|
||||
dependencies=[_airtightness_ventilation_dependency(cost=300.0)],
|
||||
)
|
||||
|
||||
# Assert — one ventilation, and the truthful total counts its penalty once:
|
||||
# 60 + 5 cavity + 4 internal − 1 ventilation = 68.
|
||||
# 60 + 5 wall + 4 glazing − 1 ventilation = 68.
|
||||
ventilation_count = sum(
|
||||
1
|
||||
for scored in package.selected
|
||||
|
|
@ -269,7 +262,7 @@ def test_ventilation_dependency_is_injected_once_across_both_phases() -> None:
|
|||
assert ventilation_count == 1
|
||||
assert _selected_types(package) == {
|
||||
"cavity_wall_insulation",
|
||||
"internal_wall_insulation",
|
||||
"double_glazing",
|
||||
"mechanical_ventilation",
|
||||
}
|
||||
assert abs(package.score.sap_continuous - 68.0) <= 1e-9
|
||||
|
|
@ -349,9 +342,6 @@ class _InteractionScorer:
|
|||
)
|
||||
|
||||
|
||||
_GLAZING_OVERLAY = EpcSimulation(glazing=GlazingOverlay(glazing_type=2))
|
||||
|
||||
|
||||
class _GlazingInteractionScorer:
|
||||
"""A stub where glazing is worthless on the raw dwelling (+0) but worth +4
|
||||
once the wall is insulated — so phase 1's max-gain fabric pass leaves it
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue