A regime is a phase of life with its own utility function, states, actions, and constraints. Models have at least one terminal regime, and usually non-terminal ones.
For how functions and dependency wiring work inside a regime, see Write Economics, Not Glue Code. This page covers the regime-level features:
Regime anatomy — what each field does
Terminal vs non-terminal regimes
Transitions — regime and state (overview; full reference in Transitions)
Where a regime is solved
Choose the regime family together with the solver.
Regimeis the general GridSearch declaration. EGM-family models useConsumptionSavingsRegimeorNestedConsumptionSavingsRegimeso liquid and outer roles are named from the outset. A collective household remains aRegimewithstakeholdersand currently uses GridSearch; see the collective example.
from lcm import AgeGrid, LogSpacedGrid, Model, Transition
from lcm_examples.tiny import (
WORKING_LIFE_LAW,
RegimeId,
retirement,
working_life,
)Regime Anatomy¶
A Regime is defined by these fields:
| Field | Type | Purpose |
|---|---|---|
states | dict[str, Grid] | State variable grids — pure outcome-space definitions. |
state_transitions | dict[str, Callable | StochasticTransition | dict] | How states evolve over time. Values are callables, StochasticTransition-wrapped callables, per-target dicts keyed by target regime name, or fixed_transition(state_name) for fixed states. |
actions | dict[str, Grid] | Choice variables with grids. |
functions | dict[str, Callable] | Must include "utility"; can include auxiliary functions. |
constraints | dict[str, Callable] | Feasibility constraints on state-action combinations. |
description | str | Optional description string. |
A regime does not declare where it goes next. Its outgoing edges, and the law that
chooses among them where there are several, live in Model(edges=...); see Transitions below.
A states grid and a functions/constraints entry are ordinarily the same
object for every period the regime is solved — a Regime is declared once, not
per period. See Age-specialized functions and grids for
loosening this: a function’s closure or a continuous state’s grid bounds can be
made to vary with age (e.g., a tax-transfer system pinned to a policy date, or an
age-dependent borrowing limit).
A state that never changes gets the identity law via fixed_transition:
from lcm import fixed_transition
state_transitions={
"wealth": next_wealth,
"education": fixed_transition("education"), # education stays fixed
}Terminal vs Non-Terminal Regimes¶
Terminal regime: no outgoing edges in
Model(edges=...). The value function equals the utility function directly — there is no continuation value.Non-terminal regime: has outgoing edges — a single destination per source age, or a
Transitionwhose law chooses among several. pylcm uses the resolved Koopmans aggregator, which combines utility with the certainty equivalent of continuation:
LinearAggregator() is the default Koopmans form and LinearExpectation() is the
default certainty equivalent. With , together they give the
recursion shown above. For the full Epstein-Zin recursion, pair CESAggregator()
with PowerMean(); the CES form alone does not define Epstein-Zin preferences.
A terminal regime provides the boundary condition for the dynamic programming problem — it is the last period’s value function. Note that the last period can vary across agents, for example through stochastic mortality. A terminal regime does not mean the agent stays in this regime forever (that would be an absorbing regime, which is non-terminal with a self-loop transition).
Common uses:
Simple retirement: one period where the agent consumes all remaining wealth
Death: terminal utility is zero or determined by a bequest function — see the three-regime example in Write Economics
RETIREMENT_AGE = 65
# The retirement regime from lcm_examples.tiny declares no state transitions: no
# edge of the model below leaves it, so it is terminal and has no continuation.
print("State transitions:", dict(retirement.state_transitions))
utility_function = retirement.functions["utility"]
print("Utility function:", getattr(utility_function, "__name__", utility_function))State transitions: {}
Utility function: utility_retirement
Transitions¶
Regimes define two kinds of transitions:
Regime transitions (declared in
Model(edges=...), not on the regime) — which regime does the agent enter next period? A source with one destination at each source age is a plain{target: source_ages}mapping: the graph is the law. A source with several destinations at some age is aTransition(targets=..., law=...), whose law is a bare callable (deterministic, returns a regime ID), aStochasticTransition(stochastic, returns probabilities over all regimes), a per-target dict supplying scalar destination probabilities, orByAgeof these. A regime with no outgoing edges is terminal.State transitions (
state_transitionsfield) — how do individual state variables evolve? Supports deterministic functions, fixed states (fixed_transition), stochastic transitions (StochasticTransition), and per-target dicts for cross-regime transitions.
See Transitions for the full reference with examples.
Where a Regime Is Solved¶
A regime’s edges in Model(edges=...) declare where it can go from each source age;
they do not decide where the regime is solved:
a regime with no outgoing edges is terminal and available at every age;
a regime with outgoing edges supplies a problem only at the source ages its edges select.
The solved problems are derived from the model’s required initial_nodes: every
root, every problem a history can reach from it, and every value those problems read.
Each demanded nonterminal problem needs an edge, and each declared target must be
available at the next age. A regime that is left at a fixed age stops its self-edge
there:
edges = {
# Working life: stay or retire before 64; at 64 retirement is the only edge,
# landing in retirement at 65, and `next_regime` must return it there.
"working_life": Transition(
targets={
"working_life": AgeRange(start=25, exclusive_stop=64),
"retirement": AgeRange(start=25, exclusive_stop=65),
},
law=DeterministicTransition(func=next_regime),
),
# Retirement: no outgoing edges, so terminal and evaluated wherever demanded.
}Example: Assembling Regimes into a Model¶
A minimal two-regime model to show the full assembly. For the economic functions and DAG wiring, see Write Economics. For model construction details, see Defining Models.
# Customize the imported working_life regime with simpler grids and functions
working_life_simple = working_life.replace(
actions={
**working_life.actions,
"consumption": LogSpacedGrid(start=0.5, stop=50, n_points=50),
},
)
age_grid = AgeGrid(start=25, inclusive_stop=65, step="20Y")
# At 25 working life can continue or end, so its law chooses; at 45 retirement
# is the only edge, and the law returns it there.
edges = {
"working_life": Transition(
targets={"working_life": 25, "retirement": (25, 45)},
law=WORKING_LIFE_LAW,
)
}
model = Model(
regimes={
"working_life": working_life_simple,
"retirement": retirement,
},
ages=age_grid,
edges=edges,
initial_nodes=((25, "working_life"),),
regime_id_class=RegimeId,
)
print("Regimes:", list(model.user_regimes.keys()))
print("Periods:", model.n_periods)
print(
"Terminal:",
[name for name, law in model.graph.laws.items() if law.terminal],
)Regimes: ['working_life', 'retirement']
Periods: 3
Terminal: ['retirement']