Model/applications/modelling_e2e/errors.py
Jun-te Kim dd06b19c77 Fail unmodellable properties with a specific, debuggable error 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:10:55 +00:00

144 lines
5.2 KiB
Python

"""Specific, debuggable failures for the modelling_e2e prediction path.
When an EPC-less Property cannot be modelled the per-property failure handler
records ``str(exc)`` in the child SubTask's outputs (see ``handler.run_subtask``).
A single generic "not predictable" string conflated three unrelated causes —
unresolved property_type, an empty same-type cohort, and a degenerate
prediction — so the output could not tell an operator *which* one fired or which
data to fix. Each cause is its own exception carrying the Property's identity
plus the cause-specific context needed to debug it from the output alone.
All subclass ``ValueError`` so the existing ``except Exception`` per-property
boundary (and any ``except ValueError`` callers) keep catching them.
"""
from __future__ import annotations
from typing import Optional
class PropertyNotModellableError(ValueError):
"""Base: an EPC-less Property could not be predicted. Subclasses name the
specific cause. Carries the Property's identity so the failed SubTask output
identifies exactly which Property (and portfolio) to investigate."""
def __init__(
self,
*,
property_id: int,
uprn: int,
postcode: str,
portfolio_id: int,
cause: str,
) -> None:
self.property_id = property_id
self.uprn = uprn
self.postcode = postcode
self.portfolio_id = portfolio_id
self.cause = cause
super().__init__(
f"cannot model property_id={property_id} uprn={uprn} "
f"postcode={postcode!r} portfolio_id={portfolio_id}: {cause}"
)
class UnresolvedPropertyTypeError(PropertyNotModellableError):
"""No lodged EPC and the Property's ``property_type`` could not be resolved,
so the hard same-type cohort filter cannot fire. Almost always a missing or
contradictory Landlord Override (e.g. a House/Bungalow lodged with an
"another dwelling above" roof is skipped by the override build), or a
``property_type`` value with no gov-EPC code."""
def __init__(
self,
*,
property_id: int,
uprn: int,
postcode: str,
portfolio_id: int,
property_type: Optional[str],
built_form: Optional[str],
) -> None:
self.property_type = property_type
self.built_form = built_form
super().__init__(
property_id=property_id,
uprn=uprn,
postcode=postcode,
portfolio_id=portfolio_id,
cause=(
"no lodged EPC and property_type could not be resolved "
f"(property_type={property_type!r}, built_form={built_form!r}), "
"so no same-type cohort can be selected. Usually a missing or "
"contradictory Landlord Override (e.g. a House/Bungalow with an "
"'another dwelling above' roof is skipped by the override build). "
"Resolve this Property's property_type override to model it."
),
)
class NoSameTypeComparablesError(PropertyNotModellableError):
"""``property_type`` resolved, but no same-type comparable was found in the
Property's own postcode or — after broadening — the nearby-postcode cohort,
so it cannot be sized from a mixed-type cohort (ADR-0031)."""
def __init__(
self,
*,
property_id: int,
uprn: int,
postcode: str,
portfolio_id: int,
property_type: str,
broadened: bool,
) -> None:
self.property_type = property_type
self.broadened = broadened
scope = (
"its own postcode or the broadened nearby-postcode cohort"
if broadened
else "its own postcode"
)
super().__init__(
property_id=property_id,
uprn=uprn,
postcode=postcode,
portfolio_id=portfolio_id,
cause=(
f"no lodged EPC; property_type={property_type!r} resolved but no "
f"same-type comparable was found in {scope}. Cannot size the "
"Property from a mixed-type cohort."
),
)
class DegeneratePredictionError(PropertyNotModellableError):
"""A same-type cohort was found, but the synthesised EPC carried no MAIN
building part — every comparable seeding the structure was lodged with a
null/unrecognised part identifier — so the SAP calculation has no main
dwelling to anchor on."""
def __init__(
self,
*,
property_id: int,
uprn: int,
postcode: str,
portfolio_id: int,
property_type: str,
cohort_size: int,
) -> None:
self.property_type = property_type
self.cohort_size = cohort_size
super().__init__(
property_id=property_id,
uprn=uprn,
postcode=postcode,
portfolio_id=portfolio_id,
cause=(
f"no lodged EPC; predicted from a {cohort_size}-member "
f"property_type={property_type!r} cohort, but the synthesised EPC "
"had no MAIN building part (the structural template was lodged "
"with a null part identifier). Cannot anchor the SAP calculation."
),
)