actuarialmath whole_life_annuity ignores benefit b at zero interest
At zero interest, the public method ignores its benefit amount: a Beta(2) example returns 20 instead of 140. Current and release fail 48 of 90 bounded checks; an isolated one-line candidate fails zero.
GitHub report · Evidence archive · Developer issue
Archive SHA-256: 78c8f63ac33d1806b6bc23d4e97fdc3476de0f210c20009e8abcd4ccfc22cf8e. Original English report and executable evidence.
Result
At upstream commit 7d18f11ad304898f177b7922b3c53f70e4c2b4f4 and in the
official PyPI 1.1.0 source, the zero-interest branch of
Annuity.whole_life_annuity returns the expected number of unit payments but
does not multiply by the documented benefit amount b.
if interest > 0:
A = self.whole_life_insurance(x, s=s, discrete=discrete)
return b * (1 - A) / interest
else:
return discrete + self.e_x(x=x, s=s, curtate=discrete)
The positive-interest branch scales by b; the zero-interest branch does not.
Independent example
The verifier constructs the public Annuity class with an explicit survival
law
P(T > t) = ((R - t) / R)^2, 0 <= t < R,
which is a Beta(2) future-lifetime model. This avoids the separate Uniform
and Beta shortcut paths. At R = 60 and benefit b = 7:
continuous EPV = 7 * integral_0^60 ((60-t)/60)^2 dt
= 7 * 20
= 140.
The current method returns 20.0.
For an annual annuity-due:
unit EPV = sum_{k=0}^{59} ((60-k)/60)^2
= (61 * 121) / (6 * 60)
= 20.502777777777...
EPV at b=7 = 143.519444444444...
The current method returns the unit value 20.502777777777773. With b=0,
it still returns the same positive unit value instead of zero.
Candidate correction
- return discrete + self.e_x(x=x, s=s, curtate=discrete)
+ return b * (discrete + self.e_x(x=x, s=s, curtate=discrete))
Executed checks
The verifier uses remaining lifetimes generated by x={10,40,70} and
s={0,5}, both continuous and annual-due payments, and
b={0,1,2,7,1000}.
- 60 zero-interest exact-oracle comparisons;
- 30 positive-interest (
i=0.05) scaling controls; - 90 calls per source variant.
Results:
| Variant | Failures / 90 | Zero-interest failures | Positive-interest failures |
|---|---|---|---|
| Current source | 48 | 48 | 0 |
| PyPI 1.1.0 source | 48 | 48 | 0 |
| Candidate | 0 | 0 | 0 |
| Restored source | 48 | 48 | 0 |
The current, release and restored annuity.py files have the same SHA-256:
3f163f58e6199bcf7c7c9c46cf3da9f779a225dc2756771aa0a91f347f38a99f.
The candidate hash is
6d1f3c8ec3e61895d46717cb14ea990a74258d08aebfbf64cad1b2cf33545ab6.
Exactly 48 numerical rows change: the zero-interest rows with b != 1.
The other 42 rows, including every positive-interest control and every unit
benefit zero-interest control, are bit-identical. Applying the patch to a
fresh source copy reproduces the candidate file; reversing it restores the
original file byte-for-byte.
Novelty and limits
The bounded duplicate review found no exact upstream report or correction. The expression is old and is not described as a new regression. This report does not establish worldwide priority, an insurer deployment, a policyholder loss, a reserve error, a regulatory breach, or security impact.
The checks cover nonnegative constant benefits, zero and one positive interest
rate, one explicit finite-support survival law, and the public convenience
method. They do not prove every annuity method correct and do not cover the
separate discrete Beta.e_x delegation error. No full upstream suite was run.
Runtime: Python 3.12.13, NumPy 2.5.3, SciPy 1.18.1, pandas 3.0.6, macOS arm64, with numerical-library threads limited to one. The investigation and writing were AI-assisted; the stated outputs come from executed source and the retained independent closed-form oracle.
Developer disclosure
The reproducer, derivation, candidate diff and validation totals were sent as actuarialmath issue #10 before broad distribution. No maintainer response, accepted patch or released correction is claimed.
