mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
feat(modelling): sense-check table for a Plan in the DB-less harness
Slice 2. `harness.plan_table.format_plan_table(plan)` renders a Plan as a plain-text table — one package summary line (baseline SAP/band -> post SAP/band, CO2 saved, cost of works + contingency, bill saved) and one line per Plan Measure (signed SAP points, cost, delivered kWh + £ savings). Pure presentation: reads the Plan, computes nothing. The DB-less First Run test now prints it (visible under `pytest -s`) so the modelled package can be eyeballed and debugged by hand. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
26d7fc036e
commit
9329978374
5 changed files with 128 additions and 0 deletions
0
harness/__init__.py
Normal file
0
harness/__init__.py
Normal file
61
harness/plan_table.py
Normal file
61
harness/plan_table.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""Render a Plan as a plain-text sense-check table.
|
||||
|
||||
The DB-less inspection harness prints this so the modelled package — its SAP
|
||||
band transition, cost, and each Plan Measure's attributed SAP / bill impact —
|
||||
can be eyeballed and debugged by hand. Pure presentation: it reads a `Plan`
|
||||
domain object and returns a string, computing nothing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from datatypes.epc.domain.epc import Epc
|
||||
from domain.modelling.plan import Plan
|
||||
|
||||
_KG_PER_TONNE = 1000.0
|
||||
|
||||
|
||||
def _band(sap_continuous: float) -> str:
|
||||
return Epc.from_sap_score(round(sap_continuous)).value
|
||||
|
||||
|
||||
def _signed_gbp(value: Optional[float]) -> str:
|
||||
return "n/a" if value is None else f"{value:+,.0f}"
|
||||
|
||||
|
||||
def _money(value: Optional[float]) -> str:
|
||||
if value is None:
|
||||
return "n/a"
|
||||
sign = "-" if value < 0 else ""
|
||||
return f"{sign}£{abs(value):,.0f}"
|
||||
|
||||
|
||||
def _signed_kwh(value: Optional[float]) -> str:
|
||||
return "n/a" if value is None else f"{value:+,.0f}"
|
||||
|
||||
|
||||
def format_plan_table(plan: Plan) -> str:
|
||||
"""A multi-line table: one package summary line, then one line per Plan
|
||||
Measure (signed so positive is an improvement / a saving)."""
|
||||
co2_tonnes_saved: float = plan.co2_savings_kg_per_yr / _KG_PER_TONNE
|
||||
header = (
|
||||
f"Plan SAP {plan.baseline.sap_continuous:.1f} ({_band(plan.baseline.sap_continuous)})"
|
||||
f" -> {plan.post_sap_continuous:.1f} ({plan.post_epc_rating.value})"
|
||||
f" CO2 saved {co2_tonnes_saved:.2f} t/yr"
|
||||
f" cost £{plan.cost_of_works:,.0f} (+£{plan.contingency_cost:,.0f} cont.)"
|
||||
f" bill saved {_money(plan.energy_bill_savings)}/yr"
|
||||
)
|
||||
columns = (
|
||||
f" {'measure':<30}{'SAP':>7}{'cost':>10}"
|
||||
f"{'kWh/yr':>10}{'£/yr':>9}"
|
||||
)
|
||||
rows = [
|
||||
f" {measure.measure_type:<30}"
|
||||
f"{measure.impact.sap_points:>+7.1f}"
|
||||
f"{('£' + format(measure.cost.total, ',.0f')):>10}"
|
||||
f"{_signed_kwh(measure.kwh_savings):>10}"
|
||||
f"{_signed_gbp(measure.energy_cost_savings):>9}"
|
||||
for measure in plan.measures
|
||||
]
|
||||
return "\n".join([header, columns, *rows])
|
||||
0
tests/harness/__init__.py
Normal file
0
tests/harness/__init__.py
Normal file
65
tests/harness/test_plan_table.py
Normal file
65
tests/harness/test_plan_table.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""The sense-check table the DB-less harness prints for a Plan."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from domain.modelling.plan import Plan, PlanMeasure
|
||||
from domain.modelling.recommendation import Cost
|
||||
from domain.modelling.scoring.package_scorer import Score
|
||||
from domain.modelling.scoring.scoring import MeasureImpact
|
||||
from harness.plan_table import format_plan_table
|
||||
|
||||
|
||||
def _plan() -> Plan:
|
||||
baseline = Score(
|
||||
sap_continuous=57.4, co2_kg_per_yr=3000.0, primary_energy_kwh_per_yr=300.0
|
||||
)
|
||||
post = Score(
|
||||
sap_continuous=61.2, co2_kg_per_yr=2100.0, primary_energy_kwh_per_yr=240.0
|
||||
)
|
||||
measures = (
|
||||
PlanMeasure(
|
||||
measure_type="cavity_wall_insulation",
|
||||
description="Cavity wall insulation",
|
||||
cost=Cost(total=500.0, contingency_rate=0.1),
|
||||
impact=MeasureImpact(
|
||||
sap_points=3.1,
|
||||
co2_savings_kg_per_yr=600.0,
|
||||
energy_savings_kwh_per_yr=1200.0,
|
||||
),
|
||||
kwh_savings=900.0,
|
||||
energy_cost_savings=120.0,
|
||||
),
|
||||
PlanMeasure(
|
||||
measure_type="mechanical_ventilation",
|
||||
description="Mechanical extract ventilation",
|
||||
cost=Cost(total=900.0, contingency_rate=0.26),
|
||||
impact=MeasureImpact(
|
||||
sap_points=-1.3,
|
||||
co2_savings_kg_per_yr=-50.0,
|
||||
energy_savings_kwh_per_yr=-200.0,
|
||||
),
|
||||
kwh_savings=-150.0,
|
||||
energy_cost_savings=-30.0,
|
||||
),
|
||||
)
|
||||
return Plan(measures=measures, baseline=baseline, post_retrofit=post)
|
||||
|
||||
|
||||
def test_table_shows_package_transition_and_each_measure() -> None:
|
||||
# Arrange
|
||||
plan: Plan = _plan()
|
||||
|
||||
# Act
|
||||
table: str = format_plan_table(plan)
|
||||
|
||||
# Assert — the package SAP transition (both bands resolve to D), and each
|
||||
# measure's signed SAP contribution against its type.
|
||||
assert "57.4" in table
|
||||
assert "61.2" in table
|
||||
assert "(D)" in table
|
||||
assert "cavity_wall_insulation" in table
|
||||
assert "+3.1" in table
|
||||
assert "mechanical_ventilation" in table
|
||||
assert "-1.3" in table
|
||||
# The package cost of works (500 + 900) appears.
|
||||
assert "1,400" in table
|
||||
|
|
@ -28,6 +28,7 @@ from repositories.fuel_rates.fuel_rates_static_file_repository import (
|
|||
FuelRatesStaticFileRepository,
|
||||
)
|
||||
from repositories.geospatial.geospatial_repository import GeospatialRepository
|
||||
from harness.plan_table import format_plan_table
|
||||
from repositories.product.product_json_repository import ProductJsonRepository
|
||||
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
|
||||
build_epc as _build_uninsulated_cavity_and_floor_epc,
|
||||
|
|
@ -147,5 +148,6 @@ def test_first_run_produces_a_multi_measure_plan_without_a_database() -> None:
|
|||
# Assert — a Plan was persisted in memory for (property 10, scenario 7),
|
||||
# with at least one Plan Measure and a post-retrofit SAP no worse than baseline.
|
||||
plan = plan_repo.saved[(10, 7)]
|
||||
print("\n" + format_plan_table(plan)) # visible under `pytest -s` for sense-checking
|
||||
assert len(plan.measures) >= 1
|
||||
assert plan.post_sap_continuous >= plan.baseline.sap_continuous
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue