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.

Grids

Grids define the outcome space for state and action variables — what values they can take. They are passed via the states and actions mappings on a Regime.

Quick Reference

Grid TypeUse CaseKey Parameters
DiscreteGridCategorical choicescategory_class
LinSpacedGridEvenly spaced continuousstart, stop, n_points
LogSpacedGridLog-spaced continuousstart, stop, n_points
IrregSpacedGridCustom point placementpoints or n_points
PiecewiseLinSpacedGridDense in some regionspieces (tuple of Piece)
PiecewiseLogSpacedGridLog-dense in some regionspieces (tuple of Piece)

All grid classes are imported from lcm:

from lcm import (
    DiscreteGrid,
    LinSpacedGrid,
    LogSpacedGrid,
    IrregSpacedGrid,
    PiecewiseLinSpacedGrid,
    PiecewiseLogSpacedGrid,
    Piece,
    categorical,
)

Discrete Grids

DiscreteGrid

For categorical variables. Requires a @categorical frozen dataclass defining the categories:

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


@categorical(ordered=True)
class LaborSupply:
    do_not_work: ScalarInt
    work: ScalarInt


actions = {"labor_supply": DiscreteGrid(LaborSupply)}

Values are integer codes (0, 1, 2, ...) auto-assigned by @categorical. In simulation output, labels are preserved via pandas Categorical.

When used as an action, no further configuration is needed. When used as a state, the transition is specified via state_transitions on the Regime — see Transitions.

Continuous Grids

LinSpacedGrid

Evenly spaced points from start to stop (inclusive). The most common grid type for wealth, consumption, and similar variables.

LinSpacedGrid(start=0, stop=100, n_points=50)

LogSpacedGrid

Points concentrated near start (logarithmic spacing). Good for variables with diminishing marginal effects. start must be positive.

LogSpacedGrid(start=0.1, stop=100, n_points=50)

IrregSpacedGrid

Explicit point placement. Use when you need specific grid points (e.g., at policy kinks):

IrregSpacedGrid(points=(0.0, 0.5, 1.0, 5.0, 10.0, 50.0))

You can also defer points to runtime by specifying only n_points. The actual points are then supplied via the params dict:

IrregSpacedGrid(n_points=4)

Runtime points driven by extra_param_names

When the gridpoints depend on a value that varies across solver iterations — a borrowing limit, a per-iteration upper bound, a calibrated cap — declare the dependency explicitly with extra_param_names:

IrregSpacedGrid(n_points=64, extra_param_names=("max_consumption",))

extra_param_names is only valid together with runtime points (points=None). Each name is added to the params template alongside the grid’s points slot, and broadcast_to_template carries it through even though no DAG function references it. The names exist so user-side code that constructs the gridpoints can read them without tripping the template validator’s Unknown keys check.

The typical workflow looks like this. Declare the grid with extra_param_names, populate the extras (e.g. as model.fixed_params["max_consumption"]), then inject the points before solving or simulating:

import jax.numpy as jnp
from lcm import IrregSpacedGrid, Model


def inject_consumption_points(*, params: dict, model: Model) -> dict:
    """Fill the runtime `consumption` gridpoints on every non-terminal regime."""
    max_consumption = jnp.asarray(model.fixed_params["max_consumption"])
    out: dict = dict(params)
    for regime_name, regime in model.regimes.items():
        if regime.terminal:
            continue
        grid = regime.actions["consumption"]
        assert isinstance(grid, IrregSpacedGrid) and grid.pass_points_at_runtime
        points = jnp.geomspace(1e-3, max_consumption, num=grid.n_points)
        regime_entry = dict(out.get(regime_name, {}))
        regime_entry["consumption"] = {"points": points}
        out[regime_name] = regime_entry
    return out


params = inject_consumption_points(params=model.get_params_template(), model=model)
model.solve(params=params)

extra_param_names carries any number of scalars (("max_consumption", "floor"), etc.). The construction logic in the user-side helper is free to combine them however the model needs — pylcm only validates that each declared name is present in the resolved params.

PiecewiseLinSpacedGrid

Multiple linearly spaced segments joined at breakpoints. Dense where you need precision, sparse elsewhere:

from lcm import Piece, PiecewiseLinSpacedGrid

PiecewiseLinSpacedGrid(
    pieces=(
        Piece(interval="[0, 10)", n_points=20),
        Piece(interval="[10, 100]", n_points=10),
    ),
)

Pieces must be adjacent: the upper bound of each piece must equal the lower bound of the next, with compatible open/closed boundaries (e.g., [0, 10) followed by [10, 100]).

PiecewiseLogSpacedGrid

Same structure as PiecewiseLinSpacedGrid but with log spacing within each piece. Good for wealth grids spanning orders of magnitude. All boundary values must be positive.

PiecewiseLogSpacedGrid(
    pieces=(
        Piece(interval="[0.1, 10)", n_points=50),
        Piece(interval="[10, 1000]", n_points=30),
    ),
)

Grid Hierarchy (advanced)

All grids inherit from the Grid base class:

The to_jax() method converts any grid to a JAX array. ContinuousGrid subclasses provide get_coordinate() for mapping values to grid coordinates, used in interpolation.

See Also