GrowthRate#

GrowthRate models benefit revaluation schedules for life annuities and insurances. It supports:

  • Geometric (compound) growth: \(F(t) = \prod_{j=0}^{t-1}(1+g_j)\)

  • Arithmetic (linear additive) growth: \(F(t) = 1 + \sum_{j=0}^{t-1} g_j\)

where \(F(0) = 1\) under the standard actuarial convention (apply_from_first=False).

Like InterestRate, growth rates can be constant, piecewise (segment rates with durations), or multi-scenario. shifted() generates time-shifted copies for prospective reserve calculations.

See also

Growth Rates — Full guide to growth rate modeling.
Growth Rate Conventions — Anniversary convention used by the annuity engine.

class lactuca.GrowthRate(rate: float | dict | None = None, growth_type: Literal['g', 'a'] = 'g', rates: Sequence[float] | NDArray[float64] | None = None, terms: Sequence[int] | None = None, apply_from_first: object = False, description: str | None = None)#

Bases: object

Growth rate curve for actuarial annuity and insurance revaluation.

Analogous to InterestRate, this class encapsulates constant, piecewise, or multi-scenario growth rate schedules for revaluation of payments in life annuities and insurances.

Growth can be geometric (compound) or arithmetic (linear additive):

  • Geometric (growth_type='g'): \(F(t) = \prod_{j=0}^{t-1}(1+g_j)\)

  • Arithmetic (growth_type='a'): \(F(t) = 1 + \sum_{j=0}^{t-1} g_j\)

where \(F(0) = 1\) when apply_from_first=False (standard convention).

Parameters:
  • rate (float or dict or None, optional) –

    • scalar float: constant growth rate for all periods.

    • dict: multi-scenario container. Each value must be a float (converted to GrowthRate) or a GrowthRate instance. Nested multi-scenario instances are not allowed.

    • None: use rates + terms to construct a piecewise curve.

  • growth_type ({'g', 'a'}, optional) – Growth type: 'g' for geometric (default), 'a' for arithmetic.

  • rates (sequence of float or None, optional) – Growth rates per segment for piecewise construction. Length must equal len(terms) + 1; the last rate applies indefinitely.

  • terms (sequence of int or None, optional) – Segment durations (strictly positive integers) for piecewise construction. Consistent with InterestRate convention. terms=[3, 2] means segment 0 covers periods 0–2, segment 1 covers 3–4, beyond that the last rate of rates applies.

  • apply_from_first (bool, optional) – If False (default): standard actuarial convention — \(F(0) = 1\), growth applies from the second payment onward. If True: \(F(0) = 1 + g\), growth applies from the first payment.

  • description (str or None, optional) – Human-readable label (stored, not used in computations).

Notes

  • Period indices t passed to factor() are non-negative integers (\(t \geq 0\)), base-0.

  • For the formula of the provision/reserve: \(V_{ts} = R \cdot \texttt{gr.factor}(ts) \cdot \texttt{ax}(x+ts,\; ts=ts,\; gr=\texttt{gr})\).

  • Negative growth rates are permitted as long as all cumulative factors remain strictly positive across the payment schedule.

  • See Growth Rate Conventions for the full reference of the anniversary convention and fractional-ts policy used by the annuity and insurance dispatchers.

Examples

Constant geometric growth:

>>> from lactuca import GrowthRate
>>> import numpy as np
>>> gr = GrowthRate(0.03)
>>> gr.factor(2)     # (1.03)^2
1.0609

Constant arithmetic growth:

>>> gr_a = GrowthRate(0.02, 'a')
>>> gr_a.factor(3)   # 1 + 0.02*3
1.06

Piecewise geometric growth (1% for the first year, 2% thereafter):

terms=[1] means the first segment (rate 1%) covers period j=0 only; from period j=1 onward the second rate (2%) applies indefinitely. factor(t) accumulates rates for periods j = 0, 1, ..., t-1:

>>> gr_p = GrowthRate(rates=[0.01, 0.02], terms=[1])
>>> gr_p.factor(0)   # no periods accumulated
1.0
>>> gr_p.factor(1)   # j=0 only  → 1 × 1.01
1.01
>>> gr_p.factor(2)   # j=0, j=1  → 1.01 × 1.02
1.0302
>>> gr_p.factor(3)   # j=0..2    → 1.01 × 1.02²
1.050804

Effect of apply_from_first=True (growth applied already at t=0):

>>> gr_f = GrowthRate(0.03, apply_from_first=True)
>>> gr_f.factor(0)   # t=0 → (1.03)^1  (growth from first payment)
1.03
>>> gr_f.factor(1)   # t=1 → (1.03)^2
1.0609
>>> gr_f.factor(2)   # t=2 → (1.03)^3
1.092727

Compare with apply_from_first=False (default):

>>> gr_d = GrowthRate(0.03, apply_from_first=False)
>>> gr_d.factor(0)   # t=0 → (1.03)^0  (reference payment, no growth yet)
1.0
>>> gr_d.factor(1)   # t=1 → (1.03)^1
1.03
>>> gr_d.factor(2)   # t=2 → (1.03)^2
1.0609

Multi-scenario (constant geometric, piecewise geometric, constant arithmetic, piecewise arithmetic — four named scenarios in one container):

>>> gr_s = GrowthRate({
...     'base':       GrowthRate(0.02),
...     'stress':     GrowthRate(0.04),
...     'piecewise':  GrowthRate(rates=[0.01, 0.03], terms=[2]),
...     'arithmetic': GrowthRate(0.02, 'a'),
...     'pw_arith':   GrowthRate(rates=[0.01, 0.03], terms=[2], growth_type='a'),
... })
>>> gr_s.active_scenario
'base'
>>> gr_s.factor(2)           # base: (1.02)^2
1.0404
>>> gr_s.active_scenario = 'stress'
>>> gr_s.factor(2)           # stress: (1.04)^2
1.0816
>>> gr_s.active_scenario = 'piecewise'
>>> gr_s.factor(1)           # j=0 → x1.01
1.01
>>> gr_s.factor(2)           # j=0 + j=1 → 1.01 x 1.01
1.0201
>>> gr_s.factor(3)           # j=0..2 → 1.01^2 x 1.03
1.050703
>>> gr_s.active_scenario = 'arithmetic'
>>> gr_s.factor(3)           # arithmetic: 1 + 0.02*3
1.06
>>> gr_s.active_scenario = 'pw_arith'
>>> gr_s.factor(1)           # j=0 → 1 + 0.01
1.01
>>> gr_s.factor(3)           # j=0..2 → 1 + 0.01 + 0.01 + 0.03
1.05

See also

GrowthRate.factor

Compute cumulative growth factor for given period indices.

GrowthRate.shifted

Return a new GrowthRate with the first ts years consumed.

GrowthRate.copy

Return an independent deep copy of this instance.

InterestRate

Analogous class for interest rate modeling.

add_scenario(name: str, scenario: GrowthRate) None#

Add a named scenario to this GrowthRate instance.

Parameters:
  • name (str) – Unique scenario name.

  • scenario (GrowthRate) – A non-nested GrowthRate instance (constant or piecewise).

Raises:
  • TypeError – If scenario is not a GrowthRate instance.

  • ValueError – If scenario itself contains scenarios (nesting not allowed).

Notes

If no active scenario is set, the new scenario becomes active automatically.

Examples

>>> from lactuca import GrowthRate
>>> gr = GrowthRate({'base': 0.02})
>>> gr.add_scenario('stress', GrowthRate(0.04))
>>> gr.active_scenario = 'stress'
amounts(times: NDArray[float64] | Sequence[float] | float, start: object, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365]) NDArray[float64]#

Generate cashflow amounts for each payment time using this growth rate.

Computes the amount for the \(k\)-th payment (0-based) as:

\[A_k = R \cdot F\!\left(\left\lfloor \frac{k}{m} \right\rfloor\right)\]

where \(R\) is the initial amount (start), \(m\) is the number of payments per year, and \(F\) is the cumulative growth factor from factor(). The anniversary index \(\lfloor k/m \rfloor\) matches the convention used internally by the calculation engine when cashflow_amounts is passed to ax().

Parameters:
  • times (array-like of float) – Payment times in years, typically from payment_times(). Used only to determine the total number of payments. Must not be None and must not contain non-finite values.

  • start (float) – Cashflow amount for the first payment (before any growth). Must be a finite numeric scalar (boolean values raise TypeError).

  • m (PaymentFrequencyLiteral) – Number of payments per year in times. Groups payments by anniversary year: payments \(k = 0, 1, \ldots, m-1\) share anniversary 0 (factor \(F(0) = 1\)), payments \(k = m, \ldots, 2m-1\) share anniversary 1, etc. Must be one of the allowed payment frequencies (1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365).

Returns:

Array of shape (len(times),) and dtype float64 with the escalated cashflow amount for each payment.

Return type:

NDArray[np.float64]

Raises:
  • TypeError – If times is None or start is boolean.

  • ValueError – If this instance has multiple scenarios, if times contains non-finite values, if start is not finite, if m is not one of the allowed payment frequencies (1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365), or if constant arithmetic growth would produce a non-positive factor for any payment anniversary.

Notes

When times is produced by payment_times() with selected_periods, pass m = len(selected_periods) (not the original frequency) so that anniversary boundaries are correctly aligned.

Examples

Monthly step-up pension: 2 % geometric annual growth, 12 payments per year.

>>> from lactuca import GrowthRate, payment_times
>>> import numpy as np
>>> gr = GrowthRate(0.02)
>>> times = payment_times(n=3, m=12)
>>> amounts = gr.amounts(times, start=1000.0, m=12)
>>> amounts[0].item()   # first year: no growth
1000.0
>>> np.testing.assert_allclose(amounts[12], 1020.0)  # second year: ×1.02

Annual arithmetic growth: +5 % per year.

>>> gr_a = GrowthRate(0.05, growth_type='a')
>>> times_a = payment_times(n=3, m=1)
>>> gr_a.amounts(times_a, start=1000.0, m=1)
array([1000., 1050., 1100.])
copy() GrowthRate#

Return a deep copy of this instance.

Returns:

Independent copy with all state duplicated.

Return type:

GrowthRate

Examples

>>> from lactuca import GrowthRate
>>> gr = GrowthRate(0.02)
>>> gr2 = gr.copy()
curve_analysis() dict#

Return a quantitative analytical summary of the growth rate curve.

Computes curve shape properties, monotonicity flags, and cumulative growth factors at key actuarial time horizons.

Returns:

Analytical summary with the following keys:

  • 'type' : 'constant', 'piecewise', or 'scenarios'

  • 'growth_type' : 'geometric' or 'arithmetic'

  • 'rate_range' : dict with 'min' and 'max' (float)

  • 'curve_properties' : dict with boolean flags:

    • 'is_flat' : all segment rates are equal

    • 'is_monotone_inc' : rates non-decreasing

    • 'is_monotone_dec' : rates non-increasing

    • 'has_zero_rates' : any rate equals zero

    • 'has_negative_rates' : any rate is negative

  • 'key_factors' : dict with 't1', 't5', 't10', 't20' — cumulative growth factors at those horizons (float)

  • 'segment_count' : int — piecewise only

  • 'total_defined_term' : int — piecewise only

  • 'active_scenario' : str — scenario container only

  • 'scenarios' : dict of per-scenario results — scenario container only

Return type:

dict

Notes

For multi-scenario containers, rate_range, curve_properties, and key_factors at the top level mirror the active scenario; per-scenario detail remains in scenarios.

See also

GrowthRate.validate

Structural validation with regulatory warnings.

InterestRate.curve_analysis

Analogous method on the interest rate class.

Examples

>>> from lactuca import GrowthRate
>>> gr = GrowthRate(0.03)
>>> ca = gr.curve_analysis()
>>> ca['curve_properties']['is_flat']
True
>>> round(ca['key_factors']['t1'], 2)
1.03
export(format: str = 'dict') dict | str#

Export the GrowthRate configuration as a dict or JSON string.

Parameters:

format ({'dict', 'json', 'regulatory'}, optional) –

Output format. Default is 'dict'.

  • 'dict': plain Python dict.

  • 'json': JSON-encoded string (2-space indented).

  • 'regulatory': enriched JSON string with audit-trail metadata (export_timestamp, format_version, library) suitable for Solvency II / IFRS 17 regulatory compliance.

Returns:

Configuration dictionary when format='dict', or a JSON string for format='json' or format='regulatory'.

Return type:

dict or str

Raises:

ValueError – If format is not one of 'dict', 'json', or 'regulatory'.

Examples

>>> from lactuca import GrowthRate
>>> gr = GrowthRate(0.03)
>>> result = gr.export()
>>> result['is_constant'], result['growth_type'], result['rate']
(True, 'g', 0.03)
>>> import json; d = json.loads(gr.export(format='regulatory'))
>>> d['format_version']
'1.0'
factor(t: float | int | Sequence[float] | NDArray[float64]) float | NDArray[float64]#

Compute the cumulative growth factor for each period index in t.

The factor depends on apply_from_first:

  • apply_from_first=False (default, standard actuarial convention):

    \[F(0) = 1, \quad F(t) = \prod_{j=0}^{t-1}(1+g_j) \;\text{(geometric)}\]

    Equivalent: \(F(t) = 1 + \sum_{j=0}^{t-1} g_j\) for arithmetic.

  • apply_from_first=True:

    \[F(t) = \prod_{j=0}^{t}(1+g_j) \;\text{(geometric)}\]

    Equivalent: \(F(t) = 1 + \sum_{j=0}^{t} g_j\) for arithmetic.

Used in the reserve formula:

\[V_{ts} = R \cdot \texttt{gr.factor}(ts) \cdot \texttt{ax}(x+ts,\; ts=ts,\; gr=gr)\]
Parameters:

t (scalar or array-like) – Non-negative integer-valued period index (or indices). Scalars return float; arrays return NDArray[np.float64].

Returns:

Cumulative growth factor(s). Same shape as input.

Return type:

float or NDArray[np.float64]

Raises:
  • TypeError – If t is None.

  • ValueError – If any element of t is negative or non-integer-valued, or if constant arithmetic growth would produce a non-positive cumulative factor at any requested t.

Examples

>>> from lactuca import GrowthRate
>>> import numpy as np
>>> gr = GrowthRate(0.03)
>>> gr.factor(0)
1.0
>>> gr.factor(2)
1.0609
>>> gr.factor(np.array([0, 1, 2, 3]))
array([1.    , 1.03  , 1.0609, 1.092727])

Piecewise: rates=[0.01, 0.02], terms=[1] — period j=0 uses 1%, period j >= 1 uses 2%. factor(t) accumulates j = 0..t-1:

>>> gr_p = GrowthRate(rates=[0.01, 0.02], terms=[1])
>>> gr_p.factor(1)   # j=0 → ×1.01
1.01
>>> gr_p.factor(2)   # j=0 + j=1 → 1.01 × 1.02
1.0302

With apply_from_first=True, factor(t) accumulates j = 0..t (one extra period), so growth is already present at t=0:

>>> gr_first = GrowthRate(0.03, apply_from_first=True)
>>> gr_first.factor(0)   # j=0 included → 1 × 1.03
1.03
>>> gr_first.factor(1)   # j=0..1 → 1.03²
1.0609
get_rate(t: int | float | Sequence[float] | NDArray[float64]) float | NDArray[float64]#

Retrieve the growth rate of the segment applying at period t.

For piecewise schedules, returns the rate of the segment containing period t (0-based anniversary index). The tail rate extends indefinitely beyond the last defined segment.

Parameters:

t (int, float, or array-like) – Non-negative period index (or indices). Scalar input returns float; array-like input returns NDArray[np.float64].

Returns:

Rate(s) applicable at period t. Shape matches the input.

Return type:

float or NDArray[np.float64]

Raises:

ValueError – If any value in t is negative, non-finite, or non-integer-valued.

Notes

For constant GrowthRate, returns the same rate for all t. t must be non-negative integer-valued (same rule as factor()).

See also

GrowthRate.get_segment_info

Full segment metadata at period t.

InterestRate.get_rate

Analogous method on the interest rate class.

Examples

>>> from lactuca import GrowthRate
>>> gr = GrowthRate(0.03)
>>> gr.get_rate(5)
0.03
>>> gr_pw = GrowthRate(rates=[0.01, 0.02], terms=[3])
>>> gr_pw.get_rate(0)   # period 0 is in segment 0 (rate 1%)
0.01
>>> gr_pw.get_rate(3)   # period 3 is the tail (rate 2%)
0.02
get_segment_info(t: int | float) dict#

Return detailed information about the growth segment at period t.

Parameters:

t (int or float) – Non-negative period index (scalar). Fractional values are rounded to the nearest integer.

Returns:

A dict with the following keys:

  • 'type' : 'constant' or 'piecewise'

  • 'rate' : float — growth rate of the applicable segment

  • 'growth_type' : 'geometric' or 'arithmetic'

  • 'apply_from_first' : bool

  • 'segment_index' : int — 0-based index of the segment

  • 'segment_start' : int — first period of segment (inclusive)

  • 'segment_end' : int or None — first period of the next segment (exclusive), or None for the open-ended tail

  • 'is_terminal' : boolTrue if segment extends indefinitely

Return type:

dict

Raises:
  • TypeError – If t is not a finite scalar numeric (including bool).

  • ValueError – If t is negative.

See also

GrowthRate.get_rate

Return only the rate at period t.

InterestRate.get_segment_info

Analogous method on the interest rate class.

Examples

>>> from lactuca import GrowthRate
>>> gr = GrowthRate(0.03)
>>> info = gr.get_segment_info(0)
>>> info['type'], info['is_terminal']
('constant', True)
>>> gr_pw = GrowthRate(rates=[0.01, 0.02], terms=[3])
>>> gr_pw.get_segment_info(2)['segment_end']   # segment 0 ends at period 3
3
>>> gr_pw.get_segment_info(3)['is_terminal']   # tail segment
True
shifted(ts: object) GrowthRate#

Return a new GrowthRate with the first ts years consumed.

Analogous to InterestRate.shifted(), this method produces a growth schedule starting from the moment ts years into the original contract, so that period index 0 in the shifted schedule corresponds to the correct contractual growth factor.

Always uses ts_int = int(ts) (truncation toward zero), since anniversary indices are integers by construction. If ts <= 0, returns an independent copy (no temporal change), matching InterestRate.shifted().

Parameters:

ts (float or int) – Number of years already elapsed. Only the integer part is used.

Returns:

Shifted growth schedule. Returns an independent copy when ts <= 0. For constant schedules with ts > 0, returns the same instance unchanged because the growth factor series is period-relative.

Return type:

GrowthRate

Raises:
  • TypeError – If ts is boolean or not a numeric type.

  • ValueError – If ts is not finite.

Notes

  • For multi-scenario containers, the call is forwarded to the active scenario and a shifted copy of that scenario is returned.

  • For constant GrowthRate with ts > 0, the original instance is returned unchanged — the growth factor series is period-relative and unaffected by time displacement.

  • For piecewise schedules, the first segment is truncated by ts_int years. If a segment is exactly consumed, it is dropped.

  • Preserves growth_type and apply_from_first.

Examples

>>> from lactuca import GrowthRate
>>> gr = GrowthRate(rates=[0.02, 0.03, 0.04], terms=[3, 2])
>>> gr.shifted(2)
GrowthRate(rates=[0.02, 0.03, 0.04], terms=[1, 2], growth_type='g')
>>> gr.shifted(3)
GrowthRate(rates=[0.03, 0.04], terms=[2], growth_type='g')
>>> gr.shifted(6)
GrowthRate(0.04, growth_type='g')  # geometric, constant
summary() str#

Alias for __str__ for compatibility.

Returns:

Enhanced human-readable representation of the growth rate curve.

Return type:

str

validate() dict#

Return a structural validation report for this growth rate curve.

Checks actuarial invariants and rate properties, returning a structured diagnostic dict suitable for compliance reporting.

Returns:

Validation report with the following keys:

  • 'type' : 'constant', 'piecewise', or 'scenarios'

  • 'growth_type' : 'geometric' or 'arithmetic'

  • 'apply_from_first' : bool

  • 'status' : 'ok' if no warnings, else 'warnings'

  • 'warnings' : list of str — informational messages

  • 'rate_statistics' : dict with 'min', 'max', 'mean', 'zero_count', 'negative_count' (omitted for scenario containers)

  • 'segment_count' : int — piecewise only

  • 'total_defined_term' : int — piecewise only

  • 'active_scenario' : str — scenario container only

  • 'scenarios' : dict of per-scenario results — scenario container only

Return type:

dict

Notes

Warnings do not indicate invalid configurations; they flag conditions that warrant review in regulatory contexts (IFRS 17, Solvency II). A zero growth rate produces flat factors; a negative rate produces shrinkage, which may be intentional (e.g. benefit reduction clauses).

See also

GrowthRate.curve_analysis

Quantitative analytical summary of curve shape.

InterestRate.validate

Analogous method on the interest rate class.

Examples

>>> from lactuca import GrowthRate
>>> gr = GrowthRate(0.0)
>>> result = gr.validate()
>>> result['status']
'warnings'
>>> gr2 = GrowthRate(0.02)
>>> gr2.validate()['status']
'ok'
property active_scenario: str | None#

Get or set the name of the active scenario.

Returns:

Name of the currently active scenario, or None if no scenario has been activated yet.

Return type:

str or None

Raises:

ValueError – When setting, if name is not found in the scenarios dict.

Examples

>>> from lactuca import GrowthRate
>>> gr = GrowthRate({'base': 0.02, 'stress': 0.04})
>>> gr.active_scenario
'base'
>>> gr.active_scenario = 'stress'
>>> gr.active_scenario
'stress'
property apply_from_first: bool#

Return True if growth applies from the first period.

When False (default), \(F(0) = 1\) and growth starts from period 1.

Return type:

bool

property growth_type: str#

'g' for geometric, 'a' for arithmetic.

Return type:

str

Type:

Return the growth type

property is_constant: bool#

Return True if the growth rate is constant (scalar).

Return type:

bool

property rates: NDArray[float64]#

Return a copy of the growth rates array.

Returns:

A copy of the internal rates array (size 1 for constant, n_segments + 1 for piecewise including the tail rate).

Return type:

NDArray[np.float64]

property scenario_names: list#

Return a list of all scenario names.

Returns:

Scenario names in insertion order. Empty list if no scenarios are defined.

Return type:

list of str

Examples

>>> from lactuca import GrowthRate
>>> gr = GrowthRate({'base': 0.02, 'stress': 0.04})
>>> gr.scenario_names
['base', 'stress']
property terms: NDArray[int64]#

Return a copy of the segment duration array (integer periods).

Returns:

Empty array for constant GrowthRate. Length n_segments for piecewise (each element is the duration of the corresponding segment in anniversary years).

Return type:

NDArray[np.int64]