Once you have defined a Model and prepared your parameters, pylcm solves via backward
induction and simulates forward.
Solving¶
V_arr_dict = model.solve(params)Performs backward induction using dynamic programming. Returns an immutable mapping of
period -> regime_name -> value_function_array.
Log levels¶
Control console output and snapshot persistence with log_level:
# Default: progress + timing
V_arr_dict = model.solve(params)
# Silent
V_arr_dict = model.solve(params, log_level="off")
# Full diagnostics + disk snapshots
V_arr_dict = model.solve(params, log_level="debug", log_path="./debug/")See Debugging for details on log levels and debug snapshots.
Simulating¶
result = model.simulate(
params=params,
initial_conditions=initial_conditions,
V_arr_dict=V_arr_dict,
)Forward simulation using solved value functions. Each agent starts from the given initial
conditions and makes optimal decisions at each period. Returns a SimulationResult
object.
Solve and Simulate (combined)¶
result = model.solve_and_simulate(
params=params,
initial_conditions=initial_conditions,
)Convenience method combining both steps. Use when you don’t need the raw value function arrays.
Initial Conditions¶
From a DataFrame¶
The standard way to supply initial conditions is as a pandas DataFrame with one row per
agent. Use initial_conditions_from_dataframe to convert it to the format expected by
simulate() and solve_and_simulate():
import pandas as pd
from lcm import initial_conditions_from_dataframe
df = pd.DataFrame({
"regime": ["working_life", "working_life", "retirement", "working_life"],
"age": [25.0, 25.0, 25.0, 25.0],
"wealth": [1.0, 5.0, 10.0, 20.0],
"health": ["good", "bad", "bad", "good"], # string labels, auto-converted
})
initial_conditions = initial_conditions_from_dataframe(df, model=model)Discrete states (those backed by a DiscreteGrid) are mapped from string labels to
integer codes automatically. See Working with DataFrames and Series for
details.
As JAX arrays¶
You can also pass initial conditions directly as JAX arrays — useful for programmatic setups like grid searches or tests:
initial_conditions = {
"age": jnp.array([25.0, 25.0, 25.0, 25.0]),
"wealth": jnp.array([1.0, 5.0, 10.0, 20.0]),
"health": jnp.array([0, 1, 1, 0]), # integer codes for discrete states
"regime_id": jnp.array([
RegimeId.working_life, RegimeId.working_life,
RegimeId.retirement, RegimeId.working_life,
]),
}Every non-shock state must have an entry.
"regime_id"must be included, with integer codes from theregime_id_class.All arrays must have the same length (= number of agents).
Shock states are drawn automatically.
Optional arguments¶
check_initial_conditions=True: Validates that initial states are on-grid and regimes are valid. Set toFalseto skip validation.seed=None: Random seed for stochastic simulations (int).log_level="progress": Controls logging verbosity (same options assolve()).log_path=None: Directory for debug snapshots (whenlog_level="debug").log_keep_n_latest=3: Maximum snapshot directories to retain.
Heterogeneous initial ages¶
"age" must always be provided in initial_conditions. Each value must be a valid point on
the model’s AgeGrid, and each subject’s initial regime must be active at their starting
age. The most common case is that all subjects start at the initial age — just pass a
constant array.
Subjects can start at different ages:
initial_conditions = {
"age": jnp.array([40.0, 60.0]),
"wealth": jnp.array([50.0, 50.0]),
"regime_id": jnp.array([
model.regime_names_to_ids["working_life"],
model.regime_names_to_ids["working_life"],
]),
}In the resulting DataFrame, each subject appears only from their starting age onward — earlier periods are omitted, not filled with placeholders.
Working with SimulationResult¶
Converting to DataFrame¶
df = result.to_dataframe()Returns a pandas DataFrame with columns: subject_id, period, age, regime,
value, plus all states and actions. Discrete variables are pandas Categorical with
string labels.
Additional targets¶
Compute functions and constraints alongside the standard output:
# Specific targets
df = result.to_dataframe(additional_targets=["utility", "consumption"])
# All available targets
df = result.to_dataframe(additional_targets="all")
# See what's available
result.available_targets # ['consumption', 'earnings', 'utility', ...]Each target is computed for regimes where it exists; rows from other regimes get NaN.
Integer codes instead of labels¶
df = result.to_dataframe(use_labels=False)Returns discrete variables as raw integer codes instead of categorical labels.
Metadata¶
result.regime_names # ['retirement', 'working_life']
result.state_names # ['health', 'wealth']
result.action_names # ['consumption', 'work']
result.n_periods # 50
result.n_subjects # 1000Serialization¶
Save and load results (requires cloudpickle):
# Save
result.to_pickle("my_results.pkl")
# Load
from lcm.simulation.result import SimulationResult
loaded = SimulationResult.from_pickle("my_results.pkl")Raw data (advanced)¶
result.raw_results # regime -> period -> PeriodRegimeSimulationData
result.internal_params # processed parameter object
result.V_arr_dict # value function arrays from solve()Typical Workflow¶
import numpy as np
import pandas as pd
from lcm import Model, initial_conditions_from_dataframe
# 1. Define model (see previous pages)
model = Model(regimes={...}, ages=..., regime_id_class=...)
# 2. Set parameters
params = {
"discount_factor": 0.95,
"interest_rate": 0.03,
...
}
# 3. Prepare initial conditions
initial_df = pd.DataFrame({
"regime": "working_life",
"age": model.ages.values[0],
"wealth": np.linspace(1, 50, 100),
})
initial_conditions = initial_conditions_from_dataframe(initial_df, model=model)
# 4. Solve and simulate
result = model.solve_and_simulate(
params=params,
initial_conditions=initial_conditions,
)
# 5. Analyze
df = result.to_dataframe(additional_targets="all")
df.groupby("period")["wealth"].mean()See Also¶
Defining Models — constructing the
ModelParameters — preparing the params dict
Working with DataFrames and Series — DataFrame conversion utilities
A Tiny Example — complete walkthrough
Examples — full worked examples