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 77d3f526d9c3 phase public_simulate begin
INFO:lcm:solve call 77d3f526d9c3 phase public_simulate begin
solve call 77d3f526d9c3 phase params_validation begin
INFO:lcm:solve call 77d3f526d9c3 phase params_validation begin
solve call 77d3f526d9c3 phase params_validation end status=ok seconds=0.134919
INFO:lcm:solve call 77d3f526d9c3 phase params_validation end status=ok seconds=0.134919
solve call 77d3f526d9c3 phase simulation_inputs begin
INFO:lcm:solve call 77d3f526d9c3 phase simulation_inputs begin
solve call 77d3f526d9c3 phase simulation_inputs end status=ok seconds=0.517629
INFO:lcm:solve call 77d3f526d9c3 phase simulation_inputs end status=ok seconds=0.517629
solve call 77d3f526d9c3 phase authority_fingerprint begin
INFO:lcm:solve call 77d3f526d9c3 phase authority_fingerprint begin
solve call 77d3f526d9c3 phase authority_fingerprint end status=ok seconds=0.005800
INFO:lcm:solve call 77d3f526d9c3 phase authority_fingerprint end status=ok seconds=0.005800
solve call 77d3f526d9c3 phase solver_param_checks begin
INFO:lcm:solve call 77d3f526d9c3 phase solver_param_checks begin
solve call 77d3f526d9c3 phase solver_param_checks end status=ok seconds=0.000047
INFO:lcm:solve call 77d3f526d9c3 phase solver_param_checks end status=ok seconds=0.000047
solve call 77d3f526d9c3 phase state_action_spaces begin
INFO:lcm:solve call 77d3f526d9c3 phase state_action_spaces begin
solve call 77d3f526d9c3 phase state_action_spaces end status=ok seconds=0.000214
INFO:lcm:solve call 77d3f526d9c3 phase state_action_spaces end status=ok seconds=0.000214
solve call 77d3f526d9c3 phase continuation_templates begin
INFO:lcm:solve call 77d3f526d9c3 phase continuation_templates begin
solve call 77d3f526d9c3 phase continuation_templates end status=ok seconds=0.001194
INFO:lcm:solve call 77d3f526d9c3 phase continuation_templates end status=ok seconds=0.001194
solve call 77d3f526d9c3 phase program_graphs begin
INFO:lcm:solve call 77d3f526d9c3 phase program_graphs begin
solve call 77d3f526d9c3 phase program_graphs end status=ok seconds=0.000492
INFO:lcm:solve call 77d3f526d9c3 phase program_graphs end status=ok seconds=0.000492
solve call 77d3f526d9c3 phase structural_resolution begin
INFO:lcm:solve call 77d3f526d9c3 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 77d3f526d9c3 phase structural_resolution end status=ok seconds=0.044019
INFO:lcm:solve call 77d3f526d9c3 phase structural_resolution end status=ok seconds=0.044019
solve call 77d3f526d9c3 phase residency_inventory begin
INFO:lcm:solve call 77d3f526d9c3 phase residency_inventory begin
solve call 77d3f526d9c3 phase residency_inventory end status=ok seconds=0.000009
INFO:lcm:solve call 77d3f526d9c3 phase residency_inventory end status=ok seconds=0.000009
solve call 77d3f526d9c3 phase compilation_waves begin
INFO:lcm:solve call 77d3f526d9c3 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 487.8ms
INFO:lcm: lowered in 487.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 377.6ms
INFO:lcm: lowered in 377.6ms
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 95.0ms
INFO:lcm: lowered in 95.0ms
compiled working_life main (age 25, widths={'action_product': 64, 'cell': 16}) 1.1s
INFO:lcm: compiled working_life main (age 25, widths={'action_product': 64, 'cell': 16}) 1.1s
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}) 279.4ms
INFO:lcm: compiled retirement main (age 45, widths={'cell': 16}) 279.4ms
compiled working_life main (age 45, widths={'action_product': 64, 'cell': 16}) 1.0s
INFO:lcm: compiled working_life main (age 45, widths={'action_product': 64, 'cell': 16}) 1.0s
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 77d3f526d9c3 phase compilation_waves end status=ok seconds=1.914433
INFO:lcm:solve call 77d3f526d9c3 phase compilation_waves end status=ok seconds=1.914433
solve call 77d3f526d9c3 phase workspace_selection begin
INFO:lcm:solve call 77d3f526d9c3 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 77d3f526d9c3 phase workspace_selection end status=ok seconds=0.026661
INFO:lcm:solve call 77d3f526d9c3 phase workspace_selection end status=ok seconds=0.026661
Starting solution
INFO:lcm:Starting solution
solve call 77d3f526d9c3 phase backward_induction begin
INFO:lcm:solve call 77d3f526d9c3 phase backward_induction begin
Age 65 (1 regimes):
INFO:lcm:Age 65 (1 regimes):
finished in 181.1ms
INFO:lcm: finished in 181.1ms
Age 45 (2 regimes):
INFO:lcm:Age 45 (2 regimes):
finished in 33.0ms
INFO:lcm: finished in 33.0ms
Age 25 (1 regimes):
INFO:lcm:Age 25 (1 regimes):
finished in 5.2ms
INFO:lcm: finished in 5.2ms
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 77d3f526d9c3 phase backward_induction end status=ok seconds=0.252443
INFO:lcm:solve call 77d3f526d9c3 phase backward_induction end status=ok seconds=0.252443
Solution complete (254.2ms)
INFO:lcm:Solution complete (254.2ms)
solve call 77d3f526d9c3 phase result_assembly begin
INFO:lcm:solve call 77d3f526d9c3 phase result_assembly begin
solve call 77d3f526d9c3 phase result_assembly end status=ok seconds=0.005421
INFO:lcm:solve call 77d3f526d9c3 phase result_assembly end status=ok seconds=0.005421
solve call 77d3f526d9c3 phase solution_resolution begin
INFO:lcm:solve call 77d3f526d9c3 phase solution_resolution begin
solve call 77d3f526d9c3 phase solution_resolution end status=ok seconds=0.002123
INFO:lcm:solve call 77d3f526d9c3 phase solution_resolution end status=ok seconds=0.002123
solve call 77d3f526d9c3 phase solution_handover begin
INFO:lcm:solve call 77d3f526d9c3 phase solution_handover begin
solve call 77d3f526d9c3 phase solution_handover end status=ok seconds=0.000004
INFO:lcm:solve call 77d3f526d9c3 phase solution_handover end status=ok seconds=0.000004
solve call 77d3f526d9c3 phase chunk_planning begin
INFO:lcm:solve call 77d3f526d9c3 phase chunk_planning begin
solve call 77d3f526d9c3 phase chunk_planning end status=ok seconds=0.000447
INFO:lcm:solve call 77d3f526d9c3 phase chunk_planning end status=ok seconds=0.000447
solve call 77d3f526d9c3 phase simulation_setup begin
INFO:lcm:solve call 77d3f526d9c3 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 77d3f526d9c3 phase simulation_setup end status=ok seconds=0.342392
INFO:lcm:solve call 77d3f526d9c3 phase simulation_setup end status=ok seconds=0.342392
solve call 77d3f526d9c3 phase simulation_chunk begin
INFO:lcm:solve call 77d3f526d9c3 phase simulation_chunk begin
Age 25 (1 regimes):
INFO:lcm:Age 25 (1 regimes):
solve call 77d3f526d9c3 phase simulation_compilation begin
INFO:lcm:solve call 77d3f526d9c3 phase simulation_compilation begin
solve call 77d3f526d9c3 phase simulation_compilation end status=ok seconds=0.671200
INFO:lcm:solve call 77d3f526d9c3 phase simulation_compilation end status=ok seconds=0.671200
solve call 77d3f526d9c3 phase simulation_compilation begin
INFO:lcm:solve call 77d3f526d9c3 phase simulation_compilation begin
solve call 77d3f526d9c3 phase simulation_compilation end status=ok seconds=0.063591
INFO:lcm:solve call 77d3f526d9c3 phase simulation_compilation end status=ok seconds=0.063591
solve call 77d3f526d9c3 phase simulation_compilation begin
INFO:lcm:solve call 77d3f526d9c3 phase simulation_compilation begin
solve call 77d3f526d9c3 phase simulation_compilation end status=ok seconds=0.046458
INFO:lcm:solve call 77d3f526d9c3 phase simulation_compilation end status=ok seconds=0.046458
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.7s
INFO:lcm: finished in 2.7s
Age 45 (2 regimes):
INFO:lcm:Age 45 (2 regimes):
solve call 77d3f526d9c3 phase simulation_compilation begin
INFO:lcm:solve call 77d3f526d9c3 phase simulation_compilation begin
solve call 77d3f526d9c3 phase simulation_compilation end status=ok seconds=0.429639
INFO:lcm:solve call 77d3f526d9c3 phase simulation_compilation end status=ok seconds=0.429639
solve call 77d3f526d9c3 phase simulation_compilation begin
INFO:lcm:solve call 77d3f526d9c3 phase simulation_compilation begin
solve call 77d3f526d9c3 phase simulation_compilation end status=ok seconds=0.046914
INFO:lcm:solve call 77d3f526d9c3 phase simulation_compilation end status=ok seconds=0.046914
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 77d3f526d9c3 phase simulation_compilation begin
INFO:lcm:solve call 77d3f526d9c3 phase simulation_compilation begin
solve call 77d3f526d9c3 phase simulation_compilation end status=ok seconds=0.054314
INFO:lcm:solve call 77d3f526d9c3 phase simulation_compilation end status=ok seconds=0.054314
transitions:
- working_life → retirement = 100
DEBUG:lcm: transitions:
- working_life → retirement = 100
finished in 1.0s
INFO:lcm: finished in 1.0s
Age 65 (1 regimes):
INFO:lcm:Age 65 (1 regimes):
transitions:
- retirement → retirement = 100
DEBUG:lcm: transitions:
- retirement → retirement = 100
finished in 87.1ms
INFO:lcm: finished in 87.1ms
solve call 77d3f526d9c3 phase simulation_chunk end status=ok seconds=3.858886
INFO:lcm:solve call 77d3f526d9c3 phase simulation_chunk end status=ok seconds=3.858886
solve call 77d3f526d9c3 phase simulation_completion begin
INFO:lcm:solve call 77d3f526d9c3 phase simulation_completion begin
Simulation complete (4.2s)
INFO:lcm:Simulation complete (4.2s)
solve call 77d3f526d9c3 phase simulation_completion end status=ok seconds=0.001432
INFO:lcm:solve call 77d3f526d9c3 phase simulation_completion end status=ok seconds=0.001432
solve call 77d3f526d9c3 phase simulation_result begin
INFO:lcm:solve call 77d3f526d9c3 phase simulation_result begin
solve call 77d3f526d9c3 phase simulation_result end status=ok seconds=0.007977
INFO:lcm:solve call 77d3f526d9c3 phase simulation_result end status=ok seconds=0.007977
solve call 77d3f526d9c3 phase result_finalization begin
INFO:lcm:solve call 77d3f526d9c3 phase result_finalization begin
solve call 77d3f526d9c3 phase result_finalization end status=ok seconds=0.000008
INFO:lcm:solve call 77d3f526d9c3 phase result_finalization end status=ok seconds=0.000008
solve call 77d3f526d9c3 phase public_simulate end status=ok seconds=7.184555
INFO:lcm:solve call 77d3f526d9c3 phase public_simulate end status=ok seconds=7.184555
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()