Skip to content

Break equal-start uprating ties the way auto-carry-over does - #582

Merged
MaxGhenis merged 6 commits into
masterfrom
fix-uprating-equal-start
Oct 6, 2026
Merged

MaxGhenis merged 6 commits into
masterfrom
fix-uprating-equal-start

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

#562 and #563 have merged, so this is now one PR on master (9a12a9a5). The head is db5c3de8: this PR's commit fcd4c4a0, rebased onto master, plus five follow-up commits that answer the reviews. Only fcd4c4a0 changes code under policyengine_core/.

Found by the final-commit review of #563 (finding 5). The tie is still on master.

Problem

When a variable with uprating has no formula result for a period, Simulation._calculate uprates the latest earlier input in its own unit. On master it picks that input with max(earlier_input_periods, key=lambda p: p.start).

Two earlier inputs can start on the same day. Holder.set_input calls a variable's set_input helper only for an input in another unit, so a yearly variable stores a year:2012:2 input as given, beside one for 2012. It does this whether or not the variable has a helper. Both inputs start on 2012-01-01, so max returned whichever came first in get_known_periods(): the one stored first, or the one in memory over the one on disk.

Executed on master 9a12a9a5, with an index rising 3.7% a year and inputs year:2012:2 = 1 and 2012 = 5:

Variable 2015, year:2012:2 set first 2015, 2012 set first
uprated_any_unit (no helper) 1.1151576 5.575788
uprated (set_input_divide_by_period) 1.1151576 5.575788
uprated_monthly, month:2012-01:3 vs 2012-01, for 2012-06 1.0 5.0

The results are the same with auto-carry-over on and off.

Fix

Use the order #562 already uses to choose the input auto-carry-over carries. Its sort key moves into one module function, _latest_input_key(period, definition_period), and both paths call it. The key ranks, in order:

  1. the variable's own unit;
  2. the latest start;
  3. the latest end (_end_order, which also orders periods ending after 9999-12-31);
  4. the larger unit;
  5. the period's string form.

On a tie, uprating now starts from the input that ends last. That is the input auto-carry-over would carry, so uprating with a flat index returns exactly what auto-carry-over returns.

The production diff is that one function and the two max calls that use it. The carry-over path's behaviour is unchanged; only its inline key moved. Two things are deliberately unchanged:

  • the uprating factor, which still runs from the source's start;
  • the candidate filter: inputs only, in the variable's own unit, starting before the period.

Why "ends last"

  • Parity with carry-over. The two paths now agree on which input is the latest one (invariant 3 below). The alternative, "shortest first", would make a variable that gains uprating start from a different input than auto-carry-over carries.
  • Most recent information. year:2012:2 says what the variable is in 2013 as well. Auto-carry-over already reads it that way.
  • It doesn't depend on the factor question. If the factor later runs from the source's last covered year (see below), "ends last" is still the right source.

Should uprating from a multi-period input be allowed at all?

The factor uses period.start. Uprate a year:2012:2 source to 2015, and two things go wrong:

  • it picks up 2012→2013 growth that the input says did not happen;
  • 2013 itself, which the input covers, comes out uprated by a year (1.037 from an input of 1).

Changing that would change results without any tie, which makes it a semantic choice rather than a determinism fix. It also sits with related multi-period behaviour that isn't about ties:

  • the helper is skipped for a same-unit multi-period input;
  • an input starting on the requested period is not an uprating source;
  • month:2012-01:12 is stored as the year 2012, because storage keys are period strings.

They are written up in #583, each one executed, with options and a recommendation. This PR keeps multi-period inputs as sources and only makes the tie deterministic.

Exposure: none known.

  • PE-US's eCPS h5 has period keys 2024 and 2025 only.
  • PE-UK's loader calls set_input(variable, year, …) for each year in dataset.years.
  • No YAML test in either repository uses a multi-period period:.
  • Every year:…:N string in either repository's source and tests is a parameter update, not a variable input.
  • These were checked on PolicyEngine/policyengine-us@fd693d66 and PolicyEngine/policyengine-uk@bfc5fea1.

Invariants

Over the generated domain:

  • each variable's own unit, including multi-period inputs that start with another input (year:2012:2, year:2012:10, month:2012-01:3 and others);
  • inputs set in any order, some of them on disk;
  • earlier requests (plain calculations, ADD, DIVIDE);
  • reads from the simulation, a branch or a nested branch, which may set its own input;
  • auto-carry-over on and off.
  1. Order independence, byte for byte. The uprated value depends only on which inputs are stored. It does not depend on the order they were set in, or on which of them are in memory and which on disk. Tested by test_uprating_does_not_depend_on_the_order_inputs_were_set.
  2. Total order. _latest_input_key never ties on two distinct periods: unit, start and end fix the period. So max never falls back to iteration order.
  3. Differential: uprating agrees with auto-carry-over. Take an index that never moves. Given the same inputs, an uprated variable returns, byte for byte, what auto-carry-over returns for a twin variable without uprating. Tested by test_flat_uprating_reads_the_input_auto_carry_over_reads.
    • The one stated exception: an input in the variable's own unit, other than the period itself, that starts on the period's first day. Auto-carry-over carries that input, but uprating takes only inputs that start before the period, as before.
  4. Reference rule. The result equals the reference rule in tests/fixtures/uprating_order.py, which is written independently of _latest_input_key and breaks ties the same way on both paths.
    • The rule's last_day numbers each period's last day without _end_order: for days, the start's ordinal plus the days after it; for months and years, Period.stop, moved back by whole 400-year Gregorian cycles.
    • A differential property checks that last_day + 1 == _end_order for days, months and years up to 5,000,000 units long, many of them ending after 9999-12-31.
  5. Unchanged without a tie. Where one earlier own-unit input starts last, the key's first two components already decide, so the source is the one master picks. The carry-over key is the same tuple as before, and every existing Carry over only inputs, the latest at or before the requested period #562 and Uprate only from inputs, so uprated values don't depend on calculation order #563 test passes unchanged.

Tests

tests/core/test_uprating_order.py goes from 97 to 141 cases, still with no Hypothesis dependency. The new cases cover:

  • both set orders, for a variable with a helper and one without;
  • monthly inputs;
  • 2012, year:2012:2 and year:2012:10, where the string order disagrees with the end;
  • memory against disk, in both directions;
  • a branch input that starts with a parent input;
  • flat uprating against carry-over;
  • the unchanged rule that an input starting on the period is not a source;
  • inputs ending after 9999-12-31: one such input, two of them (day:9999-12-30:10 and day:9999-12-30:3), and a carry-over between month:9999-01:13 and day:9999-01-01:370. Each is compared with the reference rule.

tests/core/test_uprating_order_property.py goes from 2 to 5 derandomized properties:

  • the existing property, with its domain widened to multi-period inputs and 3 new @example rows;
  • set-order and disk independence, 300 examples. Each has a 2- or 3-way same-start group, all inputs set in a drawn permutation, and a drawn subset of them on disk. It also has one explicit example that puts the winning input on disk and the losing one in memory;
  • the flat-index differential, 300 examples;
  • master's helper property, unchanged;
  • the last_day differential against _end_order, 500 examples.

tests/fixtures/uprating_order.py gains:

  • the reference rule's tie-break, through last_day;
  • uprated_flat and uprated_daily_flat, uprated by an index that never moves, and carried_any_unit, their twin without uprating;
  • the Windows guard below.

Windows (#526). OnDiskStorage names a value's file after its period. On Windows, numpy.save raises OSError for default_year:2012:2.npy, a name with two colons; CI showed this on the four Windows test jobs.

  • At import, the fixture tries to store year:2012:2 in a temporary directory. DISK_STORES_ANY_PERIOD records whether that worked.
  • Where it didn't:
    • the property keeps every drawn period whose string form has a colon in memory;
    • the "longer" memory-or-disk regression is skipped;
    • the explicit disk-winner example is a strict xfail(raises=OSError).
  • Once Storage keys need a structured (branch, period) scheme: str(period) is lossy and separators collide with branch names #526 lets OnDiskStorage store such periods, the probe succeeds and all of these run on disk again.
  • example(...).xfail needs Hypothesis 6.62.0 or later, so the dev extra now requires hypothesis>=6.62,<7. uv.lock's specifier matches. The lock's own project version was already stale on master (3.32.12 against 3.32.17), and this PR leaves it.

Against master's code, 22 of the 447 tests in the five uprating and carry-over files fail: 19 regression cases and 3 properties.

Mutation check on db5c3de8:

  • each mutant runs in a scratch tree;
  • POSIX runs the five uprating and carry-over files plus test_storage_input_index.py: 465 tests, 0 failing unmutated;
  • winsim runs the three uprating files under a plugin that makes numpy.save and tempfile.mkstemp reject a file name containing :, so the probe fails as it does on Windows: 377 pass and 2 are skipped, unmutated.

All are killed. Failing tests:

Mutant POSIX winsim
Uprating by start only (master's rule) 22 20
Same, with the candidates reversed 24 22
Uprating prefers the shorter input 37 35
Key without the end order 14 10
String form before the end order 14 10
Key without the own-unit preference 5 3
Carry-over by own unit and start only 8 3

On the reference rule itself, 3 more mutants, run on the overflow regressions and the last_day property:

  • the old overflow handling fails 6 tests;
  • dropping the 400-year shift fails 3;
  • an off-by-one last_day fails the property.

How often the set-order property reaches a tie. On the 300 examples it runs, a tie decides the source in 76. In the draws it splits between memory and disk, the winner is always in memory, which is why the explicit disk-winner example exists.

Local runs:

  • On db5c3de8:
    • the five uprating and carry-over files: 447 passed;
    • ruff format --check .: clean.
  • On f081b7c4, which differs from db5c3de8 only in the changelog fragment:
    • the full tests folder: 1532 passed, 4 skipped, 1 xfailed;
    • country-template YAML: 39 passed;
    • the three uprating files under the Windows simulation: 377 passed and 2 skipped with the guard, and the expected 3 failures without it;
    • the standalone repro: both set orders give 1.1151576, with auto-carry-over on and off. On master 9a12a9a5 it gives 5.575788 when 2012 is set first.

CI on db5c3de8 passed: all 18 checks in run 37395979986.

Review

  • GPT-6.1 Sol, on f051cc28: REQUEST CHANGES.
  • GPT-6.1 Sol, on e12993c5: APPROVE.
  • Opus 5.5, twice after the rebase onto master (6148851d, then 3f222e1f): APPROVE WITH NITS each time, no code defect. Between them they found that:
    • the guard keyed on sys.platform; it is now a probe;
    • no example put the winner on disk; the explicit example now does;
    • comments and commit messages overclaimed what Windows rejects;
    • example.xfail needs Hypothesis 6.62. All are fixed.
  • Opus 5.5 and GPT-6.1 Sol, on 578911f2: both APPROVE WITH NITS, no production defect.
    • Sol found that the reference rule misordered two inputs ending after 9999-12-31, which fell to string order, and an overflowing day input against a month input that ends later. The simulation was right. This is fixed by last_day, with regressions and the differential property.
    • The rest were commit-message and docstring wording, now fixed.
  • On f081b7c4: two independent GPT-6.1 Sol reviews, one APPROVE and one APPROVE WITH NITS. The nit was that the changelog fragment did not say the tied inputs are in the variable's own unit. Fixed.
  • On b7713e4d: two independent GPT-6.1 Sol reviews, one APPROVE and one APPROVE WITH NITS. The nit was that a commit-message example needed "with no set_input helper". db5c3de8 rewords that message; its tree is identical to b7713e4d's.

The rewordings kept every reviewed tree: the reviewers checked b43c3562, 7e2aa238 and 49ed6a4a, and git diff --quiet b7713e4d db5c3de8 is clean. The commit messages form the squash body, so they were reviewed as well.

Not in scope

axiom: n/a: core engine semantics, no policy encoded.

🤖 Generated with Claude Code

@MaxGhenis
MaxGhenis force-pushed the fix-uprating-equal-start branch from c668f1e to f051cc2 Compare October 3, 2026 12:19
MaxGhenis added a commit that referenced this pull request Oct 3, 2026
The carry-over sort key evaluated period.stop for every candidate input.
A period that ends after 9999-12-31 (day:9999-12-30:3, or
day:2012-01-01:4000000) has no stop: computing it raises OverflowError.
Master carried such inputs ([10, 20]); this branch raised. Sort by
_end_order, which puts such a period after every period that has a stop,
so it still ends last. Found by the review of #582, which uses the same
key for uprating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the fix-uprating-equal-start branch 2 times, most recently from e12993c to 2e00515 Compare October 3, 2026 18:35
MaxGhenis added a commit that referenced this pull request Oct 3, 2026
…562)

* Carry over only inputs, the latest at or before the requested period

Auto-carry-over took the latest-starting stored period of any kind and
returned the default if it started after the requested period. Any later
stored period therefore hid an earlier input: a later input (inputs for
2012 and 2014 gave the default for 2013), or a later period the simulation
had already calculated (2014 calculated first made 2013 the default). And
values the simulation calculated carried like inputs: a twelfth cached at a
month by calculate_divide (the policyengine-us monthly_age bug #557 fixes),
a value masked by defined_for, a default cached where defined_for was false
everywhere, or a formula result from before the formula's end. Results
depended on which periods were calculated first.

The rule now: a period takes the input stored for the latest-starting
period that starts no later than it (on a tie, the one ending last),
preferring the variable's own definition-period unit; with none, the
default. Values the simulation calculates are stored with derived=True:
the mark lives in the storage with the value (InMemoryStorage and
OnDiskStorage keep it per key, every put sets or clears it, it counts only
while the key is stored, and clones copy it), so deletions, direct writes
and #556's shared arrays cannot leave it stale. Holder.is_derived(period,
branch_name) reports the mark of the value get_array reads, so a value one
branch calculated never hides another branch's input. A derived value never
replaces an input the branch reads (calculate_add/calculate_divide used to
overwrite inputs stored in another unit), calculate_add caches only sums
over several sub-periods, and dump/restore keeps the marks. Storages gain
has(), which neither reads nor copies an array.

Supersedes #557's carry-over change: its 253 tests pass here.

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

* Keep uprating, input helpers, dumps and deletions consistent with input-only carry-over

Round-2 review findings:
- A default cached for a period with no earlier input became the base the
  uprating path uprated later periods from; the default is not cached for
  variables with uprating (as before).
- A value calculated inside a set_input helper was written to the input's
  branch and recorded as a user input, bypassing the guard that keeps
  inputs; derived writes now stay on the branch they were calculated on.
- dump_simulation reads each value's mark from the same branch and period
  as the value it dumps, so #552's branch dumps keep matching marks.
- Carry-over checks candidates latest first and stops at the first input,
  instead of resolving the storing branch for every known period.
- Storages drop the marks of deleted keys.

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

* Test that deleting a whole branch drops its derived marks

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

* Cache the default only where master cached a carried value

With no input at or before the period but a later period stored, master
returned the default without caching it; caching it changed what formulas
that test whether a value is stored see (policyengine-uk's maintenance
loan and current_education formulas check get_array(period.last_year)),
and gave the uprating path a calculated base. Return it uncached there, as
master did; cache it (derived) otherwise, as master cached the value it
carried. This replaces the uprating-only special case.

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

* Read input periods in one pass over readable keys; clear marks with the caches

Round-3 review findings:
- A period stored only under a branch this one cannot read was taken for
  an input (own-unit preference ranked it first) and read back as NaN.
  Holder.get_input_periods(branch_name) now lists, in one pass over the
  stored keys, the periods whose readable value is an input, taking for
  each period the key get_array reads first; carry-over picks from those.
  This also replaces the per-period walk up the branch chain.
- apply_reform's cache wipe kept the storages' marks, and rebuilding a
  disk index brought a stale mark back onto a new input; the wipe now
  clears the marks and OnDiskStorage.restore starts without any.
- Storages pickled before the marks existed failed to clone, put or
  delete; __setstate__ now defaults them (and #556's shared-key set).
- The public carry-over rule now states the own-unit preference and that
  an input for the period itself is read back as stored.

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

* Test memory-over-disk precedence, in-memory mark clearing and index rebuilds

Closes three mutants the suite did not kill.

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

* Break equal-extent carry-over ties by unit, then period

Two inputs covering the same days in units other than the variable's
(month:2013-01:2 and day:2013-01-01:59) resolved to whichever was stored
first. Prefer the larger unit, then the period's string form, so the
choice depends only on the inputs (round-4 review).

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

* Record a storage's inputs, not its derived values, and share them with clones

Measured on a policyengine-us household (policyengine-us 2.18.3, one
household_net_income calculation, 6,185 storages, 30,925 holder clones
across 5 branches), master with #578 retains 20.94 MB per simulation.
With derived marks as a set per storage that holds a derived value, the
merge retained 24.35 MB: 1,514 sets in the simulation and a copy of its
source's marks in every holder clone. That is 2.6 times the 1.3 MB per
simulation that ran policyengine-us's CI out of memory (#577).

5,201 of the 5,204 stored values are derived, so record the inputs
instead: is_derived is "stored and not an input". The 3 storages holding
inputs have a set (648 bytes); the rest read _NO_INPUTS. A storage and its
clones share one frozenset of inputs until one of them changes its own.
Retained memory is now 21.25 MB per simulation, +0.31 MB on master: one
more 8-byte attribute slot in each of the 37,110 storage objects.

A pickle records that inputs were recorded; a state from before (no
marker) counts every stored value as an input, as it did then.

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

* Carry over an input that ends after the last representable date

The carry-over sort key evaluated period.stop for every candidate input.
A period that ends after 9999-12-31 (day:9999-12-30:3, or
day:2012-01-01:4000000) has no stop: computing it raises OverflowError.
Master carried such inputs ([10, 20]); this branch raised. Sort by
_end_order, which puts such a period after every period that has a stop,
so it still ends last. Found by the review of #582, which uses the same
key for uprating.

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

* Order period ends by day number, so ends after 9999-12-31 compare too

6ddbaf9 sorted every period whose stop cannot be computed after those
that have one, but equal to each other, so of two inputs ending after
9999-12-31 the string form decided: day:9999-12-30:9 was carried over
day:9999-12-30:10, and 24 months over 800 days from the same day (delta
review, P2). Month and year periods past 9999 also raised ValueError, or
returned a stop no date can hold, rather than OverflowError.

_end_order is now the number of the day after the period's last day,
counted as date.toordinal counts, by integer arithmetic on the same
calendar. It equals stop + 1 day wherever stop has a value and orders
every other period by its true end.

Tests: the review's two cases in both set orders; a property that
_end_order agrees with Period.stop where it has a value and with numpy's
calendar everywhere (1,000 examples, sizes to 5,000,000); a longer period
from the same day ends later. Also asserts that a pickled state without
a set of shared keys gets none (a surviving mutant).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Two earlier inputs in a variable's own unit can start on the same day:
Holder.set_input stores a year:2012:2 input to a yearly variable as
given (a set_input helper is only called for another unit), beside one
for 2012. The uprating source was max(..., key=start), so the tie went
to whichever was stored first, or to the one in memory over the one on
disk: 2015 came out as [1.115, 1.115] or [5.576, 5.576] by set order.

Move #562's carry-over sort key into _latest_input_key and use it for
the uprating source too: on a tie, the input that ends last, then the
larger unit, then the period's string form. Distinct periods never tie,
so the source depends only on the stored inputs. The factor still runs
from the source's start, and the candidates are unchanged (own unit,
starting before the period, inputs only).

Tests: regressions for both set orders with and without a set_input
helper, monthly inputs, a 10-year input whose string sorts first, memory
versus disk and a branch input; Hypothesis properties for set-order and
disk-placement independence and for flat-index uprating agreeing with
auto-carry-over; the fixture's reference rule breaks ties the same way.

Review (GPT-6.1 Sol, f051cc2): the key's period.stop raised
OverflowError for a period ending after 9999-12-31 (day:9999-12-30:3),
where the start-only key did not. The key now orders ends with #562's
_end_order, which puts such a period last; regressions use a daily
variable. The flat-uprating differential now skips only the case where
the two paths really diverge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 4 commits October 5, 2026 20:18
OnDiskStorage names a value's file "{branch}_{period}.npy". On Windows,
numpy.save raises OSError for default_year:2012:2.npy and
default_year:2014:3.npy, so storing year:2012:2 or year:2014:3 on disk
fails there (#526). The tie tests put such
periods on disk, which failed the four Windows test jobs: the "longer"
memory-or-disk regression (both systems) and the set-order property.

The fixture's can_store_on_disk says whether the tests may put a period
on disk on this platform. On Windows the "longer" regression is skipped
(the "shorter" one still puts 2012 on disk beside year:2012:2 in
memory), and the property drops periods with a colon in their string
form from its drawn on-disk set, so memory-against-disk ties are still
drawn there, with the winner in memory. Elsewhere nothing changes: the
derandomized draws are the same.

Checked with a plugin that makes numpy.save and tempfile.mkstemp reject
any file name containing ":": without the guard the same 3 tests fail
with the same paths as CI; with it 3 pass and 2 skip.

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

Review of 6148851 (APPROVE WITH NITS):

- The guard keyed on sys.platform, so it would not lift when #526 lets
  OnDiskStorage store a period with a colon in its name. The fixture now
  tries storing year:2012:2 in a temporary directory once, at import, and
  DISK_STORES_ANY_PERIOD says whether that worked.
- The comments said "a period several units long", but the code checks
  for a colon in the period's string form (year:2012-03 has one;
  month:2012-01:12 prints as 2012 and has none). They now say so.
- No property example had the winning input on disk and a losing one in
  memory: in the drawn ties split between memory and disk, the winner
  was always in memory, which get_known_periods lists first. A new
  example puts year:2012:2 on disk beside 2012 in memory. Where storage
  cannot store year:2012:2 it is an expected failure with OSError.
  There, no tie this property draws can have its winner on disk. Two
  inputs tie when the key ranks them equal up to their start: the same
  start, and both in the variable's own unit or both not. This
  property's inputs are years and months, so the two are in the same
  unit, and the winner, which ends last, is the longer; every such
  period it draws has a colon in its string form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of 3f222e1 (APPROVE WITH NITS):

- The fixture said Windows does not allow ":" in a file name, so
  numpy.save raises OSError there. CI showed that for
  default_year:2012:2.npy, a name with two colons. The docstrings and
  comments now name what the probe stores (year:2012:2) and say what
  the tests do where it fails.
- example(...).xfail, which the disk-winner example uses, needs
  Hypothesis 6.62.0 or later: under 6.61.3 it raises AttributeError, so
  the module could not be collected with an older Hypothesis the dev
  extra allowed. The dev extra now requires hypothesis>=6.62,<7, and
  uv.lock's specifier matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviews of 578911f (Opus 5.5 and GPT-6.1 Sol, both APPROVE WITH NITS):

- The reference rule ordered ends by Period.stop, which raises
  OverflowError for days that end after 9999-12-31, and gave every such
  input the same end, after any input whose stop has a value. So two of
  them fell through to string order, and one beat a month input that
  ends later (month:9999-01:13's stop is 10000-01-31). The simulation
  orders them with _end_order. For uprated_daily_flat at 9999-12-31,
  {day:9999-12-30:10, day:9999-12-30:3} gave [10, 20] from the
  simulation and [1, 2] from the reference; for carried_any_unit at
  9999, {month:9999-01:13, day:9999-01-01:370} gave [1, 2] and [3, 4].
  The reference's last_day now numbers each period's last day itself:
  for days, the start's number plus the days after it; for months and
  years, stop, moved back by whole 400-year Gregorian cycles (146097
  days) to a year datetime has.
- Tests: both cases as regressions, in either set order, compared with
  the reference; and a derandomized property that last_day, written
  separately, plus one equals _end_order, for days, months and years up
  to 5,000,000 units long. Against the old reference the regressions
  fail 6 tests; without the 400-year shift, 3; with last_day off by
  one, the property fails.
- The fixture said that where the probe fails the tests keep every
  period with a colon in memory. There, the "longer" regression is
  skipped and the disk-winner example expects the OSError; it now says
  so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the fix-uprating-equal-start branch from 578911f to f081b7c Compare October 6, 2026 00:19
Review of f081b7c (GPT-6.1 Sol, APPROVE WITH NITS): the fragment said
"two earlier inputs start on the same day", but uprating reads only
inputs in the variable's own unit. A yearly variable with no set_input
helper, given 2012 and month:2012-01:25, which start together, is still
uprated from 2012, though the month input ends later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the fix-uprating-equal-start branch from b7713e4 to db5c3de Compare October 6, 2026 00:49
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 6, 2026 01:16
@MaxGhenis
MaxGhenis merged commit 0a7423a into master Oct 6, 2026
18 checks passed
@MaxGhenis
MaxGhenis deleted the fix-uprating-equal-start branch October 6, 2026 01:17
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>
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.

1 participant