Repository navigation
Conversation
LinearAverageRateTaxScale.calc built brackets only between consecutive thresholds, so a base at or above the last threshold fell in no bracket and got tax 0. Brackets (0, 0), (1, 0.1), (2, 0.2) gave [0.025, 0.1, 0.225, 0, 0, 0] at [0.5, 1, 1.5, 2, 2.5, 100]. From the last threshold up the average rate now stays at the last rate. That is what the class already implies elsewhere: bracket_indices puts those bases in the last bracket, to_marginal's top bracket levies the last rate on them, and a scale closed by an infinite threshold (as MarginalRateTaxScale.to_average makes) already gives them that rate. Scales loaded from average_rate parameter brackets have no infinite threshold, so they hit the gap. Adds regression examples, Hypothesis properties (numpy.interp reference, continuity at thresholds, an infinite last threshold changes nothing, agreement with to_marginal) and a changelog fragment. 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.
Problem and caller behavior
For a supported
LinearAverageRateTaxScalewith multiple brackets, finite rates, and a finite final threshold, finite bases at and above that threshold previously returned zero. This PR returnsbase * final_ratethere. For example:The old bracket selection only includes intervals between consecutive thresholds. The added final-rate term handles the missing upper tail. The production implementation remains exactly as adopted at
d7321e598b5df6f3fa1ed5dcaef050d13f475780; this review round changes tests and this description.Finite bases below the final threshold retain the existing interpolation. Multiple-bracket scales still return zero below the first threshold; one-bracket scales still apply their rate to every base. Scales ending with an infinite threshold retain their finite outputs. Conversion algorithms, accepted input and return types, nonfinite-base behavior, and empty-scale behavior remain unchanged. With a first threshold of zero and at least two brackets, conversion agrees with average-scale tax at thresholds and above the final one, up to rounding; within interpolation intervals the average-scale tax is quadratic and the converted marginal-scale tax follows linear secants.
ParameterScale._get_at_instantconstructs affected finite scales fromaverage_ratebrackets; the parameter-constructor regression covers this caller. The prior country-repository survey recorded in this PR found no concrete downstream country caller at the surveyed snapshots. That does not establish that every downstream model output is unchanged.Methodology
Keep the final average rate constant from the final threshold upward. Alternatives are continuing the final interpolation slope or rejecting bases beyond the finite schedule. Existing
to_marginalendpoint and upper-tail behavior and the existing infinite-threshold convention support the constant-rate reading. No statute, regulation, form, or instruction applies to this general engine contract. This PR changes master's zero tax at and above a finite final threshold tobase * final_rate; this review round makes no further production change. This is the explicit public-semantic choice for Max to assess.The 2014 enabled implementation omitted upper-tail handling while retaining conversion to a constant final marginal rate. The earlier commented draft had upper-tail terms, but a different offset; it is historical evidence of omitted handling, not proof of this exact convention.
The historical Landais–Piketty–Saez reform configuration is an example, not statutory authority. Its last two brackets give slope
(0.6 - 0.5) / (100000 - 40000) = 1/600000. By hand, tax at 99999 is99999 * (0.6 - 1/600000) = 59999.233335; this PR gives250000 * 0.6 = 150000above the final threshold. Extending that slope would reach 100% at 340000 and exceed it beyond that base.Invariants
The four Hypothesis properties retain their generated domain: 1–6 brackets, distinct finite integer thresholds from 0 through 1000000 (first threshold at most 1000), and rates in [-1, 1] in increments of 0.001. They cover thresholds, adjacent representable bases, upper-tail bases through 1e9, and random finite bases in [-1e7, 1e7].
base * numpy.interp(base, thresholds, rates)within floating-point tolerance. Below it, tax is zero except for the unchanged one-bracket shortcut.threshold * rate. Tax is continuous at thresholds after the first; the neighbor comparison now bounds the actual movement using local slope and neighbor distance, plus interpolation rounding.(inf, any_finite_rate)leaves finite-base outputs exactly unchanged. For one-bracket scales, this comparison is restricted to bases at or above the first threshold.to_marginal().calcat thresholds and above the final one, within accumulated rounding and threshold-shift error.The tolerance change keeps the existing 1e-6 absolute floor and uses local numerical exposure rather than loosening every comparison globally. All four properties still reject current master's original zero-tail bug with brackets
([0, 1], [0, 0.001]).Review round
Reproduced P2 with valid thresholds
[0, 999999, 1000000]and rates[0, 1, 0]: the old assertions gave 2 failed, 2 passed. Immediately below the last threshold the tax differs by approximately0.0001164153; conversion at that threshold leaves approximately0.0002328306. Added that scale as an explicit Hypothesis example in both affected properties, with hand arithmetic in comments.The continuity bound includes
(threshold - neighbor) * (abs(rate) + abs(neighbor * slope))plus interpolation rounding. The conversion bound iseps * (bracket_count + 8) * sum(abs(marginal_rate) * (lower + min(base, upper)))over active brackets, plus the retained absolute floor. It accounts for the unchanged marginal threshold epsilon shift, conversion, bracket-width arithmetic, and accumulation. Numerical sources are NumPy's documented float64 epsilon and adjacent representable values. The generated domain and production code are unchanged.P3 is corrected below. The public semantics and methodology choice are explicit, and the historical draft mechanism claim is qualified.
Interaction with #588 and master
#588 remains open at
0dcab378b09ebc229d771572de61fa1f47d92802. Its current conversion property already checks independentthreshold * rateexpectations at every threshold andbase * final_rateabove the final threshold. It does not exclude the final threshold or use the faulty averagecalcas its reference. The former paragraph described obsolete coverage and an older head; no current trial-merge test claim is made. That property and this PR's differential property can coexist.Fetched
masteris78a6c503ca6623f2735410412b8ca9e93816ec0d. It has no overlapping tax-scale, parameter-constructor, or tax-scale-test changes since the merge base. No merge or rebase was performed; this round preserves additive branch history.Validation
Each affected test file was run separately, in the foreground, using
uv run pytest -qwith a workspace-local environment and cache:tests/core/tax_scales/test_linear_average_rate_tax_scale.py: 14 passed.tests/core/tax_scales/test_linear_average_rate_tax_scale_property.py: 4 passed before formatting and 4 passed after formatting (seed 596), including both explicit regressions and 300 passing generated examples per property, verified with Hypothesis statistics.git diff --check: passed.The failing reproduction used the prepared read-only dependency environment through
uv run --active --no-sync; passing validation uses the workspace-local environment, Python 3.13, NumPy 2.4.2 and Hypothesis 6.168.3. No full suite or microsimulation was run; CI and the hub cover those.Documentation: the existing
calccontract docstring is rendered by the API reference. Additional durable documentation is not required for this test-only repair. Impact: medium for the PR's public caller behavior change; confidence: high for the tested engine domain. No concrete downstream country impact is established.axiom: n/a: general engine contract, no policy encoding.