Error Reference#
This page is the definitive reference for ValueError and TypeError exceptions raised
by Lactuca’s public API. For each condition you will find the exact message pattern,
the cause, a concrete fix, and code examples showing the triggering call
alongside the correct alternative.
Important
Scope of this reference — this page covers only errors that are non-obvious, cross-cutting (affecting multiple methods or components), or that arise from configuration-of-state surprises (e.g. restrictions that only appear after the object has been built in a particular way). Simple parameter validation whose error message is self-explanatory (e.g. “n must be non-negative”) is intentionally omitted.
Note
Error messages include dynamic context — the calling method name and the actual input
values — so the exact error message text will differ from the patterns shown here.
The message patterns below use {placeholders} for those dynamic parts.
Quick reference#
Scan for your exception type or a keyword from the error message, then follow the link for the full explanation, cause, and fix.
Category |
Exception |
Error condition |
Trigger |
|---|---|---|---|
Age & term |
|
Fractional age passed to |
|
Age & term |
|
Integer age passed to |
|
Age & term |
|
Age \(< 0\) passed to any method, or age \(> \omega\) passed to commutation/annuity/insurance methods ( |
|
Age & term |
|
|
|
Interest rate |
|
Force of interest ( |
|
Interest rate |
|
Wrong number of rates or non-positive terms |
|
Interest rate |
|
Non-finite (NaN or Inf) value in the |
|
Table data |
|
\(q_\omega \neq 1.0\) in a user-supplied |
|
Table data |
|
Missing, unexpected, or out-of-range |
|
Configuration |
|
Disallowed value for a config setting |
|
Configuration |
|
Negative or non-integer value for |
|
Input arrays |
|
|
|
Input arrays |
|
|
|
Interest rate |
|
|
|
Cashflow utilities |
|
Invalid |
|
Cashflow utilities |
|
Period value outside |
|
Cashflow utilities |
|
Non-integer value in |
|
Cashflow utilities |
|
|
|
Cashflow utilities |
|
NaN or Inf in |
|
Cashflow utilities |
|
|
|
GrowthRate |
|
Invalid |
|
GrowthRate |
|
|
|
GrowthRate |
|
|
|
GrowthRate |
|
|
|
GrowthRate |
|
|
|
GrowthRate |
|
|
|
Interest rate |
|
Invalid |
|
Interest rate |
|
|
|
Interest rate |
|
Nested multi-scenario |
|
Interest rate |
|
Negative |
|
Interest rate |
|
|
|
Interest rate |
|
|
|
Interest rate |
|
|
|
Interest rate |
|
|
Note
Most rows in the Quick reference table raise ValueError. Rows that raise TypeError
are: Non-finite rates= value (piecewise interest rate), Wrong type for interest_rate,
Invalid selected periods (non-integer value), times is None, GrowthRate constructor type
checks, GrowthRate.factor None, GrowthRate.add_scenario, InterestRate.add_scenario,
InterestRate.i_m/d_m, InterestRate.get_average_force.
Age and term errors#
These errors are raised when an age, duration, deferment, or time-shift argument is outside the valid domain for the requested operation.
Integer age required#
Exception: ValueError
Message pattern:
[{method}] Commutation functions require integer ages. Got non-integer age(s): {values}.
Cause: A commutation function (Dx, Nx, Cx, Mx, Sx, Rx, Lx, Tx) or
the life-expectancy method ex received a fractional age. Commutation functions are
discrete quantities defined only at integer ages; ex uses integer-age commutation
columns internally and shares the same constraint.
Note
The lowercase lx and dx methods do not raise this error — they accept fractional
ages and perform interpolation according to config.lx_interpolation.
Lx (capital L) is the commutation function
\(L_x = \int_0^1 l_{x+t}\,\mathrm{d}t\) that requires an integer age, while lx
(lowercase) is the \(l_x\) survival function that interpolates at fractional ages.
The same pairing applies to Tx (capital, integer-age) vs Tx_continuous (fractional).
Fix: Pass an integer or a float whose fractional part is within float64 rounding
tolerance (\(\leq 10^{-12}\)). The same restriction applies to ex.
For the complete list of commutation functions and their signatures, see
Commutation Functions.
from lactuca import LifeTable
lt = LifeTable("PASEM2020_Rel_1o", "m")
lt.ex(x=65) # ✔ integer age
lt.ex(x=65.0) # ✔ integer-valued float (treated as 65)
lt.Dx(x=45, ir=0.03) # ✔ commutation function with explicit ir=
lt.Dx(x=45, ir=0.03, x0=25) # ✔ x0 is the reference (start) age for discounting
lt.ex(x=65.5) # ✘ → ValueError: non-integer age
lt.Nx(x=45.7, ir=0.03) # ✘ → ValueError: non-integer age
Fractional age required#
Exception: ValueError
Message pattern (method-specific — each method hard-codes its own name and its discrete alternative):
[ex_continuous] Requires fractional (non-integer) ages. Got integer age(s): {values}.
Use ex() for integer ages instead.
Lx_continuous and Tx_continuous produce analogous messages, naming Lx() and
Tx() as their respective discrete alternatives.
Cause: A continuous-domain method (ex_continuous, Lx_continuous,
Tx_continuous) received an integer or integer-valued float. These methods compute
values via numerical integration for \(x \notin \mathbb{Z}\).
Fix: Pass a fractional age, or use the corresponding discrete method for integer
ages: ex(x), Lx(x), or Tx(x) respectively.
from lactuca import LifeTable
lt = LifeTable("PASEM2020_Rel_1o", "m")
lt.ex_continuous(65.5) # ✔ fractional age
lt.ex_continuous(65.25) # ✔ any non-integer age
lt.Lx_continuous(65.5) # ✔ continuous version of Lx, same fractional-age rule
lt.ex_continuous(65.5, m=12) # ✔ explicit m= (sub-intervals for numerical integration)
lt.ex_continuous(65.5, m=4) # ✔ coarser integration (faster, less precise)
lt.ex(65) # ✔ use ex() at integer ages
lt.Lx(x=65) # ✔ use Lx() at integer ages
lt.ex_continuous(65) # ✘ → ValueError: integer age
lt.ex_continuous(65.0) # ✘ → ValueError: integer-valued float
lt.Lx_continuous(65) # ✘ → ValueError: same rule (Lx_continuous)
Age out of range#
Exception: ValueError
Message pattern: "All ages x must be in [0, {omega}]"
Cause: The age provided is negative or exceeds the table’s terminal age \(\omega\).
Most public methods that accept an age argument apply this check, including commutation
functions, annuity and insurance methods, and continuous methods
(ex_continuous, Lx_continuous, Tx_continuous).
Note
lx is an exception: by actuarial convention, ages beyond \(\omega\) are valid and return
0.0 (all survivors have died). Only negative ages raise ValueError for lx.
Fix: Ensure the age satisfies \(0 \leq x \leq \omega\). Use lt.omega to read the
terminal age of a loaded table before computing. Vectorised calls apply the same bounds
check element-wise: a single out-of-range value in an array triggers the error for the
entire call.
from lactuca import LifeTable
lt = LifeTable("PASEM2020_Rel_1o", "m")
print(lt.omega) # inspect the terminal age of this table
lt.lx(x=65) # ✔ within [0, omega]
lt.lx(x=999) # ✔ returns 0.0 — ages beyond omega follow actuarial convention
lt.Dx(x=50, ir=0.03) # ✔ within [0, omega]
lt.Dx(x=999, ir=0.03) # ✘ → ValueError: age > omega
lt.lx(x=-1) # ✘ → ValueError: negative age
Duration, deferment, or time-shift out of range#
Exception: ValueError
Parameter |
Meaning |
Condition |
Message pattern |
|---|---|---|---|
|
term in years |
|
|
|
deferment in years |
|
|
|
time-shift in years |
|
|
Note
n = 0 does not raise — an annuity or insurance with zero term returns 0.0 without
an error. d = 0 and ts = 0 are equally valid (no deferment, no time-shift).
For whole-life products, omit n entirely (it defaults to None).
Fix: Pass n >= 0, d >= 0, and ts >= 0. n and ts must also be finite
(NaN or Inf are rejected); d checks sign only.
from lactuca import LifeTable
lt = LifeTable("PASEM2020_Rel_1o", "m")
lt.äx(65, n=20, ir=0.03) # ✔ 20-year temporary annuity
lt.äx(65, n=0, ir=0.03) # ✔ zero-term returns 0.0, no error
lt.äx(65, ir=0.03) # ✔ whole-life (n=None, the default)
lt.äx(65, n=-1, ir=0.03) # ✘ → ValueError: n < 0
lt.äx(65, n=float('inf'), ir=0.03) # ✘ → ValueError: n is non-finite
lt.äx(65, d=-2, ir=0.03) # ✘ → ValueError: d < 0
lt.äx(65, ts=-0.5, ir=0.03) # ✘ → ValueError: ts < 0
Interest rate errors#
These errors are raised by InterestRate — at construction (invalid rate values,
piecewise structure, or term_unit), through scenario management (add_scenario,
active_scenario), on method calls (vx, i_m, d_m, get_average_force), or when
an ir= argument to an annuity or insurance method carries an invalid rate — and by the
LifeTable.interest_rate property setter (and constructor argument) when it receives a
value of the wrong type.
Rate ≤ −1#
Exception: ValueError
Message pattern: depends on input type.
Input type |
Message pattern |
|---|---|
Scalar rate |
|
Array of rates |
|
Cause: An annual effective rate \(i \leq -1\) was used to compute either the
force of interest \(\delta = \ln(1+i)\) or the discount factor \(v^n = (1+i)^{-n}\).
Both operations share the same precondition: \((1+i) > 0\). A rate of exactly \(-1\)
makes \((1+i) = 0\), so \(\ln(1+i)\) is undefined and \((1+i)^{-n}\) is also undefined;
any \(i < -1\) yields complex results for both expressions.
The check fires inside InterestRate.delta(t) and InterestRate.vn(n),
but not at construction time.
Fix: Use \(i > -1\). For reference, typical life-annuity discount rates are in the range \([0.01, 0.06]\), but any value \(> -1\) is mathematically valid.
from lactuca import InterestRate
InterestRate(0.03) # ✔ 3 % annual effective rate
InterestRate(-0.5) # ✔ negative but > −1 (e.g. ECB negative deposit rate)
InterestRate(-1.0) # ← construction succeeds; error fires on first use
InterestRate(-1.0).delta(1) # ✘ → ValueError: rate ≤ −1 (ln(0) undefined)
InterestRate(-2.0).delta(1) # ✘ → ValueError: rate < −1
InterestRate(-1.0).vn(5) # ✘ → ValueError: rate ≤ −1 ((1+i) = 0)
InterestRate(-2.0).vn(1) # ✘ → ValueError: rate < −1
Note
The rate validation is lazy: InterestRate(-1.0) constructs without raising.
The ValueError fires when computing the force of interest via InterestRate.delta(t)
or the discount factor via InterestRate.vn(n) — both require \((1+i) > 0\)
by the same actuarial precondition. Rates in the range \((-1, 0)\) are economically
valid (e.g. the ECB maintained negative deposit rates of \(-0.5~\%\) from 2019 to 2022)
and are accepted by both methods without raising.
Piecewise rate input errors#
Exception: ValueError (or TypeError for non-finite rate values — see table)
Message patterns:
Condition |
Exception |
Message pattern |
|---|---|---|
|
|
|
|
|
|
Any term is non-positive |
|
|
A rate is non-finite (NaN or Inf) |
|
|
Cause: The piecewise InterestRate constructor validates terms and rates before
accepting them. Each condition is checked in order; the first failing condition raises.
Fix:
ratesmust have exactlylen(terms) + 1elements: one rate per interval plus one “tail” rate that applies indefinitely beyond the last term breakpoint.All
termsmust be strictly positive (no zero or negative durations).All rates must be real finite numbers —
math.nan,math.inf, and-math.infare rejected.All rates must also be economically valid (\(> -1\)); see Rate ≤ −1.
import math
from lactuca import InterestRate
# 2 terms → rates must have exactly 3 elements (len(terms) + 1):
ir = InterestRate(terms=[5, 10], rates=[0.03, 0.04, 0.05]) # ✔
# ✘ empty arrays — constructor rejects empty terms or rates:
InterestRate(terms=[], rates=[]) # ✘ → ValueError
# ✘ only 2 rates for 2 terms — must be len(terms)+1 = 3:
InterestRate(terms=[5, 10], rates=[0.03, 0.04]) # ✘ → ValueError
# ✘ negative term duration:
InterestRate(terms=[-5, 10], rates=[0.03, 0.04, 0.05]) # ✘ → ValueError
# ✘ NaN rate — not finite (raises TypeError, not ValueError):
InterestRate(terms=[10], rates=[0.03, math.nan]) # ✘ → TypeError
InterestRate advanced constructor#
Exception: ValueError
Message pattern: "Invalid term_unit: {value}. Must be one of {VALID_TERM_UNITS}"
Cause: The term_unit argument controls how the terms sequence is interpreted.
Passing a string outside the four allowed values raises immediately.
Allowed values: 'years', 'months', 'weeks', 'days' (default: 'years').
from lactuca import InterestRate
InterestRate(0.03) # ✔ default years
InterestRate(0.03, term_unit='months') # ✔
InterestRate(terms=[6, 12], rates=[0.01, 0.012, 0.013], term_unit='months') # ✔
InterestRate(0.03, term_unit='biweekly') # ✘ → ValueError
InterestRate(0.03, term_unit='annual') # ✘ → ValueError
InterestRate.add_scenario errors#
Exception: TypeError or ValueError
Message patterns:
Condition |
Exception |
Message pattern |
|---|---|---|
|
|
|
Nested multi-scenario |
|
|
Cause: Each scenario must be a simple (constant or piecewise) InterestRate. Passing
a multi-scenario container or any other type raises.
from lactuca import InterestRate
ir = InterestRate({'base': 0.03})
ir.add_scenario('stress', InterestRate(0.05)) # ✔
ir.add_scenario('nested', InterestRate({'a': 0.03})) # ✘ → ValueError: nested
ir.add_scenario('bad', 0.05) # ✘ → TypeError: not InterestRate
Note
Setting ir.active_scenario = 'unknown' raises ValueError: "Scenario 'unknown' not found."
if the name does not exist. Use ir.scenario_names to list valid names.
InterestRate.vx validation errors#
Exception: ValueError
Message patterns:
Condition |
Message pattern |
|---|---|
|
|
|
|
|
|
Cause: vx(x, x0) computes the discount factor \(v^{x - x_0}\) for a duration of
\(x - x_0\) years. Both arguments represent ages or time points and must be non-negative,
and x must be at least as large as x0 (the reference start).
Fix: Ensure x >= x0 >= 0 for all elements.
import numpy as np
from lactuca import InterestRate
ir = InterestRate(0.03)
ir.vx(5) # ✔ x0=0 by default
ir.vx(np.array([30, 40]), x0=25) # ✔
ir.vx(-1) # ✘ → ValueError: negative x
ir.vx(5, x0=-1) # ✘ → ValueError: negative x0
ir.vx(np.array([10, 20]), x0=np.array([15, 5])) # ✘ → ValueError: x[0] < x0[0]
InterestRate.i_m / d_m errors#
Exception: TypeError or ValueError
i_m(m, t) computes the nominal rate \(i^{(m)}\) and d_m(m, t) computes
the nominal discount rate \(d^{(m)}\). Both share identical validation.
Message patterns:
Condition |
Exception |
Message pattern |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Cause: m is the payment frequency (must be a positive plain int); t is the
period in years (non-negative).
from lactuca import InterestRate
ir = InterestRate(0.03)
ir.i_m(12, 0) # ✔ i^(12) at t=0
ir.d_m(2, 0) # ✔ d^(2) at t=0
ir.i_m(0, 0) # ✘ → ValueError: m <= 0
ir.i_m(12.0, 0) # ✘ → TypeError: float, not int
ir.i_m(True, 0) # ✘ → TypeError: bool
ir.d_m(12, -1) # ✘ → ValueError: t < 0
ir.d_m(12, 't') # ✘ → TypeError: not numeric
InterestRate.get_average_force errors#
Exception: TypeError or ValueError
Message patterns:
Condition |
Exception |
Message pattern |
|---|---|---|
|
|
|
|
|
|
Discount factor ≤ 0 |
|
|
Cause: The average force of mortality \(\bar{\delta}_n = -\ln(v^n)/n\) requires \(n > 0\) and \(v^n > 0\); the latter fails if the underlying rate is sufficiently negative.
from lactuca import InterestRate
ir = InterestRate(0.03)
ir.get_average_force(5) # ✔
ir.get_average_force(0) # ✘ → ValueError: n <= 0
ir.get_average_force(-1) # ✘ → ValueError: n <= 0
ir.get_average_force('5') # ✘ → TypeError
Wrong type for interest_rate#
Exception: TypeError
Message patterns:
Context |
Message |
|---|---|
|
|
|
|
Note
This TypeError applies to the interest_rate property setter and to
LifeTable.__init__ (the interest_rate= constructor argument). The ir= keyword
on annuity and insurance methods (äx, ax, Ax, etc.) is more permissive: it accepts
integers and automatically coerces them via InterestRate(ir) — so lt.äx(65, ir=3)
produces a 300 % discount rate without raising. Always pass a float to avoid
unintentionally enormous discount rates.
Cause: The interest_rate property setter (or the interest_rate= constructor
argument) received a value that is not a float, an InterestRate object, or None.
The most common mistake is passing an int such as 3 instead of the float 0.03.
Fix: Use a float literal, an InterestRate object, or None. Note that 3.0
is a float but means 300 % — use 0.03 for 3%: 0.03, 3.0 / 100, or
InterestRate(0.03).
from lactuca import LifeTable, InterestRate
lt = LifeTable("PASEM2020_Rel_1o", "m")
# Property assignment:
lt.interest_rate = 0.03 # ✔ float
lt.interest_rate = InterestRate(0.03) # ✔ InterestRate object
lt.interest_rate = None # ✔ clears the table default
lt.interest_rate = 3 # ✘ → TypeError (int — use 0.03 for a 3 % rate)
lt.interest_rate = "0.03" # ✘ → TypeError (str not accepted)
# ir= keyword argument — more permissive than the property setter:
lt.äx(65, ir=0.03) # ✔ float
lt.äx(65, ir=InterestRate(0.03)) # ✔ InterestRate object
lt.äx(65, ir=3) # ✔ int coerced — 3 means 300 %, use 0.03 for 3 %
Table construction errors#
These errors occur during table loading or construction — typically when a custom .ltk
file contains invalid data or when a generational table call omits required metadata.
Terminal age condition — \(q_\omega \neq 1\)#
Exception: ValueError
Message pattern: "Life table '{table}': qx_{sex}[omega={omega}] must be 1.0, got {value}."
|
Sex column |
|---|---|
|
Male ( |
|
Female ( |
|
Unisex ( |
Cause: The annual probability of death at the terminal age \(\omega\) must equal exactly 1.0 — all survivors must die by age \(\omega\). Deviations \(> 10^{-4}\) from 1.0 are rejected.
Fix: Set qx[omega] = 1.0 in your source data. Open the .ltk file and check the
last row of the relevant decrement column (qx_m, qx_f, or qx_u). Values within
\(10^{-4}\) of 1.0 (e.g. 0.9999998) are silently clamped on load and will not raise;
only deviations \(> 10^{-4}\) trigger this error.
Note
All bundled Lactuca tables are pre-validated and will never raise this error. This error
only occurs when loading a user-supplied .ltk file. See
Building Custom Table Files (.ltk) for the .ltk format specification and the
qx[omega] = 1.0 requirement.
Generational table errors#
Exception: ValueError
Causes and message patterns:
Cause |
Message pattern |
|---|---|
|
|
|
|
|
|
|
|
|
|
Fix:
Static table,
cohortsupplied: remove thecohortargument from theLifeTable(…)call — static tables are indexed by attained age only.Generational table, no
cohort: addcohort=<birth_year>(e.g.cohort=1975); the value is the policyholder’s year of birth.Missing
base_year: open the.ltkfile and addbase_year = <year>to the metadata section at the top of the file; see Building Custom Table Files (.ltk) for the full.ltkmetadata format.Non-integer / out-of-range cohort: use a plain four-digit integer between 1900 and the current year.
from lactuca import LifeTable
# Each ✔ / ✘ pair below shows independent alternative calls, not a sequence.
# PASEM2020_Rel_1o is a static (period) table — cohort not expected:
lt = LifeTable("PASEM2020_Rel_1o", "m") # ✔ no cohort
lt = LifeTable("PASEM2020_Rel_1o", "m", cohort=1975) # ✘ → ValueError: static table
# PER2020_Ind_1o is a generational table — cohort is required:
lt_gen = LifeTable("PER2020_Ind_1o", "f", cohort=1980) # ✔ cohort provided
lt_gen = LifeTable("PER2020_Ind_1o", "f") # ✘ → ValueError: no cohort
Tip
Check Bundled Actuarial Tables to confirm whether a specific bundled table is
generational (requires cohort=) or static (no cohort needed).
Configuration errors#
These errors are raised when a configuration setting receives an invalid value. Configuration is validated at the point of assignment, so the error is immediate and traceable to the offending line.
Invalid setting value#
Exception: ValueError
Message pattern: "{setting} must be one of {allowed_values}"
Cause: A direct property assignment (e.g. config.lx_interpolation = "...") or a
config.set(key, value) call received a value outside the allowed set for that setting.
Fix: Use one of the allowed values from the table below. String values are
case-sensitive: "linear" is valid, but "Linear" and "UDD" raise; similarly
"exponential" is valid but not "Exponential" or "constant_force". Numeric
settings such as days_per_year also reject string arguments (e.g. "365.25").
Setting |
Allowed values |
Default |
Guide |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Example — wrong vs. correct assignment:
from lactuca import config
# Valid — each of these is a correct assignment (run independently, not in sequence):
config.lx_interpolation = "linear" # ✔ UDD assumption (default)
config.lx_interpolation = "exponential" # ✔ constant force of mortality
config.calculation_mode = "discrete_precision" # ✔ valid
config.days_per_year = 365.25 # ✔ valid
config.reset() # restore all settings to defaults
# Invalid — each raises ValueError independently:
config.lx_interpolation = "udd" # ✘ → ValueError: not an allowed value
config.calculation_mode = "exact" # ✘ → ValueError: not an allowed value
config.days_per_year = 364 # ✘ → ValueError: 364 not in allowed set
Decimal places — negative or non-integer#
Exception: ValueError
Message pattern: "decimal places must be non-negative integers"
Cause: A config.decimals.* setter received a value that is not a non-negative
integer — for example, a negative integer (-1), a positive float (4.5),
or any other non-integer value.
Fix: Use a non-negative integer. Setting to 0 rounds output to the nearest
integer. For typical actuarial output use 4–6 decimal places; use 10 or higher
only when maximum float64 precision is needed in the output.
from lactuca import config
config.decimals.annuities = 4 # ✔ 4 decimal places for annuity output
config.decimals.lx = 8 # ✔ 8 decimal places for lx output
config.decimals.annuities = 0 # ✔ round to integer (0 decimal places)
config.decimals.annuities = -1 # ✘ → ValueError: negative integer
config.decimals.annuities = 4.5 # ✘ → ValueError: non-integer float
config.reset() # restore all decimals to defaults
Input array errors#
These errors occur when two array arguments have incompatible lengths or shapes, preventing Lactuca from broadcasting or pairing them element-wise.
Mismatched cashflow arrays#
Exception: ValueError
Message pattern: "[{method}] Length of 'cashflow_amounts' must match 'cashflow_times'"
Cause: The two arrays supplied to the irregular cashflow interface have different
lengths (cashflow_times and cashflow_amounts must have the same length).
Fix: Ensure both arrays have the same length before the call.
A common source of mismatch is omitting a payment amount for one cashflow time, or
building the two arrays from different data sources. Assert equality at the call site:
assert len(cashflow_times) == len(cashflow_amounts) before the call.
# cashflow_times and cashflow_amounts are passed to the irregular cashflow interface.
# See user_guide/irregular_cashflows for the full API and calling convention.
# Valid pair — same length, safe to pass to the cashflow method:
cashflow_times = [1.0, 2.0, 3.0] # length 3
cashflow_amounts = [100.0, 200.0, 300.0] # length 3 ✔ lengths match
# Invalid pair — different lengths, raises ValueError when passed:
cashflow_times = [1.0, 2.0, 3.0] # length 3
cashflow_amounts = [100.0, 200.0] # length 2 ✘ → ValueError
Broadcasting errors — incompatible shapes#
Exception: ValueError
Message pattern:
Incompatible shapes for 'x' and 'x0': {shape1} and {shape2}.
Both must have the same shape or one must have size 1.
{shape1} and {shape2} are NumPy shape tuples, e.g. (3,) and (2,).
Cause: InterestRate.vx(x, x0) computes discount factors at ages x relative to
reference age(s) x0. Lactuca broadcasts a length-1 (or scalar) x0 against x of
any length. If both x and x0 have more than one element but differ in length,
broadcasting fails and this error is raised.
Fix: Use equal-length arrays, or pass a scalar or single-element array for x0
(either broadcasts freely against an x array of any length).
import numpy as np
from lactuca import InterestRate
ir = InterestRate(terms=[5], rates=[0.02, 0.03]) # non-constant rate curve
ages = np.array([30, 40, 50]) # length 3
ir.vx(ages, x0=0) # ✔ scalar x0 broadcasts freely
ir.vx(ages, x0=np.array([0])) # ✔ length-1 array also broadcasts
ir.vx(ages, x0=np.array([0, 0, 0])) # ✔ same length as ages
x0_bad = np.array([0, 0]) # length 2 ≠ length 3
ir.vx(ages, x0=x0_bad) # ✘ → ValueError: incompatible shapes
Cashflow utility errors#
These errors are raised by :func:generate_payment_times, :func:tiered_amounts, and
- meth:
GrowthRate.amountswhen payment schedule or tier arguments are invalid.
Invalid payment frequency#
Exception: ValueError
Message patterns:
Function |
Message pattern |
|---|---|
|
|
|
|
Cause: The m argument is not one of the allowed payment frequencies:
1, 2, 3, 4, 6, 12, 14, 24, 26, 52, or 365.
Fix: Use an integer from the allowed set. Common values: 1 (annual), 2 (semi-annual), 4 (quarterly), 12 (monthly), 52 (weekly).
from lactuca import generate_payment_times as gpt, GrowthRate
times = gpt(n=3, m=12) # ✔ monthly
gpt(n=3, m=365) # ✔ daily
gpt(n=3, m=7) # ✘ → ValueError: 7 not in allowed set
gr = GrowthRate(0.02)
gr.amounts(times, start=1000.0, m=12) # ✔
gr.amounts(times, start=1000.0, m=7) # ✘ → ValueError
Invalid selected periods#
Exception: ValueError or TypeError
Message patterns:
Condition |
Exception |
Message pattern |
|---|---|---|
Period value outside |
|
|
Non-integer value (e.g. float) |
|
(raised by the input validator) |
Cause: Each element of selected_periods must be an integer between 1 and m
inclusive. Periods are 1-based: period 1 is the first payment of the year, period m
is the last. Float values raise TypeError; integers outside [1, m] raise ValueError.
Fix: Use integer-valued periods in the range [1, m].
from lactuca import generate_payment_times as gpt
gpt(n=5, m=4, selected_periods=[1, 3]) # ✔ Q1 and Q3
gpt(n=5, m=12, selected_periods=[1, 7]) # ✔ January and July
gpt(n=5, m=4, selected_periods=[0, 3]) # ✘ → ValueError: 0 < 1 (periods are 1-based)
gpt(n=5, m=4, selected_periods=[5]) # ✘ → ValueError: 5 > m=4
gpt(n=5, m=4, selected_periods=[1.5]) # ✘ → TypeError: non-integer value
Tier structure errors#
Exception: ValueError
Message patterns:
Condition |
Message pattern |
|---|---|
Length mismatch |
|
Breakpoints not strictly ascending |
|
Cause: tiered_amounts requires exactly one values entry per tier. With
len(breakpoints) = B boundaries there are B + 1 tiers, so len(values) must equal
B + 1. Breakpoints must also be strictly increasing — duplicate or decreasing values
are rejected.
Fix: Ensure len(values) == len(breakpoints) + 1 and sort breakpoints in strictly
ascending order.
import numpy as np
from lactuca import tiered_amounts
times = np.arange(1.0, 11.0)
tiered_amounts(times, breakpoints=[5], values=[1.0, 1.1]) # ✔ 1 bp → 2 values
tiered_amounts(times, breakpoints=[5, 8], values=[1.0, 1.1, 1.2]) # ✔ 2 bp → 3 values
tiered_amounts(times, breakpoints=[5], values=[1.0]) # ✘ → ValueError: 1 ≠ 2
tiered_amounts(times, breakpoints=[5], values=[1.0, 1.1, 1.2]) # ✘ → ValueError: 3 ≠ 2
tiered_amounts(times, breakpoints=[8, 5], values=[1.0, 1.1, 1.2]) # ✘ → ValueError: not ascending
Non-finite cashflow inputs#
Exception: ValueError
Message patterns:
Input |
Function |
Message pattern |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Cause: A NaN or infinite value was found in an input array or in the scalar start
argument. Checks are vectorized and report the index of the first offending element.
Fix: Replace or filter non-finite values before calling. Use np.isfinite(arr) to
locate them.
import numpy as np
from lactuca import tiered_amounts, GrowthRate, generate_payment_times as gpt
times = gpt(n=10, m=1)
tiered_amounts(times, [5], [1.0, 1.1]) # ✔
bad_times = times.copy()
bad_times[3] = np.nan
tiered_amounts(bad_times, [5], [1.0, 1.1]) # ✘ → ValueError: index 3
gr = GrowthRate(0.02)
gr.amounts(times, start=float('inf'), m=1) # ✘ → ValueError: start not finite
gr.amounts(times, start=float('nan'), m=1) # ✘ → ValueError: start not finite
times is None#
Exception: TypeError
Message pattern: "[GrowthRate.amounts] times must be array-like, got None."
Cause: None was passed as the times argument to GrowthRate.amounts. Unlike
other invalid inputs, None raises TypeError (not ValueError) because it is not
array-like at all.
Fix: Pass an array-like of payment times — typically the output of
- func:
generate_payment_times, a NumPy array, or a Python list of floats.
from lactuca import GrowthRate, generate_payment_times as gpt
gr = GrowthRate(0.02)
times = gpt(n=5, m=12)
gr.amounts(times, start=1000.0, m=12) # ✔
gr.amounts(None, start=1000.0, m=12) # ✘ → TypeError: times is None
GrowthRate errors#
These errors are raised by the GrowthRate class — its constructor, property setters, and
public methods.
GrowthRate constructor errors#
Exception: ValueError or TypeError
Message patterns:
Condition |
Exception |
Message pattern |
|---|---|---|
Invalid |
|
|
|
|
|
|
|
|
|
|
|
Geometric |
|
|
Arithmetic |
|
|
Piecewise geom rates ≤ -1 |
|
|
Arithmetic cumulative factor ≤ 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Term ≤ 0 |
|
|
Scenario dict empty |
|
|
Scenario name not string |
|
|
Scenario value is |
|
|
Nested multi-scenario |
|
|
Scenario value wrong type |
|
|
No valid constructor branch |
|
|
Cause: The GrowthRate constructor validates every input before storing state.
The three valid constructor paths are:
Scalar constant
GrowthRate(rate=0.02)— single rate.Piecewise
GrowthRate(rates=[0.01, 0.02], terms=[5])—len(rates)must belen(terms) + 1and all terms ≥ 1.Scenario dict
GrowthRate({'base': 0.02, 'stress': 0.04})— each value is a float or a simple (non-multi-scenario)GrowthRate.
Geometric growth requires every rate > -1 so the accumulation factor remains positive.
Arithmetic growth also requires every individual rate > -1 (factor at period 1 > 0)
and, for piecewise schedules, that no prefix sum drives the cumulative factor to 0 or below.
Fix: Use one of the three valid paths. Check growth_type is 'g' or 'a'; pass
a bool for apply_from_first; do not pass True/False as a rate value.
from lactuca import GrowthRate
# ✔ Correct forms
GrowthRate(0.02) # scalar geometric
GrowthRate(0.02, growth_type='g') # explicit geometric
GrowthRate(0.05, growth_type='a') # arithmetic
GrowthRate(rates=[0.01, 0.02, 0.03], terms=[5, 10]) # piecewise, 3 rates 2 terms
GrowthRate({'base': 0.02, 'stress': 0.04}) # scenario dict
# ✘ ValueError / TypeError
GrowthRate(0.02, growth_type='x') # ✘ unknown growth_type
GrowthRate(0.02, apply_from_first=1) # ✘ int, not bool
GrowthRate(True) # ✘ bool, not float
GrowthRate(-1.0) # ✘ rate ≤ -1
GrowthRate(rates=[0.01, 0.02]) # ✘ terms not provided
GrowthRate(rates=[0.01, 0.02], terms=[5, 10]) # ✘ 2 rates, needs 3 for 2 terms
GrowthRate(rates=[0.01, 0.02, 0.03], terms=[5, 0]) # ✘ term = 0
GrowthRate({}) # ✘ empty dict
GrowthRate property setters#
Exception: ValueError
Message patterns:
Property |
Condition |
Message pattern |
|---|---|---|
|
Multi-scenario container |
|
|
Not 1-D |
|
|
Length mismatch |
|
|
Multi-scenario container |
|
|
Constant instance |
|
|
Not 1-D |
|
|
Length mismatch |
|
|
Name not found |
|
Cause: The rates and terms setters allow in-place updates to an existing schedule
without changing the number of segments. They are not available on constant instances
(which have no terms) or multi-scenario containers. The active_scenario setter
requires the name to already exist in scenarios.
Fix: Create a new GrowthRate instance to change the segment count. To update rates
in-place, the replacement array must have the same length as the existing rates array.
from lactuca import GrowthRate
gr = GrowthRate(rates=[0.01, 0.02], terms=[5])
gr.rates = [0.015, 0.025] # ✔ same length (2)
gr.terms = [8] # ✔ same length (1)
gr.rates = [0.01, 0.02, 0.03] # ✘ → ValueError: length mismatch
gr.terms = [3, 7] # ✘ → ValueError: length mismatch
constant_gr = GrowthRate(0.02)
constant_gr.terms = [5] # ✘ → ValueError: constant instance
scen_gr = GrowthRate({'base': 0.02})
scen_gr.rates = [0.02] # ✘ → ValueError: multi-scenario container
scen_gr.active_scenario = 'x' # ✘ → ValueError: scenario not found
GrowthRate.add_scenario errors#
Exception: TypeError or ValueError
Message patterns:
Condition |
Exception |
Message pattern |
|---|---|---|
|
|
|
Nested multi-scenario |
|
|
Cause: Scenarios must be simple (constant or piecewise) GrowthRate instances.
Passing any other type — or a GrowthRate that is itself a multi-scenario container —
raises.
Fix: Pass a constant or piecewise GrowthRate directly.
from lactuca import GrowthRate
gr = GrowthRate({'base': 0.02})
gr.add_scenario('stress', GrowthRate(0.04)) # ✔
gr.add_scenario('nested', GrowthRate({'a': 0.02})) # ✘ → ValueError: nested
gr.add_scenario('bad', 0.04) # ✘ → TypeError: not GrowthRate
GrowthRate.factor errors#
Exception: TypeError
Message pattern: "[GrowthRate.factor] t must be numeric (int, float, or array), got None."
Cause: None was passed as t. Unlike invalid numeric values (which raise
ValueError), None raises TypeError because it is not numeric at all.
Fix: Pass an integer or array of non-negative integers.
from lactuca import GrowthRate
gr = GrowthRate(0.02)
gr.factor(5) # ✔
gr.factor(None) # ✘ → TypeError
See also#
Age and term
Commutation Functions — integer-age requirement; all commutation function signatures
lx Interpolation —
lx_interpolationallowed values (UDD vs. constant force of mortality)Numerical Precision — float64 tolerance and integer detection behaviour
Interest rates
Interest Rates — valid
InterestRateinputs and piecewise construction
Tables
Using Actuarial Tables — table construction, sex codes, and loading lifecycle
Building Custom Table Files (.ltk) — custom
.ltkformat andqx[omega] = 1.0requirementBundled Actuarial Tables — bundled tables, generational vs. static classification
Configuration and output
Decimal Precision and Rounding —
config.decimals.*settings and rounding behaviourCalculation Modes —
calculation_modeallowed values and their behaviourConfiguration — all configuration options, allowed values, and defaults
Force of Mortality Methods —
force_mortality_methodconfig setting:"finite_difference","spline", and"kernel"methods for estimating \(\mu(x)\)
Cashflows and growth rates
Irregular Cashflows — irregular cashflow interface and array alignment
Growth Rates —
GrowthRateconstruction,amounts(), and payment frequency conventions