Skip to content

Drop sums and twelfths an own-period input makes stale - #593

Open
MaxGhenis wants to merge 9 commits into
masterfrom
fix-own-period-input-stale-sums
Open

MaxGhenis wants to merge 9 commits into
masterfrom
fix-own-period-input-stale-sums

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #579. Stacked on #581 (base fix-set-input-helper-order), whose helper it reuses. The diff below is this PR's change only.

What this fixes

set_input for one of a variable's own periods (a month of a monthly variable, a year of a yearly one) replaced the stored value. It left what calculate had derived from the old value at periods of another size: the sum calculate_add stores for a monthly flow over a year, and the twelfth calculate_divide stores for a yearly flow at a month. A later calculate returned the old result, so it depended on whether that period was read before the input was set. The audit's witness: annual flow read as 0, then January set to 1, and the annual read stays 0.

#581 already drops such calculated values when a set_input helper spreads a longer period (_drop_calculated_overlapping). #579 is the remaining path, an input that needs no helper. This PR applies the same rule there:

  • Holder.set_input, after storing an own-period input of a non-ETERNITY variable, calls _drop_calculated_over_own_period. That drops what the simulation calculated for the variable at overlapping periods, on the input's branch and the branches it reads. Inputs are kept.
  • It also evicts calculate's fast-cache entries for the variable at overlapping periods, the input's own period included. The fast cache never holds the sums and twelfths themselves (I checked; calculate_add and calculate_divide store only in the holder). The eviction matters for inputs set on the holder directly, where Simulation.set_input's exact-key pop never runs.
  • It does not touch Simulation.set_input, whose fast-cache block draft Drop every contained period from the fast cache when deleting or setting a period #566 rewrites.

#581's property test_inputs_do_not_depend_on_what_was_calculated_before excluded other-size reads for variables given an own-period input, because of #579. This PR removes that exclusion.

Invariants

Tests

Before, on #581's head 917f0c23:

test_own_period_input_overlapping_cache.py:  7 failed (e.g. annual [0, 0] after setting January to [1, 1])
test_set_input_helper_order_property.py:     1 failed, 2 passed
  shrunk: requests_by_level=[[], [], [('flow_m', '2013')]], depth=2, one own-period input:
  AssertionError: (('flow_m', '2013'), array([0., 0.]), array([0., 1.]))

After:

test_own_period_input_overlapping_cache.py:  7 passed
test_set_input_helper_order_property.py:     3 passed

Ruff format and ruff check pass. I ran these files one at a time, because the shared host is saturated. This repository's PR workflow runs only for PRs into master, so CI runs once this PR is retargeted, after #581 lands or after this PR is merged into #581's branch.

Impact

Outputs can change wherever an own-period input is set after the simulation has calculated a sum or twelfth of that variable. policyengine-us marginal_tax_rate, for example, sets inputs on branches after calculating. Dataset loading sets inputs before any calculation, so there is nothing to drop there.

PE-US A/B (core 917f0c23 vs this head, same policyengine-us checkout and data, 2026; household_net_income, income_tax, state_income_tax, snap, marginal_tax_rate): pending. It is queued behind the shared microsim lock, and I will add results here when it finishes.

Known limits (from the independent review, which approved at 1fa8b034)

  • Inherited, not introduced here: the rule trusts the simulation's record of inputs, _user_input_keys, and Simulation.clone (so get_branch) still shares that set with the parent. An input the parent sets after branching can therefore make the branch keep a stale calculated value. Executed witness:

    • read flow_m for 2013 (0);
    • branch;
    • the parent sets twelve months as month:2013-01:12;
    • the branch sets May to 50.

    The branch's annual then reads [0, 0] instead of [50, 0]. Giving each clone its own copy of the record fixes it. That belongs with the record's own fixes (Keep the record of set_input values in step with storage #561), not this PR.

  • Cost: each own-period input scans the variable's stored keys, so N successive inputs to one variable check N(N+1)/2 keys. The reviewer measured 0.012 s for 100 inputs, 0.040 s for 200 and 0.152 s for 400. Dataset loading sets few periods per variable and starts with an empty fast cache, so that is negligible; long panels would merit a cheaper index.

Landing

Either merge this into #581's branch, so #581 (decision d899) lands both, or land it after #581. GitHub retargets it to master when #581's branch is deleted. #581 currently conflicts with master in holders/helpers.py and tools/simulation_dumper.py. This PR adds no conflict of its own: git merge-tree against master reports the same two files.

axiom: n/a: engine cache fix, no policy change

Other audit findings, handled elsewhere

The 2026-10-06 invariants audit of master 757147c7 also confirmed defects that this PR leaves alone, because other work owns them:

Branch staleness after set_input and raw-file restore are documented behaviour, not defects.

🤖 Generated with Claude Code

MaxGhenis and others added 9 commits October 2, 2026 18:08
…ger period

set_input_divide_by_period and set_input_dispatch_by_period treated any value
stored for a sub-period as already set, including values the simulation had
calculated (a cached default or formula result). The same annual input then
gave different months depending on what was calculated first: a divided
input skipped calculated months and shared the rest among the others, or
failed as inconsistent after the year had been read; a dispatched input
reused a calculated month for every later month.

The helpers now read the simulation's record of inputs (_user_input_keys):
a recorded sub-period keeps its input (and, for dispatch, passes it on to
the later sub-periods, as before), and a calculated one is replaced. After
storing, they drop the variable's calculated values over overlapping periods
(the sums calculate_add caches, the twelfths calculate_divide caches) under
the input's branch and the branches it reads through, record what they store
as inputs, and evict calculate's fast cache for those periods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
calculate does not put sums or twelfths in its fast cache today, so this is
defensive: a value held there for a period whose stored value the helper
drops goes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dump does not record which values were inputs, and restore_simulation
stored every value with put_in_cache, so a restored simulation had an empty
input record and the helpers replaced restored inputs (review finding 1).
Restore now stores each value as a recorded input, the rule #576 applies to
dumps without an inputs.txt.

Tests: a restored month keeps its value under a yearly input (divide and
dispatch); an input on a nested branch drops the sum its parent branch
calculated; an input stored for twelve months from March is kept. The
order-independence property now creates up to two nested branches, with
calculations at each level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On disk, a value calculated for a period like year:2013:2 is stored in a
file named after the period, and Windows rejects ":" in file names
(policyengine-core#526). The Windows CI jobs failed on such a read in the
on-disk examples. In disk mode the property now skips those periods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… replaces

Review round 2:

1. Restoring every dumped value as an input froze calculated values through
   a later reform. The dumper now writes, next to each variable's arrays,
   the periods whose value was an input (inputs.txt), and restore records
   exactly those; a dump without the file restores every value as an input.
   This is policyengine-core#576's dumper change, taken byte for byte so the
   two PRs merge without conflict.

2. A value stored by put_in_cache over an input (say a calculate_add sum over
   an input set for twelve months) left the input's record entry in place,
   so the helpers and apply_reform kept treating the calculated value as an
   input. Holder._set now drops the entry, in both of its forms for twelve
   months from the first of a month, when it stores a value outside
   set_input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…culate_add

With policyengine-core#571, calculate_add no longer caches a sum over an input
a plain read finds, so the input is kept and the test's premise (master's
calculate_add overwriting it) did not hold. Storing the calculated value with
put_in_cache replaces the input with or without #571.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ly for read branches

Review round 3:

- Holder._set no longer drops a record entry when a calculated value is
  stored over an input. A calculate_add at a variable's own period stores
  the unchanged input and lost its record; a clone, which shares its
  source's record until #561, deleted the source's entries; and ETERNITY
  variables keep entries under several periods. The case the cleanup was
  for, calculate_add caching a sum over an input, is what #571 stops.
- Master now gives a storage that shares nothing a frozenset for _shared
  (#578). The drop step deleted from it directly; it now leaves that to
  InMemoryStorage._stop_sharing_dropped_keys.
- The helpers evicted calculate's fast cache for a sub-period stored under
  any branch name, including one the simulation does not read. They now
  evict only for branches it reads, as #576's holder-write property
  requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_input for one of a variable's own periods (a month of a monthly
variable, a year of a yearly one) replaced the stored value but left what
calculate had derived from it at periods of another size: the sum of a
monthly flow over a year, a yearly flow's twelfth at a month. A later
calculate returned the old result, so it depended on whether that period
was read before the input was set (#579). Warm annual flow 0, January set
to 1: the annual read stayed 0.

Holder.set_input now applies the rule the set_input helpers follow
(_drop_calculated_overlapping): after storing an input for one of the
variable's own periods, it drops the values the simulation calculated for
the variable at overlapping periods, on the input's branch and the branches
it reads, and evicts calculate's fast-cache entries for overlapping
periods. Inputs are kept. ETERNITY variables are left alone.

Stacked on #581, whose helper it reuses. Removes #581's property exclusion
for #579, so test_inputs_do_not_depend_on_what_was_calculated_before now
compares reads of other-size periods for every variable. On #581's head
that property fails, shrunk to an annual flow_m read on a branch of depth
2; with this change it passes. Adds seven regression examples, all of which
fail on #581's head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis changed the base branch from fix-set-input-helper-order to master October 9, 2026 10:03

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

set_input for one period leaves the variable's cached sum or twelfth over an overlapping period in place

1 participant