mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
feat(modelling): robust offline modelling inspection (run_modelling)
Two fixes that unblock offline, no-database inspection over an arbitrary EPC dump: - Complete the harness sample catalogue with loft_insulation and solid_floor_insulation — the four fabric generators can emit five Measure Types, but the catalogue priced only three, so an offline run on a property with an uninsulated loft or solid floor raised mid-run. A new test pins the catalogue to cover every generator Measure Type. - Add `run_modelling(epc, ...)` — runs ONLY the Modelling stage (no Ingestion / Baseline), so it needs no lodged recorded-performance / RHI and inspects recommendations on any calculator-scorable EPC. `run_one` (full pipeline) stays for when you want Baseline too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b3f4609c2d
commit
98f5ee4fca
3 changed files with 97 additions and 1 deletions
|
|
@ -159,3 +159,64 @@ def run_one(
|
||||||
if print_table:
|
if print_table:
|
||||||
print("\n" + format_plan_table(plan))
|
print("\n" + format_plan_table(plan))
|
||||||
return plan
|
return plan
|
||||||
|
|
||||||
|
|
||||||
|
def run_modelling(
|
||||||
|
epc: EpcPropertyData,
|
||||||
|
*,
|
||||||
|
goal_band: str = "C",
|
||||||
|
catalogue_path: Path = DEFAULT_CATALOGUE,
|
||||||
|
current_market_value: Optional[float] = None,
|
||||||
|
print_table: bool = True,
|
||||||
|
) -> Plan:
|
||||||
|
"""Run ONLY the Modelling stage over ``epc`` with no database — skipping
|
||||||
|
Ingestion and Baseline. Modelling re-scores the EPC itself, so unlike
|
||||||
|
`run_one` this needs no lodged recorded-performance / RHI: it runs on any
|
||||||
|
EPC the calculator can score, which is what you want for inspecting
|
||||||
|
recommendations across an arbitrary EPC dump offline."""
|
||||||
|
plan_repo = FakePlanRepository()
|
||||||
|
property_repo = FakePropertyRepo(
|
||||||
|
{
|
||||||
|
_PROPERTY_ID: Property(
|
||||||
|
identity=PropertyIdentity(
|
||||||
|
portfolio_id=_PORTFOLIO_ID,
|
||||||
|
postcode="A0 0AA",
|
||||||
|
address="1 Some Street",
|
||||||
|
uprn=12345,
|
||||||
|
),
|
||||||
|
epc=epc,
|
||||||
|
current_market_value=current_market_value,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
unit = FakeUnitOfWork(
|
||||||
|
property=property_repo,
|
||||||
|
scenario=FakeScenarioRepository(
|
||||||
|
{
|
||||||
|
_SCENARIO_ID: Scenario(
|
||||||
|
id=_SCENARIO_ID,
|
||||||
|
goal="Increasing EPC",
|
||||||
|
goal_value=goal_band,
|
||||||
|
budget=None,
|
||||||
|
is_default=True,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
product=ProductJsonRepository(catalogue_path),
|
||||||
|
plan=plan_repo,
|
||||||
|
)
|
||||||
|
|
||||||
|
ModellingOrchestrator(
|
||||||
|
unit_of_work=lambda: unit,
|
||||||
|
calculator=Sap10Calculator(),
|
||||||
|
fuel_rates=FuelRatesStaticFileRepository(),
|
||||||
|
).run(
|
||||||
|
property_ids=[_PROPERTY_ID],
|
||||||
|
scenario_ids=[_SCENARIO_ID],
|
||||||
|
portfolio_id=_PORTFOLIO_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
plan = plan_repo.saved[(_PROPERTY_ID, _SCENARIO_ID)]
|
||||||
|
if print_table:
|
||||||
|
print("\n" + format_plan_table(plan))
|
||||||
|
return plan
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"cavity_wall_insulation": { "unit_cost_per_m2": 18.5 },
|
"cavity_wall_insulation": { "unit_cost_per_m2": 18.5 },
|
||||||
|
"loft_insulation": { "unit_cost_per_m2": 12.0 },
|
||||||
"suspended_floor_insulation": { "unit_cost_per_m2": 25.0 },
|
"suspended_floor_insulation": { "unit_cost_per_m2": 25.0 },
|
||||||
|
"solid_floor_insulation": { "unit_cost_per_m2": 45.0 },
|
||||||
"mechanical_ventilation": { "unit_cost_per_m2": 450.0 }
|
"mechanical_ventilation": { "unit_cost_per_m2": 450.0 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,22 @@ import pytest
|
||||||
|
|
||||||
from datatypes.epc.domain.epc import Epc
|
from datatypes.epc.domain.epc import Epc
|
||||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||||
from harness.console import run_one
|
from harness.console import DEFAULT_CATALOGUE, run_modelling, run_one
|
||||||
|
from repositories.product.product_json_repository import ProductJsonRepository
|
||||||
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
|
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
|
||||||
build_epc as _build_uninsulated_cavity_and_floor_epc,
|
build_epc as _build_uninsulated_cavity_and_floor_epc,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Every Measure Type the four fabric generators can emit; the harness catalogue
|
||||||
|
# must price all of them or an offline run raises mid-pipeline.
|
||||||
|
_GENERATOR_MEASURE_TYPES = (
|
||||||
|
"cavity_wall_insulation",
|
||||||
|
"loft_insulation",
|
||||||
|
"suspended_floor_insulation",
|
||||||
|
"solid_floor_insulation",
|
||||||
|
"mechanical_ventilation",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _uninsulated_lodged_epc() -> EpcPropertyData:
|
def _uninsulated_lodged_epc() -> EpcPropertyData:
|
||||||
epc = _build_uninsulated_cavity_and_floor_epc()
|
epc = _build_uninsulated_cavity_and_floor_epc()
|
||||||
|
|
@ -42,6 +53,28 @@ def test_run_one_returns_a_plan_and_prints_the_table(
|
||||||
assert "cavity_wall_insulation" in printed
|
assert "cavity_wall_insulation" in printed
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_modelling_inspects_a_plan_without_baseline_or_lodged_performance() -> None:
|
||||||
|
# Arrange — the RAW 000490 fixture, with NO lodged recorded-performance, so
|
||||||
|
# the Baseline stage could not run on it. Modelling re-scores the EPC itself.
|
||||||
|
epc: EpcPropertyData = _build_uninsulated_cavity_and_floor_epc()
|
||||||
|
|
||||||
|
# Act — Modelling only, no Ingestion / Baseline, no database.
|
||||||
|
plan = run_modelling(epc, goal_band="C", print_table=False)
|
||||||
|
|
||||||
|
# Assert — a multi-measure Plan came straight out of Modelling.
|
||||||
|
assert len(plan.measures) >= 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_sample_catalogue_prices_every_generator_measure_type() -> None:
|
||||||
|
# Arrange — the default offline catalogue.
|
||||||
|
products: ProductJsonRepository = ProductJsonRepository(DEFAULT_CATALOGUE)
|
||||||
|
|
||||||
|
# Act / Assert — get() raises if a Measure Type is unpriced, so an offline
|
||||||
|
# run over arbitrary EPCs never dies on a missing catalogue entry.
|
||||||
|
for measure_type in _GENERATOR_MEASURE_TYPES:
|
||||||
|
products.get(measure_type)
|
||||||
|
|
||||||
|
|
||||||
def test_run_one_threads_a_current_market_value_onto_the_plan() -> None:
|
def test_run_one_threads_a_current_market_value_onto_the_plan() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
epc: EpcPropertyData = _uninsulated_lodged_epc()
|
epc: EpcPropertyData = _uninsulated_lodged_epc()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue