Fix variable uprating when the uprating parameter is undefined, and pick the latest earlier period - #551
Merged
Conversation
…period A variable with `uprating` and no value for the requested period is uprated from its latest known earlier period by the ratio of the uprating parameter at the two period starts. Two bugs lived in that step: - The parameter can have no value at the earlier instant (an input given for a year before the index starts). The ratio then divided by None and raised TypeError. The index is now held flat where it has no value: its first value before it starts, the last value before an explicit null after one. A value known for a period the index does not reach is carried over unchanged until the index starts, then uprated with it. - The earlier period was chosen by indexing the unfiltered known-periods list with a position in the filtered list, so a later period stored first was used instead of the latest earlier one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lagged-year lookup caches the default at an earlier year; a later request between that year and the input year then deflated the later input on master. Add that regression case and describe the uprating rule, including the flat index where the parameter has no value, on Variable.uprating. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Storage-order test uses years inside the index range so it tests the base-period choice alone (5 of 6 orders fail on master). - The helper test sets an open-ended null, so the hold-after-null path is exercised at 2040, and imports the helper locally so the module runs against code without it. - Docstrings: the backward hold mirrors country-package backdating (a parameter itself returns None there); cached values count as known and a zero base carries over. Changelog covers a missing value at either period start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
to PolicyEngine/policyengine-us
that referenced
this pull request
Sep 27, 2026
policyengine-core divided by a missing uprating-index value whenever a later year read an uprated input supplied only for a year before 2015, raising TypeError. policyengine-core 3.32.8 (PolicyEngine/policyengine-core#551) holds the index flat where it has no value. Require it, and cover every affected variable (312, grouped by where each is defined) with a differential check against the same input supplied for 2015, plus the reported household. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
to PolicyEngine/policyengine-us
that referenced
this pull request
Sep 28, 2026
policyengine-core divided by a missing uprating-index value whenever a later year read an uprated input supplied only for a year before 2015, raising TypeError. policyengine-core 3.32.8 (PolicyEngine/policyengine-core#551) holds the index flat where it has no value. Require it, and cover every affected variable (312, grouped by where each is defined) with a differential check against the same input supplied for 2015, plus the reported household. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
to PolicyEngine/policyengine-us
that referenced
this pull request
Sep 28, 2026
policyengine-core divided by a missing uprating-index value whenever a later year read an uprated input supplied only for a year before 2015, raising TypeError. policyengine-core 3.32.8 (PolicyEngine/policyengine-core#551) holds the index flat where it has no value. Require it, and cover every affected variable (312, grouped by where each is defined) with a differential check against the same input supplied for 2015, plus the reported household. 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.
Problem
When a variable with
upratinghas no value for the requested period,Simulation._calculatetakes its latest known earlier period and multiplies that value byuprating_parameter(this_period.start) / uprating_parameter(latest_known_period.start). That step had two bugs.1.
TypeErrorwhen the uprating parameter has no value at the earlier instant. A parameter returnsNonebefore its first value. If a caller supplies an uprated input only for a year the index doesn't reach, any later year that reads it divides byNone. Thevalue_in_last_period == 0guard doesn't catch that. policyengine-us backdates parameters only to 2015-01-01, so this household fails onmain:2. The wrong base period. The code picked
known_periods[np.argmax(start_instants)], wherestart_instantsis a filtered list: earlier periods of the variable's unit only. Whenever a later period came first inknown_periods, that position pointed at the wrong period, and the result was silently wrong:{2020: 999, 2015: 5000}requested for 2018 return 999: the 2020 value deflated to 2018. The correct answer is 5000 uprated from 2015.Fix
_uprating_index_value). A value known for a period the index doesn't reach is carried over unchanged until the index starts, then uprated with it. This matches extending the parameter's earliest value backward, which is what policyengine-us'sbackdate_parametersalready does down to 2015. The factor falls back to 1 only if the parameter has no non-null value at all, alongside the existing zero-base guard.latest_known_period = max(earlier_known_periods, key=lambda p: p.start), taken from the filtered list itself.Variable.upratingnow documents the rule.Why hold the index flat rather than the alternatives
Invariants
For every known period K before a requested period R, with the known value v:
v × p(R) / p(K), as before.vheld with the flat index:v × p̃(R) / p̃(K), wherep̃holds the index flat.Parameter.updateand uprating runs through the ordinary defined-index path.Tests
tests/core/variables/test_variable_uprating.py(new, 233 cases):Parameter.updateon a pre-series period, an index with no values, and the helper directly.The module runs directly against
master'ssimulation.py; the helper test imports the helper locally. There, 174 of 233 cases fail:TypeError);The 59 that pass on
masterare the ascending storage order and the pairs where the index is defined at both years; they confirm invariant 2.Run locally:
pytest tests: 945 passed, 4 skipped, 1 xfailed.ruff format --check: clean.Downstream
policyengine-core>=3.30.1, and its CI resolves the newest core before testing (uv lock --upgrade-package policyengine-core). This change reaches pe-us CI and pip installs on release.753cc4e) through[tool.uv.sources], so pe-us CI runs its full suite against the fix. Later commits here change only tests and docstrings. The draft's new test fails 39 of 40 on core 3.32.7 with the reportedTypeErrorand passes 40 of 40 with this fix. After release it switches the pin to a version floor.Review
An independent Opus 5.5 review (Subfleet, code reading only) returned approve with nits. I applied the nits in
b95c86b: an isolated storage-order test, an open-ended null in the helper test, a module that runs againstmaster, and docstring and changelog wording.It traced dataset loading. h5py iterates years in name order, so standard multi-year h5 datasets load ascending, and bug 2 never touched their microsim outputs. Old and new code differ only where
masterwas wrong or order-dependent: unsorted flat-file columns, disk-spilled periods, cached defaults, and stray year periods on monthly variables.It also flagged an existing issue, the same on
master.get_known_periodsstrips branch names, so a period stored only on a non-ancestor branch could still yieldNone * factor. That's tracked as a follow-up, not changed here.axiom: n/a: engine fix to variable uprating in policyengine-core; no policy rule changes
🤖 Generated with Claude Code