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
3 changes: 3 additions & 0 deletions changelog.d/fix-lsr-branch-baseline-measurements.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Measure a branch's or clone's labor-supply and capital-gains responses against baseline policy applied to its own inputs, and give each simulation its own measurement cache. A branch of a reform simulation made before anything was calculated (and, on policyengine-core before #587, such a clone) measured against a new branch under the reform policy and reported no response, and a clone given other inputs reused its source's measurements.
Preserve zero responses without a reform, make capital-gains marginal tax rates independent of calculation order while reusing household measurements, and discard incompatible holders when applying baseline policy to measurement branches.
Forget deleted pre-response input records on Core 3.32.8 so fully calculated clones remeasure changed earnings, while retaining explicit overrides and the baseline household's inputs.
27 changes: 27 additions & 0 deletions policyengine_us/spm.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@

COUNTY_FIPS_PATTERN = re.compile(r"[0-9]{5}")

# Where a simulation keeps its behavioural-response measurements, by period
# (``variables/gov/simulation/behavioral_response_measurements.py``). They are
# cached formula output kept outside the holders, so the simulation's clone,
# branch and invalidation paths below handle them explicitly.
BEHAVIORAL_RESPONSE_CACHE_ATTR = "_behavioral_response_measurements"

COUNTY_INPUT_FIX = (
'send county_fips as a five-digit string (for example "06037"), or select '
'geography_kind="national" in the spm configuration'
Expand Down Expand Up @@ -711,7 +717,16 @@ def _isolate_parameter_tracing(self):
branch._isolate_parameter_tracing()

def get_branch(self, name="branch", clone_system=False):
creating = name != self.branch_name and name not in self.branches
branch = super().get_branch(name, clone_system)
if creating:
# A branch starts from what this simulation has calculated, its
# behavioural-response measurements included, but keeps its own
# copy (``clone`` emptied it): what either measures afterwards, the
# other does not see.
measurements = self.__dict__.get(BEHAVIORAL_RESPONSE_CACHE_ATTR)
if measurements is not None:
branch.__dict__[BEHAVIORAL_RESPONSE_CACHE_ATTR] = dict(measurements)
# Core names the branch and hands it this simulation's tracer after
# cloning, so re-prime the branch's root with what it ended up holding.
branch._isolate_parameter_tracing()
Expand Down Expand Up @@ -908,8 +923,20 @@ def clone(self, debug=False, trace=False, clone_tax_benefit_system=True):
cloned._rebind_holders()
self._rebind_method_aliases(cloned)
cloned._isolate_parameter_tracing()
# Core copies the instance dictionary by reference, which shared this
# simulation's measurements dictionary with the clone: a clone given
# other inputs reused them, and what it measured reached this
# simulation. A clone measures its own (``get_branch`` seeds a branch).
cloned.__dict__.pop(BEHAVIORAL_RESPONSE_CACHE_ATTR, None)
return cloned

def _invalidate_all_caches(self):
# Core purges cached formula output here (``apply_reform``,
# ``subsample``); the measurements are formula output kept outside the
# holders.
self.__dict__.pop(BEHAVIORAL_RESPONSE_CACHE_ATTR, None)
super()._invalidate_all_caches()

def _rebind_method_aliases(self, cloned):
"""Point copied bound-method aliases at the clone, not the original.

Expand Down
21 changes: 11 additions & 10 deletions policyengine_us/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
uprate_parameters,
)
from .tools.default_uprating import add_default_uprating
from .tools.period_branch import delete_input_values
from .tools.section_911 import validate_section_911_batch_inputs
from .tools.per_capita_uprating import (
add_per_capita_parameters_for_parameter_uprating,
Expand Down Expand Up @@ -264,27 +265,27 @@ def __init__(self, *args, **kwargs):
for known_period in employment_income.get_known_periods():
array = employment_income.get_array(known_period)
self.set_input("employment_income_before_lsr", known_period, array)
employment_income.delete_arrays(known_period)
delete_input_values(employment_income, known_period)

self_employment_income = self.get_holder("self_employment_income")
for known_period in self_employment_income.get_known_periods():
array = self_employment_income.get_array(known_period)
self.set_input("self_employment_income_before_lsr", known_period, array)
self_employment_income.delete_arrays(known_period)
delete_input_values(self_employment_income, known_period)

sstb_self_employment_income = self.get_holder("sstb_self_employment_income")
for known_period in sstb_self_employment_income.get_known_periods():
array = sstb_self_employment_income.get_array(known_period)
self.set_input(
"sstb_self_employment_income_before_lsr", known_period, array
)
sstb_self_employment_income.delete_arrays(known_period)
delete_input_values(sstb_self_employment_income, known_period)

weekly_hours = self.get_holder("weekly_hours_worked")
for known_period in weekly_hours.get_known_periods():
array = weekly_hours.get_array(known_period)
self.set_input("weekly_hours_worked_before_lsr", known_period, array)
weekly_hours.delete_arrays(known_period)
delete_input_values(weekly_hours, known_period)

# Capital gains responses

Expand All @@ -294,7 +295,7 @@ def __init__(self, *args, **kwargs):
self.set_input(
"long_term_capital_gains_before_response", known_period, array
)
cg_holder.delete_arrays(known_period)
delete_input_values(cg_holder, known_period)

# Geography backfill: state_code_str-only input -> state_code enum.
_backfill_state_code_from_str(self)
Expand Down Expand Up @@ -524,27 +525,27 @@ def __init__(self, *args, **kwargs):
for known_period in employment_income.get_known_periods():
array = employment_income.get_array(known_period)
self.set_input("employment_income_before_lsr", known_period, array)
employment_income.delete_arrays(known_period)
delete_input_values(employment_income, known_period)

self_employment_income = self.get_holder("self_employment_income")
for known_period in self_employment_income.get_known_periods():
array = self_employment_income.get_array(known_period)
self.set_input("self_employment_income_before_lsr", known_period, array)
self_employment_income.delete_arrays(known_period)
delete_input_values(self_employment_income, known_period)

sstb_self_employment_income = self.get_holder("sstb_self_employment_income")
for known_period in sstb_self_employment_income.get_known_periods():
array = sstb_self_employment_income.get_array(known_period)
self.set_input(
"sstb_self_employment_income_before_lsr", known_period, array
)
sstb_self_employment_income.delete_arrays(known_period)
delete_input_values(sstb_self_employment_income, known_period)

weekly_hours = self.get_holder("weekly_hours_worked")
for known_period in weekly_hours.get_known_periods():
array = weekly_hours.get_array(known_period)
self.set_input("weekly_hours_worked_before_lsr", known_period, array)
weekly_hours.delete_arrays(known_period)
delete_input_values(weekly_hours, known_period)

# Capital gains responses

Expand All @@ -554,7 +555,7 @@ def __init__(self, *args, **kwargs):
self.set_input(
"long_term_capital_gains_before_response", known_period, array
)
cg_holder.delete_arrays(known_period)
delete_input_values(cg_holder, known_period)

# Geography backfill: state_code_str-only input -> state_code enum.
# Datasets carry state_fips, so this only fires for situations that
Expand Down
Loading
Loading