Calculation Modes#
Lactuca supports four calculation modes that control how life contingency present values are
computed. The active mode is set globally via config.calculation_mode and applies equally
to annuities (äx, ax), insurances (Ax), and pure endowments (nEx).
Actuarial coherence across modes#
Lactuca exposes four calculation modes, not four different products. Every mode values the same actuarial present value — annuity-due \(\ddot{a}\), insurance \(A\), pure endowment \({}_n E\) — under the same global conventions:
Decrement tables and modifications (
modify_qx,table_combination, optionalcombination_mode— see Modifying Decrements)config.mortality_placement,config.lx_interpolation,config.force_mortality_methodInterest (
InterestRate), growth (GrowthRate), and payment timing (äxvsax)Joint-life independence; scalar and batch APIs on the same underlying definitions
Actuarial coherence at 100% does not mean numerical identity. Modes implement different approximation paths (exact payment grid, Woolhouse, numerical integration, adjacent-age averaging). Differences are expected and actuarially legitimate when the mode is used within its documented scope.
What must align across modes |
What may differ |
|---|---|
Product definition (benefit timing, deferment, term) |
Approximation formula per mode |
Global |
Rounded vs unrounded \(l_x\) (discrete vs continuous) |
Same table / |
Relative error of simplified vs precision |
Delegation when an approximation is invalid (e.g. fractional \(n\)) |
Speed and internal grid resolution |
Mode selection is an accuracy / speed trade-off, not a switch of actuarial model.
For regulatory and production work, discrete_precision is the reference discrete
implementation. Use discrete_simplified or continuous modes for sensitivity runs,
benchmarking, or exploratory analysis — compare results against discrete_precision rather
than assuming bit-identical output.
Important
Do not expect discrete_precision and continuous_precision to match to the last
decimal place, or discrete_simplified to match discrete_precision for all \(m\) and \(n\).
Expect coherent answers to the same actuarial question, not equality.
What the mode controls#
Each mode makes two independent decisions:
Computation method — exact payment-grid sum, Woolhouse/UDD interpolation from annual values, or numerical integration.
Mortality table precision — whether survival probabilities are derived from the rounded \(l_x\) values stored in the published table, or from the unrounded floating-point \(l_x\) values retained internally during table construction. Rounding is configured via
config.decimals.lx(default 15 decimal places). Discrete modes use rounded values — consistent with classical commutation-function practice. Continuous modes bypass rounding for higher precision in integration.
Mode comparison#
Mode |
Method |
\(l_x\) precision |
Typical use |
|---|---|---|---|
|
Exact payment-grid summation |
Rounded |
Production (default) |
|
Woolhouse interpolation from annual values |
Rounded |
Fast estimates, \(m>1\) |
|
Trapezoidal numerical integration |
Unrounded |
Highest accuracy |
|
Annual calculations + fractional interpolation |
Unrounded |
Continuous approximation |
Mode descriptions#
discrete_precision (default)#
The most commonly used mode for production calculations.
The engine constructs the exact payment timing grid and evaluates survival probabilities and discount factors at each payment time, for all values of \(m\) (including \(m=1\)) and all starting ages (including fractional).
Annuities — direct summation over the payment grid. For an annuity-due (\(\ddot{a}\), payments at period start) and an annuity-immediate (\(a\), payments at period end):
Fractional survival probabilities are computed from the rounded \(l_x\) table using the
interpolation method set in config.lx_interpolation (default "linear", i.e. the Uniform
Distribution of Deaths (UDD) assumption). This knob does not set insurance benefit
timing — see config.mortality_placement and Two independent mortality settings.
Insurances — present value of a benefit payable upon death in each sub-annual period,
discounted to the benefit payment time \(\delta_m\) controlled by config.mortality_placement
(see Insurance: mortality placement below for the offset values):
Here \({}_{j/m}p_x\) and \({}_{1/m}q_{x+j/m}\) follow lx_interpolation; \(\delta_m\) follows
mortality_placement only. Annuities and pure endowments ignore mortality_placement.
Pure endowments — exact survival probability and discount factor at the end of the term:
Because this mode constructs the exact payment timing grid, it is the only one that
handles fractional final payments — applicable to both annuities and insurances. When
\(n\) is not an exact multiple of \(1/m\): for annuities the last payment amount is
proportionally reduced; for insurances both the death probability of the last sub-annual
period and the benefit timing offset are scaled by the fractional weight. See
Last Payment Adjustment for details. discrete_simplified and the continuous modes
do not apply this adjustment.
Note
Commutation function formulas such as \(\ddot{a}_x = N_x / D_x\) are not used internally
by this (or any other) mode. The Dx(), Nx(), Cx(), Mx(), etc. methods are standalone
tools for classical calculations, not the internal engine path.
discrete_simplified#
Converts from annual-basis values to payment frequency \(m\) using the Woolhouse (UDD) 2-term
approximation. This mode requires only two annual calculations regardless of \(m\), making it
considerably faster than discrete_precision for \(m > 1\).
Annuities-due — interpolates between the annual annuity-due and annuity-immediate:
Annuities-immediate — the interpolation coefficients are swapped:
For \(m=1\) both formulas reduce to their annual counterpart (no approximation needed).
Insurances — linear age interpolation between consecutive annual insurance values:
(Woolhouse applies to annuities only; insurances use adjacent-age linear interpolation — not the textbook \((i/i^{(m)})A_x\) UDD frequency adjustment.)
Important
Insurance discrete_simplified is not the textbook formula. Actuarial manuals
often write \(A_x^{(m)} \approx (i/i^{(m)})\,A_x\) under UDD. Lactuca instead
interpolates between \(A_x\) and \(A_{x+1}\) at annual frequency, then applies the
hybrid \(k+s\) tail for fractional terms. Use discrete_precision when the exact
\(m\)-thly benefit grid matters.
Pure endowments — applies year-by-year UDD survival to evaluate \({}_np_x\):
For a fractional duration \(0 < n \leq 1\): \({}_np_x^{\text{UDD}} = 1 - n\,q_x\) (UDD within the year). For \(n > 1\), each integer year and the terminal fractional year are evaluated under UDD and multiplied: \({}_np_x^{\text{UDD}} = p_x \cdot p_{x+1} \cdots p_{x+k-1} \cdot (1 - s\,q_{x+k})\), where \(k = \lfloor n \rfloor\) and \(s = n - k\).
Slightly less accurate than discrete_precision for integer terms \(n\) and \(m > 1\)
(typical relative error \(< 0.01\%\) for \(m \leq 12\)). For fractional effective terms
\(n_\text{eff} \notin \mathbb{Z}\) with \(m > 1\), a hybrid decomposition applies:
\(k = \lfloor n_\text{eff} \rfloor\) — Woolhouse (annuities) or linear age interpolation (insurances) on annual temporaries of duration \(k\);
\(s = n_\text{eff} - k\) — exact \(m\)-thly tail using
discrete_precisionconventions (including growth anniversaries at \(k + \lfloor j/m \rfloor\)).
When \(k = 0\) (\(n_\text{eff} < 1\)), the result equals discrete_precision. When
\(n_\text{eff}\) is an integer, behaviour is unchanged from the pre-hybrid implementation.
Results with \(k \geq 1\) differ from full discrete_precision — that is expected
under mode independence. Use discrete_precision when the exact payment grid matters.
Custom cashflow_times / cashflow_amounts are not supported in this mode — use
discrete_precision explicitly. Both modes use rounded \(l_x\) values.
continuous_precision#
Computes present values via numerical integration over unrounded \(l_x\) values:
Annuities: \(\bar{a}_x = \int_0^\infty e^{-\delta t} \cdot {}_t p_x\, \mathrm{d}t\)
Insurances: \(\bar{A}_x = \int_0^\infty e^{-\delta t} \cdot {}_t p_x \cdot \mu_{x+t}\, \mathrm{d}t\)
Pure endowments: $\({}_{n}E_x = \exp\!\left(-\int_0^n \bigl[\delta(t) + \mu_{x+t}\bigr]\,\mathrm{d}t\right)\)$
The integrals are evaluated with the trapezoidal rule over a fine uniform grid. Default resolution differs by product type: annuities use 200 steps per unit of time (total steps \(= \lceil 200n \rceil\), scaling with term length); insurances use exactly 1000 integration points; pure endowments use \(\max(1000,\, 50n)\) points — ensuring at least 50 steps per year for long-duration contracts (the extra resolution kicks in when \(n > 20\) years). All survival probability evaluations use unrounded \(l_x\) values, giving the highest precision.
continuous_simplified#
Avoids numerical integration by combining exact annual calculations with actuarial
interpolation. Like continuous_precision, unrounded \(l_x\) values are used throughout.
Annuities — for term \(n\) with integer part \(k = \lfloor n \rfloor\) and fractional part \(s = n - k\), the implementation applies a composite trapezoidal rule with step 1 for the integer portion and a single trapezoidal step of width \(s\) for the terminal fractional period:
The first term — average of the \(m=1\) due and immediate annuities — is the composite trapezoidal approximation of \(\bar{a}_{x:\overline{k}|}\). The second term is a one-step trapezoid over \([k,\,n]\). Only two annual annuity calculations are needed regardless of \(k\).
Note
The annuity formula above is the composite trapezoidal rule with step \(h = 1\).
Its approximation error relative to the exact continuous integral scales as \(O(h^2)\),
the same order as the standard trapezoidal rule. For higher precision, switch to
continuous_precision, which evaluates the integral on a fine grid (default 200 steps
per year) and achieves correspondingly smaller errors.
Insurances — the value is the arithmetic mean of two continuous_precision
evaluations at ages \(x\) and \(x+1\). Denoting the precision result at age \(y\) as
\(\bar{A}_y^{\text{prec}}\), the simplified result is:
This formula applies for all starting ages (integer or fractional).
Pure endowments — uses the average force of mortality and interest:
where \(\bar{\delta} = -\ln v^n / n\) and \(\bar{\mu}_x = -\ln({}_np_x) / n\) are the average forces of interest and mortality over the term. This is considerably faster than numerical integration while remaining accurate for slowly varying mortality and interest rates.
Setting the mode#
from lactuca import config
config.calculation_mode = "discrete_precision" # default
config.calculation_mode = "discrete_simplified"
config.calculation_mode = "continuous_precision"
config.calculation_mode = "continuous_simplified"
Or in a TOML config file:
[calculation]
calculation_mode = "continuous_precision"
For a full reference of all configuration settings, see Configuration.
Payment frequencies#
The m parameter (payments per year) applies to all four modes and is restricted to these
values:
|
Payments per year |
|---|---|
|
Annual |
|
Semi-annual |
|
Every four months |
|
Quarterly |
|
Bi-monthly |
|
Monthly |
|
14 payrolls/year — Spanish “14 pagas” scheme (approximate, see note below) |
|
Twice-monthly (equispaced under 360-day commercial year) |
|
Biweekly (equispaced under 364-day year: 52×7 days) |
|
Weekly |
|
Daily |
Passing any other value raises ValueError.
Warning
m=14 is accepted for convenience but is not equispaced under any standard year
convention (365÷14≈26.07 days; 360÷14≈25.71 days). Using it in the standard fractional
annuity formula introduces a systematic approximation error. For actuarially precise
results, model the benefit as a monthly annuity (m=12) plus two extraordinary
cash flows at mid-year and year-end using Irregular Cashflows.
Two independent mortality settings#
Lactuca exposes two independent global settings. Both involve when death occurs in actuarial language, but they control different calculation steps. Changing one does not substitute for the other.
Setting |
Controls |
Used in |
Does not affect |
|---|---|---|---|
|
Bridging integer-age \(l_x\) to fractional ages — UDD ( |
\({}_{j/m}p_x\), \({}_{1/m}q_x\), annuity payment grids, pure endowment \({}_np_x\), continuous-mode integration |
Insurance discount offset \(\delta_m\); classical identity \(1 = d\,\ddot{a}_x + A_x\) |
|
Timing of the death benefit payment within each sub-annual period (\(\delta_m\) in insurance discount) |
Insurances ( |
Annuities (\(\ddot{a}_x\), \(a_x\)); pure endowments (\({}_n E_x\)); how \({}_{j/m}p_x\) is computed |
In discrete_precision insurance, both settings compose: survival probabilities and
interval death probabilities come from lx_interpolation; the present-value exponent
adds \(\delta_m\) from mortality_placement. See lx Interpolation and
Insurance: mortality placement below.
Insurance: mortality placement#
config.mortality_placement controls when within each sub-annual period the death benefit
is assumed to be paid. It affects insurances only — annuities and pure endowments are
not affected. This setting is independent of config.lx_interpolation (fractional-age
survival); see Two independent mortality settings.
|
\(\delta_m\) |
Time offset within period |
Convention |
|---|---|---|---|
|
\(0\) |
Start of period |
Payment immediately on death |
|
\(\dfrac{1}{2m}\) |
Mid-period |
Mid-sub-period payment (default) |
|
\(\dfrac{1}{m}\) |
End of period |
Traditional commutation-function convention |
from lactuca import config
config.mortality_placement = "mid" # default
config.mortality_placement = "end" # traditional end-of-year convention
config.mortality_placement = "beginning" # payment immediately on death
Warning
Default "mid" breaks the textbook identity. Classical commutation algebra gives
\(1 = d\,\ddot{a}_x + A_x\) only when benefits are discounted at end of year of death
(mortality_placement = "end") and \(m = 1\). With the library default "mid",
\(C_x = v^{x+1/2} d_x\) and the identity does not close numerically. This mid-period
benefit timing is intentional and unrelated to lx_interpolation (UDD vs CFM); set
"end" when you need textbook duality.
Accuracy comparison#
See Actuarial coherence across modes — modes are actuarially coherent but not numerically identical. The table below summarizes typical numerical closeness; it is not a guarantee of equality.
For most regulatory and commercial valuations, discrete_precision with \(m \leq 12\) is often
close to continuous_precision (within rounding for standard curves). Differences become
more visible at:
Very high payment frequency (\(m = 52\) weekly or \(m = 365\) daily).
Very small interest rates (\(i < 0.5\%\)) where the frequency adjustment is relatively large.
Extreme ages (\(x > 90\)) where the mortality gradient is steep.
discrete_simplified vs. discrete_precision differences are typically below 0.1% for
\(m \leq 12\), integer terms, and standard mortality curves. For fractional \(n\) with
\(m > 1\), simplified uses the hybrid \(k+s\) split above; annuities are typically within
0.1% of precision, while insurances may differ by a few percent on the integer-year
approximation. The Woolhouse approximation degrades slightly for very high \(m\)
or rapidly varying mortality schedules.
Classical identity \(1 = d\,\ddot{a}_x + A_x\)#
The textbook whole-life identity holds only when both conditions are met:
m = 1(annual payments), andconfig.mortality_placement = "end"(benefit at end of year of death).
Changing config.lx_interpolation ("linear" vs "exponential") does not restore
this identity — that setting governs fractional survival, not insurance benefit timing.
The library default is mortality_placement = "mid", so \(C_x = v^{x+1/2} d_x\) in commutation
functions and engine insurances use a mid-period discount offset. Do not expect
\(1 = d\,\ddot{a}_x + A_x\) numerically under default settings.
Regulatory reporting scope#
Lactuca computes actuarial present values and related quantities. Whether those outputs map to a regulatory liability depends on your inputs and reporting framework — the library does not certify compliance with any standard.
Use case |
Supported as a calculation building block |
Qualifiers |
|---|---|---|
Solvency II technical provisions (BEL component) |
Yes |
User supplies mortality, decrements, and discount assumptions. Default |
IFRS 17 fulfilment cash flows (BEL / expected CF component) |
Same as BEL |
PV machinery only — not measurement model choice (GMM vs PAA), contract boundary, or discounting methodology beyond user inputs |
IFRS 17 risk adjustment (RA), CSM, loss component |
Out of scope |
User responsibility outside Lactuca |
Solvency II SCR / internal model |
Out of scope |
No stressed scenarios or correlation engine |
For BEL-style work, discrete_precision with \(m \leq 12\) is the usual discrete reference
(see Actuarial coherence across modes and Accuracy comparison above). Continuous modes
are also valid when consistent with your valuation basis.
See also#
Modifying Decrements —
table_combination,combination_mode(independent/udd), and decrement adjustmentsInspecting Cash Flows —
return_flows=Trueparameter: full dict structures returned by each mode and product type (annuity, insurance, pure endowment)lx Interpolation — fractional-age survival probabilities under UDD and CFM
Force of Mortality Methods — force of mortality methods and
mortality_placementProspective Reserves and the ts Parameter — fractional policy year (
tsparameter)Numerical Precision — rounded vs. unrounded \(l_x\) in detail
Batch Calculations — performance notes:
discrete_precision50–250× vs all other modes ~1.5× speedup