broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    P = x['principal']
    n = x['months']
    bp = x['rate_bp']
    if bp == 0:
        exact = Fraction(P, n)
    else:
        r = Fraction(bp, 120000)
        exact = P * r / (1 - (1 + r) ** -n)
    if x['round'] == 'up':
        pay = math.floor(exact)
    else:
        pay = math.floor(exact + Fraction(1, 2))
    bal = P
    total_int = 0
    last = 0
    for k in range(1, n + 1):
        q, rem = divmod(bal * bp, 120000)
        interest = q + (1 if 2 * rem >= 120000 else 0)
        total_int += interest
        if k == n:
            last = bal + interest
        else:
            bal = bal + interest - pay
    return {'payment': pay, 'final_payment': last, 'total_interest': total_int}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: round-up mode', {'principal': 7, 'rate_bp': 725, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -1123, 'total_interest': -771}], ['control 1', {'principal': 12345, 'rate_bp': 1200, 'months': 1, 'round': 'nearest'}, {'payment': 12468, 'final_payment': 12468, 'total_interest': 123}], ['control 2', {'principal': 999999, 'rate_bp': 0, 'months': 60, 'round': 'nearest'}, {'payment': 16667, 'final_payment': 16646, 'total_interest': 0}], ['control 3', {'principal': 1000000, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333334, 'total_interest': 0}], ['control 4', {'principal': 1000000, 'rate_bp': 1200, 'months': 360, 'round': 'nearest'}, {'payment': 10286, 'final_payment': 10743, 'total_interest': 2703417}], ['control 5', {'principal': 100000, 'rate_bp': 725, 'months': 360, 'round': 'nearest'}, {'payment': 682, 'final_payment': 923, 'total_interest': 145761}], ['control 6', {'principal': 100000, 'rate_bp': 725, 'months': 36, 'round': 'nearest'}, {'payment': 3099, 'final_payment': 3106, 'total_interest': 11571}]], [['regression: round-up mode', {'principal': 500, 'rate_bp': 13, 'months': 3, 'round': 'up'}, {'payment': 167, 'final_payment': 166, 'total_interest': 0}], ['regression: round-up mode, partial-repair probe', {'principal': 100000, 'rate_bp': 499, 'months': 2, 'round': 'up'}, {'payment': 50313, 'final_payment': 50311, 'total_interest': 624}], ['control 1', {'principal': 2500000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 861250, 'final_payment': 861251, 'total_interest': 83751}], ['control 2', {'principal': 999999, 'rate_bp': 725, 'months': 360, 'round': 'nearest'}, {'payment': 6822, 'final_payment': 6503, 'total_interest': 1455602}], ['control 3', {'principal': 7, 'rate_bp': 0, 'months': 60, 'round': 'nearest'}, {'payment': 0, 'final_payment': 7, 'total_interest': 0}], ['control 4', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 5', {'principal': 999999, 'rate_bp': 0, 'months': 36, 'round': 'nearest'}, {'payment': 27778, 'final_payment': 27769, 'total_interest': 0}]], [['regression: round-up mode', {'principal': 12345, 'rate_bp': 0, 'months': 12, 'round': 'up'}, {'payment': 1029, 'final_payment': 1026, 'total_interest': 0}], ['regression: round-up mode, partial-repair probe', {'principal': 2500000, 'rate_bp': 13, 'months': 360, 'round': 'up'}, {'payment': 7082, 'final_payment': 6755, 'total_interest': 49193}], ['control 1', {'principal': 2500000, 'rate_bp': 13, 'months': 1, 'round': 'nearest'}, {'payment': 2500271, 'final_payment': 2500271, 'total_interest': 271}], ['control 2', {'principal': 100000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 34450, 'final_payment': 34450, 'total_interest': 3350}], ['control 3', {'principal': 999999, 'rate_bp': 0, 'months': 12, 'round': 'nearest'}, {'payment': 83333, 'final_payment': 83336, 'total_interest': 0}], ['control 4', {'principal': 500, 'rate_bp': 1200, 'months': 3, 'round': 'nearest'}, {'payment': 170, 'final_payment': 170, 'total_interest': 10}], ['control 5', {'principal': 1000000, 'rate_bp': 1200, 'months': 7, 'round': 'nearest'}, {'payment': 148628, 'final_payment': 148631, 'total_interest': 40399}]], [['regression: round-up mode', {'principal': 7, 'rate_bp': 13, 'months': 12, 'round': 'up'}, {'payment': 1, 'final_payment': -4, 'total_interest': 0}], ['regression: round-up mode, partial-repair probe', {'principal': 500, 'rate_bp': 725, 'months': 7, 'round': 'up'}, {'payment': 74, 'final_payment': 68, 'total_interest': 12}], ['control 1', {'principal': 500, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 172, 'final_payment': 173, 'total_interest': 17}], ['control 2', {'principal': 7, 'rate_bp': 1999, 'months': 7, 'round': 'nearest'}, {'payment': 1, 'final_payment': 1, 'total_interest': 0}], ['control 3', {'principal': 30000000, 'rate_bp': 0, 'months': 360, 'round': 'nearest'}, {'payment': 83333, 'final_payment': 83453, 'total_interest': 0}], ['control 4', {'principal': 999999, 'rate_bp': 13, 'months': 2, 'round': 'nearest'}, {'payment': 500081, 'final_payment': 500080, 'total_interest': 162}], ['control 5', {'principal': 999999, 'rate_bp': 1200, 'months': 7, 'round': 'nearest'}, {'payment': 148628, 'final_payment': 148630, 'total_interest': 40399}]], [['regression: round-up mode', {'principal': 7, 'rate_bp': 600, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -876, 'total_interest': -524}], ['control 1', {'principal': 30000000, 'rate_bp': 499, 'months': 2, 'round': 'nearest'}, {'payment': 15093627, 'final_payment': 15093627, 'total_interest': 187254}], ['control 2', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 3', {'principal': 500, 'rate_bp': 0, 'months': 2, 'round': 'up'}, {'payment': 250, 'final_payment': 250, 'total_interest': 0}], ['control 4', {'principal': 2500000, 'rate_bp': 499, 'months': 1, 'round': 'nearest'}, {'payment': 2510396, 'final_payment': 2510396, 'total_interest': 10396}], ['control 5', {'principal': 2500000, 'rate_bp': 13, 'months': 360, 'round': 'nearest'}, {'payment': 7081, 'final_payment': 7125, 'total_interest': 49204}], ['control 6', {'principal': 999999, 'rate_bp': 725, 'months': 12, 'round': 'nearest'}, {'payment': 86642, 'final_payment': 86643, 'total_interest': 39706}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    P = x['principal']
    n = x['months']
    bp = x['rate_bp']
    if bp == 0:
        exact = Fraction(P, n)
    else:
        r = Fraction(bp, 120000)
        exact = P * r / (1 - (1 + r) ** -n)
    if x['round'] == 'up':
        pay = round(exact)
    else:
        pay = math.floor(exact + Fraction(1, 2))
    bal = P
    total_int = 0
    last = 0
    for k in range(1, n + 1):
        q, rem = divmod(bal * bp, 120000)
        interest = q + (1 if 2 * rem >= 120000 else 0)
        total_int += interest
        if k == n:
            last = bal + interest
        else:
            bal = bal + interest - pay
    return {'payment': pay, 'final_payment': last, 'total_interest': total_int}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: round-up mode', {'principal': 7, 'rate_bp': 725, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -1123, 'total_interest': -771}], ['control 1', {'principal': 12345, 'rate_bp': 1200, 'months': 1, 'round': 'nearest'}, {'payment': 12468, 'final_payment': 12468, 'total_interest': 123}], ['control 2', {'principal': 999999, 'rate_bp': 0, 'months': 60, 'round': 'nearest'}, {'payment': 16667, 'final_payment': 16646, 'total_interest': 0}], ['control 3', {'principal': 1000000, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333334, 'total_interest': 0}], ['control 4', {'principal': 1000000, 'rate_bp': 1200, 'months': 360, 'round': 'nearest'}, {'payment': 10286, 'final_payment': 10743, 'total_interest': 2703417}], ['control 5', {'principal': 100000, 'rate_bp': 725, 'months': 360, 'round': 'nearest'}, {'payment': 682, 'final_payment': 923, 'total_interest': 145761}], ['control 6', {'principal': 100000, 'rate_bp': 725, 'months': 36, 'round': 'nearest'}, {'payment': 3099, 'final_payment': 3106, 'total_interest': 11571}]], [['regression: round-up mode', {'principal': 500, 'rate_bp': 13, 'months': 3, 'round': 'up'}, {'payment': 167, 'final_payment': 166, 'total_interest': 0}], ['regression: round-up mode, partial-repair probe', {'principal': 100000, 'rate_bp': 499, 'months': 2, 'round': 'up'}, {'payment': 50313, 'final_payment': 50311, 'total_interest': 624}], ['control 1', {'principal': 2500000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 861250, 'final_payment': 861251, 'total_interest': 83751}], ['control 2', {'principal': 999999, 'rate_bp': 725, 'months': 360, 'round': 'nearest'}, {'payment': 6822, 'final_payment': 6503, 'total_interest': 1455602}], ['control 3', {'principal': 7, 'rate_bp': 0, 'months': 60, 'round': 'nearest'}, {'payment': 0, 'final_payment': 7, 'total_interest': 0}], ['control 4', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 5', {'principal': 999999, 'rate_bp': 0, 'months': 36, 'round': 'nearest'}, {'payment': 27778, 'final_payment': 27769, 'total_interest': 0}]], [['regression: round-up mode', {'principal': 12345, 'rate_bp': 0, 'months': 12, 'round': 'up'}, {'payment': 1029, 'final_payment': 1026, 'total_interest': 0}], ['regression: round-up mode, partial-repair probe', {'principal': 2500000, 'rate_bp': 13, 'months': 360, 'round': 'up'}, {'payment': 7082, 'final_payment': 6755, 'total_interest': 49193}], ['control 1', {'principal': 2500000, 'rate_bp': 13, 'months': 1, 'round': 'nearest'}, {'payment': 2500271, 'final_payment': 2500271, 'total_interest': 271}], ['control 2', {'principal': 100000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 34450, 'final_payment': 34450, 'total_interest': 3350}], ['control 3', {'principal': 999999, 'rate_bp': 0, 'months': 12, 'round': 'nearest'}, {'payment': 83333, 'final_payment': 83336, 'total_interest': 0}], ['control 4', {'principal': 500, 'rate_bp': 1200, 'months': 3, 'round': 'nearest'}, {'payment': 170, 'final_payment': 170, 'total_interest': 10}], ['control 5', {'principal': 1000000, 'rate_bp': 1200, 'months': 7, 'round': 'nearest'}, {'payment': 148628, 'final_payment': 148631, 'total_interest': 40399}]], [['regression: round-up mode', {'principal': 7, 'rate_bp': 13, 'months': 12, 'round': 'up'}, {'payment': 1, 'final_payment': -4, 'total_interest': 0}], ['regression: round-up mode, partial-repair probe', {'principal': 500, 'rate_bp': 725, 'months': 7, 'round': 'up'}, {'payment': 74, 'final_payment': 68, 'total_interest': 12}], ['control 1', {'principal': 500, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 172, 'final_payment': 173, 'total_interest': 17}], ['control 2', {'principal': 7, 'rate_bp': 1999, 'months': 7, 'round': 'nearest'}, {'payment': 1, 'final_payment': 1, 'total_interest': 0}], ['control 3', {'principal': 30000000, 'rate_bp': 0, 'months': 360, 'round': 'nearest'}, {'payment': 83333, 'final_payment': 83453, 'total_interest': 0}], ['control 4', {'principal': 999999, 'rate_bp': 13, 'months': 2, 'round': 'nearest'}, {'payment': 500081, 'final_payment': 500080, 'total_interest': 162}], ['control 5', {'principal': 999999, 'rate_bp': 1200, 'months': 7, 'round': 'nearest'}, {'payment': 148628, 'final_payment': 148630, 'total_interest': 40399}]], [['regression: round-up mode', {'principal': 7, 'rate_bp': 600, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -876, 'total_interest': -524}], ['control 1', {'principal': 30000000, 'rate_bp': 499, 'months': 2, 'round': 'nearest'}, {'payment': 15093627, 'final_payment': 15093627, 'total_interest': 187254}], ['control 2', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 3', {'principal': 500, 'rate_bp': 0, 'months': 2, 'round': 'up'}, {'payment': 250, 'final_payment': 250, 'total_interest': 0}], ['control 4', {'principal': 2500000, 'rate_bp': 499, 'months': 1, 'round': 'nearest'}, {'payment': 2510396, 'final_payment': 2510396, 'total_interest': 10396}], ['control 5', {'principal': 2500000, 'rate_bp': 13, 'months': 360, 'round': 'nearest'}, {'payment': 7081, 'final_payment': 7125, 'total_interest': 49204}], ['control 6', {'principal': 999999, 'rate_bp': 725, 'months': 12, 'round': 'nearest'}, {'payment': 86642, 'final_payment': 86643, 'total_interest': 39706}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)

prompt:
Loans configured to round the payment up get a payment rounded down.

x = {'principal', 'rate_bp' (annual, monthly rate = bp/120000), 'months', 'round': 'nearest'|'up'}. The exact annuity payment P*r/(1-(1+r)^-n) (P/n at zero rate) is rounded half-up ('nearest') or up to the cent. Monthly interest is round_half_up(balance*bp/120000); months 1..n-1 pay the level payment and month n pays balance + interest. Return {'payment', 'final_payment', 'total_interest'}.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    P = x['principal']
    n = x['months']
    bp = x['rate_bp']
    if bp == 0:
        exact = Fraction(P, n)
    else:
        r = Fraction(bp, 120000)
        exact = P * r / (1 - (1 + r) ** -n)
    if x['round'] == 'up':
        pay = math.ceil(exact)
    else:
        pay = round(exact)
    bal = P
    total_int = 0
    last = 0
    for k in range(1, n + 1):
        q, rem = divmod(bal * bp, 120000)
        interest = q + (1 if 2 * rem >= 120000 else 0)
        total_int += interest
        if k == n:
            last = bal + interest
        else:
            bal = bal + interest - pay
    return {'payment': pay, 'final_payment': last, 'total_interest': total_int}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: interest-free half-cent tie', {'principal': 1001, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 501, 'final_payment': 500, 'total_interest': 0}], ['regression: nearest tie rule', {'principal': 500, 'rate_bp': 600, 'months': 1, 'round': 'nearest'}, {'payment': 503, 'final_payment': 503, 'total_interest': 3}], ['control 1', {'principal': 7, 'rate_bp': 725, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -1123, 'total_interest': -771}], ['control 2', {'principal': 1000000, 'rate_bp': 13, 'months': 36, 'round': 'up'}, {'payment': 27834, 'final_payment': 27808, 'total_interest': 1998}], ['control 3', {'principal': 12345, 'rate_bp': 1200, 'months': 1, 'round': 'nearest'}, {'payment': 12468, 'final_payment': 12468, 'total_interest': 123}], ['control 4', {'principal': 999999, 'rate_bp': 0, 'months': 60, 'round': 'nearest'}, {'payment': 16667, 'final_payment': 16646, 'total_interest': 0}], ['control 5', {'principal': 30000000, 'rate_bp': 0, 'months': 36, 'round': 'up'}, {'payment': 833334, 'final_payment': 833310, 'total_interest': 0}]], [['regression: interest-free half-cent tie', {'principal': 1001, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 501, 'final_payment': 500, 'total_interest': 0}], ['regression: nearest tie rule', {'principal': 500, 'rate_bp': 600, 'months': 1, 'round': 'nearest'}, {'payment': 503, 'final_payment': 503, 'total_interest': 3}], ['control 1', {'principal': 500, 'rate_bp': 13, 'months': 3, 'round': 'up'}, {'payment': 167, 'final_payment': 166, 'total_interest': 0}], ['control 2', {'principal': 2500000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 861250, 'final_payment': 861251, 'total_interest': 83751}], ['control 3', {'principal': 999999, 'rate_bp': 725, 'months': 360, 'round': 'nearest'}, {'payment': 6822, 'final_payment': 6503, 'total_interest': 1455602}], ['control 4', {'principal': 7, 'rate_bp': 0, 'months': 60, 'round': 'nearest'}, {'payment': 0, 'final_payment': 7, 'total_interest': 0}], ['control 5', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}]], [['regression: interest-free half-cent tie', {'principal': 1001, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 501, 'final_payment': 500, 'total_interest': 0}], ['control 1', {'principal': 2500000, 'rate_bp': 13, 'months': 1, 'round': 'nearest'}, {'payment': 2500271, 'final_payment': 2500271, 'total_interest': 271}], ['control 2', {'principal': 100000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 34450, 'final_payment': 34450, 'total_interest': 3350}], ['control 3', {'principal': 999999, 'rate_bp': 0, 'months': 12, 'round': 'nearest'}, {'payment': 83333, 'final_payment': 83336, 'total_interest': 0}], ['control 4', {'principal': 12345, 'rate_bp': 0, 'months': 12, 'round': 'up'}, {'payment': 1029, 'final_payment': 1026, 'total_interest': 0}], ['control 5', {'principal': 1000000, 'rate_bp': 0, 'months': 360, 'round': 'up'}, {'payment': 2778, 'final_payment': 2698, 'total_interest': 0}], ['control 6', {'principal': 999999, 'rate_bp': 1999, 'months': 3, 'round': 'up'}, {'payment': 344500, 'final_payment': 344499, 'total_interest': 33500}]], [['regression: interest-free half-cent tie', {'principal': 1001, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 501, 'final_payment': 500, 'total_interest': 0}], ['regression: nearest tie rule', {'principal': 12345, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 6173, 'final_payment': 6172, 'total_interest': 0}], ['control 1', {'principal': 7, 'rate_bp': 13, 'months': 12, 'round': 'up'}, {'payment': 1, 'final_payment': -4, 'total_interest': 0}], ['control 2', {'principal': 1000000, 'rate_bp': 13, 'months': 60, 'round': 'up'}, {'payment': 16722, 'final_payment': 16710, 'total_interest': 3308}], ['control 3', {'principal': 500, 'rate_bp': 725, 'months': 7, 'round': 'up'}, {'payment': 74, 'final_payment': 68, 'total_interest': 12}], ['control 4', {'principal': 7, 'rate_bp': 725, 'months': 7, 'round': 'up'}, {'payment': 2, 'final_payment': -5, 'total_interest': 0}], ['control 5', {'principal': 100000, 'rate_bp': 1999, 'months': 60, 'round': 'up'}, {'payment': 2649, 'final_payment': 2626, 'total_interest': 58917}]], [['regression: interest-free half-cent tie', {'principal': 1001, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 501, 'final_payment': 500, 'total_interest': 0}], ['regression: nearest tie rule', {'principal': 500, 'rate_bp': 600, 'months': 1, 'round': 'nearest'}, {'payment': 503, 'final_payment': 503, 'total_interest': 3}], ['control 1', {'principal': 30000000, 'rate_bp': 499, 'months': 2, 'round': 'nearest'}, {'payment': 15093627, 'final_payment': 15093627, 'total_interest': 187254}], ['control 2', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 3', {'principal': 500, 'rate_bp': 0, 'months': 2, 'round': 'up'}, {'payment': 250, 'final_payment': 250, 'total_interest': 0}], ['control 4', {'principal': 7, 'rate_bp': 600, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -876, 'total_interest': -524}], ['control 5', {'principal': 100000, 'rate_bp': 499, 'months': 60, 'round': 'up'}, {'payment': 1887, 'final_payment': 1866, 'total_interest': 13199}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)

meaning: Fixture-count band, not measured model difficulty

deps: stdlib

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    P = x['principal']
    n = x['months']
    bp = x['rate_bp']
    if bp == 0:
        exact = Fraction(P, n)
    else:
        r = Fraction(bp, 120000)
        exact = P * r / (1 - (1 + r) ** -n)
    if x['round'] == 'up':
        pay = math.ceil(exact)
    else:
        pay = math.floor(exact)
    bal = P
    total_int = 0
    last = 0
    for k in range(1, n + 1):
        q, rem = divmod(bal * bp, 120000)
        interest = q + (1 if 2 * rem >= 120000 else 0)
        total_int += interest
        if k == n:
            last = bal + interest
        else:
            bal = bal + interest - pay
    return {'payment': pay, 'final_payment': last, 'total_interest': total_int}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: interest-free half-cent tie', {'principal': 1001, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 501, 'final_payment': 500, 'total_interest': 0}], ['regression: nearest tie rule', {'principal': 500, 'rate_bp': 600, 'months': 1, 'round': 'nearest'}, {'payment': 503, 'final_payment': 503, 'total_interest': 3}], ['control 1', {'principal': 7, 'rate_bp': 725, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -1123, 'total_interest': -771}], ['control 2', {'principal': 1000000, 'rate_bp': 13, 'months': 36, 'round': 'up'}, {'payment': 27834, 'final_payment': 27808, 'total_interest': 1998}], ['control 3', {'principal': 12345, 'rate_bp': 1200, 'months': 1, 'round': 'nearest'}, {'payment': 12468, 'final_payment': 12468, 'total_interest': 123}], ['control 4', {'principal': 999999, 'rate_bp': 0, 'months': 60, 'round': 'nearest'}, {'payment': 16667, 'final_payment': 16646, 'total_interest': 0}], ['control 5', {'principal': 30000000, 'rate_bp': 0, 'months': 36, 'round': 'up'}, {'payment': 833334, 'final_payment': 833310, 'total_interest': 0}]], [['regression: interest-free half-cent tie', {'principal': 1001, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 501, 'final_payment': 500, 'total_interest': 0}], ['regression: nearest tie rule', {'principal': 500, 'rate_bp': 600, 'months': 1, 'round': 'nearest'}, {'payment': 503, 'final_payment': 503, 'total_interest': 3}], ['control 1', {'principal': 500, 'rate_bp': 13, 'months': 3, 'round': 'up'}, {'payment': 167, 'final_payment': 166, 'total_interest': 0}], ['control 2', {'principal': 2500000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 861250, 'final_payment': 861251, 'total_interest': 83751}], ['control 3', {'principal': 999999, 'rate_bp': 725, 'months': 360, 'round': 'nearest'}, {'payment': 6822, 'final_payment': 6503, 'total_interest': 1455602}], ['control 4', {'principal': 7, 'rate_bp': 0, 'months': 60, 'round': 'nearest'}, {'payment': 0, 'final_payment': 7, 'total_interest': 0}], ['control 5', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}]], [['regression: interest-free half-cent tie', {'principal': 1001, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 501, 'final_payment': 500, 'total_interest': 0}], ['control 1', {'principal': 2500000, 'rate_bp': 13, 'months': 1, 'round': 'nearest'}, {'payment': 2500271, 'final_payment': 2500271, 'total_interest': 271}], ['control 2', {'principal': 100000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 34450, 'final_payment': 34450, 'total_interest': 3350}], ['control 3', {'principal': 999999, 'rate_bp': 0, 'months': 12, 'round': 'nearest'}, {'payment': 83333, 'final_payment': 83336, 'total_interest': 0}], ['control 4', {'principal': 12345, 'rate_bp': 0, 'months': 12, 'round': 'up'}, {'payment': 1029, 'final_payment': 1026, 'total_interest': 0}], ['control 5', {'principal': 1000000, 'rate_bp': 0, 'months': 360, 'round': 'up'}, {'payment': 2778, 'final_payment': 2698, 'total_interest': 0}], ['control 6', {'principal': 999999, 'rate_bp': 1999, 'months': 3, 'round': 'up'}, {'payment': 344500, 'final_payment': 344499, 'total_interest': 33500}]], [['regression: interest-free half-cent tie', {'principal': 1001, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 501, 'final_payment': 500, 'total_interest': 0}], ['regression: nearest tie rule', {'principal': 12345, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 6173, 'final_payment': 6172, 'total_interest': 0}], ['control 1', {'principal': 7, 'rate_bp': 13, 'months': 12, 'round': 'up'}, {'payment': 1, 'final_payment': -4, 'total_interest': 0}], ['control 2', {'principal': 1000000, 'rate_bp': 13, 'months': 60, 'round': 'up'}, {'payment': 16722, 'final_payment': 16710, 'total_interest': 3308}], ['control 3', {'principal': 500, 'rate_bp': 725, 'months': 7, 'round': 'up'}, {'payment': 74, 'final_payment': 68, 'total_interest': 12}], ['control 4', {'principal': 7, 'rate_bp': 725, 'months': 7, 'round': 'up'}, {'payment': 2, 'final_payment': -5, 'total_interest': 0}], ['control 5', {'principal': 100000, 'rate_bp': 1999, 'months': 60, 'round': 'up'}, {'payment': 2649, 'final_payment': 2626, 'total_interest': 58917}]], [['regression: interest-free half-cent tie', {'principal': 1001, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 501, 'final_payment': 500, 'total_interest': 0}], ['regression: nearest tie rule', {'principal': 500, 'rate_bp': 600, 'months': 1, 'round': 'nearest'}, {'payment': 503, 'final_payment': 503, 'total_interest': 3}], ['control 1', {'principal': 30000000, 'rate_bp': 499, 'months': 2, 'round': 'nearest'}, {'payment': 15093627, 'final_payment': 15093627, 'total_interest': 187254}], ['control 2', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 3', {'principal': 500, 'rate_bp': 0, 'months': 2, 'round': 'up'}, {'payment': 250, 'final_payment': 250, 'total_interest': 0}], ['control 4', {'principal': 7, 'rate_bp': 600, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -876, 'total_interest': -524}], ['control 5', {'principal': 100000, 'rate_bp': 499, 'months': 60, 'round': 'up'}, {'payment': 1887, 'final_payment': 1866, 'total_interest': 13199}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)

prompt:
An exact half-cent payment is rounded down on even cents.

x = {'principal', 'rate_bp' (annual, monthly rate = bp/120000), 'months', 'round': 'nearest'|'up'}. The exact annuity payment P*r/(1-(1+r)^-n) (P/n at zero rate) is rounded half-up ('nearest') or up to the cent. Monthly interest is round_half_up(balance*bp/120000); months 1..n-1 pay the level payment and month n pays balance + interest. Return {'payment', 'final_payment', 'total_interest'}.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    P = x['principal']
    n = x['months']
    bp = x['rate_bp']
    if bp == 0:
        exact = Fraction(P, n)
    else:
        r = Fraction(bp, 120000)
        exact = P * r / (1 - (1 + r) ** -n)
    if x['round'] == 'up':
        pay = math.ceil(exact)
    else:
        pay = math.floor(exact + Fraction(1, 2))
    bal = P
    total_int = 0
    last = 0
    for k in range(1, n + 1):
        q, rem = divmod(bal * bp, 120000)
        interest = q
        total_int += interest
        if k == n:
            last = bal + interest
        else:
            bal = bal + interest - pay
    return {'payment': pay, 'final_payment': last, 'total_interest': total_int}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: monthly interest rounding', {'principal': 7, 'rate_bp': 725, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -1123, 'total_interest': -771}], ['control 1', {'principal': 1000000, 'rate_bp': 13, 'months': 36, 'round': 'up'}, {'payment': 27834, 'final_payment': 27808, 'total_interest': 1998}], ['control 2', {'principal': 12345, 'rate_bp': 1200, 'months': 1, 'round': 'nearest'}, {'payment': 12468, 'final_payment': 12468, 'total_interest': 123}], ['control 3', {'principal': 999999, 'rate_bp': 0, 'months': 60, 'round': 'nearest'}, {'payment': 16667, 'final_payment': 16646, 'total_interest': 0}], ['control 4', {'principal': 30000000, 'rate_bp': 0, 'months': 36, 'round': 'up'}, {'payment': 833334, 'final_payment': 833310, 'total_interest': 0}], ['control 5', {'principal': 1000000, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333334, 'total_interest': 0}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 360, 'round': 'up'}, {'payment': 278, 'final_payment': 198, 'total_interest': 0}]], [['regression: monthly interest rounding', {'principal': 2500000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 861250, 'final_payment': 861251, 'total_interest': 83751}], ['regression: monthly interest rounding, partial-repair probe', {'principal': 500, 'rate_bp': 13, 'months': 3, 'round': 'up'}, {'payment': 167, 'final_payment': 166, 'total_interest': 0}], ['control 1', {'principal': 7, 'rate_bp': 0, 'months': 60, 'round': 'nearest'}, {'payment': 0, 'final_payment': 7, 'total_interest': 0}], ['control 2', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 3', {'principal': 999999, 'rate_bp': 0, 'months': 36, 'round': 'nearest'}, {'payment': 27778, 'final_payment': 27769, 'total_interest': 0}], ['control 4', {'principal': 500, 'rate_bp': 13, 'months': 12, 'round': 'nearest'}, {'payment': 42, 'final_payment': 38, 'total_interest': 0}], ['control 5', {'principal': 30000000, 'rate_bp': 0, 'months': 7, 'round': 'nearest'}, {'payment': 4285714, 'final_payment': 4285716, 'total_interest': 0}]], [['regression: monthly interest rounding', {'principal': 2500000, 'rate_bp': 13, 'months': 1, 'round': 'nearest'}, {'payment': 2500271, 'final_payment': 2500271, 'total_interest': 271}], ['regression: monthly interest rounding, partial-repair probe', {'principal': 100000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 34450, 'final_payment': 34450, 'total_interest': 3350}], ['control 1', {'principal': 999999, 'rate_bp': 0, 'months': 12, 'round': 'nearest'}, {'payment': 83333, 'final_payment': 83336, 'total_interest': 0}], ['control 2', {'principal': 12345, 'rate_bp': 0, 'months': 12, 'round': 'up'}, {'payment': 1029, 'final_payment': 1026, 'total_interest': 0}], ['control 3', {'principal': 1000000, 'rate_bp': 0, 'months': 360, 'round': 'up'}, {'payment': 2778, 'final_payment': 2698, 'total_interest': 0}], ['control 4', {'principal': 500, 'rate_bp': 0, 'months': 7, 'round': 'nearest'}, {'payment': 71, 'final_payment': 74, 'total_interest': 0}], ['control 5', {'principal': 999999, 'rate_bp': 0, 'months': 12, 'round': 'up'}, {'payment': 83334, 'final_payment': 83325, 'total_interest': 0}]], [['regression: monthly interest rounding', {'principal': 7, 'rate_bp': 13, 'months': 12, 'round': 'up'}, {'payment': 1, 'final_payment': -4, 'total_interest': 0}], ['control 1', {'principal': 7, 'rate_bp': 1999, 'months': 7, 'round': 'nearest'}, {'payment': 1, 'final_payment': 1, 'total_interest': 0}], ['control 2', {'principal': 30000000, 'rate_bp': 600, 'months': 2, 'round': 'up'}, {'payment': 15112594, 'final_payment': 15112593, 'total_interest': 225187}], ['control 3', {'principal': 30000000, 'rate_bp': 0, 'months': 360, 'round': 'nearest'}, {'payment': 83333, 'final_payment': 83453, 'total_interest': 0}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'months': 3, 'round': 'up'}, {'payment': 33668, 'final_payment': 33665, 'total_interest': 1001}], ['control 5', {'principal': 500, 'rate_bp': 13, 'months': 36, 'round': 'up'}, {'payment': 14, 'final_payment': 10, 'total_interest': 0}], ['control 6', {'principal': 999999, 'rate_bp': 13, 'months': 2, 'round': 'nearest'}, {'payment': 500081, 'final_payment': 500080, 'total_interest': 162}]], [['regression: monthly interest rounding', {'principal': 7, 'rate_bp': 600, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -876, 'total_interest': -524}], ['control 1', {'principal': 30000000, 'rate_bp': 499, 'months': 2, 'round': 'nearest'}, {'payment': 15093627, 'final_payment': 15093627, 'total_interest': 187254}], ['control 2', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 3', {'principal': 500, 'rate_bp': 0, 'months': 2, 'round': 'up'}, {'payment': 250, 'final_payment': 250, 'total_interest': 0}], ['control 4', {'principal': 7, 'rate_bp': 600, 'months': 60, 'round': 'nearest'}, {'payment': 0, 'final_payment': 7, 'total_interest': 0}], ['control 5', {'principal': 500, 'rate_bp': 0, 'months': 12, 'round': 'up'}, {'payment': 42, 'final_payment': 38, 'total_interest': 0}], ['control 6', {'principal': 500, 'rate_bp': 0, 'months': 12, 'round': 'nearest'}, {'payment': 42, 'final_payment': 38, 'total_interest': 0}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    P = x['principal']
    n = x['months']
    bp = x['rate_bp']
    if bp == 0:
        exact = Fraction(P, n)
    else:
        r = Fraction(bp, 120000)
        exact = P * r / (1 - (1 + r) ** -n)
    if x['round'] == 'up':
        pay = math.ceil(exact)
    else:
        pay = math.floor(exact + Fraction(1, 2))
    bal = P
    total_int = 0
    last = 0
    for k in range(1, n + 1):
        q, rem = divmod(bal * bp, 120000)
        interest = q + (1 if rem else 0)
        total_int += interest
        if k == n:
            last = bal + interest
        else:
            bal = bal + interest - pay
    return {'payment': pay, 'final_payment': last, 'total_interest': total_int}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: monthly interest rounding', {'principal': 7, 'rate_bp': 725, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -1123, 'total_interest': -771}], ['control 1', {'principal': 1000000, 'rate_bp': 13, 'months': 36, 'round': 'up'}, {'payment': 27834, 'final_payment': 27808, 'total_interest': 1998}], ['control 2', {'principal': 12345, 'rate_bp': 1200, 'months': 1, 'round': 'nearest'}, {'payment': 12468, 'final_payment': 12468, 'total_interest': 123}], ['control 3', {'principal': 999999, 'rate_bp': 0, 'months': 60, 'round': 'nearest'}, {'payment': 16667, 'final_payment': 16646, 'total_interest': 0}], ['control 4', {'principal': 30000000, 'rate_bp': 0, 'months': 36, 'round': 'up'}, {'payment': 833334, 'final_payment': 833310, 'total_interest': 0}], ['control 5', {'principal': 1000000, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333334, 'total_interest': 0}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 360, 'round': 'up'}, {'payment': 278, 'final_payment': 198, 'total_interest': 0}]], [['regression: monthly interest rounding', {'principal': 2500000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 861250, 'final_payment': 861251, 'total_interest': 83751}], ['regression: monthly interest rounding, partial-repair probe', {'principal': 500, 'rate_bp': 13, 'months': 3, 'round': 'up'}, {'payment': 167, 'final_payment': 166, 'total_interest': 0}], ['control 1', {'principal': 7, 'rate_bp': 0, 'months': 60, 'round': 'nearest'}, {'payment': 0, 'final_payment': 7, 'total_interest': 0}], ['control 2', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 3', {'principal': 999999, 'rate_bp': 0, 'months': 36, 'round': 'nearest'}, {'payment': 27778, 'final_payment': 27769, 'total_interest': 0}], ['control 4', {'principal': 500, 'rate_bp': 13, 'months': 12, 'round': 'nearest'}, {'payment': 42, 'final_payment': 38, 'total_interest': 0}], ['control 5', {'principal': 30000000, 'rate_bp': 0, 'months': 7, 'round': 'nearest'}, {'payment': 4285714, 'final_payment': 4285716, 'total_interest': 0}]], [['regression: monthly interest rounding', {'principal': 2500000, 'rate_bp': 13, 'months': 1, 'round': 'nearest'}, {'payment': 2500271, 'final_payment': 2500271, 'total_interest': 271}], ['regression: monthly interest rounding, partial-repair probe', {'principal': 100000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 34450, 'final_payment': 34450, 'total_interest': 3350}], ['control 1', {'principal': 999999, 'rate_bp': 0, 'months': 12, 'round': 'nearest'}, {'payment': 83333, 'final_payment': 83336, 'total_interest': 0}], ['control 2', {'principal': 12345, 'rate_bp': 0, 'months': 12, 'round': 'up'}, {'payment': 1029, 'final_payment': 1026, 'total_interest': 0}], ['control 3', {'principal': 1000000, 'rate_bp': 0, 'months': 360, 'round': 'up'}, {'payment': 2778, 'final_payment': 2698, 'total_interest': 0}], ['control 4', {'principal': 500, 'rate_bp': 0, 'months': 7, 'round': 'nearest'}, {'payment': 71, 'final_payment': 74, 'total_interest': 0}], ['control 5', {'principal': 999999, 'rate_bp': 0, 'months': 12, 'round': 'up'}, {'payment': 83334, 'final_payment': 83325, 'total_interest': 0}]], [['regression: monthly interest rounding', {'principal': 7, 'rate_bp': 13, 'months': 12, 'round': 'up'}, {'payment': 1, 'final_payment': -4, 'total_interest': 0}], ['control 1', {'principal': 7, 'rate_bp': 1999, 'months': 7, 'round': 'nearest'}, {'payment': 1, 'final_payment': 1, 'total_interest': 0}], ['control 2', {'principal': 30000000, 'rate_bp': 600, 'months': 2, 'round': 'up'}, {'payment': 15112594, 'final_payment': 15112593, 'total_interest': 225187}], ['control 3', {'principal': 30000000, 'rate_bp': 0, 'months': 360, 'round': 'nearest'}, {'payment': 83333, 'final_payment': 83453, 'total_interest': 0}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'months': 3, 'round': 'up'}, {'payment': 33668, 'final_payment': 33665, 'total_interest': 1001}], ['control 5', {'principal': 500, 'rate_bp': 13, 'months': 36, 'round': 'up'}, {'payment': 14, 'final_payment': 10, 'total_interest': 0}], ['control 6', {'principal': 999999, 'rate_bp': 13, 'months': 2, 'round': 'nearest'}, {'payment': 500081, 'final_payment': 500080, 'total_interest': 162}]], [['regression: monthly interest rounding', {'principal': 7, 'rate_bp': 600, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -876, 'total_interest': -524}], ['control 1', {'principal': 30000000, 'rate_bp': 499, 'months': 2, 'round': 'nearest'}, {'payment': 15093627, 'final_payment': 15093627, 'total_interest': 187254}], ['control 2', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 3', {'principal': 500, 'rate_bp': 0, 'months': 2, 'round': 'up'}, {'payment': 250, 'final_payment': 250, 'total_interest': 0}], ['control 4', {'principal': 7, 'rate_bp': 600, 'months': 60, 'round': 'nearest'}, {'payment': 0, 'final_payment': 7, 'total_interest': 0}], ['control 5', {'principal': 500, 'rate_bp': 0, 'months': 12, 'round': 'up'}, {'payment': 42, 'final_payment': 38, 'total_interest': 0}], ['control 6', {'principal': 500, 'rate_bp': 0, 'months': 12, 'round': 'nearest'}, {'payment': 42, 'final_payment': 38, 'total_interest': 0}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)

prompt:
Total interest is a few cents low across the schedule.

x = {'principal', 'rate_bp' (annual, monthly rate = bp/120000), 'months', 'round': 'nearest'|'up'}. The exact annuity payment P*r/(1-(1+r)^-n) (P/n at zero rate) is rounded half-up ('nearest') or up to the cent. Monthly interest is round_half_up(balance*bp/120000); months 1..n-1 pay the level payment and month n pays balance + interest. Return {'payment', 'final_payment', 'total_interest'}.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    P = x['principal']
    n = x['months']
    bp = x['rate_bp']
    if bp == 0:
        exact = Fraction(P, n)
    else:
        r = Fraction(bp, 120000)
        exact = P * r / (1 - (1 + r) ** -n)
    if x['round'] == 'up':
        pay = math.ceil(exact)
    else:
        pay = math.floor(exact + Fraction(1, 2))
    bal = P
    total_int = 0
    last = 0
    for k in range(1, n + 1):
        q, rem = divmod(bal * bp, 120000)
        interest = q + (1 if 2 * rem >= 120000 else 0)
        total_int += interest
        if k == n:
            last = pay
        else:
            bal = bal + interest - pay
    return {'payment': pay, 'final_payment': last, 'total_interest': total_int}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: final payment amount', {'principal': 7, 'rate_bp': 725, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -1123, 'total_interest': -771}], ['control 1', {'principal': 12345, 'rate_bp': 1200, 'months': 1, 'round': 'nearest'}, {'payment': 12468, 'final_payment': 12468, 'total_interest': 123}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'months': 7, 'round': 'nearest'}, {'payment': 14573, 'final_payment': 14573, 'total_interest': 2011}], ['control 3', {'principal': 999999, 'rate_bp': 725, 'months': 3, 'round': 'up'}, {'payment': 337369, 'final_payment': 337369, 'total_interest': 12108}], ['control 4', {'principal': 500, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 250, 'final_payment': 250, 'total_interest': 0}], ['control 5', {'principal': 1000000, 'rate_bp': 725, 'months': 2, 'round': 'nearest'}, {'payment': 504536, 'final_payment': 504536, 'total_interest': 9072}], ['control 6', {'principal': 999999, 'rate_bp': 725, 'months': 3, 'round': 'nearest'}, {'payment': 337369, 'final_payment': 337369, 'total_interest': 12108}]], [['regression: final payment amount', {'principal': 500, 'rate_bp': 13, 'months': 3, 'round': 'up'}, {'payment': 167, 'final_payment': 166, 'total_interest': 0}], ['regression: final payment amount, partial-repair probe', {'principal': 2500000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 861250, 'final_payment': 861251, 'total_interest': 83751}], ['control 1', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 2', {'principal': 1000000, 'rate_bp': 725, 'months': 1, 'round': 'nearest'}, {'payment': 1006042, 'final_payment': 1006042, 'total_interest': 6042}], ['control 3', {'principal': 999999, 'rate_bp': 1200, 'months': 3, 'round': 'up'}, {'payment': 340022, 'final_payment': 340022, 'total_interest': 20067}], ['control 4', {'principal': 999999, 'rate_bp': 0, 'months': 1, 'round': 'nearest'}, {'payment': 999999, 'final_payment': 999999, 'total_interest': 0}], ['control 5', {'principal': 1000000, 'rate_bp': 0, 'months': 1, 'round': 'up'}, {'payment': 1000000, 'final_payment': 1000000, 'total_interest': 0}]], [['regression: final payment amount', {'principal': 999999, 'rate_bp': 0, 'months': 12, 'round': 'nearest'}, {'payment': 83333, 'final_payment': 83336, 'total_interest': 0}], ['regression: final payment amount, partial-repair probe', {'principal': 2500000, 'rate_bp': 13, 'months': 1, 'round': 'nearest'}, {'payment': 2500271, 'final_payment': 2500271, 'total_interest': 271}], ['control 1', {'principal': 100000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 34450, 'final_payment': 34450, 'total_interest': 3350}], ['control 2', {'principal': 500, 'rate_bp': 1200, 'months': 3, 'round': 'nearest'}, {'payment': 170, 'final_payment': 170, 'total_interest': 10}], ['control 3', {'principal': 12345, 'rate_bp': 1200, 'months': 2, 'round': 'nearest'}, {'payment': 6265, 'final_payment': 6265, 'total_interest': 185}], ['control 4', {'principal': 500, 'rate_bp': 0, 'months': 1, 'round': 'up'}, {'payment': 500, 'final_payment': 500, 'total_interest': 0}], ['control 5', {'principal': 500, 'rate_bp': 499, 'months': 1, 'round': 'nearest'}, {'payment': 502, 'final_payment': 502, 'total_interest': 2}]], [['regression: final payment amount', {'principal': 7, 'rate_bp': 13, 'months': 12, 'round': 'up'}, {'payment': 1, 'final_payment': -4, 'total_interest': 0}], ['regression: final payment amount, partial-repair probe', {'principal': 1000000, 'rate_bp': 13, 'months': 60, 'round': 'up'}, {'payment': 16722, 'final_payment': 16710, 'total_interest': 3308}], ['control 1', {'principal': 7, 'rate_bp': 1999, 'months': 7, 'round': 'nearest'}, {'payment': 1, 'final_payment': 1, 'total_interest': 0}], ['control 2', {'principal': 30000000, 'rate_bp': 0, 'months': 12, 'round': 'up'}, {'payment': 2500000, 'final_payment': 2500000, 'total_interest': 0}], ['control 3', {'principal': 30000000, 'rate_bp': 600, 'months': 1, 'round': 'up'}, {'payment': 30150000, 'final_payment': 30150000, 'total_interest': 150000}], ['control 4', {'principal': 12345, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 4115, 'final_payment': 4115, 'total_interest': 0}], ['control 5', {'principal': 30000000, 'rate_bp': 1999, 'months': 1, 'round': 'nearest'}, {'payment': 30499750, 'final_payment': 30499750, 'total_interest': 499750}]], [['regression: final payment amount', {'principal': 7, 'rate_bp': 600, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -876, 'total_interest': -524}], ['control 1', {'principal': 30000000, 'rate_bp': 499, 'months': 2, 'round': 'nearest'}, {'payment': 15093627, 'final_payment': 15093627, 'total_interest': 187254}], ['control 2', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 3', {'principal': 500, 'rate_bp': 0, 'months': 2, 'round': 'up'}, {'payment': 250, 'final_payment': 250, 'total_interest': 0}], ['control 4', {'principal': 2500000, 'rate_bp': 499, 'months': 1, 'round': 'nearest'}, {'payment': 2510396, 'final_payment': 2510396, 'total_interest': 10396}], ['control 5', {'principal': 100000, 'rate_bp': 13, 'months': 1, 'round': 'nearest'}, {'payment': 100011, 'final_payment': 100011, 'total_interest': 11}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 1, 'round': 'up'}, {'payment': 100000, 'final_payment': 100000, 'total_interest': 0}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    P = x['principal']
    n = x['months']
    bp = x['rate_bp']
    if bp == 0:
        exact = Fraction(P, n)
    else:
        r = Fraction(bp, 120000)
        exact = P * r / (1 - (1 + r) ** -n)
    if x['round'] == 'up':
        pay = math.ceil(exact)
    else:
        pay = math.floor(exact + Fraction(1, 2))
    bal = P
    total_int = 0
    last = 0
    for k in range(1, n + 1):
        q, rem = divmod(bal * bp, 120000)
        interest = q + (1 if 2 * rem >= 120000 else 0)
        total_int += interest
        if k == n:
            last = bal
        else:
            bal = bal + interest - pay
    return {'payment': pay, 'final_payment': last, 'total_interest': total_int}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: final payment amount', {'principal': 7, 'rate_bp': 725, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -1123, 'total_interest': -771}], ['control 1', {'principal': 12345, 'rate_bp': 1200, 'months': 1, 'round': 'nearest'}, {'payment': 12468, 'final_payment': 12468, 'total_interest': 123}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'months': 7, 'round': 'nearest'}, {'payment': 14573, 'final_payment': 14573, 'total_interest': 2011}], ['control 3', {'principal': 999999, 'rate_bp': 725, 'months': 3, 'round': 'up'}, {'payment': 337369, 'final_payment': 337369, 'total_interest': 12108}], ['control 4', {'principal': 500, 'rate_bp': 0, 'months': 2, 'round': 'nearest'}, {'payment': 250, 'final_payment': 250, 'total_interest': 0}], ['control 5', {'principal': 1000000, 'rate_bp': 725, 'months': 2, 'round': 'nearest'}, {'payment': 504536, 'final_payment': 504536, 'total_interest': 9072}], ['control 6', {'principal': 999999, 'rate_bp': 725, 'months': 3, 'round': 'nearest'}, {'payment': 337369, 'final_payment': 337369, 'total_interest': 12108}]], [['regression: final payment amount', {'principal': 500, 'rate_bp': 13, 'months': 3, 'round': 'up'}, {'payment': 167, 'final_payment': 166, 'total_interest': 0}], ['regression: final payment amount, partial-repair probe', {'principal': 2500000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 861250, 'final_payment': 861251, 'total_interest': 83751}], ['control 1', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 2', {'principal': 1000000, 'rate_bp': 725, 'months': 1, 'round': 'nearest'}, {'payment': 1006042, 'final_payment': 1006042, 'total_interest': 6042}], ['control 3', {'principal': 999999, 'rate_bp': 1200, 'months': 3, 'round': 'up'}, {'payment': 340022, 'final_payment': 340022, 'total_interest': 20067}], ['control 4', {'principal': 999999, 'rate_bp': 0, 'months': 1, 'round': 'nearest'}, {'payment': 999999, 'final_payment': 999999, 'total_interest': 0}], ['control 5', {'principal': 1000000, 'rate_bp': 0, 'months': 1, 'round': 'up'}, {'payment': 1000000, 'final_payment': 1000000, 'total_interest': 0}]], [['regression: final payment amount', {'principal': 999999, 'rate_bp': 0, 'months': 12, 'round': 'nearest'}, {'payment': 83333, 'final_payment': 83336, 'total_interest': 0}], ['regression: final payment amount, partial-repair probe', {'principal': 2500000, 'rate_bp': 13, 'months': 1, 'round': 'nearest'}, {'payment': 2500271, 'final_payment': 2500271, 'total_interest': 271}], ['control 1', {'principal': 100000, 'rate_bp': 1999, 'months': 3, 'round': 'nearest'}, {'payment': 34450, 'final_payment': 34450, 'total_interest': 3350}], ['control 2', {'principal': 500, 'rate_bp': 1200, 'months': 3, 'round': 'nearest'}, {'payment': 170, 'final_payment': 170, 'total_interest': 10}], ['control 3', {'principal': 12345, 'rate_bp': 1200, 'months': 2, 'round': 'nearest'}, {'payment': 6265, 'final_payment': 6265, 'total_interest': 185}], ['control 4', {'principal': 500, 'rate_bp': 0, 'months': 1, 'round': 'up'}, {'payment': 500, 'final_payment': 500, 'total_interest': 0}], ['control 5', {'principal': 500, 'rate_bp': 499, 'months': 1, 'round': 'nearest'}, {'payment': 502, 'final_payment': 502, 'total_interest': 2}]], [['regression: final payment amount', {'principal': 7, 'rate_bp': 13, 'months': 12, 'round': 'up'}, {'payment': 1, 'final_payment': -4, 'total_interest': 0}], ['regression: final payment amount, partial-repair probe', {'principal': 1000000, 'rate_bp': 13, 'months': 60, 'round': 'up'}, {'payment': 16722, 'final_payment': 16710, 'total_interest': 3308}], ['control 1', {'principal': 7, 'rate_bp': 1999, 'months': 7, 'round': 'nearest'}, {'payment': 1, 'final_payment': 1, 'total_interest': 0}], ['control 2', {'principal': 30000000, 'rate_bp': 0, 'months': 12, 'round': 'up'}, {'payment': 2500000, 'final_payment': 2500000, 'total_interest': 0}], ['control 3', {'principal': 30000000, 'rate_bp': 600, 'months': 1, 'round': 'up'}, {'payment': 30150000, 'final_payment': 30150000, 'total_interest': 150000}], ['control 4', {'principal': 12345, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 4115, 'final_payment': 4115, 'total_interest': 0}], ['control 5', {'principal': 30000000, 'rate_bp': 1999, 'months': 1, 'round': 'nearest'}, {'payment': 30499750, 'final_payment': 30499750, 'total_interest': 499750}]], [['regression: final payment amount', {'principal': 7, 'rate_bp': 600, 'months': 360, 'round': 'up'}, {'payment': 1, 'final_payment': -876, 'total_interest': -524}], ['control 1', {'principal': 30000000, 'rate_bp': 499, 'months': 2, 'round': 'nearest'}, {'payment': 15093627, 'final_payment': 15093627, 'total_interest': 187254}], ['control 2', {'principal': 999999, 'rate_bp': 0, 'months': 3, 'round': 'nearest'}, {'payment': 333333, 'final_payment': 333333, 'total_interest': 0}], ['control 3', {'principal': 500, 'rate_bp': 0, 'months': 2, 'round': 'up'}, {'payment': 250, 'final_payment': 250, 'total_interest': 0}], ['control 4', {'principal': 2500000, 'rate_bp': 499, 'months': 1, 'round': 'nearest'}, {'payment': 2510396, 'final_payment': 2510396, 'total_interest': 10396}], ['control 5', {'principal': 100000, 'rate_bp': 13, 'months': 1, 'round': 'nearest'}, {'payment': 100011, 'final_payment': 100011, 'total_interest': 11}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 1, 'round': 'up'}, {'payment': 100000, 'final_payment': 100000, 'total_interest': 0}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)

prompt:
The last payment ignores the rounding residue left in the balance.

x = {'principal', 'rate_bp' (annual, monthly rate = bp/120000), 'months', 'round': 'nearest'|'up'}. The exact annuity payment P*r/(1-(1+r)^-n) (P/n at zero rate) is rounded half-up ('nearest') or up to the cent. Monthly interest is round_half_up(balance*bp/120000); months 1..n-1 pay the level payment and month n pays balance + interest. Return {'payment', 'final_payment', 'total_interest'}.

split: open-access


prompt:
A payment that exactly equals interest produces rows that never reduce principal.

x = {'principal', 'rate_bp', 'payment', 'max_months'}. For months 1..max_months while the balance is positive: interest = round_half_up(balance*bp/120000); if payment <= interest return {'error': 'negative_amortization', 'month': k}; principal paid = min(payment - interest, balance); append [k, interest, principal, balance after]. Return {'rows', 'balloon': remaining principal}.

split: open-access


prompt:
The last schedule row drives the balance negative.

x = {'principal', 'rate_bp', 'payment', 'max_months'}. For months 1..max_months while the balance is positive: interest = round_half_up(balance*bp/120000); if payment <= interest return {'error': 'negative_amortization', 'month': k}; principal paid = min(payment - interest, balance); append [k, interest, principal, balance after]. Return {'rows', 'balloon': remaining principal}.

split: open-access


prompt:
Paid-off loans keep producing zero rows until max_months.

x = {'principal', 'rate_bp', 'payment', 'max_months'}. For months 1..max_months while the balance is positive: interest = round_half_up(balance*bp/120000); if payment <= interest return {'error': 'negative_amortization', 'month': k}; principal paid = min(payment - interest, balance); append [k, interest, principal, balance after]. Return {'rows', 'balloon': remaining principal}.

split: open-access


prompt:
Interest stays flat every month even as principal is repaid.

x = {'principal', 'rate_bp', 'payment', 'max_months'}. For months 1..max_months while the balance is positive: interest = round_half_up(balance*bp/120000); if payment <= interest return {'error': 'negative_amortization', 'month': k}; principal paid = min(payment - interest, balance); append [k, interest, principal, balance after]. Return {'rows', 'balloon': remaining principal}.

split: open-access


prompt:
The reported balloon includes another month of interest.

x = {'principal', 'rate_bp', 'payment', 'max_months'}. For months 1..max_months while the balance is positive: interest = round_half_up(balance*bp/120000); if payment <= interest return {'error': 'negative_amortization', 'month': k}; principal paid = min(payment - interest, balance); append [k, interest, principal, balance after]. Return {'rows', 'balloon': remaining principal}.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    def level(P, n):
        if n <= 0 or P <= 0:
            return 0
        if bp == 0:
            exact = Fraction(P, n)
        else:
            r = Fraction(bp, 120000)
            exact = P * r / (1 - (1 + r) ** -n)
        return math.floor(exact + Fraction(1, 2))
    bp = x['rate_bp']
    n = x['months']
    bal = x['principal']
    pay = level(bal, n)
    history = [pay]
    total = 0
    k = 0
    final = 0
    while bal > 0 and k < n:
        k += 1
        interest = rnd(bal * bp, 120000)
        total += interest
        if bal + interest <= pay or k == n:
            final = bal + interest
            bal = 0
            break
        bal -= pay - interest
        extra = x['prepay'].get(str(k), 0)
        if extra:
            bal -= extra
            if x['mode'] == 'recast' and bal > 0:
                pay = level(bal, n - k)
                history.append(pay)
            if bal == 0:
                final = pay + extra
    return {'months_paid': k, 'total_interest': total, 'final_payment': final, 'payments': history}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: extra principal cap', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'21': 5000, '12': 200000, '35': 2000000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 41663, 'payments': [1667]}], ['regression: extra principal cap, partial-repair probe', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000, '7': 20000}, 'mode': 'recast'}, {'months_paid': 8, 'total_interest': 1612, 'final_payment': 5464, 'payments': [5164, 1104]}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'prepay': {'10': 1000, '6': 5000, '7': 5000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 18501, 'payments': [20833, 20000, 19000, 18501]}], ['control 2', {'principal': 60000, 'rate_bp': 1200, 'months': 24, 'prepay': {'16': 5000, '9': 1000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 7480, 'final_payment': 2100, 'payments': [2824, 2753, 2099]}], ['control 3', {'principal': 100000, 'rate_bp': 1200, 'months': 6, 'prepay': {'5': 5000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 3478, 'final_payment': 12203, 'payments': [17255, 12203]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {'9': 5000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 3334, 'payments': [5000, 3333]}], ['control 5', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}]], [['regression: extra principal cap', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'prepay': {'19': 200000, '18': 20000, '24': 5000}, 'mode': 'recast'}, {'months_paid': 19, 'total_interest': 0, 'final_payment': 4994, 'payments': [4167, 832]}], ['regression: extra principal cap, partial-repair probe', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {'19': 20000, '1': 2000000, '3': 2000000}, 'mode': 'shorten'}, {'months_paid': 1, 'total_interest': 2500, 'final_payment': 252500, 'payments': [11768]}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'prepay': {'24': 200000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 10409, 'payments': [10417]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 41665, 'payments': [41667]}], ['control 3', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'10': 5000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 12595, 'final_payment': 4326, 'payments': [4707, 4323]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 4159, 'payments': [4167]}]], [['regression: extra principal cap', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'12': 5000, '20': 20000, '4': 1000}, 'mode': 'shorten'}, {'months_paid': 20, 'total_interest': 11928, 'final_payment': 16495, 'payments': [4707]}], ['control 1', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'prepay': {'10': 1000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 3272, 'final_payment': 8101, 'payments': [8607, 8101]}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'months': 6, 'prepay': {'5': 1000, '3': 20000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 1552, 'final_payment': 9220, 'payments': [16960, 10226, 9220]}], ['control 3', {'principal': 60000, 'rate_bp': 600, 'months': 24, 'prepay': {'15': 20000, '5': 1000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 3267, 'final_payment': 324, 'payments': [2659, 2604, 326]}], ['control 4', {'principal': 250000, 'rate_bp': 600, 'months': 36, 'prepay': {'10': 20000, '11': 1000}, 'mode': 'shorten'}, {'months_paid': 33, 'total_interest': 21026, 'final_payment': 6666, 'payments': [7605]}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}], ['control 6', {'principal': 60000, 'rate_bp': 600, 'months': 6, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 1055, 'final_payment': 10175, 'payments': [10176]}]], [['regression: extra principal cap', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'prepay': {'7': 200000, '3': 5000}, 'mode': 'shorten'}, {'months_paid': 7, 'total_interest': 0, 'final_payment': 120002, 'payments': [20833]}], ['regression: extra principal cap, partial-repair probe', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'24': 2000000, '33': 1000, '12': 2000000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 3088, 'final_payment': 43013, 'payments': [1825]}], ['control 1', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 9520, 'final_payment': 3050, 'payments': [3042]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 3', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'prepay': {'1': 200000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 4349, 'final_payment': 2927, 'payments': [22212, 2921]}], ['control 4', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 5000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 1867, 'final_payment': 63, 'payments': [5164]}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 6, 'prepay': {'1': 5000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 15665, 'payments': [16667, 15667]}]], [['regression: extra principal cap', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 7134, 'final_payment': 106515, 'payments': [21517]}], ['control 1', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 9000, 'payments': [10000]}], ['control 2', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 3', {'principal': 100000, 'rate_bp': 0, 'months': 6, 'prepay': {'3': 1000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 16333, 'payments': [16667, 16333]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {'10': 5000}, 'mode': 'shorten'}, {'months_paid': 11, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 5', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {'4': 1000, '9': 20000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 29136, 'final_payment': 11008, 'payments': [11768]}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {'22': 5000}, 'mode': 'shorten'}, {'months_paid': 35, 'total_interest': 9173, 'final_payment': 745, 'payments': [3042]}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    def level(P, n):
        if n <= 0 or P <= 0:
            return 0
        if bp == 0:
            exact = Fraction(P, n)
        else:
            r = Fraction(bp, 120000)
            exact = P * r / (1 - (1 + r) ** -n)
        return math.floor(exact + Fraction(1, 2))
    bp = x['rate_bp']
    n = x['months']
    bal = x['principal']
    pay = level(bal, n)
    history = [pay]
    total = 0
    k = 0
    final = 0
    while bal > 0 and k < n:
        k += 1
        interest = rnd(bal * bp, 120000)
        total += interest
        if bal + interest <= pay or k == n:
            final = bal + interest
            bal = 0
            break
        bal -= pay - interest
        extra = min(x['prepay'].get(str(k), 0), bal + interest)
        if extra:
            bal -= extra
            if x['mode'] == 'recast' and bal > 0:
                pay = level(bal, n - k)
                history.append(pay)
            if bal == 0:
                final = pay + extra
    return {'months_paid': k, 'total_interest': total, 'final_payment': final, 'payments': history}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: extra principal cap', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'21': 5000, '12': 200000, '35': 2000000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 41663, 'payments': [1667]}], ['regression: extra principal cap, partial-repair probe', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000, '7': 20000}, 'mode': 'recast'}, {'months_paid': 8, 'total_interest': 1612, 'final_payment': 5464, 'payments': [5164, 1104]}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'prepay': {'10': 1000, '6': 5000, '7': 5000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 18501, 'payments': [20833, 20000, 19000, 18501]}], ['control 2', {'principal': 60000, 'rate_bp': 1200, 'months': 24, 'prepay': {'16': 5000, '9': 1000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 7480, 'final_payment': 2100, 'payments': [2824, 2753, 2099]}], ['control 3', {'principal': 100000, 'rate_bp': 1200, 'months': 6, 'prepay': {'5': 5000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 3478, 'final_payment': 12203, 'payments': [17255, 12203]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {'9': 5000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 3334, 'payments': [5000, 3333]}], ['control 5', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}]], [['regression: extra principal cap', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'prepay': {'19': 200000, '18': 20000, '24': 5000}, 'mode': 'recast'}, {'months_paid': 19, 'total_interest': 0, 'final_payment': 4994, 'payments': [4167, 832]}], ['regression: extra principal cap, partial-repair probe', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {'19': 20000, '1': 2000000, '3': 2000000}, 'mode': 'shorten'}, {'months_paid': 1, 'total_interest': 2500, 'final_payment': 252500, 'payments': [11768]}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'prepay': {'24': 200000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 10409, 'payments': [10417]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 41665, 'payments': [41667]}], ['control 3', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'10': 5000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 12595, 'final_payment': 4326, 'payments': [4707, 4323]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 4159, 'payments': [4167]}]], [['regression: extra principal cap', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'12': 5000, '20': 20000, '4': 1000}, 'mode': 'shorten'}, {'months_paid': 20, 'total_interest': 11928, 'final_payment': 16495, 'payments': [4707]}], ['control 1', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'prepay': {'10': 1000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 3272, 'final_payment': 8101, 'payments': [8607, 8101]}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'months': 6, 'prepay': {'5': 1000, '3': 20000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 1552, 'final_payment': 9220, 'payments': [16960, 10226, 9220]}], ['control 3', {'principal': 60000, 'rate_bp': 600, 'months': 24, 'prepay': {'15': 20000, '5': 1000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 3267, 'final_payment': 324, 'payments': [2659, 2604, 326]}], ['control 4', {'principal': 250000, 'rate_bp': 600, 'months': 36, 'prepay': {'10': 20000, '11': 1000}, 'mode': 'shorten'}, {'months_paid': 33, 'total_interest': 21026, 'final_payment': 6666, 'payments': [7605]}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}], ['control 6', {'principal': 60000, 'rate_bp': 600, 'months': 6, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 1055, 'final_payment': 10175, 'payments': [10176]}]], [['regression: extra principal cap', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'prepay': {'7': 200000, '3': 5000}, 'mode': 'shorten'}, {'months_paid': 7, 'total_interest': 0, 'final_payment': 120002, 'payments': [20833]}], ['regression: extra principal cap, partial-repair probe', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'24': 2000000, '33': 1000, '12': 2000000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 3088, 'final_payment': 43013, 'payments': [1825]}], ['control 1', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 9520, 'final_payment': 3050, 'payments': [3042]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 3', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'prepay': {'1': 200000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 4349, 'final_payment': 2927, 'payments': [22212, 2921]}], ['control 4', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 5000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 1867, 'final_payment': 63, 'payments': [5164]}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 6, 'prepay': {'1': 5000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 15665, 'payments': [16667, 15667]}]], [['regression: extra principal cap', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 7134, 'final_payment': 106515, 'payments': [21517]}], ['control 1', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 9000, 'payments': [10000]}], ['control 2', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 3', {'principal': 100000, 'rate_bp': 0, 'months': 6, 'prepay': {'3': 1000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 16333, 'payments': [16667, 16333]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {'10': 5000}, 'mode': 'shorten'}, {'months_paid': 11, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 5', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {'4': 1000, '9': 20000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 29136, 'final_payment': 11008, 'payments': [11768]}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {'22': 5000}, 'mode': 'shorten'}, {'months_paid': 35, 'total_interest': 9173, 'final_payment': 745, 'payments': [3042]}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)

prompt:
An oversized prepayment drives the balance negative and the loan never reports a payoff payment.

x = {'principal', 'rate_bp', 'months', 'prepay': {'k': extra cents after payment k}, 'mode': 'shorten'|'recast'}. The level payment is the exact annuity rounded half-up. Each month: interest = round_half_up(balance*bp/120000); if balance + interest <= payment or it is month n, pay balance + interest and stop. Otherwise reduce the balance by payment - interest, then apply the extra (capped at the balance). In recast mode a nonzero extra that leaves a balance re-levels the payment over the n - k remaining months. An extra that clears the balance ends the loan with final payment = payment + extra. Return {'months_paid', 'total_interest', 'final_payment', 'payments': payment history}.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    def level(P, n):
        if n <= 0 or P <= 0:
            return 0
        if bp == 0:
            exact = Fraction(P, n)
        else:
            r = Fraction(bp, 120000)
            exact = P * r / (1 - (1 + r) ** -n)
        return math.floor(exact + Fraction(1, 2))
    bp = x['rate_bp']
    n = x['months']
    bal = x['principal']
    pay = level(bal, n)
    history = [pay]
    total = 0
    k = 0
    final = 0
    while bal > 0 and k < n:
        k += 1
        interest = rnd(bal * bp, 120000)
        total += interest
        if bal + interest <= pay or k == n:
            final = bal + interest
            bal = 0
            break
        bal -= pay - interest
        extra = min(x['prepay'].get(str(k), 0), bal)
        if extra:
            bal -= extra
            if x['mode'] == 'recast' and bal > 0:
                pay = level(bal, n)
                history.append(pay)
            if bal == 0:
                final = pay + extra
    return {'months_paid': k, 'total_interest': total, 'final_payment': final, 'payments': history}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: recast remaining term', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000, '7': 20000}, 'mode': 'recast'}, {'months_paid': 8, 'total_interest': 1612, 'final_payment': 5464, 'payments': [5164, 1104]}], ['control 1', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'21': 5000, '12': 200000, '35': 2000000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 41663, 'payments': [1667]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'26': 20000, '10': 5000, '3': 2000000}, 'mode': 'shorten'}, {'months_paid': 3, 'total_interest': 0, 'final_payment': 56666, 'payments': [1667]}], ['control 3', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 3445, 'final_payment': 72421, 'payments': [4432]}], ['control 4', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'33': 200000, '21': 200000}, 'mode': 'recast'}, {'months_paid': 21, 'total_interest': 4647, 'final_payment': 28147, 'payments': [1825]}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 36, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 36, 'total_interest': 0, 'final_payment': 2770, 'payments': [2778]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'10': 5000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 12595, 'final_payment': 4326, 'payments': [4707, 4323]}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'prepay': {'24': 200000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 10409, 'payments': [10417]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 41665, 'payments': [41667]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 4159, 'payments': [4167]}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 12979, 'final_payment': 4718, 'payments': [4707]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'11': 20000, '19': 20000, '2': 1000}, 'mode': 'shorten'}, {'months_paid': 19, 'total_interest': 0, 'final_payment': 8994, 'payments': [1667]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'prepay': {'10': 1000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 3272, 'final_payment': 8101, 'payments': [8607, 8101]}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'12': 5000, '20': 20000, '4': 1000}, 'mode': 'shorten'}, {'months_paid': 20, 'total_interest': 11928, 'final_payment': 16495, 'payments': [4707]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'5': 2000000, '1': 200000, '3': 20000}, 'mode': 'recast'}, {'months_paid': 1, 'total_interest': 0, 'final_payment': 60000, 'payments': [10000]}], ['control 3', {'principal': 60000, 'rate_bp': 1200, 'months': 12, 'prepay': {'4': 200000}, 'mode': 'recast'}, {'months_paid': 4, 'total_interest': 2115, 'final_payment': 46122, 'payments': [5331]}], ['control 4', {'principal': 250000, 'rate_bp': 600, 'months': 36, 'prepay': {'10': 20000, '11': 1000}, 'mode': 'shorten'}, {'months_paid': 33, 'total_interest': 21026, 'final_payment': 6666, 'payments': [7605]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'17': 20000, '29': 20000, '5': 200000}, 'mode': 'recast'}, {'months_paid': 5, 'total_interest': 1423, 'final_payment': 54123, 'payments': [1825]}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'20': 5000, '18': 2000000, '15': 1000}, 'mode': 'recast'}, {'months_paid': 18, 'total_interest': 11987, 'final_payment': 31200, 'payments': [4707, 4591]}], ['control 1', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 9520, 'final_payment': 3050, 'payments': [3042]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'prepay': {'7': 200000, '3': 5000}, 'mode': 'shorten'}, {'months_paid': 7, 'total_interest': 0, 'final_payment': 120002, 'payments': [20833]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 4', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'24': 2000000, '33': 1000, '12': 2000000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 3088, 'final_payment': 43013, 'payments': [1825]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 5000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 1867, 'final_payment': 63, 'payments': [5164]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {'9': 5000, '24': 5000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 0, 'months': 6, 'prepay': {'3': 1000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 16333, 'payments': [16667, 16333]}], ['control 1', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 7134, 'final_payment': 106515, 'payments': [21517]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 9000, 'payments': [10000]}], ['control 3', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 200000, '1': 1000, '6': 200000}, 'mode': 'shorten'}, {'months_paid': 4, 'total_interest': 0, 'final_payment': 29000, 'payments': [10000]}], ['control 5', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {'10': 5000}, 'mode': 'shorten'}, {'months_paid': 11, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {'34': 2000000, '15': 200000}, 'mode': 'recast'}, {'months_paid': 15, 'total_interest': 6138, 'final_payment': 63550, 'payments': [3042]}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    def level(P, n):
        if n <= 0 or P <= 0:
            return 0
        if bp == 0:
            exact = Fraction(P, n)
        else:
            r = Fraction(bp, 120000)
            exact = P * r / (1 - (1 + r) ** -n)
        return math.floor(exact + Fraction(1, 2))
    bp = x['rate_bp']
    n = x['months']
    bal = x['principal']
    pay = level(bal, n)
    history = [pay]
    total = 0
    k = 0
    final = 0
    while bal > 0 and k < n:
        k += 1
        interest = rnd(bal * bp, 120000)
        total += interest
        if bal + interest <= pay or k == n:
            final = bal + interest
            bal = 0
            break
        bal -= pay - interest
        extra = min(x['prepay'].get(str(k), 0), bal)
        if extra:
            bal -= extra
            if x['mode'] == 'recast' and bal > 0:
                pay = level(bal, n - k + 1)
                history.append(pay)
            if bal == 0:
                final = pay + extra
    return {'months_paid': k, 'total_interest': total, 'final_payment': final, 'payments': history}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: recast remaining term', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000, '7': 20000}, 'mode': 'recast'}, {'months_paid': 8, 'total_interest': 1612, 'final_payment': 5464, 'payments': [5164, 1104]}], ['control 1', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'21': 5000, '12': 200000, '35': 2000000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 41663, 'payments': [1667]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'26': 20000, '10': 5000, '3': 2000000}, 'mode': 'shorten'}, {'months_paid': 3, 'total_interest': 0, 'final_payment': 56666, 'payments': [1667]}], ['control 3', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 3445, 'final_payment': 72421, 'payments': [4432]}], ['control 4', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'33': 200000, '21': 200000}, 'mode': 'recast'}, {'months_paid': 21, 'total_interest': 4647, 'final_payment': 28147, 'payments': [1825]}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 36, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 36, 'total_interest': 0, 'final_payment': 2770, 'payments': [2778]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'10': 5000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 12595, 'final_payment': 4326, 'payments': [4707, 4323]}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'prepay': {'24': 200000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 10409, 'payments': [10417]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 41665, 'payments': [41667]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 4159, 'payments': [4167]}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 12979, 'final_payment': 4718, 'payments': [4707]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'11': 20000, '19': 20000, '2': 1000}, 'mode': 'shorten'}, {'months_paid': 19, 'total_interest': 0, 'final_payment': 8994, 'payments': [1667]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'prepay': {'10': 1000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 3272, 'final_payment': 8101, 'payments': [8607, 8101]}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'12': 5000, '20': 20000, '4': 1000}, 'mode': 'shorten'}, {'months_paid': 20, 'total_interest': 11928, 'final_payment': 16495, 'payments': [4707]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'5': 2000000, '1': 200000, '3': 20000}, 'mode': 'recast'}, {'months_paid': 1, 'total_interest': 0, 'final_payment': 60000, 'payments': [10000]}], ['control 3', {'principal': 60000, 'rate_bp': 1200, 'months': 12, 'prepay': {'4': 200000}, 'mode': 'recast'}, {'months_paid': 4, 'total_interest': 2115, 'final_payment': 46122, 'payments': [5331]}], ['control 4', {'principal': 250000, 'rate_bp': 600, 'months': 36, 'prepay': {'10': 20000, '11': 1000}, 'mode': 'shorten'}, {'months_paid': 33, 'total_interest': 21026, 'final_payment': 6666, 'payments': [7605]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'17': 20000, '29': 20000, '5': 200000}, 'mode': 'recast'}, {'months_paid': 5, 'total_interest': 1423, 'final_payment': 54123, 'payments': [1825]}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'20': 5000, '18': 2000000, '15': 1000}, 'mode': 'recast'}, {'months_paid': 18, 'total_interest': 11987, 'final_payment': 31200, 'payments': [4707, 4591]}], ['control 1', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 9520, 'final_payment': 3050, 'payments': [3042]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'prepay': {'7': 200000, '3': 5000}, 'mode': 'shorten'}, {'months_paid': 7, 'total_interest': 0, 'final_payment': 120002, 'payments': [20833]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 4', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'24': 2000000, '33': 1000, '12': 2000000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 3088, 'final_payment': 43013, 'payments': [1825]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 5000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 1867, 'final_payment': 63, 'payments': [5164]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {'9': 5000, '24': 5000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 0, 'months': 6, 'prepay': {'3': 1000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 16333, 'payments': [16667, 16333]}], ['control 1', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 7134, 'final_payment': 106515, 'payments': [21517]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 9000, 'payments': [10000]}], ['control 3', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 200000, '1': 1000, '6': 200000}, 'mode': 'shorten'}, {'months_paid': 4, 'total_interest': 0, 'final_payment': 29000, 'payments': [10000]}], ['control 5', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {'10': 5000}, 'mode': 'shorten'}, {'months_paid': 11, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {'34': 2000000, '15': 200000}, 'mode': 'recast'}, {'months_paid': 15, 'total_interest': 6138, 'final_payment': 63550, 'payments': [3042]}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)

prompt:
After a recast the payment drops as if the loan were brand new.

x = {'principal', 'rate_bp', 'months', 'prepay': {'k': extra cents after payment k}, 'mode': 'shorten'|'recast'}. The level payment is the exact annuity rounded half-up. Each month: interest = round_half_up(balance*bp/120000); if balance + interest <= payment or it is month n, pay balance + interest and stop. Otherwise reduce the balance by payment - interest, then apply the extra (capped at the balance). In recast mode a nonzero extra that leaves a balance re-levels the payment over the n - k remaining months. An extra that clears the balance ends the loan with final payment = payment + extra. Return {'months_paid', 'total_interest', 'final_payment', 'payments': payment history}.

split: open-access