mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
461 lines
18 KiB
Python
461 lines
18 KiB
Python
import pandas as pd
|
|
from numpy import nan
|
|
from typing import Union, Callable
|
|
from collections.abc import Mapping
|
|
from datatypes.epc.roof import EpcRoofDescriptions
|
|
from datatypes.epc.efficiency import EpcEfficiency
|
|
from datatypes.epc.construction_age_band import EpcConstructionAgeBand
|
|
|
|
roof_map = {
|
|
# Dwelling above
|
|
('AnotherDwellingAbove', 'Another Dwelling Above'): EpcRoofDescriptions.another_dwelling_above,
|
|
('SameDwellingAbove', 'Same Dwelling Above'): EpcRoofDescriptions.another_dwelling_above,
|
|
# Pitched, normal loft access, with a loft thickness
|
|
('PitchedNormalLoftAccess', 'mm25'): EpcRoofDescriptions.loft_25mm_insulation,
|
|
('PitchedNormalLoftAccess', 'mm50'): EpcRoofDescriptions.loft_50mm_insulation,
|
|
('PitchedNormalLoftAccess', 'mm75'): EpcRoofDescriptions.loft_75mm_insulation,
|
|
('PitchedNormalLoftAccess', 'mm100'): EpcRoofDescriptions.loft_100mm_insulation,
|
|
('PitchedNormalLoftAccess', 'mm150'): EpcRoofDescriptions.loft_150mm_insulation,
|
|
('PitchedNormalLoftAccess', 'mm200'): EpcRoofDescriptions.loft_200mm_insulation,
|
|
('PitchedNormalLoftAccess', 'mm250'): EpcRoofDescriptions.loft_250mm_insulation,
|
|
('PitchedNormalLoftAccess', 'mm270'): EpcRoofDescriptions.loft_270mm_insulation,
|
|
('PitchedNormalLoftAccess', 'mm300'): EpcRoofDescriptions.loft_300mm_insulation,
|
|
('PitchedNormalLoftAccess', 'mm350'): EpcRoofDescriptions.loft_350mm_insulation,
|
|
('PitchedNormalLoftAccess', 'mm400'): EpcRoofDescriptions.loft_400mm_plus_insulation,
|
|
|
|
# Pitched, no loft access, with a loft thickness
|
|
('PitchedNormalNoLoftAccess', 'mm25'): EpcRoofDescriptions.loft_25mm_insulation,
|
|
('PitchedNormalNoLoftAccess', 'mm50'): EpcRoofDescriptions.loft_50mm_insulation,
|
|
('PitchedNormalNoLoftAccess', 'mm75'): EpcRoofDescriptions.loft_75mm_insulation,
|
|
('PitchedNormalNoLoftAccess', 'mm100'): EpcRoofDescriptions.loft_100mm_insulation,
|
|
('PitchedNormalNoLoftAccess', 'mm150'): EpcRoofDescriptions.loft_150mm_insulation,
|
|
('PitchedNormalNoLoftAccess', 'mm200'): EpcRoofDescriptions.loft_200mm_insulation,
|
|
('PitchedNormalNoLoftAccess', 'mm250'): EpcRoofDescriptions.loft_250mm_insulation,
|
|
('PitchedNormalNoLoftAccess', 'mm270'): EpcRoofDescriptions.loft_270mm_insulation,
|
|
('PitchedNormalNoLoftAccess', 'mm300'): EpcRoofDescriptions.loft_300mm_insulation,
|
|
('PitchedNormalNoLoftAccess', 'mm350'): EpcRoofDescriptions.loft_350mm_insulation,
|
|
('PitchedNormalNoLoftAccess', 'mm400'): EpcRoofDescriptions.loft_400mm_plus_insulation,
|
|
|
|
# All pitched options with asbuilt or unknown got to EpcRoofDescriptions.pitched_insulated_assumed
|
|
# With access
|
|
('PitchedNormalLoftAccess', nan): EpcRoofDescriptions.pitched_insulated_assumed,
|
|
('PitchedNormalLoftAccess', 'AsBuilt'): EpcRoofDescriptions.pitched_insulated_assumed,
|
|
('PitchedNormalLoftAccess', 'Unknown'): EpcRoofDescriptions.pitched_insulated_assumed,
|
|
# No access
|
|
('PitchedNormalNoLoftAccess', nan): EpcRoofDescriptions.pitched_insulated_assumed,
|
|
('PitchedNormalNoLoftAccess', 'AsBuilt'): EpcRoofDescriptions.pitched_insulated_assumed,
|
|
('PitchedNormalNoLoftAccess', 'Unknown'): EpcRoofDescriptions.pitched_insulated_assumed,
|
|
|
|
# Flat
|
|
('Flat', 'NoInsulation'): EpcRoofDescriptions.flat_no_insulation,
|
|
# Flat - limited insulation
|
|
('Flat', '12mm'): EpcRoofDescriptions.flat_limited_insulation,
|
|
('Flat', 'mm25'): EpcRoofDescriptions.flat_limited_insulation,
|
|
('Flat', 'mm50'): EpcRoofDescriptions.flat_limited_insulation,
|
|
# Flat insulated
|
|
('Flat', 'mm75'): EpcRoofDescriptions.flat_insulated,
|
|
('Flat', 'mm100'): EpcRoofDescriptions.flat_insulated,
|
|
('Flat', 'mm150'): EpcRoofDescriptions.flat_insulated,
|
|
('Flat', 'mm200'): EpcRoofDescriptions.flat_insulated,
|
|
('Flat', 'mm250'): EpcRoofDescriptions.flat_insulated,
|
|
('Flat', 'mm300'): EpcRoofDescriptions.flat_insulated,
|
|
('Flat', 'mm350'): EpcRoofDescriptions.flat_insulated,
|
|
('Flat', 'mm400'): EpcRoofDescriptions.flat_insulated,
|
|
# Flat - as built or unknown
|
|
('Flat', 'AsBuilt'): None, # To be classified
|
|
('Flat', nan): None, # To be classified
|
|
('Flat', 'Unknown'): None, # To be classified
|
|
|
|
# 12mm = very poor & has limited insulation description
|
|
# 25, 50 = poor & has limited insulation description
|
|
# 75, 100, 125mm = average (Flat, insulated)
|
|
# 150, 175, 200, 225, 250mm = good (Flat, insulated)
|
|
# 270mm+ = very good (Flat, insulated)
|
|
|
|
# Thatched
|
|
('PitchedThatched', 'mm50'): EpcRoofDescriptions.thatched_with_additional_insulation,
|
|
('PitchedThatched', 'mm150'): EpcRoofDescriptions.thatched_with_additional_insulation,
|
|
('PitchedThatched', 'mm300'): EpcRoofDescriptions.thatched_with_additional_insulation,
|
|
('PitchedThatched', 'Unknown'): EpcRoofDescriptions.thatched, # efficiency classified based on age
|
|
|
|
# Sloping:
|
|
# Limited (12 very poor, 25-50 poor)
|
|
('PitchedWithSlopingCeiling', 'mm12'): EpcRoofDescriptions.sloping_pitched_limited_insulation,
|
|
('PitchedWithSlopingCeiling', 'mm25'): EpcRoofDescriptions.sloping_pitched_limited_insulation,
|
|
('PitchedWithSlopingCeiling', 'mm50'): EpcRoofDescriptions.sloping_pitched_limited_insulation,
|
|
# Insulated 75mm+ (75 - 125 average, 150 - 250 good, 270+ very good)
|
|
('PitchedWithSlopingCeiling', 'mm75'): EpcRoofDescriptions.sloping_pitched_insulated,
|
|
('PitchedWithSlopingCeiling', 'mm100'): EpcRoofDescriptions.sloping_pitched_insulated,
|
|
('PitchedWithSlopingCeiling', 'mm150'): EpcRoofDescriptions.sloping_pitched_insulated,
|
|
('PitchedWithSlopingCeiling', 'mm200'): EpcRoofDescriptions.sloping_pitched_insulated,
|
|
('PitchedWithSlopingCeiling', 'mm250'): EpcRoofDescriptions.sloping_pitched_insulated,
|
|
('PitchedWithSlopingCeiling', 'mm270'): EpcRoofDescriptions.sloping_pitched_insulated,
|
|
('PitchedWithSlopingCeiling', 'mm300'): EpcRoofDescriptions.sloping_pitched_insulated,
|
|
('PitchedWithSlopingCeiling', 'mm350'): EpcRoofDescriptions.sloping_pitched_insulated,
|
|
('PitchedWithSlopingCeiling', 'mm400'): EpcRoofDescriptions.sloping_pitched_insulated,
|
|
# As built/unknown
|
|
('PitchedWithSlopingCeiling', 'AsBuilt'): None, # To be classified
|
|
('PitchedWithSlopingCeiling', nan): None, # To be classified
|
|
('PitchedWithSlopingCeiling', 'Unknown'): None, #
|
|
}
|
|
|
|
roof_unknown_age_fallback = {
|
|
"Flat": EpcRoofDescriptions.flat_as_built_unknown,
|
|
"PitchedWithSlopingCeiling": EpcRoofDescriptions.sloping_pitched_as_built_unknown,
|
|
"PitchedThatched": EpcRoofDescriptions.thatched_as_built_unknown,
|
|
"PitchedNormalLoftAccess": EpcRoofDescriptions.loft_as_built_unknown,
|
|
"PitchedNormalNoLoftAccess": EpcRoofDescriptions.loft_as_built_unknown,
|
|
}
|
|
|
|
RoofEfficiencyRule = Union[
|
|
EpcEfficiency,
|
|
Callable[[EpcConstructionAgeBand, int | None], EpcEfficiency],
|
|
]
|
|
|
|
|
|
def flat_insulated_efficiency_age_band(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
"""
|
|
before 1900, 1900-1929, 1930-1949, 1950-1966, 1967-1975 -> Pitched, no insulation, Very Poor
|
|
1976-1982 -> Pitched, limited insulation, Poor
|
|
1983-1990, to 1996-2002 Pitched, insulated, Average
|
|
2003 - 2006, 2012-2022 -> Pitched, insulated, Good
|
|
2023 onwards -> Pitched, insulated, Very Good
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
|
|
start_year = age_band.start_year()
|
|
if start_year >= 2023:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
if start_year >= 2003:
|
|
return EpcEfficiency.GOOD
|
|
|
|
if start_year >= 1983:
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
if start_year >= 1976:
|
|
return EpcEfficiency.POOR
|
|
|
|
return EpcEfficiency.VERY_POOR
|
|
|
|
|
|
def flat_insulated_efficiency_thickness(insulation_thickness: int | None) -> EpcEfficiency:
|
|
"""
|
|
12mm -> Very Poor
|
|
25mm - 50mm -> Poor
|
|
75mm - 125mm -> Pitched, insulated, average
|
|
150mm - 250mm -> good
|
|
270mm+ -> very good
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:return: EpcEfficiency
|
|
"""
|
|
|
|
if insulation_thickness is None:
|
|
raise ValueError("Insulation thickness is required for flat insulated efficiency calculation")
|
|
|
|
if insulation_thickness >= 270:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
if 150 <= insulation_thickness <= 250:
|
|
return EpcEfficiency.GOOD
|
|
|
|
if 75 <= insulation_thickness <= 125:
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
if 25 <= insulation_thickness <= 50:
|
|
return EpcEfficiency.POOR
|
|
|
|
return EpcEfficiency.VERY_POOR
|
|
|
|
|
|
def flat_efficiency(insulation_thickness: int | None, age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
"""
|
|
Combines both age band and insulation thickness to determine flat roof efficiency.
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
if insulation_thickness is not None:
|
|
return flat_insulated_efficiency_thickness(insulation_thickness)
|
|
|
|
return flat_insulated_efficiency_age_band(age_band)
|
|
|
|
|
|
def loft_insulated_efficiency(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
"""
|
|
2023 onwards -> Very Good
|
|
2012-2022 -> Very Good
|
|
2007-2011 -> Very Good
|
|
2003-2006 -> Very Good
|
|
1996-2002 -> Good
|
|
1991-1995 -> Good
|
|
1983-1990 -> Average
|
|
1976-1982 -> Average
|
|
1967-1975 -> Average
|
|
1950-1966 -> Average
|
|
1930-1949 -> Average
|
|
1900-1929 -> Average
|
|
before 1900 -> Average
|
|
:param age_band: Input age band, EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
year = age_band.start_year()
|
|
if year >= 2003:
|
|
return EpcEfficiency.VERY_GOOD
|
|
if year >= 1991:
|
|
return EpcEfficiency.GOOD
|
|
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
|
|
def thatched_efficiency_age_band(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
"""
|
|
Maps thatched roof efficiency based on construction age band.
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
year = age_band.start_year()
|
|
if year >= 2023:
|
|
return EpcEfficiency.VERY_GOOD
|
|
if year >= 2003:
|
|
return EpcEfficiency.GOOD
|
|
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
|
|
def thatched_efficiency_thickness(insulation_thickness: int | None) -> EpcEfficiency:
|
|
"""
|
|
Maps thatched roof efficiency based on insulation thickness.
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:return: EpcEfficiency
|
|
"""
|
|
if insulation_thickness is None:
|
|
raise ValueError("Insulation thickness is required for thatched efficiency calculation")
|
|
|
|
if insulation_thickness >= 175:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
if insulation_thickness >= 25:
|
|
return EpcEfficiency.GOOD
|
|
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
|
|
def thatched_efficiency(
|
|
insulation_thickness: int | None,
|
|
age_band: EpcConstructionAgeBand,
|
|
) -> EpcEfficiency:
|
|
"""
|
|
Combines both age band and insulation thickness to determine thatched roof efficiency.
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
if insulation_thickness is not None:
|
|
return thatched_efficiency_thickness(insulation_thickness)
|
|
|
|
return thatched_efficiency_age_band(age_band)
|
|
|
|
|
|
def sloping_ceiling_efficiency_age_band(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
"""
|
|
Maps sloping ceiling roof efficiency based on construction age band.
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
year = age_band.start_year()
|
|
if year >= 2023:
|
|
return EpcEfficiency.VERY_GOOD
|
|
if year >= 2003:
|
|
return EpcEfficiency.GOOD
|
|
if year >= 1983:
|
|
return EpcEfficiency.AVERAGE
|
|
if year >= 1976:
|
|
return EpcEfficiency.POOR
|
|
|
|
return EpcEfficiency.VERY_POOR
|
|
|
|
|
|
def sloping_ceiling_efficiency_thickness(insulation_thickness: int | None) -> EpcEfficiency:
|
|
"""
|
|
Maps sloping ceiling roof efficiency based on insulation thickness.
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:return: EpcEfficiency
|
|
"""
|
|
if insulation_thickness is None:
|
|
raise ValueError("Insulation thickness is required for sloping ceiling efficiency calculation")
|
|
|
|
if insulation_thickness >= 270:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
if insulation_thickness >= 150:
|
|
return EpcEfficiency.GOOD
|
|
|
|
if insulation_thickness >= 75:
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
if insulation_thickness >= 25:
|
|
return EpcEfficiency.POOR
|
|
|
|
return EpcEfficiency.VERY_POOR
|
|
|
|
|
|
def sloping_ceiling_efficiency(
|
|
insulation_thickness: int | None,
|
|
age_band: EpcConstructionAgeBand,
|
|
) -> EpcEfficiency:
|
|
"""
|
|
Combines both age band and insulation thickness to determine sloping ceiling roof efficiency.
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
if insulation_thickness is not None:
|
|
return sloping_ceiling_efficiency_thickness(insulation_thickness)
|
|
|
|
return sloping_ceiling_efficiency_age_band(age_band)
|
|
|
|
|
|
def loft_insulated_at_rafters_efficiency_thickness(insulation_thickness: int | None) -> EpcEfficiency:
|
|
"""
|
|
400mm, 350mm = very good
|
|
200-300mm = good
|
|
125-175 = average
|
|
50-100 = poor
|
|
25 and below= very poor
|
|
:return:
|
|
"""
|
|
if insulation_thickness is None:
|
|
raise ValueError("Insulation thickness is required for loft insulated at rafters efficiency calculation")
|
|
|
|
if insulation_thickness >= 350:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
if insulation_thickness >= 200:
|
|
return EpcEfficiency.GOOD
|
|
|
|
if insulation_thickness >= 125:
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
if insulation_thickness >= 50:
|
|
return EpcEfficiency.POOR
|
|
|
|
return EpcEfficiency.VERY_POOR
|
|
|
|
|
|
def loft_insulated_at_rafters_efficiency_age_band(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
"""
|
|
# 2023 onwards -> Very Good
|
|
# 2003-2006, 2012-2022 -> Good
|
|
# 1983 - 1990, 1996-2002 -> Average
|
|
# 1976-1982 -> Poor
|
|
# 1967-1975 and earlier bands -> Very Poor
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
year = age_band.start_year()
|
|
if year >= 2023:
|
|
return EpcEfficiency.VERY_GOOD
|
|
if year >= 2003:
|
|
return EpcEfficiency.GOOD
|
|
if year >= 1983:
|
|
return EpcEfficiency.AVERAGE
|
|
if year >= 1976:
|
|
return EpcEfficiency.POOR
|
|
|
|
return EpcEfficiency.VERY_POOR
|
|
|
|
|
|
def loft_insulated_at_rafters_efficiency(
|
|
insulation_thickness: int | None,
|
|
age_band: EpcConstructionAgeBand,
|
|
) -> EpcEfficiency:
|
|
"""
|
|
Combines both age band and insulation thickness to determine loft insulated at rafters roof efficiency.
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
if insulation_thickness is not None:
|
|
return loft_insulated_at_rafters_efficiency_thickness(insulation_thickness)
|
|
|
|
return loft_insulated_at_rafters_efficiency_age_band(age_band)
|
|
|
|
|
|
ROOF_DESCRIPTION_EFFICIENCIES: Mapping[EpcRoofDescriptions, RoofEfficiencyRule] = {
|
|
# Flat roof
|
|
EpcRoofDescriptions.flat_no_insulation: EpcEfficiency.VERY_POOR,
|
|
EpcRoofDescriptions.flat_limited_insulation: flat_efficiency,
|
|
EpcRoofDescriptions.flat_insulated: flat_efficiency,
|
|
|
|
# Loft:
|
|
# value mappings
|
|
EpcRoofDescriptions.loft_12mm_insulation: EpcEfficiency.VERY_POOR,
|
|
EpcRoofDescriptions.loft_25mm_insulation: EpcEfficiency.POOR,
|
|
EpcRoofDescriptions.loft_50mm_insulation: EpcEfficiency.POOR,
|
|
EpcRoofDescriptions.loft_75mm_insulation: EpcEfficiency.AVERAGE,
|
|
EpcRoofDescriptions.loft_100mm_insulation: EpcEfficiency.AVERAGE,
|
|
EpcRoofDescriptions.loft_125mm_insulation: EpcEfficiency.AVERAGE,
|
|
EpcRoofDescriptions.loft_150mm_insulation: EpcEfficiency.GOOD,
|
|
EpcRoofDescriptions.loft_175mm_insulation: EpcEfficiency.GOOD,
|
|
EpcRoofDescriptions.loft_200mm_insulation: EpcEfficiency.GOOD,
|
|
EpcRoofDescriptions.loft_250mm_insulation: EpcEfficiency.GOOD,
|
|
EpcRoofDescriptions.loft_270mm_insulation: EpcEfficiency.VERY_GOOD,
|
|
EpcRoofDescriptions.loft_300mm_insulation: EpcEfficiency.VERY_GOOD,
|
|
EpcRoofDescriptions.loft_350mm_insulation: EpcEfficiency.VERY_GOOD,
|
|
EpcRoofDescriptions.loft_400mm_plus_insulation: EpcEfficiency.VERY_GOOD,
|
|
EpcRoofDescriptions.pitched_no_insulation: EpcEfficiency.VERY_POOR,
|
|
# function mappings
|
|
EpcRoofDescriptions.pitched_insulated_assumed: loft_insulated_efficiency,
|
|
|
|
# Loft af rafters
|
|
EpcRoofDescriptions.loft_insulated_at_rafters: loft_insulated_at_rafters_efficiency,
|
|
|
|
# Another dwelling above
|
|
EpcRoofDescriptions.another_dwelling_above: EpcEfficiency.NA,
|
|
|
|
# Thatched
|
|
EpcRoofDescriptions.thatched: thatched_efficiency,
|
|
EpcRoofDescriptions.thatched_with_additional_insulation: thatched_efficiency,
|
|
|
|
# Sloping ceiling
|
|
EpcRoofDescriptions.sloping_pitched_insulated: sloping_ceiling_efficiency,
|
|
EpcRoofDescriptions.sloping_pitched_limited_insulation: sloping_ceiling_efficiency,
|
|
EpcRoofDescriptions.sloping_pitched_no_insulation: EpcEfficiency.VERY_POOR,
|
|
|
|
}
|
|
|
|
|
|
def resolve_roof_efficiency(
|
|
description: EpcRoofDescriptions,
|
|
age_band: EpcConstructionAgeBand | None,
|
|
insulation_thickness: int | None,
|
|
) -> EpcEfficiency:
|
|
"""
|
|
Resolve roof efficiency from description + age band + insulation thickness.
|
|
"""
|
|
|
|
# Unknown / holding descriptions → efficiency unknown
|
|
if description in description.unknown_descriptions:
|
|
return EpcEfficiency.NA
|
|
|
|
rule = ROOF_DESCRIPTION_EFFICIENCIES.get(description)
|
|
|
|
if rule is None:
|
|
return EpcEfficiency.NA
|
|
|
|
# Fixed efficiency
|
|
if isinstance(rule, EpcEfficiency):
|
|
return rule
|
|
|
|
# Callable rule
|
|
if age_band is None or pd.isnull(age_band):
|
|
return EpcEfficiency.NA
|
|
|
|
try:
|
|
# Try (thickness, age_band)
|
|
return rule(insulation_thickness, age_band)
|
|
except TypeError:
|
|
# Fallback to (age_band)
|
|
return rule(age_band)
|