A three-period consumption-savings model with two regimes:
Working life (ages 25 and 45): The agent chooses whether to work and how much to consume. A simple tax-and-transfer system guarantees a consumption floor. Savings earn interest.
Retirement (age 65): Terminal regime. The agent consumes out of remaining wealth.
This notebook teaches the common GridSearch workflow, not a universal model template. If your own model needs EGM, nested EGM, or declared institutional kinks, start from the matching specialized regime and margin declarations. Make the solver-choice checkpoint before adapting the code below.
Model¶
An agent lives for three periods (ages 25, 45, and 65). In the first two periods (working life), the agent chooses whether to work and how much to consume . In the final period (retirement), the agent consumes out of remaining wealth.
Working life (ages 25 and 45):
subject to
where is wealth, earnings, the wage, a consumption floor guaranteed by transfers, the tax rate, and end-of-period wealth. The transfer only kicks in when the agent’s resources () fall below the consumption floor.
Retirement (age 65, terminal):
from pprint import pprint
import jax.numpy as jnp
import numpy as np
import pandas as pd
import plotly.express as px
from lcm import (
AgeGrid,
DeterministicTransition,
DiscreteGrid,
LinSpacedGrid,
LogSpacedGrid,
Model,
Regime,
Transition,
categorical,
)
from lcm.typing import (
BoolND,
ContinuousAction,
ContinuousState,
DiscreteAction,
FloatND,
ScalarInt,
)Categorical Variables¶
@categorical(ordered=True)
class LaborSupply:
do_not_work: ScalarInt
work: ScalarInt
@categorical(ordered=False)
class RegimeId:
working_life: ScalarInt
retirement: ScalarIntModel Functions¶
# Utility
def utility(
*,
consumption: ContinuousAction,
labor_supply: DiscreteAction,
disutility_of_work: float,
risk_aversion: float,
) -> FloatND:
return consumption ** (1 - risk_aversion) / (
1 - risk_aversion
) - disutility_of_work * (labor_supply == LaborSupply.work)
def utility_retirement(*, wealth: ContinuousState, risk_aversion: float) -> FloatND:
return wealth ** (1 - risk_aversion) / (1 - risk_aversion)
# Auxiliary functions
def earnings(*, labor_supply: DiscreteAction, wage: float) -> FloatND:
return jnp.where(labor_supply == LaborSupply.work, wage, 0.0)
def taxes_transfers(
*,
earnings: FloatND,
wealth: ContinuousState,
consumption_floor: float,
tax_rate: float,
) -> FloatND:
return jnp.where(
earnings >= consumption_floor,
tax_rate * (earnings - consumption_floor),
jnp.minimum(0.0, wealth + earnings - consumption_floor),
)
def end_of_period_wealth(
*,
wealth: ContinuousState,
earnings: FloatND,
taxes_transfers: FloatND,
consumption: ContinuousAction,
) -> FloatND:
return wealth + earnings - taxes_transfers - consumption
# State transition
def next_wealth(
*, end_of_period_wealth: FloatND, interest_rate: float
) -> ContinuousState:
return (1 + interest_rate) * end_of_period_wealth
# Constraints
def borrowing_constraint_working(end_of_period_wealth: FloatND) -> BoolND:
return end_of_period_wealth >= 0
# Regime transition
def next_regime(*, age: float, last_working_age: float) -> ScalarInt:
return jnp.where(
age >= last_working_age, RegimeId.retirement, RegimeId.working_life
)Regimes and Model¶
age_grid = AgeGrid(start=25, inclusive_stop=65, step="20Y")
retirement_age = age_grid.exact_values[-1]
working_life = Regime(
states={
"wealth": LinSpacedGrid(start=0, stop=50, n_points=25),
},
state_transitions={
"wealth": next_wealth,
},
actions={
"labor_supply": DiscreteGrid(category_class=LaborSupply),
"consumption": LogSpacedGrid(start=4, stop=50, n_points=100),
},
functions={
"utility": utility,
"earnings": earnings,
"taxes_transfers": taxes_transfers,
"end_of_period_wealth": end_of_period_wealth,
},
constraints={
"borrowing_constraint_working": borrowing_constraint_working,
},
)
retirement = Regime(
states={
"wealth": LinSpacedGrid(start=0, stop=50, n_points=25),
},
functions={"utility": utility_retirement},
)
# At 25 the graph permits both working life and retirement, and the age-based
# `next_regime` selects one; at 45 retirement is the only edge, and `next_regime`
# returns it there. `retirement` has no outgoing edges, which makes it terminal.
edges = {
"working_life": Transition(
targets={"working_life": 25, "retirement": (25, 45)},
law=DeterministicTransition(func=next_regime),
)
}
model = Model(
regimes={
"working_life": working_life,
"retirement": retirement,
},
ages=age_grid,
edges=edges,
initial_nodes=((25, "working_life"),),
regime_id_class=RegimeId,
description="A tiny three-period consumption-savings model.",
)Parameters¶
Use model.get_params_template() to see what parameters the model expects, organized
by regime and function. The regime-transition law belongs to the edges, so its
parameters sit under the "edges" root, keyed by the source regime.
pprint(dict(model.get_params_template())){'edges': {'working_life': {'last_working_age': 'float'}},
'retirement': {'utility': {'risk_aversion': 'float'}},
'working_life': {'borrowing_constraint_working': {},
'certainty_equivalent': {},
'earnings': {'wage': 'float'},
'end_of_period_wealth': {},
'koopmans_aggregator': {'discount_factor': 'FloatND'},
'next_wealth': {'interest_rate': 'float'},
'taxes_transfers': {'consumption_floor': 'float',
'tax_rate': 'float'},
'utility': {'disutility_of_work': 'float',
'risk_aversion': 'float'}}}
Parameters shared across regimes (risk_aversion, discount_factor,
interest_rate) can be specified at the model level. Parameters unique to one
regime go under the regime name. last_working_age, read by the law next_regime,
goes under "edges" and the source regime working_life.
params = {
"discount_factor": 0.95,
"risk_aversion": 1.5,
"interest_rate": 0.03,
"working_life": {
"utility": {"disutility_of_work": 1.0},
"earnings": {"wage": 20.0},
"taxes_transfers": {"consumption_floor": 2.0, "tax_rate": 0.2},
},
"edges": {"working_life": {"last_working_age": age_grid.exact_values[-2]}},
}Simulate¶
n_agents = 100
initial_df = pd.DataFrame(
{
"regime_name": "working_life",
"age": float(age_grid.exact_values[0]),
"wealth": np.linspace(1, 20, n_agents),
}
)
result = model.simulate(
params=params,
initial_conditions=initial_df,
log_level="debug",
)solve call b62bccda3b94 phase public_simulate begin
INFO:lcm:solve call b62bccda3b94 phase public_simulate begin
solve call b62bccda3b94 phase params_validation begin
INFO:lcm:solve call b62bccda3b94 phase params_validation begin
solve call b62bccda3b94 phase params_validation end status=ok seconds=0.108392
INFO:lcm:solve call b62bccda3b94 phase params_validation end status=ok seconds=0.108392
solve call b62bccda3b94 phase simulation_inputs begin
INFO:lcm:solve call b62bccda3b94 phase simulation_inputs begin
solve call b62bccda3b94 phase simulation_inputs end status=ok seconds=0.390828
INFO:lcm:solve call b62bccda3b94 phase simulation_inputs end status=ok seconds=0.390828
solve call b62bccda3b94 phase authority_fingerprint begin
INFO:lcm:solve call b62bccda3b94 phase authority_fingerprint begin
solve call b62bccda3b94 phase authority_fingerprint end status=ok seconds=0.004256
INFO:lcm:solve call b62bccda3b94 phase authority_fingerprint end status=ok seconds=0.004256
solve call b62bccda3b94 phase solver_param_checks begin
INFO:lcm:solve call b62bccda3b94 phase solver_param_checks begin
solve call b62bccda3b94 phase solver_param_checks end status=ok seconds=0.000046
INFO:lcm:solve call b62bccda3b94 phase solver_param_checks end status=ok seconds=0.000046
solve call b62bccda3b94 phase state_action_spaces begin
INFO:lcm:solve call b62bccda3b94 phase state_action_spaces begin
solve call b62bccda3b94 phase state_action_spaces end status=ok seconds=0.000203
INFO:lcm:solve call b62bccda3b94 phase state_action_spaces end status=ok seconds=0.000203
solve call b62bccda3b94 phase continuation_templates begin
INFO:lcm:solve call b62bccda3b94 phase continuation_templates begin
solve call b62bccda3b94 phase continuation_templates end status=ok seconds=0.001224
INFO:lcm:solve call b62bccda3b94 phase continuation_templates end status=ok seconds=0.001224
solve call b62bccda3b94 phase program_graphs begin
INFO:lcm:solve call b62bccda3b94 phase program_graphs begin
solve call b62bccda3b94 phase program_graphs end status=ok seconds=0.000650
INFO:lcm:solve call b62bccda3b94 phase program_graphs end status=ok seconds=0.000650
solve call b62bccda3b94 phase structural_resolution begin
INFO:lcm:solve call b62bccda3b94 phase structural_resolution begin
structural blueprint: miss (hits=0, misses=1, entries=1)
DEBUG:lcm:structural blueprint: miss (hits=0, misses=1, entries=1)
solve call b62bccda3b94 phase structural_resolution end status=ok seconds=0.033838
INFO:lcm:solve call b62bccda3b94 phase structural_resolution end status=ok seconds=0.033838
solve call b62bccda3b94 phase residency_inventory begin
INFO:lcm:solve call b62bccda3b94 phase residency_inventory begin
solve call b62bccda3b94 phase residency_inventory end status=ok seconds=0.000007
INFO:lcm:solve call b62bccda3b94 phase residency_inventory end status=ok seconds=0.000007
solve call b62bccda3b94 phase compilation_waves begin
INFO:lcm:solve call b62bccda3b94 phase compilation_waves begin
candidate evaluation ('working_life', 0, 'main') {'action_product': 64, 'cell': 16}
DEBUG:lcm:candidate evaluation ('working_life', 0, 'main') {'action_product': 64, 'cell': 16}
candidate evaluation ('working_life', 1, 'main') {'action_product': 64, 'cell': 16}
DEBUG:lcm:candidate evaluation ('working_life', 1, 'main') {'action_product': 64, 'cell': 16}
candidate evaluation ('retirement', 1, 'main') {'cell': 16}
DEBUG:lcm:candidate evaluation ('retirement', 1, 'main') {'cell': 16}
candidate evaluation ('retirement', 2, 'main') {'cell': 16}
DEBUG:lcm:candidate evaluation ('retirement', 2, 'main') {'cell': 16}
AOT compilation wave 0: 3 unique lowerings for 4 regime-period-core triples (2 workers)
INFO:lcm:AOT compilation wave 0: 3 unique lowerings for 4 regime-period-core triples (2 workers)
1/3 working_life main (age 25, widths={'action_product': 64, 'cell': 16})
INFO:lcm:1/3 working_life main (age 25, widths={'action_product': 64, 'cell': 16})
lowering working_life main (age 25, widths={'action_product': 64, 'cell': 16}) ...
INFO:lcm: lowering working_life main (age 25, widths={'action_product': 64, 'cell': 16}) ...
lowered in 327.8ms
INFO:lcm: lowered in 327.8ms
compiling working_life main (age 25, widths={'action_product': 64, 'cell': 16}) ...
INFO:lcm: compiling working_life main (age 25, widths={'action_product': 64, 'cell': 16}) ...
2/3 working_life main (age 45, widths={'action_product': 64, 'cell': 16})
INFO:lcm:2/3 working_life main (age 45, widths={'action_product': 64, 'cell': 16})
lowering working_life main (age 45, widths={'action_product': 64, 'cell': 16}) ...
INFO:lcm: lowering working_life main (age 45, widths={'action_product': 64, 'cell': 16}) ...
lowered in 267.9ms
INFO:lcm: lowered in 267.9ms
compiling working_life main (age 45, widths={'action_product': 64, 'cell': 16}) ...
INFO:lcm: compiling working_life main (age 45, widths={'action_product': 64, 'cell': 16}) ...
3/3 retirement main (age 45, widths={'cell': 16})
INFO:lcm:3/3 retirement main (age 45, widths={'cell': 16})
lowering retirement main (age 45, widths={'cell': 16}) ...
INFO:lcm: lowering retirement main (age 45, widths={'cell': 16}) ...
lowered in 48.9ms
INFO:lcm: lowered in 48.9ms
compiled working_life main (age 25, widths={'action_product': 64, 'cell': 16}) 686.2ms
INFO:lcm: compiled working_life main (age 25, widths={'action_product': 64, 'cell': 16}) 686.2ms
compiling retirement main (age 45, widths={'cell': 16}) ...
INFO:lcm: compiling retirement main (age 45, widths={'cell': 16}) ...
compiled retirement main (age 45, widths={'cell': 16}) 235.1ms
INFO:lcm: compiled retirement main (age 45, widths={'cell': 16}) 235.1ms
compiled working_life main (age 45, widths={'action_product': 64, 'cell': 16}) 652.7ms
INFO:lcm: compiled working_life main (age 45, widths={'action_product': 64, 'cell': 16}) 652.7ms
AOT compilation wave 0: 3 unique variants compiled, 0 fallback evaluations skipped, 0 fallback requests avoided
INFO:lcm:AOT compilation wave 0: 3 unique variants compiled, 0 fallback evaluations skipped, 0 fallback requests avoided
solve call b62bccda3b94 phase compilation_waves end status=ok seconds=1.284767
INFO:lcm:solve call b62bccda3b94 phase compilation_waves end status=ok seconds=1.284767
solve call b62bccda3b94 phase workspace_selection begin
INFO:lcm:solve call b62bccda3b94 phase workspace_selection begin
core plan record {"active_replica_bytes":0,"axis_extents":{"action_product":200,"cell":25},"compiled_collectives":{},"compiler_peak_bytes":939,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"action_product":"streamed","cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[25],"period":0,"period_transfer_scratch_bytes":{},"physical_value_shape":[25],"regime":"working_life","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":25},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"action_product":64,"cell":16}}
DEBUG:lcm:core plan record {"active_replica_bytes":0,"axis_extents":{"action_product":200,"cell":25},"compiled_collectives":{},"compiler_peak_bytes":939,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"action_product":"streamed","cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[25],"period":0,"period_transfer_scratch_bytes":{},"physical_value_shape":[25],"regime":"working_life","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":25},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"action_product":64,"cell":16}}
core plan record {"active_replica_bytes":0,"axis_extents":{"action_product":200,"cell":25},"compiled_collectives":{},"compiler_peak_bytes":835,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"action_product":"streamed","cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[25],"period":1,"period_transfer_scratch_bytes":{},"physical_value_shape":[25],"regime":"working_life","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":25},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"action_product":64,"cell":16}}
DEBUG:lcm:core plan record {"active_replica_bytes":0,"axis_extents":{"action_product":200,"cell":25},"compiled_collectives":{},"compiler_peak_bytes":835,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"action_product":"streamed","cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[25],"period":1,"period_transfer_scratch_bytes":{},"physical_value_shape":[25],"regime":"working_life","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":25},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"action_product":64,"cell":16}}
core plan record {"active_replica_bytes":0,"axis_extents":{"cell":25},"compiled_collectives":{},"compiler_peak_bytes":236,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[25],"period":1,"period_transfer_scratch_bytes":{},"physical_value_shape":[25],"regime":"retirement","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":25},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"cell":16}}
DEBUG:lcm:core plan record {"active_replica_bytes":0,"axis_extents":{"cell":25},"compiled_collectives":{},"compiler_peak_bytes":236,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[25],"period":1,"period_transfer_scratch_bytes":{},"physical_value_shape":[25],"regime":"retirement","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":25},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"cell":16}}
core plan record {"active_replica_bytes":0,"axis_extents":{"cell":25},"compiled_collectives":{},"compiler_peak_bytes":236,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[25],"period":2,"period_transfer_scratch_bytes":{},"physical_value_shape":[25],"regime":"retirement","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":25},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"cell":16}}
DEBUG:lcm:core plan record {"active_replica_bytes":0,"axis_extents":{"cell":25},"compiled_collectives":{},"compiler_peak_bytes":236,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[25],"period":2,"period_transfer_scratch_bytes":{},"physical_value_shape":[25],"regime":"retirement","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":25},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"cell":16}}
solve call b62bccda3b94 phase workspace_selection end status=ok seconds=0.023551
INFO:lcm:solve call b62bccda3b94 phase workspace_selection end status=ok seconds=0.023551
Starting solution
INFO:lcm:Starting solution
solve call b62bccda3b94 phase backward_induction begin
INFO:lcm:solve call b62bccda3b94 phase backward_induction begin
Age 65 (1 regimes):
INFO:lcm:Age 65 (1 regimes):
finished in 138.2ms
INFO:lcm: finished in 138.2ms
Age 45 (2 regimes):
INFO:lcm:Age 45 (2 regimes):
finished in 26.6ms
INFO:lcm: finished in 26.6ms
Age 25 (1 regimes):
INFO:lcm:Age 25 (1 regimes):
finished in 4.5ms
INFO:lcm: finished in 4.5ms
Inf in V_arr for 1 other state(s) for regime 'retirement', period 2 (age 65.0)
WARNING:lcm:Inf in V_arr for 1 other state(s) for regime 'retirement', period 2 (age 65.0)
Inf in V_arr for 1 other state(s) for regime 'retirement', period 1 (age 45.0)
WARNING:lcm:Inf in V_arr for 1 other state(s) for regime 'retirement', period 1 (age 45.0)
retirement age 65.0 V min=-inf max=-0.283 mean=-inf
DEBUG:lcm: retirement age 65.0 V min=-inf max=-0.283 mean=-inf
working_life age 45.0 V min=-2.35 max=-0.774 mean=-1.29
DEBUG:lcm: working_life age 45.0 V min=-2.35 max=-0.774 mean=-1.29
retirement age 45.0 V min=-inf max=-0.283 mean=-inf
DEBUG:lcm: retirement age 45.0 V min=-inf max=-0.283 mean=-inf
working_life age 25.0 V min=-3.42 max=-1.38 mean=-2.15
DEBUG:lcm: working_life age 25.0 V min=-3.42 max=-1.38 mean=-2.15
solve call b62bccda3b94 phase backward_induction end status=ok seconds=0.196658
INFO:lcm:solve call b62bccda3b94 phase backward_induction end status=ok seconds=0.196658
Solution complete (199.3ms)
INFO:lcm:Solution complete (199.3ms)
solve call b62bccda3b94 phase result_assembly begin
INFO:lcm:solve call b62bccda3b94 phase result_assembly begin
solve call b62bccda3b94 phase result_assembly end status=ok seconds=0.004568
INFO:lcm:solve call b62bccda3b94 phase result_assembly end status=ok seconds=0.004568
solve call b62bccda3b94 phase solution_resolution begin
INFO:lcm:solve call b62bccda3b94 phase solution_resolution begin
solve call b62bccda3b94 phase solution_resolution end status=ok seconds=0.001900
INFO:lcm:solve call b62bccda3b94 phase solution_resolution end status=ok seconds=0.001900
solve call b62bccda3b94 phase solution_handover begin
INFO:lcm:solve call b62bccda3b94 phase solution_handover begin
solve call b62bccda3b94 phase solution_handover end status=ok seconds=0.000004
INFO:lcm:solve call b62bccda3b94 phase solution_handover end status=ok seconds=0.000004
solve call b62bccda3b94 phase chunk_planning begin
INFO:lcm:solve call b62bccda3b94 phase chunk_planning begin
solve call b62bccda3b94 phase chunk_planning end status=ok seconds=0.000382
INFO:lcm:solve call b62bccda3b94 phase chunk_planning end status=ok seconds=0.000382
solve call b62bccda3b94 phase simulation_setup begin
INFO:lcm:solve call b62bccda3b94 phase simulation_setup begin
Starting simulation
INFO:lcm:Starting simulation
Simulation plan: route=legacy; 1 cpu device(s) (0,); outer chunks=1 (widths=(100,)); unbudgeted.
INFO:lcm:Simulation plan: route=legacy; 1 cpu device(s) (0,); outer chunks=1 (widths=(100,)); unbudgeted.
Simulation plan detail: route=legacy; subject_devices=(cpu) (0,); axis_widths_by_regime=[working_life: {}; retirement: {}]; outer_chunk_count=1; admitted_chunk_widths=(100,); budget_mode=unbudgeted; effective_device_memory_bytes=None; subject_grouping=None.
DEBUG:lcm:Simulation plan detail: route=legacy; subject_devices=(cpu) (0,); axis_widths_by_regime=[working_life: {}; retirement: {}]; outer_chunk_count=1; admitted_chunk_widths=(100,); budget_mode=unbudgeted; effective_device_memory_bytes=None; subject_grouping=None.
solve call b62bccda3b94 phase simulation_setup end status=ok seconds=0.260313
INFO:lcm:solve call b62bccda3b94 phase simulation_setup end status=ok seconds=0.260313
solve call b62bccda3b94 phase simulation_chunk begin
INFO:lcm:solve call b62bccda3b94 phase simulation_chunk begin
Age 25 (1 regimes):
INFO:lcm:Age 25 (1 regimes):
solve call b62bccda3b94 phase simulation_compilation begin
INFO:lcm:solve call b62bccda3b94 phase simulation_compilation begin
solve call b62bccda3b94 phase simulation_compilation end status=ok seconds=0.481677
INFO:lcm:solve call b62bccda3b94 phase simulation_compilation end status=ok seconds=0.481677
solve call b62bccda3b94 phase simulation_compilation begin
INFO:lcm:solve call b62bccda3b94 phase simulation_compilation begin
solve call b62bccda3b94 phase simulation_compilation end status=ok seconds=0.046736
INFO:lcm:solve call b62bccda3b94 phase simulation_compilation end status=ok seconds=0.046736
solve call b62bccda3b94 phase simulation_compilation begin
INFO:lcm:solve call b62bccda3b94 phase simulation_compilation begin
solve call b62bccda3b94 phase simulation_compilation end status=ok seconds=0.035510
INFO:lcm:solve call b62bccda3b94 phase simulation_compilation end status=ok seconds=0.035510
Off-grid next states: regime='working_life', target='retirement', period=0, state='wealth', phase='realized', count=0
INFO:lcm:Off-grid next states: regime='working_life', target='retirement', period=0, state='wealth', phase='realized', count=0
Off-grid next states: regime='working_life', target='working_life', period=0, state='wealth', phase='realized', count=0
INFO:lcm:Off-grid next states: regime='working_life', target='working_life', period=0, state='wealth', phase='realized', count=0
transitions:
- working_life → working_life = 100
DEBUG:lcm: transitions:
- working_life → working_life = 100
finished in 2.1s
INFO:lcm: finished in 2.1s
Age 45 (2 regimes):
INFO:lcm:Age 45 (2 regimes):
solve call b62bccda3b94 phase simulation_compilation begin
INFO:lcm:solve call b62bccda3b94 phase simulation_compilation begin
solve call b62bccda3b94 phase simulation_compilation end status=ok seconds=0.313432
INFO:lcm:solve call b62bccda3b94 phase simulation_compilation end status=ok seconds=0.313432
solve call b62bccda3b94 phase simulation_compilation begin
INFO:lcm:solve call b62bccda3b94 phase simulation_compilation begin
solve call b62bccda3b94 phase simulation_compilation end status=ok seconds=0.038413
INFO:lcm:solve call b62bccda3b94 phase simulation_compilation end status=ok seconds=0.038413
Off-grid next states: regime='working_life', target='retirement', period=1, state='wealth', phase='realized', count=0
INFO:lcm:Off-grid next states: regime='working_life', target='retirement', period=1, state='wealth', phase='realized', count=0
solve call b62bccda3b94 phase simulation_compilation begin
INFO:lcm:solve call b62bccda3b94 phase simulation_compilation begin
solve call b62bccda3b94 phase simulation_compilation end status=ok seconds=0.042205
INFO:lcm:solve call b62bccda3b94 phase simulation_compilation end status=ok seconds=0.042205
transitions:
- working_life → retirement = 100
DEBUG:lcm: transitions:
- working_life → retirement = 100
finished in 751.8ms
INFO:lcm: finished in 751.8ms
Age 65 (1 regimes):
INFO:lcm:Age 65 (1 regimes):
transitions:
- retirement → retirement = 100
DEBUG:lcm: transitions:
- retirement → retirement = 100
finished in 66.9ms
INFO:lcm: finished in 66.9ms
solve call b62bccda3b94 phase simulation_chunk end status=ok seconds=2.901917
INFO:lcm:solve call b62bccda3b94 phase simulation_chunk end status=ok seconds=2.901917
solve call b62bccda3b94 phase simulation_completion begin
INFO:lcm:solve call b62bccda3b94 phase simulation_completion begin
Simulation complete (3.2s)
INFO:lcm:Simulation complete (3.2s)
solve call b62bccda3b94 phase simulation_completion end status=ok seconds=0.001282
INFO:lcm:solve call b62bccda3b94 phase simulation_completion end status=ok seconds=0.001282
solve call b62bccda3b94 phase simulation_result begin
INFO:lcm:solve call b62bccda3b94 phase simulation_result begin
solve call b62bccda3b94 phase simulation_result end status=ok seconds=0.005464
INFO:lcm:solve call b62bccda3b94 phase simulation_result end status=ok seconds=0.005464
solve call b62bccda3b94 phase result_finalization begin
INFO:lcm:solve call b62bccda3b94 phase result_finalization begin
solve call b62bccda3b94 phase result_finalization end status=ok seconds=0.000008
INFO:lcm:solve call b62bccda3b94 phase result_finalization end status=ok seconds=0.000008
solve call b62bccda3b94 phase public_simulate end status=ok seconds=5.269754
INFO:lcm:solve call b62bccda3b94 phase public_simulate end status=ok seconds=5.269754
df = result.to_dataframe(additional_targets="all")
df["age"] = df["age"].astype(int)
df.loc[df["age"] == retirement_age, "consumption"] = df.loc[
df["age"] == retirement_age, "wealth"
]
columns = [
"regime_name",
"labor_supply",
"consumption",
"wealth",
"earnings",
"taxes_transfers",
"end_of_period_wealth",
"value",
]
df.set_index(["subject_id", "age"])[columns].head(20).style.format(
precision=1,
na_rep="",
)Source
# Classify agents by work pattern across the two working-life periods
first_working_age = age_grid.exact_values[0]
last_working_age = age_grid.exact_values[-2]
df_working = df[df["regime_name"] == "working_life"]
work_by_age = df_working.pivot_table(
index="subject_id",
columns="age",
values="labor_supply",
aggfunc="first",
)
work_pattern = (
work_by_age[first_working_age].astype(str)
+ ", "
+ work_by_age[last_working_age].astype(str)
)
assert "work, work" not in work_pattern.to_numpy(), (
"Plotting assumes that no agent works in both periods of working life."
)
label_map = {
"work, do_not_work": "low", # work early, not later
"do_not_work, work": "medium", # coast early, work later
"do_not_work, do_not_work": "high", # never work
}
groups = work_pattern.map(label_map).rename("initial_wealth")
# Combined descriptives and work decisions table
initial_wealth = df[df["age"] == first_working_age].set_index("subject_id")["wealth"]
group_desc = initial_wealth.groupby(groups).agg(["min", "max"]).round(1)
df_groups = df.copy()
df_groups["initial_wealth"] = df_groups["subject_id"].map(groups)
df_mean = df_groups.groupby(["initial_wealth", "age"], as_index=False).mean(
numeric_only=True,
)
work_table = df_mean[df_mean["age"] < retirement_age].pivot_table(
index="initial_wealth",
columns="age",
values="earnings",
)
work_table = (work_table > 0).astype(int)
work_table.columns = [f"works {c}" for c in work_table.columns]
summary = pd.concat([group_desc, work_table], axis=1)
summary.index.name = "initial_wealth"
summary.loc[["low", "medium", "high"]].style.format(precision=1, na_rep="")Source
fig = px.line(
df_mean,
x="age",
y="consumption",
color="initial_wealth",
title="Consumption by Age",
template="plotly_dark",
)
fig.show()Source
fig = px.line(
df_mean,
x="age",
y="wealth",
color="initial_wealth",
title="Wealth by Age",
template="plotly_dark",
)
fig.show()