Repository navigation
Validate implicit dataset role capacities and role array shapes - #595
Merged
Merged
Conversation
build_from_dataset sized the default-role fallback by the group IDs, so a dataset without role columns got one members_role entry per group. Role queries on such a population then raised (nb_persons, sum with role=) or broadcast one group's entry over everyone (project, max with role=). Size the fallback by the membership array, and resolve the default against each group entity: a role it names, the first subrole of a role with subroles it names, or else the entity's first role, as people given no role in a situation get. join_with_persons now rejects a role array whose length differs from the persons'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Oct 6, 2026
Addresses the independent review of d647f78 (three should-fix findings). - group_positions compared IDs through np.searchsorted, which promotes mixed dtypes. int64 with float64, or int64 with uint64, meet as float64, where neighbouring large IDs become equal. Groups [2**53+3, 2**53+4] with a float64 person ID 2**53+4 still joined the first group, and int64 IDs [2**53, 2**53+1] with uint64 memberships raised as undeclared. IDs of one kind still use the sorted search (exact within a kind); IDs of different kinds, or object arrays, are now matched one by one through a dict, which compares Python values exactly. - Flat files with a household_id column but no person_household_id column worked on master and raised KeyError here. The membership now comes from the person's membership column or, failing that, the group's ID column repeated on each member's row. A frame with neither raises the same KeyError as master. - Default roles were sized by the number of groups. With the flat file's household count now right, two persons in one household got one role and role-filtered sums raised. They are now one per person (as in #595). Adds regressions for each, and a Hypothesis property: numeric IDs near 0, 2**31, 2**53 and 2**62 match by value for int32/int64/uint64/float64 pairs. All fail on d647f78; the property shrinks to int64 groups and uint64 memberships near 2**62. The real-dataset mapping A/B (populace-us, enhanced CPS 2024, UK enhanced FRS 1.40.3 and 1.57.4) is still identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Oct 18, 2026
* Join each person to the group their ID names SimulationBuilder.join_with_persons ranked the distinct group IDs persons belong to and used that rank to index the declared IDs in sorted order. A group with no members has no rank, so when one sorted before an occupied group every later person moved down one group. With groups ["a", "b"] and one person in "b" (income 100), members_entity_id was [0] and the household sums [100, 0]. Simulation.build_from_dataset calls it for every group entity. join_with_persons now looks each person's group ID up among all declared IDs (group_positions: stable argsort plus searchsorted). Empty groups can sit anywhere in the declared order. A person whose ID names no declared group, and a declared ID that repeats, raise ValueError instead of silently landing somewhere. The flat-file path in build_from_dataset relied on the old ranking: it declared groups 0..n-1 and passed the raw membership IDs. It now numbers the distinct membership IDs itself, which gives the same mapping. A flat file with an unsuffixed household_id column skipped that path and declared the previous entity's IDs as its households (three persons in two households made three); it now takes the same path. Adds regressions (the audit witness, an interior empty group, an ARRAYS dataset with an empty household, both flat-file paths, the two errors) and a Hypothesis property comparing membership, sums, counts and projections with a dict oracle over random IDs, orders, empty groups and assignments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Match group IDs by value across dtypes; keep the flat-file fallbacks Addresses the independent review of d647f78 (three should-fix findings). - group_positions compared IDs through np.searchsorted, which promotes mixed dtypes. int64 with float64, or int64 with uint64, meet as float64, where neighbouring large IDs become equal. Groups [2**53+3, 2**53+4] with a float64 person ID 2**53+4 still joined the first group, and int64 IDs [2**53, 2**53+1] with uint64 memberships raised as undeclared. IDs of one kind still use the sorted search (exact within a kind); IDs of different kinds, or object arrays, are now matched one by one through a dict, which compares Python values exactly. - Flat files with a household_id column but no person_household_id column worked on master and raised KeyError here. The membership now comes from the person's membership column or, failing that, the group's ID column repeated on each member's row. A frame with neither raises the same KeyError as master. - Default roles were sized by the number of groups. With the flat file's household count now right, two persons in one household got one role and role-filtered sums raised. They are now one per person (as in #595). Adds regressions for each, and a Hypothesis property: numeric IDs near 0, 2**31, 2**53 and 2**62 match by value for int32/int64/uint64/float64 pairs. All fail on d647f78; the property shrinks to int64 groups and uint64 memberships near 2**62. The real-dataset mapping A/B (populace-us, enhanced CPS 2024, UK enhanced FRS 1.40.3 and 1.57.4) is still identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Read flat-file group values by membership; reject missing memberships Addresses the second independent review (of c3a6459): - should-fix: build_from_dataset took a flat-file column as already per group whenever its length equalled the group count. A flat file has one row per person, so that happens exactly when every group has one person, and then values went in by row order while groups are numbered in ID order. With household_id [9, 7, 8] each person read another's rent ([800, 900, 700] for [900, 700, 800]). Master had this for frames with both ID columns; this PR's household_id fallback newly loaded frames master refused. Group variables in a flat file now always take each group's first member's value. - nit: np.unique merged every NaN membership into one group. A flat file with missing memberships now raises ValueError. - nit: the changelog now says default roles are one per person in every dataset format (every PE-US Microsimulation has no role column); adds an ARRAYS test for that path. The repeated-ID message lists each ID once. - nit: the dtype property also covers uint64 beyond the int64 limit, uint64/float64 pairs and negative IDs. The row-order and NaN regressions fail on c3a6459 and pass here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Preserve empty flat-file loading after group identity joins --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
US + core hub merge audit, core#595 at
|
Contributor
Author
|
Hub control after merge (confirmation). A full-sample A/B ran on PE-US's default dataset for 2026, with |
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.
Summary
Relative to current
master, this PR rejects recognized implicit dataset defaults that exceed a role's finite per-group capacity and rejects non-vector or wrong-length role arrays during joining. Master already assigns one literal default key per person. Accepted defaults keep their role meaning, cardinality, query results, and serialization.An earlier round integrated the overlapping change from merged #573 through a plain merge of
gh/masterat8fdf59e95a2faa990ed179f8554b722978024836. That merge preserved master's person-count default sizing and detection of period-suffixed entity role columns, and added regression coverage for their precedence. The caller comparison now uses this master baseline. Capacity enforcement newly rejects some datasets whose queries previously succeeded; it does more than move an existing failure earlier. The fallback that selected a first role or subrole was removed in the previous round and remains removed.Caller semantics
Unless stated otherwise, these examples use the country template's household roles and no role column.
master[1,2,3], household[10], memberships[10,10,10], default"child";nb_persons(role=CHILD)CHILDroles;[3][3]"member"or compound key"parent"; parent count and parent sum of[1,2,3][0,0,0]; count[0], sum[0]0 + 0 + 0 = 0[1,2], households[10,20], memberships[10,10], default"first_parent"; unique-role lookup of[100,200]ValueErrorValueErrorbecause2 > 1memberrole withmax=2; persons[1,2,3], group[10], memberships[10,10,10]; member count and filtered sum of[1,2,3][3], sum[6], since1 + 2 + 3 = 6ValueErrorbecause3 > 2; no query runs["child"]project(np.array([7]), role=CHILD)broadcasts to[7,7,7]ValueError; non-vector arrays are also rejectedEntity-specific role columns, including period-suffixed flat-file columns, still override generic
role, which overrides the implicit default. Explicit integer indices and literal strings retain their interpretation.default_role=Nonestill requires a role column; one global default key still applies to every group entity. Well-shaped explicit dataset assignments, direct-join capacity handling, situation defaults, and situation capacity validation remain unchanged. Only implicit defaults receive the new capacity check; excessive explicit capacities are not newly rejected.The role-shape guard checks role-array dimensionality and equality with the supplied membership array's length before group-ID mapping. The join also retains master's declared-ID mapping and membership validation from #592; the role-shape guard does not independently validate membership dimensionality or agreement with the declared person count. Preserving master's person-count default sizing means a malformed implicit dataset with a different membership count is rejected by this role-length check.
subsample()can now raise the default-role capacity error when the system has noperson_<entity>_rolevariable, even if explicit roles were supplied initially, because the rebuild loses those roles and falls back to the default; the older role loss is separate work.Methodology
These are Core simulation contracts. No statute, regulation, form, or tax-policy instruction applies. Primary sources are master's defaults and role-column precedence, literal flattened-role matching, the capacity descriptor, subrole uniqueness, situation capacity enforcement, and the unique-role lookup contract.
Invariants and coverage
For well-formed datasets without role columns, with defined group roles, string default keys, and one-dimensional membership arrays containing exactly one declared group ID per person, across
ARRAYS,TIME_PERIOD_ARRAYS, andFLAT_FILE:len(members_role) == len(members_entity_id) == number of persons, including zero persons and empty declared groups.Role.max. Capacities zero, one, and greater than one have coverage.For datasets with explicit roles, entity roles take precedence over generic roles and defaults, including period-suffixed flat-file columns. The new Hypothesis property verifies this with integer and string encodings, conflicting generic roles, and repeated unique explicit roles whose capacity handling remains unchanged.
Hypothesis generated 200 default-role/capacity/query cases, 50 malformed-role-shape cases, and 100 suffix-precedence cases, plus eight fixed examples. Its default-query generator places empty groups after occupied groups in sorted ID order, and its flat-file cases use consecutive occupied IDs. These default-role properties retain that coverage scope; the join-identity and ID-renumbering behavior fixed by #592 has separate example and property coverage, both rerun after this merge.
Merge with master after #592
Merged
gh/masterat8e9725c8e6192bafa75dee005da13b27df41cf33into the approved headef0f9d5e2c59c791c33ed24adacbb0971a42824bwith a plain merge, without rebasing. Master's #592 and this PR both changedjoin_with_personsinpolicyengine_core/simulations/simulation_builder.py. The merge keeps #592's value-basedgroup_positionslookup with exact cross-dtype matching, rejection of repeated declared IDs, undeclared memberships, unmatchable kinds and NaN memberships, and flat-file ID renumbering. It also keeps this PR's role-array dimensionality and membership-length validation before ID mapping, so malformed roles still receive the role-shape error, and the finite-capacity check for recognized implicit default roles inbuild_from_dataset.All six requested files passed separately in the foreground with
uv run pytest -q <file>, Python 3.14.4, and the required four-thread caps:tests/core/test_join_with_persons_identity.pytests/core/test_join_with_persons_identity_property.pytests/core/test_dataset_default_role.pytests/core/test_dataset_default_role_property.pytests/core/test_simulation_builder.pytests/core/test_subsample_structural_ids.pyTotal: 135 passed. The default-role example file includes six added cases proving that malformed-role errors take precedence over repeated or undeclared group IDs.
ruff format --checkandruff checkpassed on the resolved builder and strengthened test file; both staged and unstagedgit diff --checkpassed, and no tracked conflict markers or unmerged index entries remain. The dependency setup's incidentaluv.lockversion refresh is excluded from the merge.Earlier validation (before this merge)
Every file below ran separately in the foreground with Python 3.13.9 through
uv run --no-sync. A workspace-local runner reused preinstalled hub dependencies read-only while the workspace dependency install completed; tested Core source came from this checkout, or the pinned master export for baseline witnesses.tests/core/test_dataset_default_role.py: 35 passed.tests/core/test_dataset_default_role_property.py: 3 passed, with 350 passing generated cases and eight fixed examples.tests/core/test_simulation_builder.py: 35 passed.tests/core/test_bincount_minlength.py: 2 passed.tests/core/test_entities.py: 23 passed.tests/core/test_projectors.py: 6 passed.tests/core/test_dump_restore.py: 1 passed.tests/core/test_dataset_unknown_column_warning.py: 2 passed.9ba5d19head, both new suffix regressions failed: absent generic roles caused the capacity error; conflicting generic roles incorrectly won. Both pass after the merge in the complete example file. The merge preview and actual merge both reproduced the default-branch conflict.ruff format --check .: 397 files already formatted;ruff check .: passed.git diff --check: passed.No full suite, microsimulation, or documentation build was run locally for this focused round. API documentation uses autodoc; the loader and join source docstrings describe the durable contract. The towncrier fragment now describes the residual capacity and shape changes relative to current master. Documentation review: impact medium, confidence high from source inspection, baseline witnesses, and focused coverage; zero-person maxima remain outside the default-role property oracle, while master's #592 resolves the earlier join-identity limitation.