Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/717.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
UK runs now recalculate after each step that changes the built simulation: the policy's simulation modifier, the dynamic's parameter values written after it, and the dynamic's simulation modifier. Each step ends by dropping every value the simulation has calculated, keeping its inputs, including values a modifier stores with `set_input`, so the next step and the output read values calculated from the simulation as that step left it. Before, a value calculated before one of these steps kept its old value. A dynamic's parameter values written after a policy modifier missed every value the modifier had calculated, so a dynamic adding £1,000 a year to Child Benefit left a policy-warmed Child Benefit unchanged. Values a policy modifier calculated before storing an input with `set_input` did not include that input. A failed step still drops what it calculated, then ends the run. Runs without a simulation modifier are unchanged; with one, values the modifier calculated are calculated again afterwards. Labour supply responses to dynamic elasticities written after a policy modifier also need a policyengine-uk release with PolicyEngine/policyengine-uk#2673; on policyengine-uk 2.132.6 the response stays zero even without a modifier.
35 changes: 32 additions & 3 deletions src/policyengine/tax_benefit_models/uk/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,14 +245,20 @@ def run(self, simulation: "Simulation") -> "Simulation":
dataset=_policyengine_uk_input(dataset), reform=reform_dict
)

# Each step on the built simulation ends by dropping the values it
# calculated, so the next step and the output read values calculated
# from the simulation as that step left it (see _apply_step).
if policy_modifier is not None:
policy_modifier(microsim)
_apply_step(policy_modifier, microsim)

if late_dynamic_values is not None:
simulation_modifier_from_parameter_values(late_dynamic_values)(microsim)
_apply_step(
simulation_modifier_from_parameter_values(late_dynamic_values),
microsim,
)

if dynamic_modifier is not None:
dynamic_modifier(microsim)
_apply_step(dynamic_modifier, microsim)

# The capital gains realisation response this run applies, read once
# every reform and modifier has run.
Expand Down Expand Up @@ -310,6 +316,29 @@ def run(self, simulation: "Simulation") -> "Simulation":
)


def _apply_step(step, microsim) -> None:
"""Run one step that changes a built simulation, then drop what it calculated.

policyengine-uk keeps each value it calculates. A modifier can calculate
values and then change what they were calculated from: a parameter, whose
change refreshes nothing calculated before, or an input it replaces with
``set_input``, whose dependants policyengine-core refreshes only on a
branch. The late dynamic values change parameters that values a policy
modifier calculated may read. Without the drop, the next step and the
output would read those values as they were
(PolicyEngine/policyengine.py#717).

``drop_computed_arrays`` keeps every input, including those a modifier
stored with ``set_input``, and deletes every other value, which is
calculated again when next read. It runs when the step fails too; the
failure then ends the run.
"""
try:
step(microsim)
finally:
microsim.drop_computed_arrays()


def _policyengine_uk_input(dataset: PolicyEngineUKDataset):
"""Build the policyengine-uk dataset a run of ``dataset`` simulates.

Expand Down
20 changes: 19 additions & 1 deletion tests/test_uk_dated_reforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ def test_merged_override_periods_come_last_and_win_where_they_overlap():
]


# Each step on the built simulation ends by dropping the values the
# simulation calculated (#717).
DROP = "drop calculated values"


class _StopRun(Exception):
pass

Expand All @@ -153,6 +158,9 @@ class RecordingMicrosimulation:
def __init__(self, *, dataset, reform=None):
events.append(("construct", reform))

def drop_computed_arrays(self):
events.append(DROP)

def calculate(self, *args, **kwargs):
raise _StopRun

Expand Down Expand Up @@ -223,6 +231,7 @@ def table(columns, weights):
[
("construct", {INCOME_TAX_RATE: {"2027-04-06.2100-12-31": 0.21}}),
"dynamic modifier",
DROP,
],
),
(
Expand All @@ -232,13 +241,15 @@ def table(columns, weights):
[
("construct", None),
"policy modifier",
DROP,
("late parameter values", [(INCOME_TAX_RATE, 0.30)]),
DROP,
],
),
(
"custom",
"custom",
[("construct", None), "policy modifier", "dynamic modifier"],
[("construct", None), "policy modifier", DROP, "dynamic modifier", DROP],
),
# A modifier never replaces its object's parameter values (#572: a
# Policy or Dynamic sum carrying both lost its values).
Expand All @@ -248,6 +259,7 @@ def table(columns, weights):
[
("construct", {INCOME_TAX_RATE: {"2027-04-06.2100-12-31": 0.21}}),
"policy modifier",
DROP,
],
),
(
Expand All @@ -256,7 +268,9 @@ def table(columns, weights):
[
("construct", {INCOME_TAX_RATE: {"2027-04-06.2100-12-31": 0.21}}),
"policy modifier",
DROP,
("late parameter values", [(INCOME_TAX_RATE, 0.30)]),
DROP,
],
),
(
Expand All @@ -265,6 +279,7 @@ def table(columns, weights):
[
("construct", {INCOME_TAX_RATE: {"2027-04-06.2100-12-31": 0.30}}),
"dynamic modifier",
DROP,
],
),
(
Expand All @@ -273,8 +288,11 @@ def table(columns, weights):
[
("construct", {INCOME_TAX_RATE: {"2027-04-06.2100-12-31": 0.21}}),
"policy modifier",
DROP,
("late parameter values", [(INCOME_TAX_RATE, 0.30)]),
DROP,
"dynamic modifier",
DROP,
],
),
],
Expand Down
Loading
Loading