From 0c182895bff222b3bfa9fb171e9ee084159a40ae Mon Sep 17 00:00:00 2001 From: Charlotte Avery <143102500+charlotte-avery@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:30:53 +0100 Subject: [PATCH] Update model assumptions (#141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update BUS grant Now using same grant of £7.5k for both GSHP and ASHP. * Set default ban announcement to 2025 * Update BUS tests * Lower EPC threshold to D Threshold for household to be suitable for a heat pump reduced from EPC C to D. * Exclude agents in social rentals * Add extended BUS intervention * Add hassle factor for rented as input argument Set default value for rented of 0.4, and default value for owner occupied as 0.1. * Increase extended BUS funding cap to 1.65B £150m of total funding per year from 2024 to 2035 * Update fuel costs Update using 2024 fuel prices and adjust associated policy costs * Start simulation 1 Jan 2024 * Delay HP discount date to 2026 HP prices are set based on 2020 data, however prices have remained approximately unchanged (looking at 2024 data), so it no longer makes sense to discount the HP price in 2023. Delay discount to 2026. --- k8s/job.jsonnet | 109 ++++++++++++++++++------------ simulation/__main__.py | 18 +++-- simulation/agents.py | 39 +++++++++-- simulation/constants.py | 1 + simulation/costs.py | 42 +++++++++++- simulation/model.py | 4 ++ simulation/tests/common.py | 9 +-- simulation/tests/test_agents.py | 46 +++++++++++-- simulation/tests/test_costs.py | 113 +++++++++++++++++++++++++++++--- simulation/tests/test_main.py | 21 +++++- simulation/tests/test_model.py | 14 ++-- 11 files changed, 336 insertions(+), 80 deletions(-) diff --git a/k8s/job.jsonnet b/k8s/job.jsonnet index 4efc451..647d660 100644 --- a/k8s/job.jsonnet +++ b/k8s/job.jsonnet @@ -17,7 +17,7 @@ local job(name, args_excl_output) = { command: ['python', '-m', 'simulation'], args: args_excl_output + [ '--bigquery', - 'select * from %s.prod_domestic_heating.household_agents where abs(mod(farm_fingerprint(id), 307)) = 1' % std.extVar('PROJECT_ID'), + "select * from %s.prod_domestic_heating.household_agents where abs(mod(farm_fingerprint(id), 307)) = 1 and occupant_type != 'rented_social'" % std.extVar('PROJECT_ID'), 'gs://%s/%s/{uuid}/output.jsonl.gz' % [std.extVar('BUCKET_NAME'), name], ], env: [ @@ -40,36 +40,25 @@ local job(name, args_excl_output) = { [ // scenarios - - job('01-%s-rhi' % std.extVar('SHORT_SHA'), [ - '--intervention', - 'rhi', - '--start-date', - '2012-01-01', - '--steps', - '120', - '--heat-pump-installer-annual-growth-rate', - '0', - ]), job('02-%s-baseline' % std.extVar('SHORT_SHA'), [ '--air-source-heat-pump-price-discount-date', - '2023-01-01:0.3', + '2026-01-01:0.3', ]), job('03a-%s-bus' % std.extVar('SHORT_SHA'), [ '--intervention', 'boiler_upgrade_scheme', '--air-source-heat-pump-price-discount-date', - '2023-01-01:0.3', + '2026-01-01:0.3', ]), job('03b-%s-bus-policy' % std.extVar('SHORT_SHA'), [ '--intervention', 'boiler_upgrade_scheme', '--air-source-heat-pump-price-discount-date', - '2023-01-01:0.3', + '2026-01-01:0.3', '--price-gbp-per-kwh-gas', - '0.0589', + '0.0682', '--price-gbp-per-kwh-electricity', - '0.1494', + '0.182', ]), job('03c-%s-bus-policy-high-awareness' % std.extVar('SHORT_SHA'), [ '--intervention', @@ -77,11 +66,39 @@ local job(name, args_excl_output) = { '--heat-pump-awareness', '0.5', '--air-source-heat-pump-price-discount-date', - '2023-01-01:0.3', + '2026-01-01:0.3', + '--price-gbp-per-kwh-gas', + '0.0682', + '--price-gbp-per-kwh-electricity', + '0.182', + ]), + job('03d-%s-extended-bus' % std.extVar('SHORT_SHA'), [ + '--intervention', + 'extended_boiler_upgrade_scheme', + '--air-source-heat-pump-price-discount-date', + '2026-01-01:0.3', + ]), + job('03e-%s-extended-bus-policy' % std.extVar('SHORT_SHA'), [ + '--intervention', + 'extended_boiler_upgrade_scheme', + '--air-source-heat-pump-price-discount-date', + '2026-01-01:0.3', + '--price-gbp-per-kwh-gas', + '0.0682', + '--price-gbp-per-kwh-electricity', + '0.182', + ]), + job('03f-%s-extended-bus-policy-high-awareness' % std.extVar('SHORT_SHA'), [ + '--intervention', + 'extended_boiler_upgrade_scheme', + '--heat-pump-awareness', + '0.5', + '--air-source-heat-pump-price-discount-date', + '2026-01-01:0.3', '--price-gbp-per-kwh-gas', - '0.0589', + '0.0682', '--price-gbp-per-kwh-electricity', - '0.1494', + '0.182', ]), job('04a-%s-max-policy' % std.extVar('SHORT_SHA'), [ '--intervention', @@ -95,11 +112,11 @@ local job(name, args_excl_output) = { '--heat-pump-awareness', '0.5', '--air-source-heat-pump-price-discount-date', - '2023-01-01:0.3', + '2026-01-01:0.3', '--price-gbp-per-kwh-gas', - '0.0589', + '0.0682', '--price-gbp-per-kwh-electricity', - '0.1494', + '0.182', '--include-new-builds', ]), job('04b-%s-max-policy-unlimited-installers' % std.extVar('SHORT_SHA'), [ @@ -114,11 +131,11 @@ local job(name, args_excl_output) = { '--heat-pump-awareness', '0.5', '--air-source-heat-pump-price-discount-date', - '2023-01-01:0.3', + '2026-01-01:0.3', '--price-gbp-per-kwh-gas', - '0.0589', + '0.0682', '--price-gbp-per-kwh-electricity', - '0.1494', + '0.182', '--heat-pump-installer-count', '10000000000' ]), @@ -134,11 +151,11 @@ local job(name, args_excl_output) = { '--heat-pump-awareness', '0.5', '--air-source-heat-pump-price-discount-date', - '2023-01-01:0.3', + '2026-01-01:0.3', '--price-gbp-per-kwh-gas', - '0.0589', + '0.0682', '--price-gbp-per-kwh-electricity', - '0.1494', + '0.182', '--include-new-builds' ]), job('05-%s-max-industry' % std.extVar('SHORT_SHA'), [ @@ -146,11 +163,13 @@ local job(name, args_excl_output) = { '0.5', '--heating-system-hassle-factor', '0', + '--rented-heating-system-hassle-factor', + '0', '--all-agents-heat-pump-suitable', '--air-source-heat-pump-price-discount-date', - '2023-01-01:0.3', + '2026-01-01:0.3', '--air-source-heat-pump-price-discount-date', - '2025-01-01:0.6', + '2028-01-01:0.6', ]), job('06a-%s-max-policy-max-industry' % std.extVar('SHORT_SHA'), [ '--intervention', @@ -165,15 +184,17 @@ local job(name, args_excl_output) = { '0.5', '--heating-system-hassle-factor', '0', + '--rented-heating-system-hassle-factor', + '0', '--all-agents-heat-pump-suitable', '--air-source-heat-pump-price-discount-date', - '2023-01-01:0.3', + '2026-01-01:0.3', '--air-source-heat-pump-price-discount-date', - '2025-01-01:0.6', + '2028-01-01:0.6', '--price-gbp-per-kwh-gas', - '0.0589', + '0.0682', '--price-gbp-per-kwh-electricity', - '0.1494', + '0.182', '--include-new-builds' ]), job('06b-%s-max-policy-max-industry-unlimited-installers' % std.extVar('SHORT_SHA'), [ @@ -189,15 +210,17 @@ local job(name, args_excl_output) = { '0.5', '--heating-system-hassle-factor', '0', + '--rented-heating-system-hassle-factor', + '0', '--all-agents-heat-pump-suitable', '--air-source-heat-pump-price-discount-date', - '2023-01-01:0.3', + '2026-01-01:0.3', '--air-source-heat-pump-price-discount-date', - '2025-01-01:0.6', + '2028-01-01:0.6', '--price-gbp-per-kwh-gas', - '0.0589', + '0.0682', '--price-gbp-per-kwh-electricity', - '0.1494', + '0.182', '--heat-pump-installer-count', '10000000000' ]), @@ -234,10 +257,14 @@ local job(name, args_excl_output) = { job('56-%s-heating-system-hassle-low' % std.extVar('SHORT_SHA'), [ '--heating-system-hassle-factor', '0', + '--rented-heating-system-hassle-factor', + '0.2', ]), job('57-%s-heating-system-hassle-high' % std.extVar('SHORT_SHA'), [ '--heating-system-hassle-factor', '0.2', + '--rented-heating-system-hassle-factor', + '0.6', ]), job('58-%s-heat-pump-suitable-low' % std.extVar('SHORT_SHA'), []), @@ -264,11 +291,11 @@ local job(name, args_excl_output) = { job('61-%s-ashp-discount-low' % std.extVar('SHORT_SHA'), [ '--air-source-heat-pump-price-discount-date', - '2023-01-01:0', + '2026-01-01:0', ]), job('62-%s-ashp-discount-high' % std.extVar('SHORT_SHA'), [ '--air-source-heat-pump-price-discount-date', - '2023-01-01:0.6', + '2026-01-01:0.6', ]), job('63-%s-hp-installer-growth-low' % std.extVar('SHORT_SHA'), [ diff --git a/simulation/__main__.py b/simulation/__main__.py index 5f09a83..101191f 100644 --- a/simulation/__main__.py +++ b/simulation/__main__.py @@ -70,7 +70,7 @@ def format_uuid(str): "--start-date", dest="start_datetime", type=convert_to_datetime, - default=datetime.datetime(2022, 1, 1), + default=datetime.datetime(2024, 1, 1), ) parser.add_argument( @@ -97,6 +97,13 @@ def format_uuid(str): help="A value between 0 and 1 which suppresses the likelihood of a household choosing a given heating system (the higher the value, the lower the likelihood)", ) + parser.add_argument( + "--rented-heating-system-hassle-factor", + type=float_between_0_and_1, + default=0.4, + help="A value between 0 and 1 which suppresses the likelihood of a household choosing a given heating system (the higher the value, the lower the likelihood)", + ) + parser.add_argument( "--intervention", action="append", @@ -161,15 +168,15 @@ def check_string_is_isoformat_datetime(string) -> str: parser.add_argument( "--gas-oil-boiler-ban-announce-date", - default=datetime.datetime(2035, 1, 1), + default=datetime.datetime(2025, 1, 1), type=convert_to_datetime, ) # SOURCE: Default values from https://energysavingtrust.org.uk/about-us/our-data/ (England, Scotland and Wales) # These fuel prices were last updated in November 2021, based on predicted fuel prices for 2022 - parser.add_argument("--price-gbp-per-kwh-gas", type=float, default=0.0465) - parser.add_argument("--price-gbp-per-kwh-electricity", type=float, default=0.2006) - parser.add_argument("--price-gbp-per-kwh-oil", type=float, default=0.0482) + parser.add_argument("--price-gbp-per-kwh-gas", type=float, default=0.062) + parser.add_argument("--price-gbp-per-kwh-electricity", type=float, default=0.245) + parser.add_argument("--price-gbp-per-kwh-oil", type=float, default=0.068) return parser.parse_args(args) @@ -207,6 +214,7 @@ def validate_args(args): args.annual_renovation_rate, args.household_num_lookahead_years, args.heating_system_hassle_factor, + args.rented_heating_system_hassle_factor, args.intervention, args.all_agents_heat_pump_suitable, args.gas_oil_boiler_ban_date, diff --git a/simulation/agents.py b/simulation/agents.py index f57af89..a744c64 100644 --- a/simulation/agents.py +++ b/simulation/agents.py @@ -57,6 +57,7 @@ LOFT_INSULATION_JOISTS_COST, discount_annual_cash_flow, estimate_boiler_upgrade_scheme_grant, + estimate_extended_boiler_upgrade_scheme_grant, estimate_rhi_annual_payment, get_heating_fuel_costs_net_present_value, get_unit_and_install_costs, @@ -250,7 +251,7 @@ def is_heat_pump_suitable(self) -> bool: if not all( [ self.is_heat_pump_suitable_archetype, - self.potential_epc_rating.value >= EPCRating.C.value, + self.potential_epc_rating.value >= EPCRating.D.value, ] ) else True @@ -493,6 +494,12 @@ def get_total_heating_system_costs( subsidies = estimate_boiler_upgrade_scheme_grant(heating_system, model) if subsidies > 0: self.boiler_upgrade_grant_available = True + elif InterventionType.EXTENDED_BOILER_UPGRADE_SCHEME in model.interventions: + subsidies = estimate_extended_boiler_upgrade_scheme_grant( + heating_system, model + ) + if subsidies > 0: + self.boiler_upgrade_grant_available = True elif InterventionType.RHI in model.interventions: rhi_annual_payment = estimate_rhi_annual_payment(self, heating_system) @@ -506,8 +513,24 @@ def get_total_heating_system_costs( return unit_and_install_costs, fuel_costs_net_present_value, -subsidies + def reset_heating_system_hassle( + self, + heating_system_hassle_factor: float, + rented_heating_system_hassle_factor: float, + ): + if ( + self.occupant_type == OccupantType.RENTED_PRIVATE + or self.occupant_type == OccupantType.RENTED_SOCIAL + ): + return rented_heating_system_hassle_factor + else: + return heating_system_hassle_factor + def choose_heating_system( - self, costs: Dict[HeatingSystem, float], heating_system_hassle_factor: float + self, + costs: Dict[HeatingSystem, float], + heating_system_hassle_factor: float, + rented_heating_system_hassle_factor: float, ): weights = [] @@ -519,6 +542,10 @@ def choose_heating_system( ) weight = 1 / math.exp(cost_as_proportion_of_budget) if self.is_heating_system_hassle(heating_system): + heating_system_hassle_factor = self.reset_heating_system_hassle( + heating_system_hassle_factor, + rented_heating_system_hassle_factor, + ) weight *= 1 - heating_system_hassle_factor weights.append(weight) @@ -539,9 +566,9 @@ def install_heating_system( if self.boiler_upgrade_grant_available: if heating_system == HeatingSystem.HEAT_PUMP_AIR_SOURCE: - self.boiler_upgrade_grant_used = 5_000 + self.boiler_upgrade_grant_used = 7_500 if heating_system == HeatingSystem.HEAT_PUMP_GROUND_SOURCE: - self.boiler_upgrade_grant_used = 6_000 + self.boiler_upgrade_grant_used = 7_500 def reset_previous_heating_decision_log(self) -> None: @@ -641,7 +668,9 @@ def make_decisions(self, model): } chosen_heating_system = self.choose_heating_system( - heating_system_replacement_costs, model.heating_system_hassle_factor + heating_system_replacement_costs, + model.heating_system_hassle_factor, + model.rented_heating_system_hassle_factor, ) self.install_heating_system(chosen_heating_system, model) diff --git a/simulation/constants.py b/simulation/constants.py index 68d44ff..56e8e9d 100644 --- a/simulation/constants.py +++ b/simulation/constants.py @@ -205,6 +205,7 @@ class InterventionType(enum.Enum): RHI = 0 BOILER_UPGRADE_SCHEME = 1 GAS_OIL_BOILER_BAN = 2 + EXTENDED_BOILER_UPGRADE_SCHEME = 3 # Source: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/householdsbytypeofhouseholdandfamilyregionsofenglandandukconstituentcountries diff --git a/simulation/costs.py b/simulation/costs.py index 71734e7..22e57a1 100644 --- a/simulation/costs.py +++ b/simulation/costs.py @@ -327,7 +327,47 @@ def estimate_boiler_upgrade_scheme_grant( return 0 if heating_system == HeatingSystem.HEAT_PUMP_AIR_SOURCE: + return 7_500 + + if heating_system == HeatingSystem.HEAT_PUMP_GROUND_SOURCE: + return 7_500 + + +def estimate_extended_boiler_upgrade_scheme_grant( + heating_system: HeatingSystem, + model: "DomesticHeatingABM", +): + + if heating_system not in [ + HeatingSystem.HEAT_PUMP_AIR_SOURCE, + HeatingSystem.HEAT_PUMP_GROUND_SOURCE, + ]: + return 0 + + model_population_scale = ENGLAND_WALES_HOUSEHOLD_COUNT_2020 / model.household_count + boiler_upgrade_funding_cap_gbp = 1_650_000_000 / model_population_scale + if ( + model.boiler_upgrade_scheme_cumulative_spend_gbp + >= boiler_upgrade_funding_cap_gbp + ): + return 0 + + if ( + not datetime.date(2022, 4, 1) + <= model.current_datetime.date() + < datetime.date(2035, 4, 1) + ): + return 0 + + if ( + not datetime.date(2022, 4, 1) + <= model.current_datetime.date() + < datetime.date(2028, 4, 1) + ): return 5_000 + if heating_system == HeatingSystem.HEAT_PUMP_AIR_SOURCE: + return 7_500 + if heating_system == HeatingSystem.HEAT_PUMP_GROUND_SOURCE: - return 6_000 + return 7_500 diff --git a/simulation/model.py b/simulation/model.py index f984173..d4c0e2a 100644 --- a/simulation/model.py +++ b/simulation/model.py @@ -33,6 +33,7 @@ def __init__( annual_renovation_rate: float, household_num_lookahead_years: int, heating_system_hassle_factor: float, + rented_heating_system_hassle_factor: float, interventions: Optional[List[InterventionType]], gas_oil_boiler_ban_datetime: datetime.datetime, gas_oil_boiler_ban_announce_datetime: datetime.datetime, @@ -52,6 +53,7 @@ def __init__( self.annual_renovation_rate = annual_renovation_rate self.household_num_lookahead_years = household_num_lookahead_years self.heating_system_hassle_factor = heating_system_hassle_factor + self.rented_heating_system_hassle_factor = rented_heating_system_hassle_factor self.interventions = interventions or [] self.boiler_upgrade_scheme_cumulative_spend_gbp = 0 self.gas_oil_boiler_ban_datetime = gas_oil_boiler_ban_datetime @@ -247,6 +249,7 @@ def create_and_run_simulation( annual_renovation_rate: float, household_num_lookahead_years: int, heating_system_hassle_factor: float, + rented_heating_system_hassle_factor: float, interventions: Optional[List[InterventionType]], all_agents_heat_pump_suitable: bool, gas_oil_boiler_ban_datetime: datetime.datetime, @@ -268,6 +271,7 @@ def create_and_run_simulation( annual_renovation_rate=annual_renovation_rate, household_num_lookahead_years=household_num_lookahead_years, heating_system_hassle_factor=heating_system_hassle_factor, + rented_heating_system_hassle_factor=rented_heating_system_hassle_factor, interventions=interventions, gas_oil_boiler_ban_datetime=gas_oil_boiler_ban_datetime, gas_oil_boiler_ban_announce_datetime=gas_oil_boiler_ban_announce_datetime, diff --git a/simulation/tests/common.py b/simulation/tests/common.py index 6ee9df8..f79d42b 100644 --- a/simulation/tests/common.py +++ b/simulation/tests/common.py @@ -46,12 +46,13 @@ def model_factory(**model_attributes): "annual_renovation_rate": 0.05, "household_num_lookahead_years": 3, "heating_system_hassle_factor": 0.7, + "rented_heating_system_hassle_factor": 0.7, "interventions": [], "gas_oil_boiler_ban_datetime": datetime.datetime(2035, 1, 1), - "gas_oil_boiler_ban_announce_datetime": datetime.datetime(2035, 1, 1), - "price_gbp_per_kwh_gas": 0.0465, - "price_gbp_per_kwh_electricity": 0.2006, - "price_gbp_per_kwh_oil": 0.0482, + "gas_oil_boiler_ban_announce_datetime": datetime.datetime(2025, 1, 1), + "price_gbp_per_kwh_gas": 0.062, + "price_gbp_per_kwh_electricity": 0.245, + "price_gbp_per_kwh_oil": 0.068, "air_source_heat_pump_price_discount_schedule": None, "heat_pump_installer_count": 2_800, "heat_pump_installer_annual_growth_rate": 0, diff --git a/simulation/tests/test_agents.py b/simulation/tests/test_agents.py index 746c1ea..33fb74c 100644 --- a/simulation/tests/test_agents.py +++ b/simulation/tests/test_agents.py @@ -233,12 +233,12 @@ def test_impact_of_installing_insulation_measures_is_capped_at_potential_epc( assert household_at_potential_epc.roof_energy_efficiency == 5 assert household_at_potential_epc.epc_rating == epc - def test_households_with_potential_epc_below_C_are_not_heat_pump_suitable( + def test_households_with_potential_epc_below_D_are_not_heat_pump_suitable( self, ) -> None: low_potential_epc_household = household_factory( - potential_epc_rating=EPCRating.D + potential_epc_rating=EPCRating.E ) assert not low_potential_epc_household.is_heat_pump_suitable @@ -259,7 +259,7 @@ def test_heat_pumps_not_in_heating_system_options_if_household_not_heat_pump_sui event_trigger, ) -> None: - unsuitable_household = household_factory(potential_epc_rating=EPCRating.D) + unsuitable_household = household_factory(potential_epc_rating=EPCRating.E) model = model_factory() assert not HEAT_PUMPS.intersection( unsuitable_household.get_heating_system_options( @@ -509,7 +509,11 @@ def test_heat_pumps_never_selected_if_hassle_factor_is_one_and_not_currently_ins } assert ( - household.choose_heating_system(costs, heating_system_hassle_factor=1) + household.choose_heating_system( + costs, + heating_system_hassle_factor=1, + rented_heating_system_hassle_factor=1, + ) == HeatingSystem.BOILER_GAS ) @@ -627,7 +631,7 @@ def test_gas_boilers_have_higher_expected_fuel_costs_at_heating_decision_if_gas_ household = household_factory(heating_system=random.choices(list(BOILERS))[0]) model = model_factory( - price_gbp_per_kwh_gas=0.0465, + price_gbp_per_kwh_gas=0.062, ) _, costs_fuel, _ = household.get_total_heating_system_costs( @@ -635,7 +639,7 @@ def test_gas_boilers_have_higher_expected_fuel_costs_at_heating_decision_if_gas_ ) model_with_higher_gas_price = model_factory( - price_gbp_per_kwh_gas=0.0465 * 1.2, + price_gbp_per_kwh_gas=0.062 * 1.2, ) _, costs_fuel_higher_gas_price, _ = household.get_total_heating_system_costs( @@ -834,3 +838,33 @@ def test_heat_pump_suitable_households_can_choose_heat_pumps_in_all_event_trigge assert all( heating_system in heating_system_options for heating_system in HEAT_PUMPS ) + + @pytest.mark.parametrize("occupant_type", list(OccupantType)) + def test_landlord_heat_pump_hassle_if_not_already_installed( + self, + occupant_type, + ) -> None: + household = household_factory( + heating_system=random.choices(list(BOILERS))[0], occupant_type=occupant_type + ) + owner_occupier_hassle_factor = 0.1 + rented_hassle_factor = 0.4 + if ( + occupant_type == OccupantType.RENTED_PRIVATE + or occupant_type == OccupantType.RENTED_SOCIAL + ): + assert ( + household.reset_heating_system_hassle( + heating_system_hassle_factor=owner_occupier_hassle_factor, + rented_heating_system_hassle_factor=rented_hassle_factor, + ) + == rented_hassle_factor + ) + else: + assert ( + household.reset_heating_system_hassle( + heating_system_hassle_factor=owner_occupier_hassle_factor, + rented_heating_system_hassle_factor=rented_hassle_factor, + ) + == owner_occupier_hassle_factor + ) diff --git a/simulation/tests/test_costs.py b/simulation/tests/test_costs.py index 4e069b8..0d6e779 100644 --- a/simulation/tests/test_costs.py +++ b/simulation/tests/test_costs.py @@ -13,6 +13,7 @@ DECOMMISSIONING_COST_MAX, MEAN_COST_GBP_BOILER_GAS, estimate_boiler_upgrade_scheme_grant, + estimate_extended_boiler_upgrade_scheme_grant, estimate_rhi_annual_payment, get_heating_fuel_costs_net_present_value, get_unit_and_install_costs, @@ -168,10 +169,10 @@ def test_air_source_heat_pumps_unit_install_costs_are_adjusted_by_discount_facto discount_factor = 0.3 household = household_factory(heating_system=HeatingSystem.HEAT_PUMP_AIR_SOURCE) model = model_factory( - start_datetime=datetime.datetime(2022, 1, 1), + start_datetime=datetime.datetime(2024, 1, 1), step_interval=datetime.timedelta(minutes=1440), air_source_heat_pump_price_discount_schedule=[ - (datetime.datetime(2022, 1, 2), discount_factor), + (datetime.datetime(2024, 1, 2), discount_factor), ], ) first_quote = get_unit_and_install_costs( @@ -190,7 +191,7 @@ def test_boiler_upgrade_scheme_grant_is_zero_for_boilers_within_grant_window( self, boiler ): - start_datetime = datetime.datetime(2022, 4, 1, 0, 0) + start_datetime = datetime.datetime(2024, 4, 1, 0, 0) end_datetime = datetime.datetime(2025, 4, 1, 0, 0) random_n_days = random.randrange((end_datetime - start_datetime).days) start_datetime = start_datetime + datetime.timedelta(days=random_n_days) @@ -251,22 +252,22 @@ def test_boiler_upgrade_scheme_grant_is_non_zero_for_heat_pumps_when_grant_is_ac estimate_boiler_upgrade_scheme_grant( HeatingSystem.HEAT_PUMP_AIR_SOURCE, model ) - == 5_000 + == 7_500 ) assert ( estimate_boiler_upgrade_scheme_grant( HeatingSystem.HEAT_PUMP_GROUND_SOURCE, model ) - == 6_000 + == 7_500 ) def test_air_source_heat_pump_unit_and_install_costs_floored_at_gas_boiler_cost_for_households_with_air_source_heat_pump( self, ): model = model_factory( - start_datetime=datetime.datetime(2022, 1, 1), + start_datetime=datetime.datetime(2024, 1, 1), air_source_heat_pump_price_discount_schedule=[ - (datetime.datetime(2022, 1, 1), 1.0) + (datetime.datetime(2024, 1, 1), 1.0) ], ) @@ -283,9 +284,9 @@ def test_air_source_heat_pump_unit_and_install_costs_floored_at_gas_boiler_plus_ ): model = model_factory( - start_datetime=datetime.datetime(2022, 1, 1), + start_datetime=datetime.datetime(2024, 1, 1), air_source_heat_pump_price_discount_schedule=[ - (datetime.datetime(2022, 1, 1), 1.0) + (datetime.datetime(2024, 1, 1), 1.0) ], ) household = household_factory(heating_system=HeatingSystem.BOILER_GAS) @@ -295,3 +296,97 @@ def test_air_source_heat_pump_unit_and_install_costs_floored_at_gas_boiler_plus_ ashp_price_min_cap = MEAN_COST_GBP_BOILER_GAS[household.property_size] assert heat_pump_cost <= ashp_price_min_cap + DECOMMISSIONING_COST_MAX + + @pytest.mark.parametrize("boiler", set(BOILERS)) + def test_extended_boiler_upgrade_scheme_grant_is_zero_for_boilers_within_grant_window( + self, boiler + ): + + start_datetime = datetime.datetime(2022, 4, 1, 0, 0) + end_datetime = datetime.datetime(2035, 4, 1, 0, 0) + random_n_days = random.randrange((end_datetime - start_datetime).days) + start_datetime = start_datetime + datetime.timedelta(days=random_n_days) + + model = model_factory( + start_datetime=start_datetime, + ) + + assert estimate_extended_boiler_upgrade_scheme_grant(boiler, model) == 0 + + @pytest.mark.parametrize("heating_system", set(HeatingSystem)) + def test_extended_boiler_upgrade_scheme_grant_is_zero_when_outside_grant_window( + self, heating_system + ): + + model = model_factory(start_datetime=datetime.datetime(2036, 1, 1, 0, 0)) + model.add_agents([household_factory()]) + + assert estimate_extended_boiler_upgrade_scheme_grant(heating_system, model) == 0 + + @pytest.mark.parametrize("heat_pump", set(HEAT_PUMPS)) + def test_extended_boiler_upgrade_scheme_grant_is_zero_when_grant_cap_exceeded( + self, heat_pump + ): + + model = model_factory( + start_datetime=datetime.datetime(2024, 1, 1, 0, 0), + ) + + num_households = random.randint(1, 5) + model.add_agents([household_factory()] * num_households) + + model_population_scale = ( + ENGLAND_WALES_HOUSEHOLD_COUNT_2020 / model.household_count + ) + boiler_upgrade_scheme_budget_scaled = 1_650_000_000 / model_population_scale + + model.boiler_upgrade_scheme_cumulative_spend_gbp = ( + boiler_upgrade_scheme_budget_scaled * 0.8 + ) + assert estimate_extended_boiler_upgrade_scheme_grant(heat_pump, model) > 0 + + model.boiler_upgrade_scheme_cumulative_spend_gbp = ( + boiler_upgrade_scheme_budget_scaled + ) + assert estimate_extended_boiler_upgrade_scheme_grant(heat_pump, model) == 0 + + def test_extended_boiler_upgrade_scheme_grant_is_non_zero_for_heat_pumps_when_grant_is_active( + self, + ): + + model = model_factory( + start_datetime=datetime.datetime(2025, 1, 1, 0, 0), + ) + model.add_agents([household_factory()]) + + assert ( + estimate_extended_boiler_upgrade_scheme_grant( + HeatingSystem.HEAT_PUMP_AIR_SOURCE, model + ) + == 7_500 + ) + assert ( + estimate_extended_boiler_upgrade_scheme_grant( + HeatingSystem.HEAT_PUMP_GROUND_SOURCE, model + ) + == 7_500 + ) + + # Tapered grant after 2028 + model = model_factory( + start_datetime=datetime.datetime(2028, 5, 1, 0, 0), + ) + model.add_agents([household_factory()]) + + assert ( + estimate_extended_boiler_upgrade_scheme_grant( + HeatingSystem.HEAT_PUMP_AIR_SOURCE, model + ) + == 5_000 + ) + assert ( + estimate_extended_boiler_upgrade_scheme_grant( + HeatingSystem.HEAT_PUMP_GROUND_SOURCE, model + ) + == 5_000 + ) diff --git a/simulation/tests/test_main.py b/simulation/tests/test_main.py index 7355486..da62c41 100644 --- a/simulation/tests/test_main.py +++ b/simulation/tests/test_main.py @@ -51,9 +51,9 @@ def test_start_date_returns_datetime(self, mandatory_local_args): args = parse_args([*mandatory_local_args, "--start-date", "2021-01-01"]) assert args.start_datetime == datetime.datetime(2021, 1, 1) - def test_start_date_default_is_start_of_2022(self, mandatory_local_args): + def test_start_date_default_is_start_of_2024(self, mandatory_local_args): args = parse_args(mandatory_local_args) - assert args.start_datetime == datetime.datetime(2022, 1, 1) + assert args.start_datetime == datetime.datetime(2024, 1, 1) def test_default_seed_is_current_datetime_string(self, mandatory_local_args): datetime_before = datetime.datetime.now() @@ -79,12 +79,26 @@ def test_heating_system_hassle_factor(self, mandatory_local_args): ) assert args.heating_system_hassle_factor == 0.5 + def test_rented_heating_system_hassle_factor(self, mandatory_local_args): + args = parse_args( + [*mandatory_local_args, "--rented-heating-system-hassle-factor", "0.5"] + ) + assert args.rented_heating_system_hassle_factor == 0.5 + def test_heating_system_hassle_factor_must_be_between_0_and_1( self, mandatory_local_args ): with pytest.raises(SystemExit): parse_args([*mandatory_local_args, "--heating-system-hassle-factor", "10"]) + def test_rented_heating_system_hassle_factor_must_be_between_0_and_1( + self, mandatory_local_args + ): + with pytest.raises(SystemExit): + parse_args( + [*mandatory_local_args, "--rented-heating-system-hassle-factor", "10"] + ) + def test_help_flag(self): with pytest.raises(SystemExit): parse_args(["-h"]) @@ -142,12 +156,15 @@ def test_intervention_argument(self, mandatory_local_args): "rhi", "--intervention", "boiler_upgrade_scheme", + "--intervention", + "extended_boiler_upgrade_scheme", ] ) assert args.intervention == [ InterventionType.RHI, InterventionType.BOILER_UPGRADE_SCHEME, + InterventionType.EXTENDED_BOILER_UPGRADE_SCHEME, ] def test_gas_oil_boiler_ban_date_returns_datetime(self, mandatory_local_args): diff --git a/simulation/tests/test_model.py b/simulation/tests/test_model.py index 462ee5c..eb40441 100644 --- a/simulation/tests/test_model.py +++ b/simulation/tests/test_model.py @@ -39,10 +39,10 @@ def test_increment_timestep_updates_boiler_upgrade_scheme_cumulative_spend_gbp( assert model.boiler_upgrade_scheme_cumulative_spend_gbp == 0 household_using_boiler_upgrade_grant_ASHP = household_factory() - household_using_boiler_upgrade_grant_ASHP.boiler_upgrade_grant_used = 5_000 + household_using_boiler_upgrade_grant_ASHP.boiler_upgrade_grant_used = 7_500 household_using_boiler_upgrade_grant_GSHP = household_factory() - household_using_boiler_upgrade_grant_GSHP.boiler_upgrade_grant_used = 6_000 + household_using_boiler_upgrade_grant_GSHP.boiler_upgrade_grant_used = 7_500 model.add_agents( [ @@ -52,10 +52,10 @@ def test_increment_timestep_updates_boiler_upgrade_scheme_cumulative_spend_gbp( ) model.increment_timestep() - assert model.boiler_upgrade_scheme_cumulative_spend_gbp == 11_000 + assert model.boiler_upgrade_scheme_cumulative_spend_gbp == 15_000 model.increment_timestep() - assert model.boiler_upgrade_scheme_cumulative_spend_gbp == 22_000 + assert model.boiler_upgrade_scheme_cumulative_spend_gbp == 30_000 def test_air_source_heat_pump_discount_factor_is_zero_if_no_discount_schedule_passed( self, @@ -70,10 +70,10 @@ def test_air_source_heat_pump_discount_factor_changes_when_crosses_discount_sche ): model = model_factory( - start_datetime=datetime.datetime(2022, 2, 1), + start_datetime=datetime.datetime(2024, 2, 1), air_source_heat_pump_price_discount_schedule=[ - (datetime.datetime(2022, 2, 1), 0.1), - (datetime.datetime(2022, 2, 2), 0.3), + (datetime.datetime(2024, 2, 1), 0.1), + (datetime.datetime(2024, 2, 2), 0.3), ], step_interval=datetime.timedelta(minutes=1440), )