[#95] Stable amortization for tiny loan rates: expm1/log1p, +Inf books impossible #96

Merged
robertismo merged 1 commit from 94-amort into master 2026-09-30 02:16:50 +00:00
Owner

Closes #95.

The bug

amortPayment(P, r, n) used P*r / (1 - (1+r)^-n). For a legal but tiny positive loan rate (empire's clearing-spread bid landed at exactly 2^-62 in duel seed 87113, inside the [0, cap] clamp), (1+r) rounds to exactly 1.0 in float64, the denominator becomes 0, and every payment on that loan becomes +Inf. The poisoned cash/equity then propagated into AI inputs, the #91 guard failed the other banks (bond-offer-size, bankruptcy-bid), the auction winner booked +Inf bargain gain, and iterations.json silently dropped the run's record (json: unsupported value: +Inf).

Full trace and cascade in #95. Repro: duel preset seed 87113, roster super/empire/mill, master 8ec3eca.

The fix

Rewrite the formula with math.Expm1/math.Log1p: den = -expm1(-n*log1p(r)). For tiny r this is exactly n*r (no cancellation), so the payment lands on P/n instead of +Inf. Belt-and-suspenders: zero/negative denominator and any non-finite result fall back to P/n; r <= 0 keeps the old P/n path; n <= 0 returns P.

Verification

  • New regression tests: TestAmortPaymentTinyRateStable (r = 2^-62, the exact repro value), TestAmortPaymentZeroRate, TestAmortPaymentOrdinaryRate (classic 5% amortization value pinned).
  • go build + go test -count=1 . + go vet ./... all green.
  • Repro spectate (seed 87113, same config + roster): 0 NON-FINITE, 0 DIVERGENCE, 0 AI faults, sane finite final ranking.
  • 7-config gate spectates (base/storm/drought/patient/tight-cash/cash-glut/duel, seed 33): all 0 [ai errors, 0 DIVERGENCE, 0 NON-FINITE, 0 stack overflow.
  • 30x13 bench on the fixed engine: 0 NON-FINITE, 0 DIVERGENCE, 0 faults; iteration record written without error.

Files: game.go (amortPayment) + amort_test.go. Nothing else touched.

Closes #95. ## The bug `amortPayment(P, r, n)` used `P*r / (1 - (1+r)^-n)`. For a legal but tiny positive loan rate (empire's clearing-spread bid landed at exactly 2^-62 in duel seed 87113, inside the [0, cap] clamp), `(1+r)` rounds to exactly 1.0 in float64, the denominator becomes 0, and every payment on that loan becomes +Inf. The poisoned cash/equity then propagated into AI inputs, the #91 guard failed the other banks (bond-offer-size, bankruptcy-bid), the auction winner booked +Inf bargain gain, and iterations.json silently dropped the run's record (`json: unsupported value: +Inf`). Full trace and cascade in #95. Repro: duel preset seed 87113, roster super/empire/mill, master 8ec3eca. ## The fix Rewrite the formula with `math.Expm1`/`math.Log1p`: `den = -expm1(-n*log1p(r))`. For tiny r this is exactly n*r (no cancellation), so the payment lands on P/n instead of +Inf. Belt-and-suspenders: zero/negative denominator and any non-finite result fall back to P/n; r <= 0 keeps the old P/n path; n <= 0 returns P. ## Verification - New regression tests: `TestAmortPaymentTinyRateStable` (r = 2^-62, the exact repro value), `TestAmortPaymentZeroRate`, `TestAmortPaymentOrdinaryRate` (classic 5% amortization value pinned). - `go build` + `go test -count=1 .` + `go vet ./...` all green. - Repro spectate (seed 87113, same config + roster): 0 NON-FINITE, 0 DIVERGENCE, 0 AI faults, sane finite final ranking. - 7-config gate spectates (base/storm/drought/patient/tight-cash/cash-glut/duel, seed 33): all 0 `[ai` errors, 0 DIVERGENCE, 0 NON-FINITE, 0 stack overflow. - 30x13 bench on the fixed engine: 0 NON-FINITE, 0 DIVERGENCE, 0 faults; iteration record written without error. Files: game.go (amortPayment) + amort_test.go. Nothing else touched.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Winnie/bankwars!96
No description provided.