Repository navigation
Conversation
… into it SimulationBuilder.build_from_dict (and build_from_entities, build_from_variables and build_default_simulation) can write a situation into an existing simulation. It wrote the situation into that simulation's populations as they were. A simulation built over a dataset and then given a situation: - kept is_over_dataset and dataset, so the macro cache and country models that read is_over_dataset treated the situation as data; - answered from its fast cache with values calculated for the dataset; - kept the dataset's arrays for every variable the situation did not set; - kept the dataset's input record, so to_input_dataframe failed; - kept its branches, the baseline among them, on the dataset. Once the situation parses, the builder now gives the simulation new populations (Simulation._replace_population), with no dataset, cache, input record or store history from the old ones. After writing it records the input variables and axes and rebuilds the baseline branch on the situation (Simulation._rebuild_branches, moved out of subsample, which rebuilt it the same way). subsample now says it needs a dataset instead of failing on the missing one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the first commit (GPT-6.1 Sol, static): - A branch shares its parent's population and baseline, so writing a situation into one replaced its population but not its baseline. _replace_population now refuses a branch. - A formula could replace the population of the simulation calculating it. _replace_population now refuses while a calculation runs, and advances _input_epoch so a calculation elsewhere that read the simulation does not keep what it calculated from the old population. - requires_computation_after accepted a prerequisite requested for the old population, and a FullTracer kept the old population's values. Both are reset. - build_from_variables resized the populations before checking the situation. It now checks variable names and periods first, so a situation that fails to parse leaves the simulation as it was, as build_from_entities did. A situation that fails while it is written leaves the simulation replaced with what was written; the builder now records it and rebuilds branches in that case too. - The property test compared the baseline with one built under the baseline system, which differs whenever a reform changes the default of a variable the situation gives for some people only (a known limitation of the situation builder). It now compares with Simulation(situation=..., reform=...)'s baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Overlap note from #613 (fixes #612, the reform default leaking into a situation's baseline, which this PR's body lists as a known limitation). #613 makes Whichever of the two merges second should:
|
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.
WIP: Sol re-review, full suite and CI pending.
Problem
SimulationBuilder.build_from_dict(system, situation, simulation=existing)writes a situation into an existing simulation's populations as they are. So dobuild_from_entities,build_from_variablesandbuild_default_simulation. Reproduced onmaster8fdf59e with the country template (10 people in 5 households, then one person with a salary of 3,000), a simulation built over a dataset and then given a situation:is_over_dataset=Trueand itsdataset. Socheck_macro_cachestill allows macro-cache reads, and country models that read the flag treat the situation as data. policyengine-us reads it incountyandmedicaid_slcsp_state_denominator. policyengine-uk reads it once Impute UC deductions in constituency and local-authority runs policyengine-uk#1905 lands, whose review found the UC deduction draw at 0.88331 instead of 1.0 and private school attendance imputed for the situation's child (reproduced below);_fast_cachewith values calculated for the dataset:income_taxreturned the ten people's[150, …]instead of[450];age,household_weight);to_input_dataframe()raisedLength of values (10) does not match length of index (1);requires_computation_after), and aFullTracer's trees with the old values.Change
Simulation._replace_population()gives the simulation new populations from its system and clears what described the old ones:dataset,is_over_dataset,situation_input,has_axes,input_variables,_store_history,_user_input_keys,_requested_variables,invalidated_caches,_fast_cacheand aFullTracer's trees. It also advances_input_epoch, so a calculation elsewhere that read the simulation does not keep what it calculated from the old population. It keeps the tax-benefit system and settings. It refuses a branch (which shares its parent's population and baseline) and a simulation with a calculation running. It does not call_drop_computed(), because that reaches_note_unobserved_activity(), which flags restricted frames in every simulation in the process, andSimulation(situation=...)now passes through this path.build_from_entitiesjust before writing, and inbuild_from_variablesafter checking variable names and periods. A situation that fails to parse therefore leaves the simulation as it was. It then calls_finish_replacing, also when writing fails partway, so the records and branches describe what was written. That records axes and input variables, then callsSimulation._rebuild_branches(): other branches are dropped, and the baseline is rebuilt on the situation under the baseline system. That code moved verbatim out ofsubsample, which rebuilt branches the same way.build_from_dictskips its own input-variable recount when replacing, soSimulation(situation=...)makes no extra pass over the variables.subsampleraisesValueError("Only a simulation with a dataset can be subsampled…")whendatasetisNone. Before, a simulation built from a situation raisedAttributeErrorreading the default seed, and one written over data failed on the leftover inputs' length.Only the path where a simulation is passed in changes.
Simulation(situation=...)and policyengine-uk'sbuild_from_situationalready pass fresh populations, so for them the replacement just re-instantiates entities.Invariants (property-tested)
The property test draws any dataset (1–4 households of 1–2 people, optional ages), any reform (none, a rate change, a neutralized variable, a changed default, an added variable), and any prior calculations, branches or earlier situations. It then writes any situation (with or without entities, with or without axes) using
build_from_dict. Afterwards:is_over_datasetis false anddatasetisNone._user_input_keys,input_variablesandhas_axesasSimulation(situation=..., reform=...), and exports the sameto_input_dataframe(). Its baseline calculates and records what that simulation's baseline does."baseline"(none without a reform), and it is the simulation'sbaseline.The baseline is compared with
Simulation(situation=..., reform=...)'s baseline, not with a build under the baseline system. When a reform changes the default of a variable the situation gives for some people only, the situation builder stores the reform's default for the others as an input. That happens in both constructions, is a known limitation (test_reform_baseline_system.py), and is out of scope here.Tests and evidence
tests/core/test_builder_replaces_population.py: 20 examples covering each symptom above, every builder entry point, parse and write failures, branches, a formula that overwrites its own simulation,requires_computation_after, the trace, the reform baseline, the macro cache, andsubsamplewithout a dataset. Run againstmaster, every one fails except the parse-error example, which guards behaviour that should not change.tests/core/test_builder_replaces_population_property.py: the invariants above, with 200 Hypothesis examples.Mutation check: each reset or guard is removed in turn. So far every removal fails a new test, apart from the defensive resets of
situation_input,input_variablesand_store_history(nothing reads the first after a successful write,_finish_replacingrecomputes the second, and a stale store history only widens drops). Full results to follow.End to end on policyengine-uk#1905's branch (a
git archiveoforigin/built-from-dataset-imputation-gates), using its synthetictables(n=40, weight_scale=SMALL_NATIONAL_SCALE)andSITUATION. A coreSimulationover that data is overwritten with the situation (with the review's fresh-holder control onmaster):built_from_datauc_deduction_random_drawattends_private_schoolmasterSimulation(situation=...)construction on the UK system, medians: core 3.3–4.2 ms (this PR) vs 3.5 ms (master); UKSimulationabout 1.4–1.5 s either way. No measurable change.Existing builder, simulation, entity, axes, subsample, reform-baseline, clone and macro-cache tests: 350 passed on the first commit. Full suite on this head: pending.
Review: GPT-6.1 Sol review 1 (static; its sandbox could not run Python) requested changes on six points. All are addressed in f6673ae. Re-review pending.
A follow-up in policyengine-uk will raise its
policyengine-corefloor to the release with this fix and drop the caveat inbuilt_from_data's docstring.axiom: n/a: engine change in policyengine-core, no policy encoded.
🤖 Generated with Claude Code