Skip to content
6 changes: 6 additions & 0 deletions .beads/deletions.jsonl
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
{"id":"policyengine-uk-lo8","ts":"2025-12-01T05:12:40.281291Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
{"id":"policyengine-uk-exv","ts":"2025-12-01T05:12:40.289337Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
{"id":"policyengine-uk-e55","ts":"2025-12-01T05:12:40.293639Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
{"id":"policyengine-uk-5oc","ts":"2026-01-17T20:59:14.576055Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
{"id":"policyengine-uk-iy2","ts":"2026-01-17T20:59:14.594357Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
{"id":"policyengine-uk-eao","ts":"2026-01-17T20:59:14.601098Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
{"id":"policyengine-uk-cur","ts":"2026-01-17T20:59:14.606669Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
{"id":"policyengine-uk-agn","ts":"2026-01-17T20:59:14.618579Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
{"id":"policyengine-uk-bza","ts":"2026-01-17T20:59:14.623346Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
4 changes: 4 additions & 0 deletions changelog_entry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- bump: minor
changes:
changed:
- Refactor reforms to use two-layer pattern with bypass parameter, matching PolicyEngine US architecture. All gov.contrib parameter references have been removed from gov/ variables - contrib parameters now only affect behavior via reforms. Structural reforms are now applied at CountryTaxBenefitSystem initialization (in addition to Simulation), ensuring YAML tests work correctly with dynamic parameter setting. This enables programmatic reform application and cleaner separation between core law (gov/) and policy proposals (contrib/).
61 changes: 61 additions & 0 deletions policyengine_uk/reforms/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,62 @@
from .reforms import create_structural_reforms_from_parameters

# Import reform creators for direct use (bypass pattern)
from .conservatives import (
create_cb_hitc_household_based,
create_household_based_hitc_reform,
cb_hitc_household_based_reform,
)
from .cps import (
create_expanded_ma_reform,
create_marriage_neutral_income_tax_reform,
create_marriage_tax_reform,
expanded_ma_reform,
marriage_neutral_income_tax_reform,
)
from .policyengine import (
create_disable_simulated_benefits,
disable_simulated_benefits,
disable_simulated_benefits_reform,
create_budget_adjustment,
adjust_budget,
adjust_budgets,
)
from .scotland import (
create_scottish_child_payment_baby_bonus_reform,
create_scottish_child_payment_reform,
scottish_child_payment_reform,
)
from .ubi_center import (
create_basic_income_interactions,
create_basic_income_interactions_reform,
basic_income_interactions_reform,
)

__all__ = [
"create_structural_reforms_from_parameters",
# Conservatives
"create_cb_hitc_household_based",
"create_household_based_hitc_reform",
"cb_hitc_household_based_reform",
# CPS
"create_expanded_ma_reform",
"create_marriage_neutral_income_tax_reform",
"create_marriage_tax_reform",
"expanded_ma_reform",
"marriage_neutral_income_tax_reform",
# PolicyEngine
"create_disable_simulated_benefits",
"disable_simulated_benefits",
"disable_simulated_benefits_reform",
"create_budget_adjustment",
"adjust_budget",
"adjust_budgets",
# Scotland
"create_scottish_child_payment_baby_bonus_reform",
"create_scottish_child_payment_reform",
"scottish_child_payment_reform",
# UBI Center
"create_basic_income_interactions",
"create_basic_income_interactions_reform",
"basic_income_interactions_reform",
]
12 changes: 11 additions & 1 deletion policyengine_uk/reforms/conservatives/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
from .household_based_hitc import *
from .household_based_hitc import (
create_cb_hitc_household_based,
create_household_based_hitc_reform,
cb_hitc_household_based_reform,
)

__all__ = [
"create_cb_hitc_household_based",
"create_household_based_hitc_reform",
"cb_hitc_household_based_reform",
]
67 changes: 45 additions & 22 deletions policyengine_uk/reforms/conservatives/household_based_hitc.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,56 @@
from policyengine_uk.model_api import *


class CB_HITC(Variable):
value_type = float
entity = Person
label = "Child Benefit High-Income Tax Charge"
definition_period = YEAR
reference = "https://www.legislation.gov.uk/ukpga/2003/1/part/10/chapter/8"
unit = GBP
defined_for = "is_higher_earner"

def formula(person, period, parameters):
CB_received = person.benunit("child_benefit", period)
hitc = parameters(period).gov.hmrc.income_tax.charges.CB_HITC
personal_income = person("adjusted_net_income", period)
income = person.benunit.sum(personal_income)
percentage = max_(income - hitc.phase_out_start, 0) / (
hitc.phase_out_end - hitc.phase_out_start
def create_cb_hitc_household_based() -> Reform:
"""
Reform that makes Child Benefit HITC assessed on household income
rather than individual income.

Policy: Assess CB-HITC based on combined household income rather than
the highest individual earner's income.

Source: Conservative Party proposal
"""

class CB_HITC(Variable):
value_type = float
entity = Person
label = "Child Benefit High-Income Tax Charge"
definition_period = YEAR
reference = (
"https://www.legislation.gov.uk/ukpga/2003/1/part/10/chapter/8"
)
return min_(percentage, 1) * CB_received
unit = GBP
defined_for = "is_higher_earner"

def formula(person, period, parameters):
CB_received = person.benunit("child_benefit", period)
hitc = parameters(period).gov.hmrc.income_tax.charges.CB_HITC
personal_income = person("adjusted_net_income", period)
income = person.benunit.sum(personal_income)
percentage = max_(income - hitc.phase_out_start, 0) / (
hitc.phase_out_end - hitc.phase_out_start
)
return min_(percentage, 1) * CB_received

class reform(Reform):
def apply(self):
self.update_variable(CB_HITC)

class make_cb_hitc_household_based(Variable):
def apply(self):
self.update_variable(CB_HITC)
return reform


def create_household_based_hitc_reform(parameters, period):
def create_household_based_hitc_reform(
parameters, period, bypass: bool = False
):
if bypass:
return create_cb_hitc_household_based()

if parameters(period).gov.contrib.conservatives.cb_hitc_household:
return make_cb_hitc_household_based
return create_cb_hitc_household_based()
else:
return None


# For direct import
cb_hitc_household_based_reform = create_cb_hitc_household_based()
16 changes: 15 additions & 1 deletion policyengine_uk/reforms/cps/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
from .marriage_tax_reforms import create_marriage_tax_reform
from .marriage_tax_reforms import (
create_expanded_ma_reform,
create_marriage_neutral_income_tax_reform,
create_marriage_tax_reform,
expanded_ma_reform,
marriage_neutral_income_tax_reform,
)

__all__ = [
"create_expanded_ma_reform",
"create_marriage_neutral_income_tax_reform",
"create_marriage_tax_reform",
"expanded_ma_reform",
"marriage_neutral_income_tax_reform",
]
33 changes: 30 additions & 3 deletions policyengine_uk/reforms/cps/marriage_tax_reforms.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from policyengine_uk.model_api import *
from typing import Union, Optional
from typing import Optional


def create_expanded_ma_reform(
max_child_age: Optional[int] = None,
child_education_levels: Optional[List[str]] = None,
child_education_levels: Optional[list[str]] = None,
) -> Reform:
class meets_expanded_ma_conditions(Variable):
label = "Qualifies for an expanded Marriage Allowance"
Expand Down Expand Up @@ -209,7 +209,27 @@ def apply(self):
return reform


def create_marriage_tax_reform(parameters, period):
def create_marriage_tax_reform(parameters, period, bypass: bool = False):
"""
Create marriage tax reforms based on CPS proposals.

Policy: Expand Marriage Allowance eligibility and/or implement
marriage-neutral income tax.

Args:
parameters: The parameter tree
period: The time period
bypass: If True, return default reform without parameter checks

Source: Centre for Policy Studies
"""
if bypass:
# Return both reforms with default settings when bypassed
return (
create_expanded_ma_reform(),
create_marriage_neutral_income_tax_reform(),
)

cps = parameters(period).gov.contrib.cps.marriage_tax_reforms
remove_income_condition = cps.expanded_ma.remove_income_condition
rate = cps.expanded_ma.ma_rate
Expand Down Expand Up @@ -243,3 +263,10 @@ def create_marriage_tax_reform(parameters, period):
return it_reform
else:
return None


# For direct import
expanded_ma_reform = create_expanded_ma_reform()
marriage_neutral_income_tax_reform = (
create_marriage_neutral_income_tax_reform()
)
68 changes: 67 additions & 1 deletion policyengine_uk/reforms/policyengine/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,67 @@
from .disable_simulated_benefits import *
from .disable_simulated_benefits import (
create_disable_simulated_benefits,
disable_simulated_benefits,
disable_simulated_benefits_reform,
)
from .adjust_budgets import (
create_budget_adjustment,
adjust_budget,
adjust_budgets,
)
from .abolish_council_tax import (
create_abolish_council_tax,
create_abolish_council_tax_reform,
abolish_council_tax_reform,
)
from .freeze_pension_credit import (
create_freeze_pension_credit,
create_freeze_pension_credit_reform,
freeze_pension_credit_reform,
)
from .employer_ni_pension_exemption import (
create_employer_ni_pension_exemption,
create_employer_ni_pension_exemption_reform,
employer_ni_pension_exemption_reform,
)
from .salary_sacrifice_haircut import (
create_salary_sacrifice_haircut,
create_salary_sacrifice_haircut_reform,
salary_sacrifice_haircut_reform,
)
from .two_child_limit_age_exemption import (
create_two_child_limit_age_exemption,
create_two_child_limit_age_exemption_reform,
two_child_limit_age_exemption_reform,
)
from .contrib_aggregates import (
create_contrib_aggregates,
create_contrib_aggregates_reform,
contrib_aggregates_reform,
)

__all__ = [
"create_disable_simulated_benefits",
"disable_simulated_benefits",
"disable_simulated_benefits_reform",
"create_budget_adjustment",
"adjust_budget",
"adjust_budgets",
"create_abolish_council_tax",
"create_abolish_council_tax_reform",
"abolish_council_tax_reform",
"create_freeze_pension_credit",
"create_freeze_pension_credit_reform",
"freeze_pension_credit_reform",
"create_employer_ni_pension_exemption",
"create_employer_ni_pension_exemption_reform",
"employer_ni_pension_exemption_reform",
"create_salary_sacrifice_haircut",
"create_salary_sacrifice_haircut_reform",
"salary_sacrifice_haircut_reform",
"create_two_child_limit_age_exemption",
"create_two_child_limit_age_exemption_reform",
"two_child_limit_age_exemption_reform",
"create_contrib_aggregates",
"create_contrib_aggregates_reform",
"contrib_aggregates_reform",
]
Loading