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.
Files changed (258) hide show
  1. policyengine_uk/__init__.py +5 -3
  2. policyengine_uk/data/__init__.py +1 -0
  3. policyengine_uk/data/dataset_schema.py +70 -18
  4. policyengine_uk/data/economic_assumptions.py +36 -10
  5. policyengine_uk/data/filter_dataset.py +52 -0
  6. policyengine_uk/dynamics/labour_supply.py +343 -0
  7. policyengine_uk/dynamics/participation.py +629 -0
  8. policyengine_uk/dynamics/progression.py +384 -0
  9. policyengine_uk/microsimulation.py +105 -0
  10. policyengine_uk/model_api.py +1 -0
  11. policyengine_uk/parameters/gov/boe/base_rate.yaml +34 -0
  12. policyengine_uk/parameters/gov/boe/index.yaml +2 -0
  13. policyengine_uk/parameters/gov/contrib/behavioral_responses/employee_salary_sacrifice_reduction_rate.yaml +14 -0
  14. policyengine_uk/parameters/gov/contrib/behavioral_responses/salary_sacrifice_broad_base_haircut_rate.yaml +22 -0
  15. policyengine_uk/parameters/gov/contrib/cec/state_pension_increase.yaml +1 -1
  16. policyengine_uk/parameters/gov/contrib/ubi_center/carbon_tax.yaml +2 -2
  17. policyengine_uk/parameters/gov/contrib/ubi_center/land_value_tax.yaml +3 -3
  18. policyengine_uk/parameters/gov/dcms/bbc/tv_licence/colour.yaml +5 -5
  19. policyengine_uk/parameters/gov/dfe/education_spending.yaml +1 -1
  20. policyengine_uk/parameters/gov/dft/rail/fare_index.yaml +32 -0
  21. policyengine_uk/parameters/gov/dft/rail/prior_law_fare_index.yaml +32 -0
  22. policyengine_uk/parameters/gov/dft/rail/ridership_index.yaml +30 -0
  23. policyengine_uk/parameters/gov/dft/spending.yaml +2 -2
  24. policyengine_uk/parameters/gov/dwp/ESA/income/earn_disregard.yaml +1 -1
  25. policyengine_uk/parameters/gov/dwp/ESA/income/income_disregard_couple.yaml +1 -1
  26. policyengine_uk/parameters/gov/dwp/ESA/income/income_disregard_lone_parent.yaml +1 -1
  27. policyengine_uk/parameters/gov/dwp/ESA/income/income_disregard_single.yaml +1 -1
  28. policyengine_uk/parameters/gov/dwp/ESA/income/pension_disregard.yaml +1 -1
  29. policyengine_uk/parameters/gov/dwp/IIDB/maximum.yaml +1 -1
  30. policyengine_uk/parameters/gov/dwp/JSA/contrib/amount_over_25.yaml +1 -1
  31. policyengine_uk/parameters/gov/dwp/JSA/contrib/earn_disregard.yaml +1 -1
  32. policyengine_uk/parameters/gov/dwp/JSA/contrib/pension_disregard.yaml +1 -1
  33. policyengine_uk/parameters/gov/dwp/JSA/income/amount_18_24.yaml +7 -7
  34. policyengine_uk/parameters/gov/dwp/JSA/income/amount_over_25.yaml +7 -7
  35. policyengine_uk/parameters/gov/dwp/JSA/income/income_disregard_couple.yaml +1 -1
  36. policyengine_uk/parameters/gov/dwp/JSA/income/income_disregard_lone_parent.yaml +1 -1
  37. policyengine_uk/parameters/gov/dwp/JSA/income/income_disregard_single.yaml +1 -1
  38. policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_age_threshold.yaml +12 -0
  39. policyengine_uk/parameters/gov/dwp/attendance_allowance/higher.yaml +7 -7
  40. policyengine_uk/parameters/gov/dwp/attendance_allowance/lower.yaml +7 -7
  41. policyengine_uk/parameters/gov/dwp/benefit_cap.yaml +3 -3
  42. policyengine_uk/parameters/gov/dwp/carer_premium/couple.yaml +2 -2
  43. policyengine_uk/parameters/gov/dwp/carer_premium/single.yaml +6 -6
  44. policyengine_uk/parameters/gov/dwp/carers_allowance/rate.yaml +7 -7
  45. policyengine_uk/parameters/gov/dwp/disability_premia/disability_couple.yaml +1 -1
  46. policyengine_uk/parameters/gov/dwp/disability_premia/enhanced_couple.yaml +1 -1
  47. policyengine_uk/parameters/gov/dwp/disability_premia/enhanced_single.yaml +1 -1
  48. policyengine_uk/parameters/gov/dwp/disability_premia/severe_couple.yaml +1 -1
  49. policyengine_uk/parameters/gov/dwp/dla/mobility/higher.yaml +4 -4
  50. policyengine_uk/parameters/gov/dwp/dla/mobility/lower.yaml +8 -8
  51. policyengine_uk/parameters/gov/dwp/dla/self_care/higher.yaml +7 -7
  52. policyengine_uk/parameters/gov/dwp/dla/self_care/lower.yaml +8 -8
  53. policyengine_uk/parameters/gov/dwp/dla/self_care/middle.yaml +7 -7
  54. policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/aged.yaml +1 -1
  55. policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/older.yaml +3 -3
  56. policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/aged.yaml +1 -1
  57. policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/older.yaml +3 -3
  58. policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/worker.yaml +1 -1
  59. policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/amount.yaml +1 -1
  60. policyengine_uk/parameters/gov/dwp/housing_benefit/takeup.yaml +7 -7
  61. policyengine_uk/parameters/gov/dwp/income_support/amounts/amount_16_24.yaml +1 -1
  62. policyengine_uk/parameters/gov/dwp/income_support/amounts/amount_couples_over_18.yaml +1 -1
  63. policyengine_uk/parameters/gov/dwp/income_support/means_test/earn_disregard.yaml +1 -1
  64. policyengine_uk/parameters/gov/dwp/income_support/means_test/income_disregard_couple.yaml +1 -1
  65. policyengine_uk/parameters/gov/dwp/income_support/means_test/income_disregard_lone_parent.yaml +1 -1
  66. policyengine_uk/parameters/gov/dwp/income_support/means_test/income_disregard_single.yaml +1 -1
  67. policyengine_uk/parameters/gov/dwp/income_support/means_test/pension_disregard.yaml +1 -1
  68. policyengine_uk/parameters/gov/dwp/income_support/takeup.yaml +7 -7
  69. policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/carer/addition.yaml +4 -4
  70. policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee.yaml +9 -9
  71. policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/severe_disability/addition.yaml +3 -3
  72. policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/threshold.yaml +5 -5
  73. policyengine_uk/parameters/gov/dwp/pip/daily_living/enhanced.yaml +7 -7
  74. policyengine_uk/parameters/gov/dwp/pip/daily_living/standard.yaml +8 -8
  75. policyengine_uk/parameters/gov/dwp/pip/mobility/enhanced.yaml +4 -4
  76. policyengine_uk/parameters/gov/dwp/pip/mobility/standard.yaml +9 -9
  77. policyengine_uk/parameters/gov/dwp/sda/maximum.yaml +7 -7
  78. policyengine_uk/parameters/gov/dwp/state_pension/basic_state_pension/amount.yaml +11 -11
  79. policyengine_uk/parameters/gov/dwp/state_pension/new_state_pension/amount.yaml +4 -4
  80. policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/limit/child_count.yaml +10 -1
  81. policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/takeup.yaml +7 -7
  82. policyengine_uk/parameters/gov/dwp/tax_credits/working_tax_credit/takeup.yaml +7 -7
  83. policyengine_uk/parameters/gov/dwp/universal_credit/elements/carer/amount.yaml +3 -3
  84. policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/amount.yaml +2 -4
  85. policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/disabled/amount.yaml +2 -4
  86. policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/first/higher_amount.yaml +6 -8
  87. policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/limit/child_count.yaml +6 -1
  88. policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/severely_disabled/amount.yaml +3 -5
  89. policyengine_uk/parameters/gov/dwp/universal_credit/elements/childcare/cap.yaml +2 -6
  90. policyengine_uk/parameters/gov/dwp/universal_credit/elements/disabled/amount.yaml +4 -6
  91. policyengine_uk/parameters/gov/dwp/universal_credit/elements/housing/non_dep_deduction/amount.yaml +4 -1
  92. policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/active.yaml +9 -0
  93. policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/new_claimant_health_element.yaml +9 -0
  94. policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/standard_allowance_uplift.yaml +13 -0
  95. policyengine_uk/parameters/gov/dwp/universal_credit/standard_allowance/amount.yaml +5 -5
  96. policyengine_uk/parameters/gov/dwp/winter_fuel_payment/eligibility/taxable_income_test/maximum_taxable_income.yaml +2 -1
  97. policyengine_uk/parameters/gov/dwp/winter_fuel_payment/eligibility/taxable_income_test/use_maximum_taxable_income.yaml +1 -0
  98. policyengine_uk/parameters/gov/dynamic/obr_labour_supply_assumptions.yaml +9 -0
  99. policyengine_uk/parameters/gov/economic_assumptions/create_economic_assumption_indices.py +1 -1
  100. policyengine_uk/parameters/gov/economic_assumptions/yoy_growth.yaml +522 -153
  101. policyengine_uk/parameters/gov/hmrc/cgt/additional_rate.yaml +5 -0
  102. policyengine_uk/parameters/gov/hmrc/cgt/basic_rate.yaml +5 -0
  103. policyengine_uk/parameters/gov/hmrc/cgt/higher_rate.yaml +4 -0
  104. policyengine_uk/parameters/gov/hmrc/child_benefit/amount/additional.yaml +6 -6
  105. policyengine_uk/parameters/gov/hmrc/child_benefit/amount/eldest.yaml +8 -8
  106. policyengine_uk/parameters/gov/hmrc/child_benefit/takeup/by_age.yaml +1 -1
  107. policyengine_uk/parameters/gov/hmrc/fuel_duty/calculate_fuel_duty_rates.py +464 -0
  108. policyengine_uk/parameters/gov/hmrc/fuel_duty/petrol_and_diesel.yaml +86 -10
  109. policyengine_uk/parameters/gov/hmrc/income_tax/allowances/personal_allowance/amount.yaml +6 -0
  110. policyengine_uk/parameters/gov/hmrc/income_tax/earned_taxable_income_exclusions.yaml +2 -1
  111. policyengine_uk/parameters/gov/hmrc/income_tax/income_tax_additions.yaml +1 -0
  112. policyengine_uk/parameters/gov/hmrc/income_tax/rates/dividends.yaml +12 -0
  113. policyengine_uk/parameters/gov/hmrc/income_tax/rates/property.yaml +46 -0
  114. policyengine_uk/parameters/gov/hmrc/income_tax/rates/savings.yaml +46 -0
  115. policyengine_uk/parameters/gov/hmrc/income_tax/rates/scotland/rates.yaml +2 -2
  116. policyengine_uk/parameters/gov/hmrc/income_tax/rates/uk.yaml +14 -2
  117. policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/additional.yaml +4 -6
  118. policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employer.yaml +3 -3
  119. policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml +14 -4
  120. policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/flat_rate.yaml +2 -2
  121. policyengine_uk/parameters/gov/hmrc/national_insurance/salary_sacrifice_pension_cap.yaml +16 -0
  122. policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/index.yaml +12 -0
  123. policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_1/boe_margin.yaml +11 -0
  124. policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_2/additional_rate.yaml +27 -0
  125. policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_2/index.yaml +16 -0
  126. policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_2/upper_threshold.yaml +48 -0
  127. policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/postgraduate_additional_rate.yaml +11 -0
  128. policyengine_uk/parameters/gov/hmrc/student_loans/postgraduate_repayment_rate.yaml +9 -0
  129. policyengine_uk/parameters/gov/hmrc/student_loans/repayment_rate.yaml +9 -0
  130. policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_1.yaml +25 -0
  131. policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_2.yaml +58 -0
  132. policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_4.yaml +19 -0
  133. policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_5.yaml +16 -0
  134. policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/postgraduate.yaml +21 -0
  135. policyengine_uk/parameters/gov/hmrc/vat/reduced_rate_share.yaml +3 -3
  136. policyengine_uk/parameters/gov/indices/private_rent_index.yaml +9 -9
  137. policyengine_uk/parameters/gov/revenue_scotland/lbtt/non_residential.yaml +2 -2
  138. policyengine_uk/parameters/gov/revenue_scotland/lbtt/rent.yaml +2 -2
  139. policyengine_uk/parameters/gov/revenue_scotland/lbtt/residential/first_time_buyer_rate.yaml +2 -2
  140. policyengine_uk/parameters/gov/revenue_scotland/lbtt/residential/rate.yaml +2 -2
  141. policyengine_uk/parameters/gov/wra/land_transaction_tax/non_residential.yaml +2 -2
  142. policyengine_uk/parameters/gov/wra/land_transaction_tax/rent.yaml +2 -2
  143. policyengine_uk/parameters/gov/wra/land_transaction_tax/residential/higher_rate.yaml +1 -1
  144. policyengine_uk/parameters/gov/wra/land_transaction_tax/residential/primary.yaml +2 -2
  145. policyengine_uk/parameters/household/consumption/carbon/consumption.yaml +8 -6
  146. policyengine_uk/parameters/household/consumption/carbon/intensity.yaml +4 -1
  147. policyengine_uk/parameters/household/consumption/carbon/production.yaml +12 -7
  148. policyengine_uk/parameters/household/consumption/carbon/production_by_source.yaml +76 -41
  149. policyengine_uk/parameters/household/consumption/fuel/prices/petrol.yaml +1 -1
  150. policyengine_uk/parameters/household/poverty/absolute_poverty_threshold_bhc.yaml +1 -1
  151. policyengine_uk/reforms/policyengine/adjust_budgets.py +0 -1
  152. policyengine_uk/scenarios/__init__.py +4 -0
  153. policyengine_uk/scenarios/pip_reform.py +23 -0
  154. policyengine_uk/scenarios/reindex_benefit_cap.py +32 -0
  155. policyengine_uk/scenarios/repeal_two_child_limit.py +10 -0
  156. policyengine_uk/scenarios/uc_reform.py +50 -0
  157. policyengine_uk/simulation.py +619 -0
  158. policyengine_uk/system.py +3 -257
  159. policyengine_uk/tax_benefit_system.py +141 -0
  160. policyengine_uk/tests/behavioral_responses/test_labor_supply_responses.yaml +183 -0
  161. policyengine_uk/tests/microsimulation/reforms_config.yaml +8 -8
  162. policyengine_uk/tests/microsimulation/test_reform_impacts.py +2 -2
  163. policyengine_uk/tests/microsimulation/test_salary_sacrifice_cap_reform.py +401 -0
  164. policyengine_uk/tests/microsimulation/test_validity.py +2 -3
  165. policyengine_uk/tests/microsimulation/update_reform_impacts.py +104 -40
  166. policyengine_uk/tests/policy/baseline/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.yaml +105 -0
  167. policyengine_uk/tests/policy/baseline/finance/benefit/family/child_benefit.yaml +2 -0
  168. policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support.yaml +0 -23
  169. policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/tv_licence.yaml +3 -0
  170. policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.yaml +17 -17
  171. policyengine_uk/tests/policy/baseline/gov/dwp/basic_state_pension.yaml +44 -0
  172. policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/gift_aid.yaml +71 -0
  173. policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/personal_allowance.yaml +161 -0
  174. policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.yaml +107 -0
  175. policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.yaml +95 -0
  176. policyengine_uk/tests/policy/baseline/gov/hmrc/student_loans/student_loan_interest_rate.yaml +153 -0
  177. policyengine_uk/tests/policy/baseline/gov/hmrc/student_loans/student_loan_repayment.yaml +130 -0
  178. policyengine_uk/tests/policy/baseline/household/wealth/vehicle.yaml +27 -0
  179. policyengine_uk/tests/policy/reforms/nov_2025_budget/income_source_tax_rates.yaml +235 -0
  180. policyengine_uk/tests/policy/reforms/nov_2025_budget/income_tax_freeze.yaml +83 -0
  181. policyengine_uk/tests/policy/reforms/parametric/basic_income/basic_income.yaml +1 -0
  182. policyengine_uk/tests/test_behavioral_responses.py +215 -0
  183. policyengine_uk/tests/test_fiscal_year_parameters.py +131 -0
  184. policyengine_uk/utils/__init__.py +1 -0
  185. policyengine_uk/utils/compare.py +28 -0
  186. policyengine_uk/utils/create_ahc_deflator.py +169 -0
  187. policyengine_uk/utils/create_triple_lock.py +1 -1
  188. policyengine_uk/utils/dependencies.py +259 -0
  189. policyengine_uk/utils/parameters.py +12 -1
  190. policyengine_uk/utils/scenario.py +225 -0
  191. policyengine_uk/utils/solve_private_school_attendance_factor.py +4 -6
  192. policyengine_uk/variables/contrib/policyengine/education_budget_change.py +0 -1
  193. policyengine_uk/variables/contrib/policyengine/employer_ni/baseline_employer_cost.py +5 -1
  194. policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.py +23 -23
  195. policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_response_capital_incidence.py +1 -1
  196. policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_response_consumer_incidence.py +1 -1
  197. policyengine_uk/variables/contrib/policyengine/other_public_spending_budget_change.py +0 -1
  198. policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_eligible.py +0 -1
  199. policyengine_uk/variables/gov/dft/rail_subsidy_spending.py +16 -1
  200. policyengine_uk/variables/gov/dft/rail_usage.py +16 -0
  201. policyengine_uk/variables/gov/dwp/BRMA_LHA_rate.py +7 -2
  202. policyengine_uk/variables/gov/dwp/LHA_category.py +4 -2
  203. policyengine_uk/variables/gov/dwp/additional_state_pension.py +4 -2
  204. policyengine_uk/variables/gov/dwp/basic_state_pension.py +26 -8
  205. policyengine_uk/variables/gov/dwp/is_CTC_eligible.py +1 -1
  206. policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py +9 -13
  207. policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py +66 -0
  208. policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py +75 -0
  209. policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py +66 -0
  210. policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py +5 -4
  211. policyengine_uk/variables/gov/gov_tax.py +0 -2
  212. policyengine_uk/variables/gov/hmrc/household_tax.py +0 -1
  213. policyengine_uk/variables/gov/hmrc/income_tax/allowances/gift_aid.py +23 -0
  214. policyengine_uk/variables/gov/hmrc/income_tax/allowances/personal_allowance.py +9 -2
  215. policyengine_uk/variables/gov/hmrc/income_tax/income_tax_pre_charges.py +1 -0
  216. policyengine_uk/variables/gov/hmrc/income_tax/liability/property_income_tax.py +75 -0
  217. policyengine_uk/variables/gov/hmrc/income_tax/liability/savings_income_tax.py +4 -4
  218. policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_broad_base_haircut.py +43 -0
  219. policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.py +38 -0
  220. policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.py +27 -0
  221. policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_adjusted.py +31 -0
  222. policyengine_uk/variables/gov/hmrc/pensions/salary_sacrifice_returned_to_income.py +41 -0
  223. policyengine_uk/variables/gov/hmrc/student_loans/__init__.py +2 -0
  224. policyengine_uk/variables/gov/hmrc/student_loans/plan_1_interest_rate.py +23 -0
  225. policyengine_uk/variables/gov/hmrc/student_loans/plan_2_interest_rate.py +31 -0
  226. policyengine_uk/variables/gov/hmrc/student_loans/plan_4_interest_rate.py +22 -0
  227. policyengine_uk/variables/gov/hmrc/student_loans/plan_5_interest_rate.py +18 -0
  228. policyengine_uk/variables/gov/hmrc/student_loans/postgraduate_interest_rate.py +23 -0
  229. policyengine_uk/variables/gov/hmrc/student_loans/student_loan_plan.py +27 -0
  230. policyengine_uk/variables/gov/hmrc/student_loans/student_loan_repayment.py +91 -0
  231. policyengine_uk/variables/gov/hmrc/student_loans/student_loan_repayment_rate.py +31 -0
  232. policyengine_uk/variables/gov/hmrc/would_claim_child_benefit.py +5 -1
  233. policyengine_uk/variables/household/demographic/benunit/benunit_count_adults.py +11 -0
  234. policyengine_uk/variables/household/demographic/is_disabled_for_benefits.py +13 -1
  235. policyengine_uk/variables/household/income/hbai_household_net_income.py +29 -1
  236. policyengine_uk/variables/household/income/hbai_household_net_income_ahc.py +13 -0
  237. policyengine_uk/variables/household/income/household_net_income.py +5 -1
  238. policyengine_uk/variables/household/income/inflation_adjustment.py +24 -0
  239. policyengine_uk/variables/household/post_tax_income.py +12 -0
  240. policyengine_uk/variables/household/wealth/num_vehicles.py +9 -0
  241. policyengine_uk/variables/household/wealth/owns_vehicle.py +17 -0
  242. policyengine_uk/variables/input/consumption/property/council_tax.py +0 -35
  243. policyengine_uk/variables/input/consumption/property/employee_pension_contributions.py +8 -1
  244. policyengine_uk/variables/input/consumption/property/employee_pension_contributions_reported.py +16 -0
  245. policyengine_uk/variables/input/consumption/property/pension_contributions_via_salary_sacrifice.py +16 -0
  246. policyengine_uk/variables/input/employment_income.py +2 -0
  247. policyengine_uk/variables/input/rent.py +0 -40
  248. policyengine_uk/variables/input/savings_interest_income.py +3 -1
  249. {policyengine_uk-2.40.1.dist-info → policyengine_uk-2.65.6.dist-info}/METADATA +17 -8
  250. {policyengine_uk-2.40.1.dist-info → policyengine_uk-2.65.6.dist-info}/RECORD +252 -173
  251. {policyengine_uk-2.40.1.dist-info → policyengine_uk-2.65.6.dist-info}/WHEEL +1 -1
  252. policyengine_uk/repo.py +0 -3
  253. policyengine_uk/tests/policy/baseline/gov/abolitions/abolition_parameters.yaml +0 -250
  254. policyengine_uk/variables/contrib/policyengine/high_income_incident_tax_change.py +0 -22
  255. policyengine_uk-2.40.1.data/data/share/openfisca/openfisca-country-template/CHANGELOG.md +0 -2285
  256. policyengine_uk-2.40.1.data/data/share/openfisca/openfisca-country-template/README.md +0 -37
  257. policyengine_uk-2.40.1.dist-info/licenses/LICENSE +0 -661
  258. {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 policyengine import Simulation
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 = Simulation(scope="macro", country="uk")
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(f"Loaded configuration from {config_path}")
49
- print(f"Found {len(config['reforms'])} reforms to update\n")
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
- for reform in config["reforms"]:
56
- print(f"Processing reform: {reform['name']}") if verbose else None
57
- old_impact = reform["expected_impact"]
58
- new_impact = round(get_fiscal_impact(reform["parameters"]), 1)
59
-
60
- if (
61
- abs(old_impact - new_impact) > 0.01
62
- ): # Only record meaningful changes
63
- changes.append(
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
- if verbose:
75
- print(f"Reform: {reform['name']}")
76
- print(f" Old impact: {old_impact:.1f} billion")
77
- print(f" New impact: {new_impact:.1f} billion")
78
- if abs(old_impact - new_impact) > 0.01:
79
- print(f" Change: {new_impact - old_impact:+.1f} billion")
80
- print()
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
- print("\nSummary of changes:")
85
- print("-" * 70)
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
- print(
88
- f"{change['name']:<50} {change['old']:>6.1f} → {change['new']:>6.1f} ({change['diff']:+.1f})"
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
- print("-" * 70)
91
- print(f"Total changes: {len(changes)}")
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("\nNo significant changes detected.")
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(f"\nConfiguration updated successfully!")
113
- print(f"Backup saved to: {backup_path}")
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("\nDry run - no changes written to file.")
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(f"Error: Configuration file '{args.config}' not found!")
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