Model/tests/domain/modelling/test_plan.py
Khalim Conn-Kowlessar 0ebd9cc7fd feat(modelling): domain Plan + PlanMeasure types (#1157)
Slice 2 of #1157. The per-Property output of one Scenario's modelling
run, per ADR-0017.

- PlanMeasure: a selected Measure Option frozen with its installed Cost
  and role-3 (final-package cascade) attributed MeasureImpact — the
  output counterpart of a Recommendation's candidate Option.
- Plan: the selected Plan Measures + baseline/post-retrofit Scores.
  Single-phase (ADR-0005); derives the persisted headline figures —
  cost_of_works, contingency_cost, co2_savings_kg_per_yr (kg; the mapper
  converts to tonnes), post_sap_continuous, and post_epc_rating (band
  from the rounded SAP via Epc.from_sap_score).

1 unit test, pyright strict clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:40:27 +00:00

47 lines
1.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Behaviour of the Plan / PlanMeasure domain types: the per-Property output
of one Scenario's modelling run. A Plan carries its selected Plan Measures
(the Optimised Package) plus the baseline/post-retrofit Scores, and derives
the persisted headline figures (cost aggregates, CO₂ saving, post-retrofit
band). Single-phase, flat post-retrofit figures (ADR-0005 / ADR-0017).
"""
from __future__ import annotations
from datatypes.epc.domain.epc import Epc
from domain.modelling.package_scorer import Score
from domain.modelling.plan import Plan, PlanMeasure
from domain.modelling.recommendation import Cost
from domain.modelling.scoring import MeasureImpact
def _measure(measure_type: str, total: float, rate: float) -> PlanMeasure:
return PlanMeasure(
measure_type=measure_type,
description=measure_type.replace("_", " "),
cost=Cost(total=total, contingency_rate=rate),
impact=MeasureImpact(
sap_points=1.0, co2_savings_kg_per_yr=1.0, energy_savings_kwh_per_yr=1.0
),
)
def test_plan_aggregates_cost_and_savings_and_bands_the_post_sap() -> None:
# Arrange
measures: tuple[PlanMeasure, ...] = (
_measure("cavity_wall_insulation", total=1000.0, rate=0.10),
_measure("loft_insulation", total=500.0, rate=0.20),
)
baseline = Score(
sap_continuous=40.0, co2_kg_per_yr=4000.0, primary_energy_kwh_per_yr=20000.0
)
post = Score(
sap_continuous=70.4, co2_kg_per_yr=3600.0, primary_energy_kwh_per_yr=18000.0
)
plan = Plan(measures=measures, baseline=baseline, post_retrofit=post)
# Act / Assert
assert abs(plan.cost_of_works - 1500.0) <= 1e-9
assert abs(plan.contingency_cost - 200.0) <= 1e-9 # 1000*0.10 + 500*0.20
assert abs(plan.co2_savings_kg_per_yr - 400.0) <= 1e-9 # baseline - post
assert abs(plan.post_sap_continuous - 70.4) <= 1e-9
assert plan.post_epc_rating is Epc.C # round(70.4) = 70 → band C (6980)