Model/domain/modelling/contingencies.py
Khalim Conn-Kowlessar 0d1ec2228d feat(modelling): cost data for secondary-heating-removal (ADR-0028)
Flat per-dwelling decommission price (sample_catalogue \£250) + 0.25 contingency
(covers unknown heater count / hard-wired-vs-plugged / repaint extent). The JSON
repo joins the contingency from config, proven by the new repo test. No composite
Products machinery — a lodged secondary is one roughly-fixed job, not room-scaled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 13:51:16 +00:00

42 lines
1.5 KiB
Python

"""Per-Measure-Type contingency rates.
The one cost component carried separately from a Product's fully-loaded total
(CONTEXT.md). Mirrors the legacy `recommendations/Costs.py::Costs.CONTINGENCIES`;
extended as each measure type lands.
"""
_CONTINGENCY_RATES: dict[str, float] = {
"cavity_wall_insulation": 0.10,
"loft_insulation": 0.10,
"sloping_ceiling_insulation": 0.10,
"flat_roof_insulation": 0.10,
"suspended_floor_insulation": 0.20,
"solid_floor_insulation": 0.26,
"mechanical_ventilation": 0.26,
"external_wall_insulation": 0.26,
"internal_wall_insulation": 0.26,
"double_glazing": 0.15,
"secondary_glazing": 0.15,
"low_energy_lighting": 0.26,
"high_heat_retention_storage_heaters": 0.10,
"air_source_heat_pump": 0.25,
"gas_boiler_upgrade": 0.26,
"system_tune_up": 0.10,
"system_tune_up_zoned": 0.10,
"solar_pv": 0.15,
# Decommissioning a fixed secondary heater + localised making-good is small,
# uncertain work: the rate covers the unknown heater count / hard-wired vs
# plugged status / repaint extent (ADR-0028).
"secondary_heating_removal": 0.25,
}
def contingency_rate(measure_type: str) -> float:
"""Return the contingency rate for a Measure Type, raising if unknown
(strict — do not silently default, per the repo's strict-raise convention)."""
try:
return _CONTINGENCY_RATES[measure_type]
except KeyError as exc:
raise ValueError(
f"no contingency rate configured for measure type {measure_type!r}"
) from exc