Skip to content

Support threshold uprating in scale parameters#507

Merged
MaxGhenis merged 2 commits into
masterfrom
scale-threshold-uprating
Jul 6, 2026
Merged

Support threshold uprating in scale parameters#507
MaxGhenis merged 2 commits into
masterfrom
scale-threshold-uprating

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

Fixes #390.

Problem

Scale (marginal-rate / amount) parameters only uprated thresholds through two placements:

  • the per-leaf form brackets[i].threshold.metadata.uprating, and
  • a scale-wide metadata.uprating combined with metadata.uprate_thresholds: true.

Every other placement was silently dead, so bend points and indexed brackets froze at their last explicit value:

  • bracket-level brackets[i].metadata.uprating (the amount stayed frozen — policyengine-us#8814),
  • the issue's own brackets[i].metadata.threshold.uprating sub-key form,
  • a scale-level metadata.threshold_uprating dedicated to thresholds.

(The controlled experiments confirming which forms worked are in the #390 comments.)

Change

ParameterScaleBracket.propagate_uprating now resolves an uprating entry for each component leaf, and ParameterScale.propagate_uprating passes through the scale-level threshold_uprating. Resolution order per component, highest priority first:

  1. the component leaf's own metadata.uprating (the existing per-leaf form — unchanged);
  2. a bracket per-component override, e.g. brackets[i].metadata.threshold.uprating / brackets[i].metadata.amount.uprating;
  3. a bracket-level default — brackets[i].metadata.uprating for the value side (amount/rate/base/average_rate), and for the threshold either brackets[i].metadata.threshold_uprating or the bare bracket uprating when brackets[i].metadata.uprate_thresholds is set;
  4. the scale-level defaults — metadata.uprating (gated by uprate_thresholds for the threshold) and the new metadata.threshold_uprating for a threshold-only index.

This matches the issue's proposed solution: threshold_uprating in scale metadata, uprating in individual threshold metadata, and both (individual overriding the scale default).

Semantics I had to interpret

The issue and its comments show two shapes for bracket-level metadata: a bare brackets[i].metadata.uprating (expected to uprate the amount, per policyengine-us#8814) and an explicit brackets[i].metadata.threshold.uprating (to uprate the threshold). I read a bare bracket uprating as value-side only — mirroring the existing scale-level rule where a bare uprating never touches thresholds unless uprate_thresholds opts them in. The threshold is therefore reached only via the threshold sub-key, a bracket/scale threshold_uprating, or an uprate_thresholds flag. If a bare bracket uprating were instead meant to hit the threshold too, that is a one-line change — flagging the assumption explicitly.

uprate_parameters already reaches every bracket leaf through get_descendants (locked with a dedicated test), so no change was needed there; the gap was purely in propagation.

Tests

New tests/core/parameters/operations/test_scale_threshold_uprating.py (13 tests): the three previously-working forms are locked as regressions, each newly-supported placement is covered, plus threshold_uprating combined with a distinct amount index, per-leaf override of the scale default, threshold-index leaving amounts frozen, and get_descendants bracket-leaf coverage. Also verified the exact PIA example from the issue body uprates (bend point 1226 → 1284.38 in 2026).

Checks run

  • uv run pytest tests -m "not smoke" → 599 passed, 1 skipped, 1 xfailed.
  • policyengine-core test on the bundled country template → 39 passed.
  • ruff format (clean) and ruff check (passed).
  • Not run: the -m smoke suite (requires network / GitHub microdata token).

Note on overlap with #506

Independent of the sibling PR #506 (issue #505). #506 renames the bracket's component-key tuple from _allowed_keys to _component_keys and adds the common keys for validation; this PR keeps iterating _allowed_keys. Whichever merges second will have a trivial rename conflict in parameter_scale_bracket.py.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits July 6, 2026 06:13
Scale thresholds could only be uprated via the per-leaf form
`brackets[i].threshold.metadata.uprating` or a scale-wide
`uprating`+`uprate_thresholds` pair. Bracket-level metadata and a
threshold-specific scale index were silently ignored, so bend points and
indexed brackets froze at their last explicit value.

Resolve uprating for each bracket component leaf from, in priority order: the
leaf's own metadata; a bracket per-component override
(`brackets[i].metadata.threshold.uprating`); a bracket default
(`brackets[i].metadata.uprating` for the value side, plus
`threshold_uprating`/`uprate_thresholds` for the threshold); and finally the
scale-level defaults, now including `metadata.threshold_uprating` for a
threshold-only index. The working per-leaf form is unchanged and locked with
regression tests.

Fixes #390

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…prating

# Conflicts:
#	policyengine_core/parameters/parameter_scale_bracket.py
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Fable review: TDD reproductions of every broken form, four-level resolution precedence with the value-side/threshold distinction preserved (bare uprating never silently hits thresholds, consistent with scale-level semantics), the documented interpretation of bare bracket uprating flagged for the issue author, and the post-#506 rebase iterates component keys correctly. 608 core tests + 39 country-template green; the issue-body PIA example verified uprating (1226 → 1284.38).

@MaxGhenis MaxGhenis merged commit 0dd716a into master Jul 6, 2026
22 checks passed
@MaxGhenis MaxGhenis deleted the scale-threshold-uprating branch July 6, 2026 11:04
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.

Scale parameter thresholds don't support uprating

1 participant