Conversation
…g over Holder.get_known_periods() lists the periods of every stored key, with the branch name stripped, but Holder.get_array() reads only the requested branch, its parent_branch ancestors and "default". Simulation._calculate took the latest known period from the unscoped list, so a period stored only under an unrelated branch read back as None: uprating raised TypeError and auto-carry-over cached NaN. - Holder._readable_branch_names() is the one definition of what a branch can read; get_array() and the new get_known_periods(branch_name) both use it. - _calculate uses get_known_periods(self.branch_name). - OnDiskStorage splits "<branch>_<period>" keys on the last "_": branch names like "no_salt" raised ValueError, "y_2019" listed the wrong period, and delete(None, "pre_tcja") also wiped "pre_tcja_ctc". - dump_simulation saves the values the dumped branch reads instead of reading every period under "default" and saving None. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Follow-up to #551, whose review flagged this as an existing issue that #551 left unchanged.
Problem
Holder.get_known_periods()lists the period of every stored key with the branch name stripped.Holder.get_array(period, branch_name)reads only that branch, itsparent_branchancestors anddefault.Simulation._calculatetook the latest known period from the unscoped list and read it back underself.branch_name, so a period stored only under an unreadable branch came back asNone:None * factor→TypeError: unsupported operand type(s) for *: 'NoneType' and 'float'.Noneto_cast_formula_result, which cachedNaNwith no error.The same gap showed up in two more places, all confirmed by running them on
master:OnDiskStorageparsedf"{branch}_{period}"keys withsplit("_")[1]. policyengine-us branch names such asno_salt,mtr_for_adult_1andpre_tcja_ctccontain_:no_salt→ period"salt"→ValueError.y_2019→ period2019, a period that was never stored. Uprating then picked it and hit theTypeErrorabove.get_known_branch_periodsraised "too many values to unpack".delete(None, "pre_tcja")prefix-matched and also wipedpre_tcja_ctc.dump_simulationread every listed period underdefault. Dumping a branch savedNonefor its branch-only periods (anddefault's value where the branch overrode it).restore_simulationthen failed with "Object arrays cannot be loaded when allow_pickle=False".How a holder gets an unreadable period
simulation.memory_config, then create a holder after it (for example with a variable added to the system after the simulation is built). A branch whose name contains_then stores its input on disk, andbranch.calculatefor a later year raises.Simulation.__init__creates a holder for every existing variable beforememory_configcan be set, so on-disk storage needs that setup. policyengine-us doesn't setmemory_config(git grep).Holder.set_input(period, array, branch_name)orput_in_cache(..., branch_name). ThroughSimulationmethods I found no construction that does it: sibling branches, nested branches, andset_inputon a branch after_user_input_contextsexists. The review suggestedHolder._setwriting under_user_input_contexts[-1]as a route. That list is shared by reference acrossSimulation.clone, but a branch'sset_inputwrote only that branch's own holder. An independent search for other constructions is running; I'll update this section with its result.Fix
Holder._readable_branch_names(branch_name)returns the branch, then (unless it isdefault) eachparent_branchancestor, thendefault.get_arrayuses it, and the lookup order is unchanged; the branch's own value is still checked first, before the list is built.Holder.get_known_periods(branch_name=None). With a branch name, it lists only the periods that branch can read. Without one, it behaves as before._calculatecallsholder.get_known_periods(self.branch_name). Uprating and carry-over now use the latest period this branch can read; with none, they fall through to the default value.OnDiskStoragesplits keys on the last_(a period's string form never contains_; seePeriod.__str__).delete(None, branch)compares the parsed branch name exactly.dump_simulationsaves, once per period, the value the dumped simulation's branch reads.Invariants
For every holder
h, branchband stored state:set(h.get_known_periods(b)) == {p : h.get_array(p, b) is not None}, with no duplicates. Checked exhaustively on every non-empty subset of stored branches {default,a,a_b,c}, in memory and on disk. The readers are the root,a,a_b, the siblingc, and a branch literally nameddefaultnested undera.NaNor raises because of an unreadable period._and every period form (year, month, day, multi-unit, anchoredyear:2025-03,ETERNITY),get_known_branch_periodsreturns exactly what was put,getreads it back, andrestore()rebuilds the same index.master. The existing suite passes unchanged.Tests
tests/core/test_known_periods_branch_visibility.py(new, 532 cases). Onmaster, 280 of them fail:masterTypeError(None * float)NaNValueError(unpack / period parse)get_known_periods(branch_name)argumentThe 252 that pass on
masterare the cases with nothing unreadable stored, and nested branches reading ancestor periods. They confirm invariant 5.Run locally on this branch:
uv run pytest tests: 1477 passed, 4 skipped, 1 xfailed.policyengine-core test policyengine_core/country_template/tests -c policyengine_core.country_template): 39 passed.uvx ruff format --check .: clean.make documentation. The Holder page autodocs its members, so the updatedget_arrayandget_known_periodsdocstrings are the documentation change.Grid tests rather than Hypothesis, as in #551: Hypothesis isn't a dependency, and these domains are small enough to enumerate.
axiom: n/a: engine fix to branch storage lookups in policyengine-core; no policy rule changes
🤖 Generated with Claude Code