Model/domain/modelling/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

73 lines
2.6 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.

"""Plan and Plan Measure — the Modelling stage's persisted output (ADR-0017).
A **Plan** is the per-Property output of one Scenario's modelling run: the
selected **Optimised Package** (its **Plan Measures**) plus the Property's
post-retrofit figures. It is single-phase — multi-phase is deferred
(ADR-0005) — so the headline figures are flat on the Plan.
A **Plan Measure** is the *output* counterpart of a Recommendation's candidate
Option: the one Option the Optimiser kept, frozen with its installed **Cost**
and its final-package (role-3) attributed **impact**. See CONTEXT.md.
"""
from dataclasses import dataclass
from datatypes.epc.domain.epc import Epc
from domain.modelling.package_scorer import Score
from domain.modelling.recommendation import Cost
from domain.modelling.scoring import MeasureImpact
@dataclass(frozen=True)
class PlanMeasure:
"""One selected Measure Option as it lands in a Plan: the measure, its
installed Cost, and its role-3 (final-package cascade) attributed impact."""
measure_type: str
description: str
cost: Cost
impact: MeasureImpact
@dataclass(frozen=True)
class Plan:
"""A Property's Plan for one Scenario: the selected Plan Measures and the
baseline / post-retrofit whole-package Scores. The persisted headline
figures are derived from these (cost aggregates, CO₂ saving, post band)."""
measures: tuple[PlanMeasure, ...]
baseline: Score
post_retrofit: Score
@property
def cost_of_works(self) -> float:
"""Sum of the Plan Measures' fully-loaded Costs."""
return sum((measure.cost.total for measure in self.measures), 0.0)
@property
def contingency_cost(self) -> float:
"""Sum of each Plan Measure's contingency (its Cost total × its
per-Measure-Type contingency rate)."""
return sum(
(
measure.cost.total * measure.cost.contingency_rate
for measure in self.measures
),
0.0,
)
@property
def post_sap_continuous(self) -> float:
"""The whole-package re-score's un-rounded SAP rating."""
return self.post_retrofit.sap_continuous
@property
def post_epc_rating(self) -> Epc:
"""The post-retrofit EPC band, from the rounded SAP rating."""
return Epc.from_sap_score(round(self.post_retrofit.sap_continuous))
@property
def co2_savings_kg_per_yr(self) -> float:
"""Whole-package CO₂ reduction (kg/yr) vs the baseline re-score. The
persistence mapper converts to tonnes for the live column contract."""
return self.baseline.co2_kg_per_yr - self.post_retrofit.co2_kg_per_yr