Repository navigation
Uprate only from inputs, so uprated values don't depend on calculation order - #563
Merged
Merged
Conversation
MaxGhenis
force-pushed
the
fix-uprating-order
branch
2 times, most recently
from
October 2, 2026 07:07
1092dc6 to
1e88ee1
Compare
MaxGhenis
force-pushed
the
fix-uprating-order
branch
from
October 2, 2026 07:11
1e88ee1 to
386cbf8
Compare
MaxGhenis
force-pushed
the
fix-uprating-order
branch
2 times, most recently
from
October 2, 2026 08:42
a278233 to
2120f78
Compare
This was referenced Oct 2, 2026
MaxGhenis
force-pushed
the
fix-uprating-order
branch
from
October 3, 2026 03:01
2120f78 to
57c0d36
Compare
Simulation._calculate uprated a variable from its latest earlier stored period in its own unit, including periods the simulation had calculated itself, so the result depended on calculation order. Executed on fix-carry-over-order (index +3.7%/yr, input at 2012, 2015 asked alone vs after 2013 and 2014): - int: [1116, 85] alone, [1115, 83] after (truncation compounds) - float: float32 rounding compounds - defined_for false for one person in 2013 only: [1115.16, 1115.16] alone, [0.0, 1115.16] after (the 2013 mask carries into 2015) Also: a cached default, a monthly input carried into a calculated year, and a reform formula's result before its end each became an uprating source. For variables with uprating, keep only the earlier same-unit periods in Holder.get_input_periods(branch_name) (the carry-over PR's branch-aware provenance): periods whose value this branch reads is an input. That also drops periods stored only under a branch this one cannot read, which read back as None and raised TypeError. With no earlier input, the carry-over path or the default applies, as when the period is asked alone. Tests: tests/core/test_uprating_order.py (regressions, no hypothesis), tests/core/test_uprating_order_property.py (derandomized Hypothesis: any requests, branches and branch inputs, carry-over on/off == alone, byte for byte, and == reference rule), shared fixtures in tests/fixtures/uprating_order.py. #551's path-independence test is now exact instead of rel=1e-5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
force-pushed
the
fix-uprating-order
branch
from
October 4, 2026 14:04
57c0d36 to
1478541
Compare
set_input_dispatch_by_period and set_input_divide_by_period skipped any month that held a stored value, including one the simulation had calculated and cached as derived. Given a yearly input after a month of that year was calculated: - dispatch kept the calculated month and copied it into the later months as an input; - divide took it out of the year's total and divided the rest among the other months. The month read back as calculated, and since uprating now starts only from inputs, the next month was uprated from an earlier month (review finding P1 on #563, review/review_final_rebase.md). The helpers now treat only a stored input as present (get_stored_input): a derived month is replaced, as an input set for that month alone would replace it. Simulation.set_input also drops the fast-cache entries for the months a helper writes, which otherwise returned the calculated value. Executed probe (p1_helper_probe.py: stock and flow, carry-over on/off, root/branch/nested, three calculate-first patterns): 108 cases, mismatches vs a fresh simulation given the same input were 84 on master, 96 on #563 before this commit, 0 after. Tests: regressions for both helpers in test_uprating_order.py, and a derandomized Hypothesis property (300 examples) in test_uprating_order_property.py. A fresh simulation given the same inputs in the same order returns the same bytes for every month of the input's year and for another month, and every month of the year is an input. The fixtures add uprated_monthly_stock, with a month-by-month index. Ablations: the regressions fail 56/56 without either change, and 52/56 with the helper change alone. The property's generated examples alone fail without the change and with either half alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OnDiskStorage.clone shares its directory and every file stored so far, and put wrote each value to its key's path. So a write through one storage changed what the other read. The clone docstring noted it. - Simulation.clone() keeps the branch name. A set_input on the clone wrote over the source's file, and with the previous commit so did a yearly helper input replacing a calculated month (review of 168a710, review/review_final_rebase2.md): the source's December became the clone's input. - The source writing a period again after cloning (delete, then calculate) changed the clone's value (round-1 review, finding P3). A storage now writes over a file only if it wrote it and has not shared it since (_own_files). Otherwise, if any storage in its clone family wrote or stored that file (_family_files, one set per family), the value goes to a new file of its own under replaced/, which restore() never reads. Storages not cloned from one another, such as restore() and a separate writer, still write over each other's files as before. Pickling counts as cloning. A state pickled before this change treats every file it stores as shared. Tests: tests/core/test_on_disk_storage_clones.py covers the review's sequence for both helpers and both carry-over modes, own-unit set_input on a clone, the source rewriting a period, and storage-level cases: - clones, a clone of a clone and in-place writes of own files; - independent storages over one directory, and restore() reading only usual names; - a restored storage's clone, deepcopy/pickle, and old pickles. On 168a710, 14 of the 15 fail; the one that passes is the independent-storage guard. The helper property in test_uprating_order_property.py now also runs on disk, and with a clone taken before the input whose stored months must not change. With explicit rows removed and no shrinking, it fails on 168a710. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss restore Round-3 review of #563 (review/review_final_rebase3.md), two blocking findings: 1. Simulation.clone() shared _user_input_keys with its source. A yearly helper input on the clone now replaces a month the source had calculated, and it recorded that month's key in the shared set. The source's apply_reform then kept its own calculated month as an input and uprated from it. The shared record predates this PR, but the helper change made it reachable this way. Each clone now gets a copy of the record. Nothing read the record across simulations: apply_reform and the input export look up keys in the simulation's own holders. 2. restore() cleared the storage's own files, so a storage that was never cloned moved its next write to replaced/, where a separate reader of the directory no longer saw it. A storage now remembers, for each key, the file it last wrote and has not shared since (_own_paths, replacing _own_files). restore() keeps it, and so do delete and apply_reform's wipe. Writing a deleted key again therefore reuses that file instead of making a new one each time (the review's P3: 100 delete/write cycles after cloning had left 101 files). Tests: - test_uprating_order.py: test_an_input_set_on_a_clone_is_not_replayed_by_its_source (both helpers, carry-over on/off, memory and disk; 8/8 fail on 1ee63a8). - test_on_disk_storage_clones.py: restore-then-write and delete-then-write reuse (both fail on 1ee63a8). The restore test now uses a month period, because Windows file names cannot hold the ':' of month:2012-03:3; that is what failed Windows CI on 1ee63a8. - The helper property: the clone also applies a no-op reform, and its input periods must not change. Its generated examples alone fail on 1ee63a8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fourth commit (c5c7bf4) gave each Simulation.clone() a copy of _user_input_keys instead of sharing the source's set, which changes what apply_reform keeps as an input after an input on the other simulation. The fragment only described the on-disk file change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
October 5, 2026 03:40
MaxGhenis
added a commit
that referenced
this pull request
Oct 6, 2026
Brings in #562 (carry over only inputs), #563 and #582 (uprate only from inputs), #578 (shared-key sets), #574 (per-call parameter tracing) and #584. One input record instead of two: the storages' `_inputs` (memory) and `_derived` (disk) from #562 replace this branch's `_input_keys`, so an input is a value stored without `derived=True`, as on master. Sequence numbers stay alongside. Disk storage takes master's `_path_to_write` (a clone never writes over a file it shares) in place of this branch's per-store file names, so the process tokens and restore ordering go. Dumps write master's `derived_periods.txt` (branch-aware) and restore inputs first, then calculated values under one later number. `apply_reform` marks every value set_input did not store as derived and drops it, keeping master's semantics without wiping and replaying. Tests that expected a calculated result to carry over now check that the result is cached, since only inputs carry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
to MaxGhenis/policyengine-core
that referenced
this pull request
Oct 6, 2026
Conflicts resolved by keeping master's bookkeeping: - holder.py: master's derived put (PolicyEngine#562) and branch helpers; this PR's fast-cache eviction after every storage write and delete. - simulation.py: master's input-only uprating (PolicyEngine#563/PolicyEngine#582); this PR's calculate-time value-type check runs just before an input is uprated. - simulation_dumper.py: both records for now (inputs.txt and master's derived_periods.txt); a value marked derived restores as derived. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
to MaxGhenis/policyengine-core
that referenced
this pull request
Oct 6, 2026
Brings in PolicyEngine#562, PolicyEngine#563, PolicyEngine#582 (input/derived storage marks, input-only carry-over and uprating), PolicyEngine#574, PolicyEngine#584 (parameter tracing and copy guards), PolicyEngine#578 (shared keys only while sharing) and PolicyEngine#585 (temporary storage directories). Conflict in holders/holder.py, resolved by keeping master's bookkeeping: - _set keeps master's ``derived`` mark (stored with the value, and a derived value is never redirected to the running set_input's branch) and this branch's ``is_input``, which decides what the record names. - put_in_cache keeps master's guard (a derived value never replaces a readable input) and passes ``is_input=False``. - master's ``Holder._stores`` (storage ``has``) replaces this branch's equivalent ``_stores``. simulation.py merged cleanly but copied the record in ``clone`` twice (PolicyEngine#562 added the same copy); kept master's copy and added the ``_user_input_contexts`` reset next to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Oct 7, 2026
Reconcile #552 with master after the merge: - Holder.get_known_periods(branch_name) uses master's _readable_branches (added by #562) instead of a second copy of the lookup, and lists each period once. get_array is master's again. - _dump_holder reads the dumped simulation's branch from the holder, as #560 does, and keeps master's derived_periods.txt with each value's mark read on that branch. - The _calculate comment says what scoping still changes now that uprating and carry-over read only inputs (#562/#563): a later period stored under an unreadable branch no longer leaves a default uncached. Tests: the isolation grid now also compares what the branch then reads (periods and derived marks); a period stored under several readable branches is listed once; a restored branch dump calculates what the branch calculates; and a Hypothesis differential checks get_known_periods against get_array and get_input_periods against is_derived for random values on a five-branch lineage, in memory and on disk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Oct 8, 2026
…ct non-numeric uprating and defined_for (#576) * Restore inputs as inputs, evict the fast cache on holder writes, reject non-numeric uprating and defined_for Three bugs found by the review of #563, all present on master b78b0ba: - restore_simulation put every value back with put_in_cache, which records no input, so apply_reform on a restored simulation dropped its inputs. dump_simulation now lists each variable's input periods in inputs.txt and restore_simulation records exactly those in _user_input_keys. - Holder.set_input, put_in_cache and delete_arrays left the simulation's fast cache untouched, so calculate kept returning the replaced value. Every holder write and delete now drops the entries it changes, in the holder's own simulation and only for branches that simulation reads. - An Enum, str or date variable with uprating, and a variable defined_for one, raised TypeError in the middle of a calculation. Both are now rejected when the variables are registered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Make the legacy-dump test strip every sidecar file, not only inputs.txt A dump written before inputs were recorded holds the arrays and nothing else, so the test stays right when another change adds its own sidecar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Define the fast-cache eviction helper at the end of Holder Next to delete_arrays, git merged it with other changes to that method without a conflict but left their lines inside the new helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the branch fast-cache tests valid if a branch input also drops what was calculated from it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address review: branch inputs in dumps, replace rollback, calculate-time checks, legacy-dump warning - Test that an input set on a branch (which shares the input record) does not mark the default branch's calculated value as an input, with a branch step in the restore property. - replace_variable keeps the existing variable when the replacement is rejected. - calculate repeats the uprating check before it uprates, for an uprating assigned on a class that declares uprating itself; the defined_for message is tested for str and date as well as Enum. - The message for an inherited uprating points to replace_variable. - restore_simulation warns when a dump does not record its inputs. - Changelog: say that such systems no longer load, including a group variable defined_for a person Enum, which master masked on summed indices. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the branch-input case by its outcome, not by whether the record is shared policyengine-core#561 makes a branch copy the input record instead of sharing it; the test now holds either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address review round 2: check defined_for by type before mapping; pin three cache paths - calculate checks the defined_for variable's type before calculating and mapping it. Mapped to a group entity, an Enum's indices were summed into numbers and str or date values failed inside the mapping, so the values-based check missed a cross-entity defined_for set after registration. - Regression tests for three cache paths no test pinned (each one a surviving mutant in GPT-6.1 Sol's review): a write under an ancestor branch name in a nested branch, a write to disk storage, and an ETERNITY value requested without a period. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Use derived-period dump provenance and rebuild input replay records (#576) Write a format marker so modern all-input dumps can be distinguished from ambiguous legacy dumps. Register non-derived restored values through the input context without dispatching helpers again; preserve calculated marks. Cover cached storage inputs, stale input records, Enum replay and differential round trips, and remove the obsolete calculated-month helper exception now fixed on master. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fold #566 regression tests and cover current-master holder storage paths (#576) Copy the three #566 fixture/test files unchanged from c825cab without its production change or fragment. Add disk clone/branch and shared-key cache regressions plus an on-disk cache/reference differential property, preserving master provenance and eviction. Keep #566 open and its branch unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Describe master input-only uprating in the variable API (#576) Correct the inherited attribute documentation to use the latest visible earlier input rather than a calculated or defaulted cache entry. No runtime change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
#562 (input-only carry-over) has merged, as
58e23f56. This PR is five commits on masterbd459bdf: the uprating fix, three fixes that its review on master asked for, and a changelog-only fifth commit (45f2bfc3, the head). The code is the fourth commit's,c5c7bf4c.Problem
Simulation._calculateuprates a variable that hasupratingand no formula result for a period. It takes the latest earlier period stored in the variable's own unit and scales that value by the index ratio. That period could be one the simulation had calculated itself: an earlier uprated, masked or truncated value. So an uprated value depended on which periods had been calculated first.Executed on #562 (index +3.7% a year, input for 2012; 2015 asked alone vs after 2013 and 2014):
[1001, 77][1116, 85][1115, 83]: truncation compounds[1001.3, 77.7]1116.60729980468751116.607177734375: float32 rounding compoundsdefined_forFalse for person 0 in 2013 only[1115.16, 1115.16][0.0, 1115.16]: the 2013 mask carries into 2015The same drift happens when one of these, cached at an intermediate period, becomes the value uprated:
end.#551's path-independence test held only to
rel=1e-5, which hid the bitwise drift.Fix
Uprate only from inputs (first commit)
Uprate only from an input this branch reads. #562 marks every value the simulation caches as derived.
Holder.get_input_periods(branch_name), also from #562, lists the periods whose valueget_array(period, branch_name)actually reads is an input. It ranks stored keys inget_array's lookup order (the branch, then its ancestors, thendefault; memory before disk) and leaves out periods stored only under branches this one cannot read. For variables withuprating, the uprating path keeps only the earlier same-unit periods in that set. With no earlier input, control falls through to #562's carry-over or the default, exactly as when the period is asked alone.It also fixes a crash that was already on master: a period stored only under a branch this simulation cannot read, for example through
Holder.set_input(..., "sibling"), used to be picked as the uprating source and read back asNone, which raisedTypeError. A regression test covers it.This commit's production diff is one filtered list in
simulation.py, plus a comment in #562's no-input branch. It rebased onto master without conflicts.git range-diffshows it is patch-identical to the commit reviewed before #562 merged (57c0d36b).A yearly input given through a
set_inputhelper replaces months already calculated (second commit)A monthly variable stores a yearly input month by month through its
set_inputhelper:set_input_dispatch_by_periodcopies it to each month for a stock, andset_input_divide_by_perioddivides it between them for a flow. Both skipped any month that already held a value, including one the simulation had calculated:The month read back as calculated. Since uprating now starts only from inputs, the next January was uprated from an earlier month.
An executed probe compared 108 cases with a fresh simulation given the same input. The cases covered both helpers, carry-over on and off, root, branch and nested branch, and three calculate-first patterns. Mismatches:
Master matched only where the calculated value happened to equal the input.
The helpers now count only a stored input as present (
get_stored_input), so a calculated month is replaced, as an input set for that month alone would replace it.Simulation.set_inputalso drops the fast-cache entriescalculatemade for the months a helper writes. Otherwise a month read back from the fast cache would still give the calculated value. This holds for any input a helper spreads over sub-periods that were calculated, such as a two-year input, not only a yearly one.A simulation given all its inputs before it calculates anything behaves as before: the helpers meet no calculated value. A calculated month restored from a dump (which keeps #562's marks) is replaced the same way.
Clones never write over each other's files on disk (third commit)
OnDiskStorage.cloneshares the directory and every file stored so far, andputwrote each value to its key's path. So a write through one storage changed what the other read:Simulation.clone()(which keeps the branch name) wrote over the source's file. The review of the second commit found this.set_inputon a clone did the same, already on master.A storage now writes over a file only if it wrote it and has not shared it since. If any storage in its clone family wrote that file, or held it when a clone was made, the value goes to a new file of its own under
replaced/, whichrestore()never reads. Storages not cloned from one another, such asrestore()and a separate writer over the same directory, still write over each other's files as before. Pickling counts as cloning, except for files the storage only read back withrestore()(see "Not in scope").Each clone records its own inputs; a storage keeps its own files across
restore()(fourth commit)The review of the third commit found two blocking defects:
Simulation.clone()shared_user_input_keyswith its source. Already on master, an input set on a clone for a period the source had calculated made the source'sapply_reformkeep its calculated value there as an input, and the source's input export listed it. Executed: input 2012 =[1001, 77], the source calculates 2013, a clone sets its own 2013 input, the source applies a no-op reform; the source's 2015 is[1116, 84]where a fresh simulation gives[1116, 85]. The second commit added another route: a yearly helper input on the clone replaces a month the source had calculated and records that month's key in the shared set, so the source'sapply_reformkept its calculated month as an input and uprated from it. Each clone now gets a copy of the record.apply_reformand the input export look up the recorded keys in the simulation's own holders, so nothing needs the record shared.restore()forgot which files a storage had written (third commit). A storage that was never cloned then moved its next write toreplaced/, where a separate reader of the directory did not see it. A storage now remembers, for each key, the file it last wrote and has not shared since (_own_paths).restore(),deleteandapply_reform's wipe keep that record;clone()and pickling clear it. Writing a deleted key again therefore reuses its file. Before, each delete-and-write cycle after a clone left one more file (100 cycles left 101 files).Invariants
The domain: uprated variables of each value type (float, int, and bool masks via
defined_for), inputs in each variable's own unit (or any unit for a variable with noset_inputhelper), and earlier requests that are plain calculations in either unit,ADDorDIVIDE. Over that domain:-0.0differs from0.0). What a simulation returns for a period equals what a fresh simulation with the same inputs returns for that period alone. This holds when reading from the simulation, from a branch, or from a nested branch forked after those requests, including a branch that sets its own input. It holds with auto-carry-over on and off.index(period) / index(input period), cast to the variable's type and masked by its owndefined_for. With no such input, it is Carry over only inputs, the latest at or before the requested period #562's carry-over input or the default.For yearly inputs given through a
set_inputhelper to a monthly stock or flow, stored in memory or on disk, after calculations inside that year:apply_reformstill keeps only the clone's own inputs. At the storage level, a write through one storage never changes what a storageclone()d from it, or one it was cloned from, reads, unless one of them has since calledrestore(). For copies made by pickling ordeepcopy, it holds only for files the storage or its clone family wrote before the copy (see "Not in scope").Tests
tests/core/test_uprating_order.py: 97 regressions with no Hypothesis dependency, so the smoke job runs them too.defined_formasks and all-false defaults, cached non-zero defaults, cross-unit carried inputs, a reform formula withend, branches and nested branches, branch-only inputs, and over-correction guards.apply_reform: both helpers × carry-over on and off × memory and disk.tests/core/test_uprating_order_property.py: two derandomized Hypothesis properties. Each docstring states its domain.@examplerows and covers invariants 1 and 2 byte for byte. Half the requests target the variable checked, and branches may set their own input.apply_reformafterwards.tests/core/test_on_disk_storage_clones.py: 17 tests.set_inputon a clone, and the source rewriting a period.restore()reading only usual file names; a restored storage's clone; a never-cloned storage written afterrestore(); deleting and writing a key again after a clone; deepcopy and pickle; and pickles from before this change.tests/fixtures/uprating_order.py: the variables, the reference rule (which rejects inputs it doesn't model) andassert_bitwise_equal. It now also hasuprated_monthly_stock, a stock with a month-by-month index, so which month a value is uprated from shows. It reuses Carry over only inputs, the latest at or before the requested period #562's simulation helpers.test_result_does_not_depend_on_intermediate_years_computed: now asserts exact equality instead ofrel=1e-5.Each commit's production code, run with this head's
test_uprating_order.py,test_uprating_order_property.py,test_on_disk_storage_clones.pyandvariables/test_variable_uprating.py(349 tests):bd459bdf(with #562)14785411168a710a1ee63a80restore(), reusing a file after delete, and the old-pickle test, whose setup reads the renamed_own_pathsfield)c5c7bf4c(this PR's code)The tests that pass on earlier code check behavior it already had: the over-correction guards (inputs still count), an uprated value cached as derived (#562 marks it), the cross-unit case with carry-over off, storages not cloned from one another still writing over each other's files, and, before the third commit, a never-cloned storage still writing its own file after
restore().Mutation check and ablations.
The uprating filter (
mutants.py): all 6 mutants are killed, and the property alone kills all 6. The mutants are: no filter, a filter that ignores the branch, an inverted filter,maxover the unfiltered list, dropping theupratingguard, and uprating from the earliest input.The second commit's helper regressions:
The helper property, generated examples only (explicit rows removed). It fails:
On the fourth commit it passes all 300 generated examples.
Local runs on
c5c7bf4c(this PR's code):ruff format --check: clean.CI: 18 of 18 checks pass on
c5c7bf4cand on the head,45f2bfc3, Windows included.Independent reviews.
2120f78f) each returned APPROVE WITH NITS and found no defect introduced by the PR. Their documentation and test nits are fixed in the first commit, except an optional one:get_input_periodsruns twice for an uprated variable with no earlier input when carry-over is on. The pre-existing defects they reported are fixed here or listed under "Not in scope".ADDand a day-unit input, are under "Not in scope".restore()forgetting a storage's own files were blocking; replacement files piled up over delete-and-write cycles (all three fixed in the fourth commit). Its pre-existing findings are under "Not in scope".c5c7bf4c: APPROVE. Every blocking round-3 repro (findings 1, 2 and 4) is resolved, and its pre-existing finding-3 cases reproduce the same on master and on this code. It also ran 16 input-record scenarios (branches, nested branches, clones, a subclassclone, both input exports, reforms on sources, branches and clones) and 48 file-ownership cases across clone, deepcopy, pickle, delete andrestore(). It rebuilt its round-3 clone-family state machine and ran it: 400 examples and 280,298 value and provenance checks. The seven related test files passed (237 tests).Measurements
The confirmation pass and the final-head check ran the first commit's filter (
get_input_periods), before the rebase onto master. The first pass (1e88ee16) ran an earlier form,not holder.is_derived(p, branch), which differs only for a period stored only under a branch this one cannot read, where it raisedTypeError. The full-eCPS 2025-2026 row comes from the first pass only.The other three commits change results only in the cases they fix:
set_inputhelper after a period it covers was calculated (or restored from a dump as calculated): a yearly or multi-year input, or a day input to a monthly variable;apply_reform, or an export of inputs, on a simulation after an input was set on its clone, or on the clone after an input was set on the source.The PE-UK and PE-US runs were not repeated for them.
These are real PE-UK and PE-US microsimulations, with no scaling. Cores compared: master
b78b0ba9, #562 and this change.First pass: the full-eCPS and multi-year runs used Carry over only inputs, the latest at or before the requested period #562
27d1bc35, with this change at1e88ee16.Confirmation pass: on Carry over only inputs, the latest at or before the requested period #562
35e20d9fwith this change ata2782333. It reran PE-UK single and multi, the PE-US subsample and full-eCPS 2026. Every comparison matched the first pass.Final heads: Carry over only inputs, the latest at or before the requested period #562
912a4ac5and this change at2120f78f, whose production code the first commit keeps. They changed only Carry over only inputs, the latest at or before the requested period #562's carry-over tie-break for equal-extent inputs in another unit. Checked against the confirmation outputs, they matched byte for byte:This held for both Carry over only inputs, the latest at or before the requested period #562 and this change.
Shadow logger: every run on this change was repeated with a shadow logger, which left outputs bitwise unchanged. It records each uprating decision where master's rule picks a different source period.
PE-UK: policyengine-uk 7b9fc379, Enhanced FRS 2024-25, full sample.
PE-US: policyengine-us 6c5170fd, Enhanced CPS (inputs at 2024), a 3,000-household subsample (seed
core-cow-bench) and the full sample.Single-year simulations (what policyengine.py and the APIs build): no output changes.
spm_unit_net_incomefor 14 SPM units: #562's state_fips/FPG fix, see #562.)Multi-year in one simulation: this change removes uprated-input drift.
The multi-year differences that remain are not uprating:
current_educationformula copies last year's value only if last year happens to be cached, and otherwise imputes from age. That's a PE-UK order dependence. Separate task.ctc_limiting_tax_liabilityandtax_liability_if_itemizingfor 63-120 units). These are computed in PE-US's persistentitemizing/not_itemizingbranches, created in 2025 and reused in 2026. policyengine-us#9738 (get_branch_for_period) re-forks them.ParameterNotFoundError(md...informal.rates.UNKNOWN) on Carry over only inputs, the latest at or before the requested period #562 and this change. It doesn't raise on master, but master's 2026 is wrong there (income tax $33bn, the Carry over only values stored at the variable's definition period #557 age/12 bug). The cause is in PE-US: itscountyformula reads the default branch's latest period from inside a reused branch. The carry-over session is filing the PE-US fix.spm_unit_is_in_spm_povertyraisesSPMInputErroron every core, master included. This is the known eCPS SPM composition defect, us: name the SPM composition defect in seconds, and refuse it by name in the release microcosm#948. It isn't measured here.Not in scope
These are pre-existing on master. The reviews found most of them. None is made worse here, except the day-unit case noted below.
set_input: Make set_input on a branch drop values calculated from the input it replaces #560 covers branches. For the root simulation, apart from the months a helper writes (second commit), values calculated from an input stay cached when it changes, as on master: later periods, dependent variables and a yearlyADDsum. This includes:ADDsum cached before a helper input to that year, which stays cached at the year (the months, andADDitself, are recomputed);Inconsistent input. Now both store it, as a fresh simulation would, butcalculatefor that month still returns the cached value.apply_reform's replay of user inputs:None, which fails to restore.deepcopythat each store a key neither had before the copy write the same file;restore()reads files its source still writes over;restore()but never wrote: after a pickle ordeepcopy, either copy writing it changes what the other reads.set_inputhelper given inputs foryear:2012:2and2012, the uprating source is whichever was set first, as on master: task_5fbde569.restore_simulationdrops the input registry,Holder.set_inputhas a stale fast cache, and Enum uprating raises. These are a new task.axiom: n/a: engine fix to uprating source selection and input storage in policyengine-core; no policy rule changes
🤖 Generated with Claude Code