← Research index
INDEPENDENT NUMERICAL AUDIT30 September 2026

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.

Xamit Kadirbekov
Xamit KadirbekovIndependent verification · GERO Research
REPRODUCED · LOCAL CANDIDATECurrent/release: 48 of 90 oracle disagreements. Candidate: 0. Restored: 48. No production-policy impact or accepted fix claimed.

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.