Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Continuous stochastic processes

A continuous stochastic process is a stochastic state variable whose class bundles both the discretized grid and its transition mechanism. Unlike ordinary grids, a process computes its own grid points and transition matrix from a distribution and its parameters — so you place it in states and never in state_transitions.

Process classes follow the naming convention <Distribution><Kind>Process and are imported directly from lcm:

from lcm import NormalIIDProcess, TauchenAR1Process

Check solver support before adding the shock. A process can enlarge the continuation-node product or change stored value-array topology when fold=True. EGM-family and collective regimes support narrower combinations than GridSearch; build the intended specialized regime early and let model validation check it.

IID Processes

Processes whose draws are independent across periods.

NormalIIDProcess

Discretized normal distribution N(μ,σ2)N(\mu, \sigma^2).

NormalIIDProcess(n_points=7, gauss_hermite=False, mu=0.0, sigma=1.0, n_std=2.0)

Parameters:

LogNormalIIDProcess

Discretized log-normal distribution where lnXN(μ,σ2)\ln X \sim N(\mu, \sigma^2).

LogNormalIIDProcess(n_points=7, gauss_hermite=False, mu=0.0, sigma=0.5, n_std=2.0)

Same parameters as NormalIIDProcess. Grid points are exp() of the underlying normal grid.

UniformIIDProcess

Discretized uniform distribution U(start,stop)U(\text{start}, \text{stop}). Both endpoints are included in the grid.

UniformIIDProcess(n_points=5, start=0.0, stop=1.0)

Equally spaced points with uniform probabilities (all 1/n_points).

NormalMixtureIIDProcess

Two-component normal mixture: εp1N(μ1,σ12)+(1p1)N(μ2,σ22)\varepsilon \sim p_1 \, N(\mu_1, \sigma_1^2) + (1 - p_1) \, N(\mu_2, \sigma_2^2).

NormalMixtureIIDProcess(
    n_points=9,
    n_std=2.0,
    p1=0.9,
    mu1=0.0,
    sigma1=0.1,
    mu2=0.0,
    sigma2=1.0,
)

Grid spans the mixture mean ±nstd\pm n_\text{std} mixture standard deviations.

AR(1) Processes

Processes with serial correlation. The process is yt=μ+ρyt1+εty_t = \mu + \rho \, y_{t-1} + \varepsilon_t. The innovation distribution depends on the class:

TauchenAR1Process

Discretization via Tauchen (1986). Uses CDF-based transition probabilities.

TauchenAR1Process(
    n_points=7,
    gauss_hermite=False,
    rho=0.9,
    sigma=0.1,
    mu=0.0,
    n_std=2.0,
)

RouwenhorstAR1Process

Discretization via Rouwenhorst (1995) / Kopecky & Suen (2010). Better for highly persistent processes (ρ\rho close to 1).

RouwenhorstAR1Process(n_points=7, rho=0.95, sigma=0.1, mu=0.0)

TauchenNormalMixtureAR1Process

AR(1) with mixture-of-normals innovations, discretized via Tauchen. Following Fella et al. (2019).

TauchenNormalMixtureAR1Process(
    n_points=9,
    rho=0.9,
    mu=0.0,
    n_std=2.0,
    p1=0.9,
    mu1=0.0,
    sigma1=0.1,
    mu2=0.0,
    sigma2=1.0,
)

Using a Continuous Stochastic Process in a Regime

A process goes in states. It must not appear in state_transitions — it manages its own transition:

from lcm import LinSpacedGrid, NormalIIDProcess, Regime

working = Regime(
    transition=next_regime,
    states={
        "wealth": LinSpacedGrid(start=0, stop=100, n_points=50),
        "income_shock": NormalIIDProcess(
            n_points=5,
            gauss_hermite=False,
            mu=0.0,
            sigma=1.0,
            n_std=2.0,
        ),
    },
    state_transitions={
        "wealth": next_wealth,
        # income_shock does NOT appear here — it manages its own transitions
    },
    actions={...},
    functions={
        "utility": utility,
        "earnings": lambda wage, income_shock: wage * jnp.exp(income_shock),
    },
)

Key Rules

  1. A process goes in states — it defines the values the shock can take.

  2. A process must not appear in state_transitions — placing it there is a validation error.

  3. Process parameters can be specified at construction or deferred to runtime (set to None).

  4. Runtime params follow the same hierarchy as other params (see Parameters).

  5. A shock whose size depends on a discrete state is declared with StateConditioned, and is then fixed at build time (see State-Conditioned Shock Size).

Runtime Parameters

Set distribution parameters to None at construction to supply them at runtime:

NormalIIDProcess(n_points=5, gauss_hermite=False, mu=None, sigma=None, n_std=None)

Then supply the values in the params dict, keyed by regime name:

params = {
    "regime_name": {
        "mu": 0.0,
        "sigma": 1.0,
        "n_std": 2.0,
    },
}

n_points and gauss_hermite are structural, not distribution parameters — they must always be given at construction.

Folding an IID shock out of stored values

An IID process used as a state may declare fold=True:

income_shock = NormalIIDProcess(
    n_points=7,
    gauss_hermite=True,
    mu=0.0,
    sigma=1.0,
    fold=True,
)

Folding removes the process axis from each stored value function; it does not remove the shock from the economic model. The solve evaluates every shock node, evaluates the feasible action choice at that node, and only then averages the resulting values with the process’s own quadrature weights. Consequently a folded period stores one fewer axis, but utility and the chosen action may still depend on the realized shock.

For an eligible JIT GridSearch fold, pylcm evaluates the action product in bounded blocks at each shock node before applying that same quadrature. The fold-node axis itself is still evaluated and reduced in full: this route changes only action evaluation, not the stochastic rule or reduction order. Runtime and peak-memory effects require paired measurement.

Forward simulation is unchanged economically. A subject entering from another regime draws the shock on that transition; a subject whose initial regime is the folding regime must seed the process state in initial_conditions. Subsequent IID transitions redraw it, and the realized value remains available to utility, policy, and the output table. With the same seed, a folded model and its otherwise identical unfolded model produce the same simulated panel; only value-function storage differs.

This is a narrow exact reduction. Model construction enforces all of the following:

Those rules characterize a shock that is drawn, used for the within-period decision, and discarded before the value is stored. If its realization changes a later state, selects a regime, or is needed by a value-dependent endpoint, retain the ordinary unfolded axis.

A folded StateConditioned IID shock has one further timing restriction. Within a non-terminal folding regime, every declared law for the conditioner must be fixed_transition(conditioner_name); a terminal folding regime has no local law. Additionally, every structurally reachable incoming source’s target-local cell toward the folding regime must be fixed. Such an incoming source may move the conditioner toward a different target. The shock entering period tt was drawn using the category at t1t-1, whereas the fold gathers a quadrature row along the category visible at tt; those rows are the same only when the conditioner cannot move into the fold. A model that needs a moving conditioner there must use fold=False.

State-Conditioned Shock Size

The size of a shock often depends on where the subject currently is: earnings innovations are more variable out of work than in it, returns more variable in a high-volatility regime. Declare that with StateConditioned on the process.

from lcm import DiscreteGrid, NormalIIDProcess, StateConditioned, categorical
from lcm.typing import ScalarInt


@categorical(ordered=False)
class EmploymentStatus:
    employed: ScalarInt
    unemployed: ScalarInt


income_shock = NormalIIDProcess(
    n_points=7,
    gauss_hermite=False,
    mu=0.0,
    n_std=3.0,
    sigma=StateConditioned(
        on="employment_status",
        by={"employed": 0.2, "unemployed": 0.5},
    ),
)

on names a DiscreteGrid state the regime carries, and by gives the innovation standard deviation for each of its categories. The regime declares both:

working = Regime(
    transition=next_regime,
    states={
        "wealth": LinSpacedGrid(start=0, stop=100, n_points=50),
        "employment_status": DiscreteGrid(category_class=EmploymentStatus),
        "income_shock": income_shock,
    },
    state_transitions={
        "wealth": next_wealth,
        "employment_status": MarkovTransition(next_employment_status),
    },
    actions={...},
    functions={...},
)

The declaration stands where the scalar would

StateConditioned is written in place of the parameter it conditions, so which parameter varies is explicit and there is no way to give that parameter twice.

A discretized process has one axis in the value function, so every category has to share one set of nodes. Those nodes are placed from the widest value in by — the narrowest axis that still covers every category. The per-category values never move the nodes; they enter only the transition probabilities. To widen the axis beyond that, raise n_std.

Only sigma can be conditioned today, and only for the processes whose transition probabilities carry it: the CDF-binned NormalIIDProcess and TauchenAR1Process. A Rouwenhorst transition depends on rho alone, so fixing the nodes would leave a conditioned sigma no channel at all, and the model refuses to build.

The conditioning value is dated t

Writing sts_t for the time-tt value of on and σst\sigma_{s_t} for by[s_t], an AR(1) process transitions as

yt+1yt,stN(μ+ρyt, σst2),y_{t+1} \mid y_t, s_t \sim N(\mu + \rho y_t,\ \sigma_{s_t}^2),

with an IID process dropping the ρyt\rho y_t term. The variance of the innovation realized between tt and t+1t+1 is therefore set by where the subject is at tt — the employment status they are leaving, not the one they arrive in.

When a process is declared in more than one regime, the values in force are the ones declared by the regime being entered, selected by the conditioning state at tt. Two regimes may declare different values on purpose; build the conditioning DiscreteGrid from the same @categorical class in each of them, so the categories line up.

Everything is fixed at build time

A state-conditioned process cannot defer any parameter to runtime, and the values in by never appear in the params template. Both are rejected or absent by design, so these values cannot be estimated — they are part of the model’s structure, not its parameters. Give every parameter at construction.

Which processes support it

Conditioning rides in the transition CDF, so it is available exactly where sigma sits there:

ProcessSupported
NormalIIDProcess(gauss_hermite=False)yes
TauchenAR1Process(gauss_hermite=False)yes
Either with gauss_hermite=Trueno — the nodes scale with sigma
RouwenhorstAR1Processno — its transition depends on rho only

Anything else raises at model build.

See Also

References
  1. Tauchen, G. (1986). Finite State Markov-Chain Approximations to Univariate and Vector Autoregressions. Economics Letters, 20(2), 177–181. 10.1016/0165-1765(86)90168-0
  2. Rouwenhorst, K. G. (1995). Asset Pricing Implications of Equilibrium Business Cycle Models. In T. F. Cooley (Ed.), Frontiers of Business Cycle Research (pp. 294–330). Princeton University Press. 10.1515/9780691218052-014
  3. Kopecky, K. A., & Suen, R. M. H. (2010). Finite State Markov-Chain Approximations to Highly Persistent Processes. Review of Economic Dynamics, 13(3), 701–714. 10.1016/j.red.2010.02.002
  4. Fella, G., Gallipoli, G., & Pan, J. (2019). Markov-Chain Approximations for Life-Cycle Models. Review of Economic Dynamics, 34, 183–201. 10.1016/j.red.2019.03.013