Skip to content

Hard Constraints Not Enforcing Maximum Household Weights When USE_HARD_CONSTRAINTS = True #221

Open

Description

@amsamimi

Description

TfNSW, RSG, and WSP collaborated on the Sydney ABM model development in 2026. As part of this project, we upgraded PopulationSim to v0.10 and enabled USE_HARD_CONSTRAINTS in the model settings. Based on our configuration:

  • Initial household weight ≈ 51 (Total Target HHs / Total Seed HHs)
  • max_expansion_factor = 50

We therefore expected the maximum allowable household weight to be approximately 51 × 50 = 2,550
However, the final synthesised population contains household weights as high as 53,910, which is more than 20 times the expected cap.
This suggests that the hard constraint mechanism may not be enforcing the maximum expansion factor as intended.

Why This Matters

This is potentially an important limitation for users who apply PopulationSim to expand Household Travel Survey (HTS) data.
Very large record weights can result in a small number of survey households representing a disproportionately large share of the expanded data. This may:

  • Increase sensitivity to individual records.
  • Reduce robustness of validation and calibration results.
  • Undermine one of the key purposes of applying weight caps.

Attachments

hh_weight_distribution.pdf
populationsim.log
settings_mp.yaml
settings.yaml

Activity

  1. amsamimi commented on Aug 18, 2026

    @amsamimi
    Author

    @jfdman
    Following our email discussion in February 2026, I've opened this issue to capture the behaviour we're seeing with USE_HARD_CONSTRAINTS and household weight limits. Please add any additional background or details that would be helpful for the developers.

    FYI @jpn-- @joecastiglione @asiripanich @dhensle @nick-fournier

  2. nick-fournier commented on Aug 24, 2026

    @nick-fournier
    Contributor

    Hi @amsamimi let me know if you want to meet and live-debug, or if you discovered anything new since last week. Otherwise, my leading hunch is that multi-processing is somehow silently passing False to hard constraints so might be worth a quick test with only single thread.

  3. amsamimi commented on Aug 24, 2026

    @amsamimi
    Author

    Hi @nick-fournier
    I sent a meeting invite for Tuesday at 9:30 AM Sydney time to nick.fournier@rsginc.com. Did you get that?
    We can discuss early test strategies and plan our next steps. I'm also available to run tests and explore options to help speed up this fix.

  4. nick-fournier commented on Aug 24, 2026

    @nick-fournier
    Contributor

    Ah, @amsamimi my current email is nfournier@bayareametro.gov

  5. nick-fournier commented on Aug 25, 2026

    @nick-fournier
    Contributor

    @amsamimi I dug up a few other possible things. I'm actually not so sure it's multi-processing. Two issues are relatively minor and the third is mostly sloppy engineering but not necessarily wrong. Fixing the first two are easy but the third will have a lot of touch points.

    1. The upper bound gets rounded to a whole number (balancing/wrappers.py:88). It rounds the cap and never lets it go below 1. With big survey weights that's ~1% over. With small ones it blows up — a weight of 0.1 gets a cap of 10x. The rounding does nothing useful; the integerizer takes floats and does its own rounding later. Probably not your issue, but is a correctness thing.

    2. Grouped weights are labelled as household weights (steps/setup_data_structures.py:288). When GROUP_BY_INCIDENCE_SIGNATURE is on, identical households get collapsed into groups, but the group id gets stored in a column named hh_id. Join that back to households and you get nonsense. This might be what you're seeing, but not sure. Worth a quick test with GROUP_BY_INCIDENCE_SIGNATURE: False

    3. Grouping also truncates the weights to integers (same file, line 270). There's an astype(np.int64) on the whole table that was meant for the join keys, but it hits sample_weight too. Loses 0.3% of total weight in our example, much more if the survey weights are small.

    1 and 3 are easy fixes. 2 is a bigger lift since the column name is being intentionally reused on purpose in a few places. We would need to define a generic column id that isn't overloaded hh_id, but that changes the schema contract for every past weighting, which maybe isn't that bad so long as people don't run first pass with version 0.10 and then run re-pop on version 0.11 or something.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions