Skip to content

Accept decode_enums in Microsimulation.calculate_add and calculate_divide - #647

Draft
MaxGhenis wants to merge 4 commits into
masterfrom
fix-microsim-add-divide-decode-enums
Draft

MaxGhenis wants to merge 4 commits into
masterfrom
fix-microsim-add-divide-decode-enums

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

On master, a formula that reads a variable with options=[ADD] or options=[DIVIDE] raises this error in a Microsimulation, though a Simulation computes it:

TypeError: Microsimulation.calculate_add() got an unexpected keyword argument 'decode_enums'

A policyengine-us impact run hit the error on spm_unit("snap", period, options=[ADD]) in mt_liheap_categorical_eligibility.py (PolicyEngine/policyengine-us#10141 (comment)). On policyengine-us main (PolicyEngine/policyengine-us@2b39b88e2f), 21 formulas read with options=[ADD], and each raises the error whenever a Microsimulation computes it.

Cause

Population.__call__ has passed decode_enums=False with every read since 0abe43a (December 2022), and it sends reads with options=[ADD] or options=[DIVIDE] to calculate_add or calculate_divide (population.py#L147-L163). Simulation.calculate_add and calculate_divide take it (simulation.py#L2057-L2062, #L2126-L2131), but Microsimulation overrides both without it (microsimulation.py#L84-L118), so Python rejects the call.

Plain reads go to calculate, which takes the flag. When a formula reads a monthly flow over a year or a yearly flow over a month, Simulation._calculate calls calculate_add or calculate_divide without the flag (simulation.py#L1741-L1753), so those reads kept working.

Fix

Both overrides now take decode_enums: bool = False, Simulation's default, and pass it to super(). Simulation.calculate_add and calculate_divide accept the flag without using it, and the new parameter goes last, so every call that worked before returns the same values.

Invariants and tests

tests/fixtures/microsimulation_option_reads.py adds to the country template nine formulas that read with ADD or DIVIDE. They read person and household variables, sum members' values into households, compare a sum with zero and call the add helper. tests/core/test_microsimulation_option_reads_property.py checks two properties with Hypothesis:

  • A Microsimulation's unweighted reads equal a Simulation's, or both raise the same type of error. Hypothesis generates one to four households of one to three people, with weights and monthly and yearly inputs, and up to six reads in sequence. The reads cover the formulas, the population calls they make, calculate_add and calculate_divide with either value of decode_enums, and plain reads at months, years and a three-month period (200 examples).
  • Each formula returns what the same arithmetic gives in NumPy, in both classes (100 examples).

Example tests in tests/core/test_microsimulation_option_reads.py check hand-computed values for each formula, the weights on a weighted result, seven population calls, and both methods with each value of decode_enums.

On master, 23 of the new tests fail with the TypeError, and Hypothesis shrinks the first property to ("calculate", "allowance_divided", "2022"). On this branch the new tests pass, apart from one strict xfail, and so do the full suite (uv run pytest tests: 2,873 passed, 4 skipped, 4 xfailed) and ruff format --check ..

A policyengine-us Microsimulation hasn't run on this branch, because the machine that built it caps memory for local microsimulation runs. Each of the 21 policyengine-us formulas reaches Microsimulation.calculate_add through Population.__call__, then clips, compares, divides or adds the result, or sums it over a group with spm_unit.sum, .all or add. Those operations return the same values for a MicroSeries as for an array.

Not changed here

  • Simulation.calculate_add and calculate_divide read each term with self.calculate, which in a Microsimulation decodes Enums and applies weights by default (simulation.py#L2093-L2096, #L2152-L2160). So an Enum read with DIVIDE at a year returns labels in a Microsimulation and an EnumArray in a Simulation, and test_enum_read_with_divide_in_a_microsimulation_matches_a_simulation records the difference as a strict xfail. No policyengine-us formula reads an Enum this way: the 21 formulas read 41 variables, all floats.
  • In a Microsimulation these reads return a MicroSeries, as reads that Simulation._calculate sends to calculate_add already do on master. Its .sum() and .mean() apply weights, and none of the 21 formulas calls either on the read.
  • For a simulation class that doesn't subclass core's Microsimulation but has "micro" in its name, such as policyengine-uk's, Population.__call__ passes unweighted=True to the calculate methods. Simulation.calculate_add and calculate_divide don't accept that argument, but on policyengine-uk main (PolicyEngine/policyengine-uk@1d4d020aa) no formula reads with ADD or DIVIDE.

impact: no output moves for any variable that computed before; the 21 PE-US formulas that crashed in Microsimulation now compute

axiom: n/a: core infrastructure fix

🤖 Generated with Claude Code

MaxGhenis and others added 4 commits November 3, 2026 07:16
…vide

Population.__call__ passes decode_enums=False to calculate_add or
calculate_divide for every options=[ADD] or options=[DIVIDE] read in a
formula, as it has since 0abe43a. Simulation's methods take that
argument, but Microsimulation's overrides did not, so every such read
raised a TypeError in a Microsimulation.

Both overrides now take decode_enums, with Simulation's default of
False, and pass it to super(). Simulation.calculate_add and
calculate_divide accept the flag without using it, so every call that
worked before returns the same values.

Example tests run formulas that read with ADD and DIVIDE on a small
country-template system. Hypothesis properties check that a
Microsimulation's unweighted reads equal a Simulation's over generated
data and read sequences, and that formula results equal the same
arithmetic in NumPy. A strict xfail records that an Enum read with
DIVIDE returns labels in a Microsimulation and an EnumArray in a
Simulation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a Microsimulation, calculate_add and calculate_divide return a
MicroSeries, and pandas pairs two Series' values by index label.
GroupPopulation.project labels each member's value with its group's
position. So a formula that projected such a read and combined it with
another Series raised a ValueError, or, when every group had one member
and the data listed them out of group order, paired members with another
group's value. policyengine-us's wi_wheap_categorically_eligible has
that shape (lines 38-44).

Population.__call__ now returns the NumPy array a Simulation returns for
these reads. They raised a TypeError in every Microsimulation on master,
so no value that computed before moves.

The fixture now takes each person's household, so data can list members
in any order, and adds fee_or_all_paid, a reader with the PE-US
formula's shape. The Enum DIVIDE test now records both classes' results
instead of failing as a strict xfail.

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

Population calls and calculate_add and calculate_divide requests now
pair each variable with periods its option accepts, and population calls
read only variables the population can read, so they return values
instead of mostly raising in both classes. The data lists people in any
order. hypothesis.event reports the mix of values, zeros and errors.

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

A new property checks that each population call a formula makes with
ADD or DIVIDE returns a NumPy array in both classes. The NumPy-arithmetic
property now runs for each formula, 25 examples each: with 100 examples
drawn across all ten, one seeded run never drew fee_or_all_paid or
members_grant_divided.

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

This branch has not been deployed

No deployments
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