mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
test(modelling): pin least-cost-to-target end-to-end through the orchestrator
The orchestrator already threads budget/target_sap/dependencies into optimise_package, so no orchestrator change was needed. Add an integration test proving the new objective end-to-end on the real calculator: a band-D property (~57.4) with a goal of band D — already met — yields a Plan with NO measures and zero cost (the old max-gain objective would have recommended wall+floor+vent, improving within the band it is already in). Clarified that the existing multi-measure test now exercises the max-gain fallback (goal C unreachable from D, tops out ~61). Narrowed Optional sap_points/estimated_cost through locals to keep pyright strict-clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
af501fce0e
commit
641c1bd7f6
1 changed files with 102 additions and 4 deletions
|
|
@ -198,7 +198,10 @@ def test_modelling_optimises_and_persists_a_multi_measure_plan(
|
|||
# fields, so the Baseline stage (not under test here) can't run on it.
|
||||
# SAP-numeric correctness is pinned in test_elmhurst_cascade_pins; here we
|
||||
# prove the multi-measure Plan is optimised, priced, attributed and
|
||||
# persisted.
|
||||
# persisted. The property is band D (~57.4) and tops out at ~61, so the
|
||||
# goal-C target is unreachable — this exercises the least-cost-to-target
|
||||
# objective's **max-gain fallback** (ADR-0016 amendment): best effort, all
|
||||
# measures, below target.
|
||||
with Session(db_engine) as session:
|
||||
session.add(
|
||||
PropertyRow(
|
||||
|
|
@ -293,8 +296,103 @@ def test_modelling_optimises_and_persists_a_multi_measure_plan(
|
|||
assert rec.estimated_cost is not None
|
||||
# The forced ventilation costs two £450 units and is priced even though it
|
||||
# was never a free choice in the pool.
|
||||
assert abs(by_type["mechanical_ventilation"].estimated_cost - 900.0) <= 1e-6
|
||||
vent_cost: float | None = by_type["mechanical_ventilation"].estimated_cost
|
||||
assert vent_cost is not None
|
||||
assert abs(vent_cost - 900.0) <= 1e-6
|
||||
# The insulation measures earn positive SAP; ventilation's contribution is
|
||||
# not positive (it only ever costs SAP — ADR-0016).
|
||||
assert by_type["cavity_wall_insulation"].sap_points > 0.0
|
||||
assert by_type["mechanical_ventilation"].sap_points <= 0.0
|
||||
wall_sap: float | None = by_type["cavity_wall_insulation"].sap_points
|
||||
vent_sap: float | None = by_type["mechanical_ventilation"].sap_points
|
||||
assert wall_sap is not None and vent_sap is not None
|
||||
assert wall_sap > 0.0
|
||||
assert vent_sap <= 0.0
|
||||
|
||||
|
||||
def test_modelling_recommends_nothing_when_already_at_the_target_band(
|
||||
db_engine: Engine,
|
||||
) -> None:
|
||||
# Arrange — the same band-D property (~57.4), but a goal of band D, which it
|
||||
# already meets. Least-cost-to-target recommends the cheapest package that
|
||||
# *reaches* the target — and the target is already reached, so the cheapest
|
||||
# package is the empty one. (The old max-gain objective would have
|
||||
# recommended wall + floor + ventilation here, improving within the band the
|
||||
# property is already in — exactly the over-recommendation this objective
|
||||
# removes.) ADR-0016 amendment.
|
||||
with Session(db_engine) as session:
|
||||
session.add(
|
||||
PropertyRow(
|
||||
id=31,
|
||||
portfolio_id=1,
|
||||
postcode="A0 0AA",
|
||||
address="4 Some Street",
|
||||
uprn=44444,
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
ScenarioRow(
|
||||
id=8, goal="Increasing EPC", goal_value="D", is_default=True
|
||||
)
|
||||
)
|
||||
# The fabric Generators + the ventilation dependency builder still run
|
||||
# during candidate generation, so their Products must exist even though
|
||||
# nothing is ultimately selected.
|
||||
session.add_all(
|
||||
[
|
||||
MaterialRow(
|
||||
id=10,
|
||||
type="cavity_wall_insulation",
|
||||
total_cost=18.5,
|
||||
cost_unit="gbp_per_m2",
|
||||
is_active=True,
|
||||
description="Cavity wall insulation",
|
||||
),
|
||||
MaterialRow(
|
||||
id=11,
|
||||
type="suspended_floor_insulation",
|
||||
total_cost=25.0,
|
||||
cost_unit="gbp_per_m2",
|
||||
is_active=True,
|
||||
description="Suspended floor insulation",
|
||||
),
|
||||
MaterialRow(
|
||||
id=12,
|
||||
type="mechanical_ventilation",
|
||||
total_cost=450.0,
|
||||
cost_unit="gbp_per_unit",
|
||||
is_active=True,
|
||||
description="Mechanical extract ventilation unit",
|
||||
),
|
||||
]
|
||||
)
|
||||
session.commit()
|
||||
EpcPostgresRepository(session).save(
|
||||
_build_uninsulated_cavity_and_floor_epc(),
|
||||
property_id=31,
|
||||
portfolio_id=1,
|
||||
)
|
||||
session.commit()
|
||||
|
||||
def unit_of_work() -> PostgresUnitOfWork:
|
||||
return PostgresUnitOfWork(lambda: Session(db_engine))
|
||||
|
||||
# Act
|
||||
ModellingOrchestrator(
|
||||
unit_of_work=unit_of_work, calculator=Sap10Calculator()
|
||||
).run(property_ids=[31], scenario_ids=[8], portfolio_id=1)
|
||||
|
||||
# Assert — a Plan is persisted with no measures and zero cost; the
|
||||
# post-retrofit figure is the unchanged baseline (still band D).
|
||||
with Session(db_engine) as session:
|
||||
plan = session.exec(
|
||||
select(PlanRow).where(col(PlanRow.property_id) == 31)
|
||||
).first()
|
||||
assert plan is not None
|
||||
rec_rows = session.exec(
|
||||
select(RecommendationRow).where(
|
||||
col(RecommendationRow.plan_id) == plan.id
|
||||
)
|
||||
).all()
|
||||
|
||||
assert rec_rows == []
|
||||
assert plan.cost_of_works == 0.0
|
||||
assert plan.post_epc_rating is Epc.D
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue