policyengine-uk 2.40.1__py3-none-any.whl → 2.65.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- policyengine_uk/__init__.py +5 -3
- policyengine_uk/data/__init__.py +1 -0
- policyengine_uk/data/dataset_schema.py +70 -18
- policyengine_uk/data/economic_assumptions.py +36 -10
- policyengine_uk/data/filter_dataset.py +52 -0
- policyengine_uk/dynamics/labour_supply.py +343 -0
- policyengine_uk/dynamics/participation.py +629 -0
- policyengine_uk/dynamics/progression.py +384 -0
- policyengine_uk/microsimulation.py +105 -0
- policyengine_uk/model_api.py +1 -0
- policyengine_uk/parameters/gov/boe/base_rate.yaml +34 -0
- policyengine_uk/parameters/gov/boe/index.yaml +2 -0
- policyengine_uk/parameters/gov/contrib/behavioral_responses/employee_salary_sacrifice_reduction_rate.yaml +14 -0
- policyengine_uk/parameters/gov/contrib/behavioral_responses/salary_sacrifice_broad_base_haircut_rate.yaml +22 -0
- policyengine_uk/parameters/gov/contrib/cec/state_pension_increase.yaml +1 -1
- policyengine_uk/parameters/gov/contrib/ubi_center/carbon_tax.yaml +2 -2
- policyengine_uk/parameters/gov/contrib/ubi_center/land_value_tax.yaml +3 -3
- policyengine_uk/parameters/gov/dcms/bbc/tv_licence/colour.yaml +5 -5
- policyengine_uk/parameters/gov/dfe/education_spending.yaml +1 -1
- policyengine_uk/parameters/gov/dft/rail/fare_index.yaml +32 -0
- policyengine_uk/parameters/gov/dft/rail/prior_law_fare_index.yaml +32 -0
- policyengine_uk/parameters/gov/dft/rail/ridership_index.yaml +30 -0
- policyengine_uk/parameters/gov/dft/spending.yaml +2 -2
- policyengine_uk/parameters/gov/dwp/ESA/income/earn_disregard.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/ESA/income/income_disregard_couple.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/ESA/income/income_disregard_lone_parent.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/ESA/income/income_disregard_single.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/ESA/income/pension_disregard.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/IIDB/maximum.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/JSA/contrib/amount_over_25.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/JSA/contrib/earn_disregard.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/JSA/contrib/pension_disregard.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/JSA/income/amount_18_24.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/JSA/income/amount_over_25.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/JSA/income/income_disregard_couple.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/JSA/income/income_disregard_lone_parent.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/JSA/income/income_disregard_single.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_age_threshold.yaml +12 -0
- policyengine_uk/parameters/gov/dwp/attendance_allowance/higher.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/attendance_allowance/lower.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/benefit_cap.yaml +3 -3
- policyengine_uk/parameters/gov/dwp/carer_premium/couple.yaml +2 -2
- policyengine_uk/parameters/gov/dwp/carer_premium/single.yaml +6 -6
- policyengine_uk/parameters/gov/dwp/carers_allowance/rate.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/disability_premia/disability_couple.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/disability_premia/enhanced_couple.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/disability_premia/enhanced_single.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/disability_premia/severe_couple.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/dla/mobility/higher.yaml +4 -4
- policyengine_uk/parameters/gov/dwp/dla/mobility/lower.yaml +8 -8
- policyengine_uk/parameters/gov/dwp/dla/self_care/higher.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/dla/self_care/lower.yaml +8 -8
- policyengine_uk/parameters/gov/dwp/dla/self_care/middle.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/aged.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/older.yaml +3 -3
- policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/aged.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/older.yaml +3 -3
- policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/worker.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/amount.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/housing_benefit/takeup.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/income_support/amounts/amount_16_24.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/amounts/amount_couples_over_18.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/means_test/earn_disregard.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/means_test/income_disregard_couple.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/means_test/income_disregard_lone_parent.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/means_test/income_disregard_single.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/means_test/pension_disregard.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/takeup.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/carer/addition.yaml +4 -4
- policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee.yaml +9 -9
- policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/severe_disability/addition.yaml +3 -3
- policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/threshold.yaml +5 -5
- policyengine_uk/parameters/gov/dwp/pip/daily_living/enhanced.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/pip/daily_living/standard.yaml +8 -8
- policyengine_uk/parameters/gov/dwp/pip/mobility/enhanced.yaml +4 -4
- policyengine_uk/parameters/gov/dwp/pip/mobility/standard.yaml +9 -9
- policyengine_uk/parameters/gov/dwp/sda/maximum.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/state_pension/basic_state_pension/amount.yaml +11 -11
- policyengine_uk/parameters/gov/dwp/state_pension/new_state_pension/amount.yaml +4 -4
- policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/limit/child_count.yaml +10 -1
- policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/takeup.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/tax_credits/working_tax_credit/takeup.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/carer/amount.yaml +3 -3
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/amount.yaml +2 -4
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/disabled/amount.yaml +2 -4
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/first/higher_amount.yaml +6 -8
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/limit/child_count.yaml +6 -1
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/severely_disabled/amount.yaml +3 -5
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/childcare/cap.yaml +2 -6
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/disabled/amount.yaml +4 -6
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/housing/non_dep_deduction/amount.yaml +4 -1
- policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/active.yaml +9 -0
- policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/new_claimant_health_element.yaml +9 -0
- policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/standard_allowance_uplift.yaml +13 -0
- policyengine_uk/parameters/gov/dwp/universal_credit/standard_allowance/amount.yaml +5 -5
- policyengine_uk/parameters/gov/dwp/winter_fuel_payment/eligibility/taxable_income_test/maximum_taxable_income.yaml +2 -1
- policyengine_uk/parameters/gov/dwp/winter_fuel_payment/eligibility/taxable_income_test/use_maximum_taxable_income.yaml +1 -0
- policyengine_uk/parameters/gov/dynamic/obr_labour_supply_assumptions.yaml +9 -0
- policyengine_uk/parameters/gov/economic_assumptions/create_economic_assumption_indices.py +1 -1
- policyengine_uk/parameters/gov/economic_assumptions/yoy_growth.yaml +522 -153
- policyengine_uk/parameters/gov/hmrc/cgt/additional_rate.yaml +5 -0
- policyengine_uk/parameters/gov/hmrc/cgt/basic_rate.yaml +5 -0
- policyengine_uk/parameters/gov/hmrc/cgt/higher_rate.yaml +4 -0
- policyengine_uk/parameters/gov/hmrc/child_benefit/amount/additional.yaml +6 -6
- policyengine_uk/parameters/gov/hmrc/child_benefit/amount/eldest.yaml +8 -8
- policyengine_uk/parameters/gov/hmrc/child_benefit/takeup/by_age.yaml +1 -1
- policyengine_uk/parameters/gov/hmrc/fuel_duty/calculate_fuel_duty_rates.py +464 -0
- policyengine_uk/parameters/gov/hmrc/fuel_duty/petrol_and_diesel.yaml +86 -10
- policyengine_uk/parameters/gov/hmrc/income_tax/allowances/personal_allowance/amount.yaml +6 -0
- policyengine_uk/parameters/gov/hmrc/income_tax/earned_taxable_income_exclusions.yaml +2 -1
- policyengine_uk/parameters/gov/hmrc/income_tax/income_tax_additions.yaml +1 -0
- policyengine_uk/parameters/gov/hmrc/income_tax/rates/dividends.yaml +12 -0
- policyengine_uk/parameters/gov/hmrc/income_tax/rates/property.yaml +46 -0
- policyengine_uk/parameters/gov/hmrc/income_tax/rates/savings.yaml +46 -0
- policyengine_uk/parameters/gov/hmrc/income_tax/rates/scotland/rates.yaml +2 -2
- policyengine_uk/parameters/gov/hmrc/income_tax/rates/uk.yaml +14 -2
- policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/additional.yaml +4 -6
- policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employer.yaml +3 -3
- policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml +14 -4
- policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/flat_rate.yaml +2 -2
- policyengine_uk/parameters/gov/hmrc/national_insurance/salary_sacrifice_pension_cap.yaml +16 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/index.yaml +12 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_1/boe_margin.yaml +11 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_2/additional_rate.yaml +27 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_2/index.yaml +16 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_2/upper_threshold.yaml +48 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/postgraduate_additional_rate.yaml +11 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/postgraduate_repayment_rate.yaml +9 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/repayment_rate.yaml +9 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_1.yaml +25 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_2.yaml +58 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_4.yaml +19 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_5.yaml +16 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/postgraduate.yaml +21 -0
- policyengine_uk/parameters/gov/hmrc/vat/reduced_rate_share.yaml +3 -3
- policyengine_uk/parameters/gov/indices/private_rent_index.yaml +9 -9
- policyengine_uk/parameters/gov/revenue_scotland/lbtt/non_residential.yaml +2 -2
- policyengine_uk/parameters/gov/revenue_scotland/lbtt/rent.yaml +2 -2
- policyengine_uk/parameters/gov/revenue_scotland/lbtt/residential/first_time_buyer_rate.yaml +2 -2
- policyengine_uk/parameters/gov/revenue_scotland/lbtt/residential/rate.yaml +2 -2
- policyengine_uk/parameters/gov/wra/land_transaction_tax/non_residential.yaml +2 -2
- policyengine_uk/parameters/gov/wra/land_transaction_tax/rent.yaml +2 -2
- policyengine_uk/parameters/gov/wra/land_transaction_tax/residential/higher_rate.yaml +1 -1
- policyengine_uk/parameters/gov/wra/land_transaction_tax/residential/primary.yaml +2 -2
- policyengine_uk/parameters/household/consumption/carbon/consumption.yaml +8 -6
- policyengine_uk/parameters/household/consumption/carbon/intensity.yaml +4 -1
- policyengine_uk/parameters/household/consumption/carbon/production.yaml +12 -7
- policyengine_uk/parameters/household/consumption/carbon/production_by_source.yaml +76 -41
- policyengine_uk/parameters/household/consumption/fuel/prices/petrol.yaml +1 -1
- policyengine_uk/parameters/household/poverty/absolute_poverty_threshold_bhc.yaml +1 -1
- policyengine_uk/reforms/policyengine/adjust_budgets.py +0 -1
- policyengine_uk/scenarios/__init__.py +4 -0
- policyengine_uk/scenarios/pip_reform.py +23 -0
- policyengine_uk/scenarios/reindex_benefit_cap.py +32 -0
- policyengine_uk/scenarios/repeal_two_child_limit.py +10 -0
- policyengine_uk/scenarios/uc_reform.py +50 -0
- policyengine_uk/simulation.py +619 -0
- policyengine_uk/system.py +3 -257
- policyengine_uk/tax_benefit_system.py +141 -0
- policyengine_uk/tests/behavioral_responses/test_labor_supply_responses.yaml +183 -0
- policyengine_uk/tests/microsimulation/reforms_config.yaml +8 -8
- policyengine_uk/tests/microsimulation/test_reform_impacts.py +2 -2
- policyengine_uk/tests/microsimulation/test_salary_sacrifice_cap_reform.py +401 -0
- policyengine_uk/tests/microsimulation/test_validity.py +2 -3
- policyengine_uk/tests/microsimulation/update_reform_impacts.py +104 -40
- policyengine_uk/tests/policy/baseline/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.yaml +105 -0
- policyengine_uk/tests/policy/baseline/finance/benefit/family/child_benefit.yaml +2 -0
- policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support.yaml +0 -23
- policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/tv_licence.yaml +3 -0
- policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.yaml +17 -17
- policyengine_uk/tests/policy/baseline/gov/dwp/basic_state_pension.yaml +44 -0
- policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/gift_aid.yaml +71 -0
- policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/personal_allowance.yaml +161 -0
- policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.yaml +107 -0
- policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.yaml +95 -0
- policyengine_uk/tests/policy/baseline/gov/hmrc/student_loans/student_loan_interest_rate.yaml +153 -0
- policyengine_uk/tests/policy/baseline/gov/hmrc/student_loans/student_loan_repayment.yaml +130 -0
- policyengine_uk/tests/policy/baseline/household/wealth/vehicle.yaml +27 -0
- policyengine_uk/tests/policy/reforms/nov_2025_budget/income_source_tax_rates.yaml +235 -0
- policyengine_uk/tests/policy/reforms/nov_2025_budget/income_tax_freeze.yaml +83 -0
- policyengine_uk/tests/policy/reforms/parametric/basic_income/basic_income.yaml +1 -0
- policyengine_uk/tests/test_behavioral_responses.py +215 -0
- policyengine_uk/tests/test_fiscal_year_parameters.py +131 -0
- policyengine_uk/utils/__init__.py +1 -0
- policyengine_uk/utils/compare.py +28 -0
- policyengine_uk/utils/create_ahc_deflator.py +169 -0
- policyengine_uk/utils/create_triple_lock.py +1 -1
- policyengine_uk/utils/dependencies.py +259 -0
- policyengine_uk/utils/parameters.py +12 -1
- policyengine_uk/utils/scenario.py +225 -0
- policyengine_uk/utils/solve_private_school_attendance_factor.py +4 -6
- policyengine_uk/variables/contrib/policyengine/education_budget_change.py +0 -1
- policyengine_uk/variables/contrib/policyengine/employer_ni/baseline_employer_cost.py +5 -1
- policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.py +23 -23
- policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_response_capital_incidence.py +1 -1
- policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_response_consumer_incidence.py +1 -1
- policyengine_uk/variables/contrib/policyengine/other_public_spending_budget_change.py +0 -1
- policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_eligible.py +0 -1
- policyengine_uk/variables/gov/dft/rail_subsidy_spending.py +16 -1
- policyengine_uk/variables/gov/dft/rail_usage.py +16 -0
- policyengine_uk/variables/gov/dwp/BRMA_LHA_rate.py +7 -2
- policyengine_uk/variables/gov/dwp/LHA_category.py +4 -2
- policyengine_uk/variables/gov/dwp/additional_state_pension.py +4 -2
- policyengine_uk/variables/gov/dwp/basic_state_pension.py +26 -8
- policyengine_uk/variables/gov/dwp/is_CTC_eligible.py +1 -1
- policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py +9 -13
- policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py +66 -0
- policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py +75 -0
- policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py +66 -0
- policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py +5 -4
- policyengine_uk/variables/gov/gov_tax.py +0 -2
- policyengine_uk/variables/gov/hmrc/household_tax.py +0 -1
- policyengine_uk/variables/gov/hmrc/income_tax/allowances/gift_aid.py +23 -0
- policyengine_uk/variables/gov/hmrc/income_tax/allowances/personal_allowance.py +9 -2
- policyengine_uk/variables/gov/hmrc/income_tax/income_tax_pre_charges.py +1 -0
- policyengine_uk/variables/gov/hmrc/income_tax/liability/property_income_tax.py +75 -0
- policyengine_uk/variables/gov/hmrc/income_tax/liability/savings_income_tax.py +4 -4
- policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_broad_base_haircut.py +43 -0
- policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.py +38 -0
- policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.py +27 -0
- policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_adjusted.py +31 -0
- policyengine_uk/variables/gov/hmrc/pensions/salary_sacrifice_returned_to_income.py +41 -0
- policyengine_uk/variables/gov/hmrc/student_loans/__init__.py +2 -0
- policyengine_uk/variables/gov/hmrc/student_loans/plan_1_interest_rate.py +23 -0
- policyengine_uk/variables/gov/hmrc/student_loans/plan_2_interest_rate.py +31 -0
- policyengine_uk/variables/gov/hmrc/student_loans/plan_4_interest_rate.py +22 -0
- policyengine_uk/variables/gov/hmrc/student_loans/plan_5_interest_rate.py +18 -0
- policyengine_uk/variables/gov/hmrc/student_loans/postgraduate_interest_rate.py +23 -0
- policyengine_uk/variables/gov/hmrc/student_loans/student_loan_plan.py +27 -0
- policyengine_uk/variables/gov/hmrc/student_loans/student_loan_repayment.py +91 -0
- policyengine_uk/variables/gov/hmrc/student_loans/student_loan_repayment_rate.py +31 -0
- policyengine_uk/variables/gov/hmrc/would_claim_child_benefit.py +5 -1
- policyengine_uk/variables/household/demographic/benunit/benunit_count_adults.py +11 -0
- policyengine_uk/variables/household/demographic/is_disabled_for_benefits.py +13 -1
- policyengine_uk/variables/household/income/hbai_household_net_income.py +29 -1
- policyengine_uk/variables/household/income/hbai_household_net_income_ahc.py +13 -0
- policyengine_uk/variables/household/income/household_net_income.py +5 -1
- policyengine_uk/variables/household/income/inflation_adjustment.py +24 -0
- policyengine_uk/variables/household/post_tax_income.py +12 -0
- policyengine_uk/variables/household/wealth/num_vehicles.py +9 -0
- policyengine_uk/variables/household/wealth/owns_vehicle.py +17 -0
- policyengine_uk/variables/input/consumption/property/council_tax.py +0 -35
- policyengine_uk/variables/input/consumption/property/employee_pension_contributions.py +8 -1
- policyengine_uk/variables/input/consumption/property/employee_pension_contributions_reported.py +16 -0
- policyengine_uk/variables/input/consumption/property/pension_contributions_via_salary_sacrifice.py +16 -0
- policyengine_uk/variables/input/employment_income.py +2 -0
- policyengine_uk/variables/input/rent.py +0 -40
- policyengine_uk/variables/input/savings_interest_income.py +3 -1
- {policyengine_uk-2.40.1.dist-info → policyengine_uk-2.65.6.dist-info}/METADATA +17 -8
- {policyengine_uk-2.40.1.dist-info → policyengine_uk-2.65.6.dist-info}/RECORD +252 -173
- {policyengine_uk-2.40.1.dist-info → policyengine_uk-2.65.6.dist-info}/WHEEL +1 -1
- policyengine_uk/repo.py +0 -3
- policyengine_uk/tests/policy/baseline/gov/abolitions/abolition_parameters.yaml +0 -250
- policyengine_uk/variables/contrib/policyengine/high_income_incident_tax_change.py +0 -22
- policyengine_uk-2.40.1.data/data/share/openfisca/openfisca-country-template/CHANGELOG.md +0 -2285
- policyengine_uk-2.40.1.data/data/share/openfisca/openfisca-country-template/README.md +0 -37
- policyengine_uk-2.40.1.dist-info/licenses/LICENSE +0 -661
- {policyengine_uk-2.40.1.data/data/share/openfisca/openfisca-country-template → policyengine_uk-2.65.6.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test suite for salary sacrifice pension cap reform fiscal impacts.
|
|
3
|
+
|
|
4
|
+
This tests the £2,000 salary sacrifice cap policy announced in Autumn Budget 2025,
|
|
5
|
+
which takes effect from April 2029.
|
|
6
|
+
|
|
7
|
+
Methodology (matching blog at https://policyengine.org/uk/research/uk-salary-sacrifice-cap):
|
|
8
|
+
- Employees redirect excess above £2,000 to regular pension contributions
|
|
9
|
+
- Regular pension contributions get income tax relief but NOT NI relief
|
|
10
|
+
- Broad-base haircut: employers spread NI costs across ALL workers (~0.16% of employment income)
|
|
11
|
+
- Revenue comes from NI on the redirected excess
|
|
12
|
+
|
|
13
|
+
Expected revenue: ~£3.3 billion in 2029-30
|
|
14
|
+
|
|
15
|
+
Reference: https://policyengine.org/uk/research/uk-salary-sacrifice-cap
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pandas as pd
|
|
21
|
+
from policyengine_uk import Microsimulation
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Policy year when the salary sacrifice cap takes effect
|
|
25
|
+
POLICY_YEAR = 2030 # Use 2030 to ensure cap is active (cap starts 2029-04-06)
|
|
26
|
+
|
|
27
|
+
# Expected revenue impact in billions (from blog)
|
|
28
|
+
# PolicyEngine baseline estimate: £3.3 billion
|
|
29
|
+
# OBR static estimate: £4.9 billion
|
|
30
|
+
# OBR post-behavioural: £4.7 billion
|
|
31
|
+
EXPECTED_REVENUE_BILLION = 3.3
|
|
32
|
+
TOLERANCE_BILLION = (
|
|
33
|
+
1.5 # Allow reasonable tolerance for year/methodology differences
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _create_no_cap_baseline():
|
|
38
|
+
"""Create baseline simulation without the salary sacrifice cap."""
|
|
39
|
+
return {
|
|
40
|
+
"gov.hmrc.national_insurance.salary_sacrifice_pension_cap": {
|
|
41
|
+
"2029": float("inf"),
|
|
42
|
+
"2030": float("inf"),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture(scope="module")
|
|
48
|
+
def baseline_simulation():
|
|
49
|
+
"""Create baseline simulation without the salary sacrifice cap."""
|
|
50
|
+
return Microsimulation(reform=_create_no_cap_baseline())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture(scope="module")
|
|
54
|
+
def reform_simulation():
|
|
55
|
+
"""Create reform simulation with the £2,000 salary sacrifice cap.
|
|
56
|
+
|
|
57
|
+
Current law already has the cap from 2029-04-06, so we use
|
|
58
|
+
a plain Microsimulation without additional reform parameters.
|
|
59
|
+
"""
|
|
60
|
+
return Microsimulation()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_salary_sacrifice_cap_revenue_impact(
|
|
64
|
+
baseline_simulation, reform_simulation
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Test that the £2,000 salary sacrifice cap raises ~£3.3 billion.
|
|
68
|
+
|
|
69
|
+
This matches the PolicyEngine blog methodology:
|
|
70
|
+
- Employees redirect excess to regular pension contributions
|
|
71
|
+
- Full excess subject to NI (broad-base haircut reduces all workers' income)
|
|
72
|
+
- Income tax relief preserved via regular pension contributions
|
|
73
|
+
"""
|
|
74
|
+
baseline_gov_balance = baseline_simulation.calculate(
|
|
75
|
+
"gov_balance", POLICY_YEAR
|
|
76
|
+
).sum()
|
|
77
|
+
reform_gov_balance = reform_simulation.calculate(
|
|
78
|
+
"gov_balance", POLICY_YEAR
|
|
79
|
+
).sum()
|
|
80
|
+
|
|
81
|
+
revenue_impact_billion = (reform_gov_balance - baseline_gov_balance) / 1e9
|
|
82
|
+
|
|
83
|
+
print(f"\nBaseline gov_balance: £{baseline_gov_balance/1e9:.3f} billion")
|
|
84
|
+
print(f"Reform gov_balance: £{reform_gov_balance/1e9:.3f} billion")
|
|
85
|
+
print(f"Revenue impact: £{revenue_impact_billion:.3f} billion")
|
|
86
|
+
|
|
87
|
+
# The reform should raise revenue (positive impact)
|
|
88
|
+
assert revenue_impact_billion > 0, (
|
|
89
|
+
f"Salary sacrifice cap should raise revenue, "
|
|
90
|
+
f"but impact is {revenue_impact_billion:.2f} billion"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Revenue should be approximately £3.3 billion
|
|
94
|
+
assert (
|
|
95
|
+
abs(revenue_impact_billion - EXPECTED_REVENUE_BILLION)
|
|
96
|
+
< TOLERANCE_BILLION
|
|
97
|
+
), (
|
|
98
|
+
f"Salary sacrifice cap revenue is {revenue_impact_billion:.2f} billion, "
|
|
99
|
+
f"expected ~{EXPECTED_REVENUE_BILLION:.1f} billion "
|
|
100
|
+
f"(±{TOLERANCE_BILLION:.1f} billion tolerance)"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_ni_increases_with_reform(baseline_simulation, reform_simulation):
|
|
105
|
+
"""
|
|
106
|
+
Test that total NI increases when the cap is applied.
|
|
107
|
+
|
|
108
|
+
The reform adds excess salary sacrifice back to employment income,
|
|
109
|
+
which is then subject to NI through the normal Class 1 calculation.
|
|
110
|
+
"""
|
|
111
|
+
baseline_ni = baseline_simulation.calculate(
|
|
112
|
+
"total_national_insurance", POLICY_YEAR
|
|
113
|
+
).sum()
|
|
114
|
+
reform_ni = reform_simulation.calculate(
|
|
115
|
+
"total_national_insurance", POLICY_YEAR
|
|
116
|
+
).sum()
|
|
117
|
+
|
|
118
|
+
ni_increase = reform_ni - baseline_ni
|
|
119
|
+
|
|
120
|
+
print(f"\nBaseline NI: £{baseline_ni/1e9:.3f}bn")
|
|
121
|
+
print(f"Reform NI: £{reform_ni/1e9:.3f}bn")
|
|
122
|
+
print(f"NI increase: £{ni_increase/1e9:.3f}bn")
|
|
123
|
+
|
|
124
|
+
# NI should increase with the reform
|
|
125
|
+
assert (
|
|
126
|
+
ni_increase > 0
|
|
127
|
+
), f"NI should increase with cap, but change is £{ni_increase/1e9:.3f}bn"
|
|
128
|
+
|
|
129
|
+
# NI increase should be significant (at least £1bn)
|
|
130
|
+
assert (
|
|
131
|
+
ni_increase > 1e9
|
|
132
|
+
), f"NI increase should be >£1bn, got £{ni_increase/1e9:.3f}bn"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_income_tax_impact(baseline_simulation, reform_simulation):
|
|
136
|
+
"""
|
|
137
|
+
Test the income tax impact of the reform.
|
|
138
|
+
|
|
139
|
+
The excess is added to employment income and to employee pension
|
|
140
|
+
contributions for relief. Due to pension relief caps, some people
|
|
141
|
+
don't get full relief, resulting in a small positive income tax impact.
|
|
142
|
+
"""
|
|
143
|
+
baseline_tax = baseline_simulation.calculate(
|
|
144
|
+
"income_tax", POLICY_YEAR
|
|
145
|
+
).sum()
|
|
146
|
+
reform_tax = reform_simulation.calculate("income_tax", POLICY_YEAR).sum()
|
|
147
|
+
|
|
148
|
+
tax_change = reform_tax - baseline_tax
|
|
149
|
+
|
|
150
|
+
print(f"\nBaseline income tax: £{baseline_tax/1e9:.3f}bn")
|
|
151
|
+
print(f"Reform income tax: £{reform_tax/1e9:.3f}bn")
|
|
152
|
+
print(f"Income tax change: £{tax_change/1e9:.3f}bn")
|
|
153
|
+
|
|
154
|
+
# Income tax should increase slightly (due to pension relief caps)
|
|
155
|
+
# Expected to be around £1-2bn
|
|
156
|
+
assert (
|
|
157
|
+
tax_change > 0
|
|
158
|
+
), f"Income tax should increase, got £{tax_change/1e9:.3f}bn"
|
|
159
|
+
assert (
|
|
160
|
+
tax_change < 3e9
|
|
161
|
+
), f"Income tax increase should be <£3bn, got £{tax_change/1e9:.3f}bn"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_excess_redirected_to_pension(reform_simulation):
|
|
165
|
+
"""
|
|
166
|
+
Test that full excess is redirected to employee pension contributions.
|
|
167
|
+
|
|
168
|
+
The blog assumes employees maintain their total pension contributions
|
|
169
|
+
by redirecting the full excess to regular pension contributions.
|
|
170
|
+
"""
|
|
171
|
+
redirected = reform_simulation.calculate(
|
|
172
|
+
"salary_sacrifice_returned_to_income", POLICY_YEAR
|
|
173
|
+
).sum()
|
|
174
|
+
|
|
175
|
+
# Should be significant (blog says £13.8bn excess - full amount redirected)
|
|
176
|
+
assert (
|
|
177
|
+
redirected > 12e9
|
|
178
|
+
), f"Redirected amount should be >£12bn, got £{redirected/1e9:.2f}bn"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_salary_sacrifice_data_exists(reform_simulation):
|
|
182
|
+
"""
|
|
183
|
+
Test that salary sacrifice data exists in the simulation.
|
|
184
|
+
|
|
185
|
+
Blog: 4.9 million workers with SS contributions, £22.7 billion total.
|
|
186
|
+
"""
|
|
187
|
+
ss_contributions = reform_simulation.calculate(
|
|
188
|
+
"pension_contributions_via_salary_sacrifice", POLICY_YEAR
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
total_ss = ss_contributions.sum()
|
|
192
|
+
num_contributors = (ss_contributions > 0).sum()
|
|
193
|
+
|
|
194
|
+
# Should have significant SS contributions
|
|
195
|
+
assert (
|
|
196
|
+
total_ss > 20e9
|
|
197
|
+
), f"Total SS contributions should be >£20bn, got £{total_ss/1e9:.2f}bn"
|
|
198
|
+
assert (
|
|
199
|
+
num_contributors > 4e6
|
|
200
|
+
), f"Should have >4 million contributors, got {num_contributors/1e6:.1f}m"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_affected_population(reform_simulation):
|
|
204
|
+
"""
|
|
205
|
+
Test that a reasonable number of people are affected by the cap.
|
|
206
|
+
|
|
207
|
+
Blog: 3.3 million workers exceed the £2,000 cap (68% of contributors).
|
|
208
|
+
"""
|
|
209
|
+
ss_contributions = reform_simulation.calculate(
|
|
210
|
+
"pension_contributions_via_salary_sacrifice", POLICY_YEAR
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
cap = 2000
|
|
214
|
+
affected_count = (ss_contributions > cap).sum()
|
|
215
|
+
|
|
216
|
+
# Should be around 3.3 million
|
|
217
|
+
assert (
|
|
218
|
+
affected_count > 2.5e6
|
|
219
|
+
), f"Expected >2.5 million affected, got {affected_count/1e6:.1f}m"
|
|
220
|
+
assert (
|
|
221
|
+
affected_count < 5e6
|
|
222
|
+
), f"Expected <5 million affected, got {affected_count/1e6:.1f}m"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_full_excess_redirected(reform_simulation):
|
|
226
|
+
"""
|
|
227
|
+
Test that the full excess is redirected (no targeted haircut).
|
|
228
|
+
|
|
229
|
+
The broad-base haircut reduces ALL workers' employment income,
|
|
230
|
+
but the full excess above cap is redirected to regular pension contributions.
|
|
231
|
+
"""
|
|
232
|
+
# Get weighted totals using map_to for proper aggregation
|
|
233
|
+
ss_contributions = reform_simulation.calculate(
|
|
234
|
+
"pension_contributions_via_salary_sacrifice",
|
|
235
|
+
POLICY_YEAR,
|
|
236
|
+
map_to="person",
|
|
237
|
+
)
|
|
238
|
+
weights = reform_simulation.calculate("person_weight", POLICY_YEAR)
|
|
239
|
+
redirected = reform_simulation.calculate(
|
|
240
|
+
"salary_sacrifice_returned_to_income", POLICY_YEAR, map_to="person"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
cap = 2000
|
|
244
|
+
raw_excess = (np.maximum(ss_contributions - cap, 0) * weights).sum()
|
|
245
|
+
redirected_total = (redirected * weights).sum()
|
|
246
|
+
|
|
247
|
+
# Redirected should be 100% of raw excess (no targeted haircut)
|
|
248
|
+
ratio = redirected_total / raw_excess if raw_excess > 0 else 0
|
|
249
|
+
|
|
250
|
+
assert 0.95 < ratio < 1.05, (
|
|
251
|
+
f"Full excess should be redirected (ratio ~1.0). "
|
|
252
|
+
f"Raw excess: £{raw_excess/1e9:.2f}bn, "
|
|
253
|
+
f"Redirected: £{redirected_total/1e9:.2f}bn, "
|
|
254
|
+
f"Ratio: {ratio:.2f}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_broad_base_haircut_affects_all_workers(reform_simulation):
|
|
259
|
+
"""
|
|
260
|
+
Test that the broad-base haircut reduces employment income for all workers.
|
|
261
|
+
|
|
262
|
+
The broad-base haircut (default 0.16%) applies to ALL workers,
|
|
263
|
+
not just those with salary sacrifice.
|
|
264
|
+
"""
|
|
265
|
+
haircut = reform_simulation.calculate(
|
|
266
|
+
"salary_sacrifice_broad_base_haircut", POLICY_YEAR
|
|
267
|
+
)
|
|
268
|
+
weights = reform_simulation.calculate("person_weight", POLICY_YEAR)
|
|
269
|
+
employment_income = reform_simulation.calculate(
|
|
270
|
+
"employment_income_before_lsr", POLICY_YEAR
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# All workers with employment income should have a haircut
|
|
274
|
+
has_employment = employment_income > 0
|
|
275
|
+
has_haircut = haircut < 0 # Haircut is negative
|
|
276
|
+
|
|
277
|
+
# Count workers with employment income but no haircut
|
|
278
|
+
workers_with_employment = (has_employment * weights).sum()
|
|
279
|
+
workers_with_haircut = (has_haircut * weights).sum()
|
|
280
|
+
|
|
281
|
+
print(
|
|
282
|
+
f"\nWorkers with employment income: {workers_with_employment/1e6:.1f}m"
|
|
283
|
+
)
|
|
284
|
+
print(f"Workers with haircut: {workers_with_haircut/1e6:.1f}m")
|
|
285
|
+
|
|
286
|
+
# Most workers with employment income should have a haircut
|
|
287
|
+
haircut_coverage = workers_with_haircut / workers_with_employment
|
|
288
|
+
assert haircut_coverage > 0.9, (
|
|
289
|
+
f"Broad-base haircut should affect most workers, "
|
|
290
|
+
f"but only {haircut_coverage:.1%} are affected"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_decile_impact_negative_for_higher_earners(
|
|
295
|
+
baseline_simulation, reform_simulation
|
|
296
|
+
):
|
|
297
|
+
"""
|
|
298
|
+
Test that higher income deciles experience negative income changes.
|
|
299
|
+
|
|
300
|
+
The salary sacrifice cap reduces household net income for affected workers
|
|
301
|
+
(those with SS contributions > £2,000) because they now pay NI on the excess.
|
|
302
|
+
Higher deciles should have larger negative impacts as they're more likely
|
|
303
|
+
to have high salary sacrifice contributions.
|
|
304
|
+
|
|
305
|
+
This validates the distributional impact methodology used in uk-budget-data.
|
|
306
|
+
"""
|
|
307
|
+
# Calculate household net income for baseline and reform
|
|
308
|
+
baseline_income = baseline_simulation.calculate(
|
|
309
|
+
"household_net_income", period=POLICY_YEAR, map_to="household"
|
|
310
|
+
)
|
|
311
|
+
reform_income = reform_simulation.calculate(
|
|
312
|
+
"household_net_income", period=POLICY_YEAR, map_to="household"
|
|
313
|
+
)
|
|
314
|
+
household_decile = baseline_simulation.calculate(
|
|
315
|
+
"household_income_decile", period=POLICY_YEAR, map_to="household"
|
|
316
|
+
)
|
|
317
|
+
household_weight = baseline_simulation.calculate(
|
|
318
|
+
"household_weight", period=POLICY_YEAR, map_to="household"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Build decile DataFrame
|
|
322
|
+
decile_df = pd.DataFrame(
|
|
323
|
+
{
|
|
324
|
+
"household_income_decile": household_decile.values,
|
|
325
|
+
"baseline_income": baseline_income.values,
|
|
326
|
+
"reform_income": reform_income.values,
|
|
327
|
+
"income_change": (reform_income - baseline_income).values,
|
|
328
|
+
"household_weight": household_weight.values,
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
decile_df = decile_df[decile_df["household_income_decile"] >= 1]
|
|
332
|
+
|
|
333
|
+
# Calculate weighted relative change by decile
|
|
334
|
+
decile_names = [
|
|
335
|
+
"1st",
|
|
336
|
+
"2nd",
|
|
337
|
+
"3rd",
|
|
338
|
+
"4th",
|
|
339
|
+
"5th",
|
|
340
|
+
"6th",
|
|
341
|
+
"7th",
|
|
342
|
+
"8th",
|
|
343
|
+
"9th",
|
|
344
|
+
"10th",
|
|
345
|
+
]
|
|
346
|
+
results = []
|
|
347
|
+
|
|
348
|
+
for decile_num in range(1, 11):
|
|
349
|
+
decile_data = decile_df[
|
|
350
|
+
decile_df["household_income_decile"] == decile_num
|
|
351
|
+
]
|
|
352
|
+
if len(decile_data) > 0:
|
|
353
|
+
weighted_change = (
|
|
354
|
+
decile_data["income_change"] * decile_data["household_weight"]
|
|
355
|
+
).sum()
|
|
356
|
+
weighted_baseline = (
|
|
357
|
+
decile_data["baseline_income"]
|
|
358
|
+
* decile_data["household_weight"]
|
|
359
|
+
).sum()
|
|
360
|
+
rel_change = (
|
|
361
|
+
(weighted_change / weighted_baseline) * 100
|
|
362
|
+
if weighted_baseline > 0
|
|
363
|
+
else 0
|
|
364
|
+
)
|
|
365
|
+
results.append(
|
|
366
|
+
{
|
|
367
|
+
"decile": decile_names[decile_num - 1],
|
|
368
|
+
"decile_num": decile_num,
|
|
369
|
+
"rel_change_pct": rel_change,
|
|
370
|
+
"abs_change": weighted_change,
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
print("\nDecile Impact (relative % change in household net income):")
|
|
375
|
+
for r in results:
|
|
376
|
+
print(f" {r['decile']}: {r['rel_change_pct']:.3f}%")
|
|
377
|
+
|
|
378
|
+
# Higher deciles (8th, 9th, 10th) should have negative impacts
|
|
379
|
+
# These workers are more likely to have high salary sacrifice
|
|
380
|
+
high_decile_results = [r for r in results if r["decile_num"] >= 8]
|
|
381
|
+
|
|
382
|
+
for r in high_decile_results:
|
|
383
|
+
assert r["rel_change_pct"] < 0, (
|
|
384
|
+
f"Decile {r['decile']} should have negative impact "
|
|
385
|
+
f"(losers from reform), got {r['rel_change_pct']:.3f}%"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# The 10th decile should have the largest negative impact
|
|
389
|
+
decile_10 = next(r for r in results if r["decile_num"] == 10)
|
|
390
|
+
assert decile_10["rel_change_pct"] < -0.1, (
|
|
391
|
+
f"10th decile should have significant negative impact (<-0.1%), "
|
|
392
|
+
f"got {decile_10['rel_change_pct']:.3f}%"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Overall impact should be negative (reform takes money from households)
|
|
396
|
+
total_change = sum(r["abs_change"] for r in results)
|
|
397
|
+
print(f"\nTotal household income change: £{total_change/1e9:.3f}bn")
|
|
398
|
+
assert total_change < 0, (
|
|
399
|
+
f"Total household income should decrease, "
|
|
400
|
+
f"got £{total_change/1e9:.3f}bn change"
|
|
401
|
+
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from
|
|
1
|
+
from policyengine_uk import Microsimulation
|
|
2
2
|
import pytest
|
|
3
3
|
|
|
4
4
|
YEARS = range(2024, 2026)
|
|
@@ -6,8 +6,7 @@ YEARS = range(2024, 2026)
|
|
|
6
6
|
|
|
7
7
|
@pytest.mark.parametrize("year", YEARS)
|
|
8
8
|
def test_not_nan(year):
|
|
9
|
-
baseline =
|
|
10
|
-
baseline = baseline.baseline_simulation
|
|
9
|
+
baseline = Microsimulation()
|
|
11
10
|
for variable in baseline.tax_benefit_system.variables:
|
|
12
11
|
requires_computation_after = baseline.tax_benefit_system.variables[
|
|
13
12
|
variable
|
|
@@ -8,6 +8,18 @@ from pathlib import Path
|
|
|
8
8
|
from policyengine_uk import Microsimulation
|
|
9
9
|
import argparse
|
|
10
10
|
from datetime import datetime
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.progress import (
|
|
14
|
+
Progress,
|
|
15
|
+
SpinnerColumn,
|
|
16
|
+
TextColumn,
|
|
17
|
+
BarColumn,
|
|
18
|
+
TaskProgressColumn,
|
|
19
|
+
)
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich import print as rprint
|
|
22
|
+
import traceback
|
|
11
23
|
|
|
12
24
|
baseline = Microsimulation()
|
|
13
25
|
|
|
@@ -40,57 +52,97 @@ def update_impacts(
|
|
|
40
52
|
dry_run: If True, show changes without writing to file
|
|
41
53
|
verbose: If True, show detailed output
|
|
42
54
|
"""
|
|
55
|
+
console = Console()
|
|
56
|
+
|
|
43
57
|
# Load current configuration
|
|
44
58
|
with open(config_path, "r") as f:
|
|
45
59
|
config = yaml.safe_load(f)
|
|
46
60
|
|
|
47
61
|
if verbose:
|
|
48
|
-
print(
|
|
49
|
-
|
|
62
|
+
console.print(
|
|
63
|
+
Panel.fit(
|
|
64
|
+
f"[bold cyan]Loaded configuration from {config_path}[/bold cyan]\n"
|
|
65
|
+
f"[green]Found {len(config['reforms'])} reforms to update[/green]",
|
|
66
|
+
title="Configuration loaded",
|
|
67
|
+
)
|
|
68
|
+
)
|
|
50
69
|
|
|
51
70
|
# Track changes
|
|
52
71
|
changes = []
|
|
53
72
|
|
|
54
|
-
# Update each reform's expected impact
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"name": reform["name"],
|
|
66
|
-
"old": old_impact,
|
|
67
|
-
"new": new_impact,
|
|
68
|
-
"diff": new_impact - old_impact,
|
|
69
|
-
}
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
reform["expected_impact"] = new_impact
|
|
73
|
+
# Update each reform's expected impact with progress bar
|
|
74
|
+
with Progress(
|
|
75
|
+
SpinnerColumn(),
|
|
76
|
+
TextColumn("[progress.description]{task.description}"),
|
|
77
|
+
BarColumn(),
|
|
78
|
+
TaskProgressColumn(),
|
|
79
|
+
console=console,
|
|
80
|
+
) as progress:
|
|
81
|
+
task = progress.add_task(
|
|
82
|
+
"[cyan]Processing reforms...", total=len(config["reforms"])
|
|
83
|
+
)
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
for reform in config["reforms"]:
|
|
86
|
+
progress.update(
|
|
87
|
+
task, description=f"[cyan]Processing: {reform['name'][:40]}..."
|
|
88
|
+
)
|
|
89
|
+
old_impact = reform["expected_impact"]
|
|
90
|
+
new_impact = round(get_fiscal_impact(reform["parameters"]), 1)
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
abs(old_impact - new_impact) > 0.01
|
|
94
|
+
): # Only record meaningful changes
|
|
95
|
+
changes.append(
|
|
96
|
+
{
|
|
97
|
+
"name": reform["name"],
|
|
98
|
+
"old": old_impact,
|
|
99
|
+
"new": new_impact,
|
|
100
|
+
"diff": new_impact - old_impact,
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
reform["expected_impact"] = new_impact
|
|
105
|
+
progress.advance(task)
|
|
106
|
+
|
|
107
|
+
# Show detailed output if verbose
|
|
108
|
+
if verbose and changes:
|
|
109
|
+
console.print("\n[bold]Detailed changes:[/bold]")
|
|
110
|
+
for change in changes:
|
|
111
|
+
color = "red" if change["diff"] < 0 else "green"
|
|
112
|
+
console.print(
|
|
113
|
+
f" [yellow]{change['name']}[/yellow]\n"
|
|
114
|
+
f" Old impact: [dim]{change['old']:.1f} billion[/dim]\n"
|
|
115
|
+
f" New impact: [bold]{change['new']:.1f} billion[/bold]\n"
|
|
116
|
+
f" Change: [{color}]{change['diff']:+.1f} billion[/{color}]\n"
|
|
117
|
+
)
|
|
81
118
|
|
|
82
119
|
# Show summary of changes
|
|
83
120
|
if changes:
|
|
84
|
-
|
|
85
|
-
|
|
121
|
+
table = Table(
|
|
122
|
+
title="Summary of changes",
|
|
123
|
+
show_header=True,
|
|
124
|
+
header_style="bold magenta",
|
|
125
|
+
)
|
|
126
|
+
table.add_column("Reform", style="cyan", no_wrap=False)
|
|
127
|
+
table.add_column("Old impact (£bn)", justify="right")
|
|
128
|
+
table.add_column("New impact (£bn)", justify="right")
|
|
129
|
+
table.add_column("Change (£bn)", justify="right")
|
|
130
|
+
|
|
86
131
|
for change in changes:
|
|
87
|
-
|
|
88
|
-
|
|
132
|
+
color = "red" if change["diff"] < 0 else "green"
|
|
133
|
+
table.add_row(
|
|
134
|
+
change["name"],
|
|
135
|
+
f"{change['old']:.1f}",
|
|
136
|
+
f"{change['new']:.1f}",
|
|
137
|
+
f"[{color}]{change['diff']:+.1f}[/{color}]",
|
|
89
138
|
)
|
|
90
|
-
|
|
91
|
-
print(
|
|
139
|
+
|
|
140
|
+
console.print("\n", table)
|
|
141
|
+
console.print(
|
|
142
|
+
f"\n[bold cyan]Total changes: {len(changes)}[/bold cyan]"
|
|
143
|
+
)
|
|
92
144
|
else:
|
|
93
|
-
print("\
|
|
145
|
+
console.print("\n[green]✓ No significant changes detected.[/green]")
|
|
94
146
|
|
|
95
147
|
# Write updated configuration
|
|
96
148
|
if not dry_run:
|
|
@@ -109,13 +161,22 @@ def update_impacts(
|
|
|
109
161
|
with open(config_path, "w") as f:
|
|
110
162
|
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
111
163
|
|
|
112
|
-
print(
|
|
113
|
-
|
|
164
|
+
console.print(
|
|
165
|
+
Panel.fit(
|
|
166
|
+
f"[green]✓ Configuration updated successfully![/green]\n"
|
|
167
|
+
f"[dim]Backup saved to: {backup_path}[/dim]",
|
|
168
|
+
title="Success",
|
|
169
|
+
border_style="green",
|
|
170
|
+
)
|
|
171
|
+
)
|
|
114
172
|
else:
|
|
115
|
-
print(
|
|
173
|
+
console.print(
|
|
174
|
+
"\n[yellow]⚠ Dry run - no changes written to file.[/yellow]"
|
|
175
|
+
)
|
|
116
176
|
|
|
117
177
|
|
|
118
178
|
def main():
|
|
179
|
+
console = Console()
|
|
119
180
|
parser = argparse.ArgumentParser(
|
|
120
181
|
description="Update reform impact expectations with current model values"
|
|
121
182
|
)
|
|
@@ -143,14 +204,17 @@ def main():
|
|
|
143
204
|
args = parser.parse_args()
|
|
144
205
|
|
|
145
206
|
if not args.config.exists():
|
|
146
|
-
print(
|
|
207
|
+
console.print(
|
|
208
|
+
f"[bold red]Error:[/bold red] Configuration file '{args.config}' not found!"
|
|
209
|
+
)
|
|
147
210
|
return 1
|
|
148
211
|
|
|
149
212
|
try:
|
|
150
213
|
update_impacts(args.config, dry_run=args.dry_run, verbose=args.verbose)
|
|
151
214
|
return 0
|
|
152
215
|
except Exception as e:
|
|
153
|
-
print(f"Error updating impacts: {e}")
|
|
216
|
+
console.print(f"[bold red]Error updating impacts:[/bold red] {e}")
|
|
217
|
+
console.print(traceback.format_exc())
|
|
154
218
|
return 1
|
|
155
219
|
|
|
156
220
|
|