Repository navigation
Conversation
…eline system The situation builder fills the people a situation gives no value with the default of the system it builds under and stores the whole array as an input. A reform simulation's baseline branch inherited those arrays, so it read the reform's default for those people, and it had no input for a variable the reform neutralizes. The baseline branch now gets new populations from the baseline system and the situation is built into them there. A new SimulationBuilder mode, skip_rejected_inputs, leaves out the inputs that system cannot take (variables it lacks or defines for another entity, values it rejects, axes over variables it lacks, inputs its set_input refuses for the period), as _bind_to_tax_benefit_system drops holders built for the reform. Fixes #612. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 12, 2026
A situation without entities is set through Simulation.set_input, which stores under the simulation's branch name, so the rebuilt baseline kept those inputs under "baseline" where a simulation built from the situation (and master's baseline) keeps them under "default". The property test now also draws situations without entities, and the skip mode's comment and the changelog say exactly what is left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft: CI and the independent GPT-6.1 Sol review are pending. The review is queued in Subfleet; the Codex lanes are on hold until 22:09Z.
Fixes #612.
Problem
Simulation(situation=..., reform=...)builds the situation under the reform's system and makes its baseline a branch of that simulation under the baseline system. A stored input is a whole array:SimulationBuildergives the people a situation gives no value for a variable it gives others (or sets along axes) that variable's default from the system it builds under. The branch inherited those arrays, so the baseline read the reform's default for those people. It also had no input for a variable the reform neutralizes, because the reform's system ignores that input. Reproduced onmasterb909d5a:a, none forb; salary default 0 → 1,000[3000, 1000][3000, 0]a's salary (0, 5,000), none forb; same reform[0, 1000, 5000, 1000][0, 0, 5000, 0]income_tax7 given; reform neutralizesincome_tax450(calculated)7Change
Simulation.__init__now builds a reform simulation's baseline branch from the situation under the baseline system, through a newSimulation._build_from_situation. Before, it re-pointed the inherited holders with_bind_to_tax_benefit_system. The branch gets new populations from its own system, and the situation is built into them by the same builder code a simulation under that system alone uses. The inputs are stored under"default", as in a simulation built from the situation, and the branch reads them through that. The records that described the inherited populations go with them:_user_input_keys,_store_history,_fast_cache,_requested_variablesandinvalidated_caches. The branch is still bound as before when there is no situation (a dataset, orpopulationsgiven) or no baseline system.SimulationBuilder.skip_rejected_inputs(off by default; only the baseline build turns it on). In this mode the builder leaves out, rather than raises for, the inputs its system cannot take, as_bind_to_tax_benefit_systemdrops holders built for the reform:set_inputrefuses with aValueError. That covers a period the variable's definition does not take (a variable the reform makes eternal), and also a NaN or yearly and monthly inputs that do not add up.Entities, roles and the syntax of periods are still checked. The reform's strict build runs first on the same situation, so skip mode only acts where the two systems differ: a variable's definition (its entity, values, period or
set_inputhandler), or a variable the reform neutralizes, whose inputs the reform's system ignores without checking. An input of the second kind that the baseline system rejects (a NaN, say) is left out of the baseline;master's baseline had no input for that variable either.Why rebuild rather than mask: a stored input is all-or-nothing in
Holder.set_input,_user_input_keys,StoreHistory,to_input_dataframeandsubsample.set_input_divide_by_periodandset_input_dispatch_by_periodcombine a larger period's array with the sub-period inputs already stored, so the default one person gets for one month also changes what the other months of a yearly input are divided into (test_a_default_given_for_one_month_changes_how_a_year_is_divided). A mask of given entries would have to replay that handler arithmetic for each system. Building the situation again under the baseline system gets it right by construction.The reform simulation itself is unchanged. policyengine-us reaches this code through core's
Simulation.__init__. policyengine-uk builds its baseline as a separateSimulationfrom the situation and is not affected.Not changed here:
apply_reformon a simulation built from a situation keeps the old system's defaults in its inputs. That is the same bug in the other direction, and so doesSimulation(tax_benefit_system=..., reform=...), which builds the situation before applying the reform. Both are tracked in #614.Overlap with #611, which also edits this part of
__init__and the builder: whichever merges second moves its baseline rebuild onto_build_from_situation(see the note on #611).Invariants (property-tested)
tests/core/test_reform_baseline_situation_property.py, 200 Hypothesis examples per run.The reforms are any mix of:
The situations have one to three people in one or two households. They give any of those variables for any of several periods to some people and not others, with no axes, one dimension of parallel axes, or two perpendicular dimensions. Some situations have no entities and give each variable's values for everyone.
For every such reform and situation:
SimulationBuildermakes of the situation under the baseline system alone calculates, for 13 outputs at their periods. The reform-only variable is left out of the situation first._user_input_keys,input_variables) and exports the sameto_input_dict()."baseline"branch, under the baseline system.Tests and evidence
tests/core/test_reform_baseline_situation.pyhas 27 examples. Againstmaster, 22 of the 28 new tests fail, the property among them. The 6 that pass guard behaviour the rebuild must keep:_bind_to_tax_benefit_systemalready handled: a variable only the reform has, an entity move, a period refusal and an axis over a reform-only variable;"default"input keys without entities.On generated examples alone (no
@examples), the property fails on each of 5 seeds against the unfixed__init__, and again against the"baseline"-key slip described below.Mutation check, each part of the change removed in turn. Each of these fails at least one test:
_user_input_keys,_fast_cacheandhas_axes.The resets of
_store_history,_requested_variablesandinvalidated_cachessurvive. On the only call path the branch is a fresh clone, where these are empty or only widen drops. They stay becausepurge_cache_of_invalid_valuesdeletes the arrays, inputs included, of staleinvalidated_cachesentries, and_requested_variablesvouches for the old populations' prerequisites.Local checks:
ruff format --checkandruff checkpass;pytest testson 41848f9: 2,531 passed, 4 skipped, 3 xfailed;_build_from_situation's branch name, the comments and the tests: these tests andtest_reform_baseline_system.py, 45 passed.Downstream, policyengine-us 1.822.5. The household had three people, earnings for one adult and an axis over the other's (five points), plus a CTC base-amount reform. It ran through
master's core and this branch's.Simulation(situation=...). A parametric reform changes no input defaults.Simulation(situation, reform)took 8.09 and 10.12 s (median of 5) onmasterand 8.11 and 9.14 s here, interleaved on a loaded host: no difference measurable above the noise.Review. An in-session Opus 5.5 adversarial review approved with three nits, each reproduced, all addressed in ac6236f:
"baseline", not"default". Fixed, with an example; the property now draws situations without entities.axiom: n/a: engine change in policyengine-core, no policy encoded.
🤖 Generated with Claude Code