This notebook shows how to implement beta-delta discounting in PyLCM. The naive
agent needs a phase-variant Koopmans aggregator, a custom declared via
Phased(solve=..., simulate=...); the sophisticated and partially naive agents
choose with one aggregator and store the value of another, declared via
Decoupled(objective=..., value=...). It covers:
The beta-delta discount function
Why naive agents need different for solve vs. simulate
A 3-period consumption-savings model with analytical solutions
Verifying numerical results against closed-form solutions
Sophisticated and partially naive agents with
Decoupled
Background: Beta-Delta Discounting¶
Standard exponential discounting uses a single discount factor to weight future utility:
Quasi-hyperbolic (beta-delta) discounting (Laibson, 1997) introduces a present-bias parameter that discounts all future periods relative to the present:
The discount weights are . When this reduces to exponential discounting. When the agent is present-biased — they overweight current utility relative to any future period.
Note the key structure: appears once (not compounded). The weight periods ahead is , not .
PyLCM’s Koopmans Aggregator and the Naive Agent¶
PyLCM defines the value function recursively via an aggregation function :
The default is .
A naive beta-delta agent believes their future selves will be exponential discounters, but at each decision point applies present bias to the continuation value. This requires two different implementations:
Solve phase (backward induction): use exponential discounting with to compute the perceived continuation values
Simulate phase (action selection): use to choose actions, applying present bias to the perceived
Phase is a broadcast dimension of the regime spec: a bare function applies to
both phases, while Phased(solve=..., simulate=...) gives each phase its own
implementation. Declaring koopmans_aggregator as
Phased(solve=LinearAggregator(), simulate=linear_beta_delta) is all the naive
agent needs — the params template becomes the union of both variants’
parameters, so one params dict serves both phases.
Why not just use for both phases? Because the recursion would compound at every step, giving effective weights instead of the correct . Splitting the phases ensures is applied exactly once — at the action selection step.
Setup: A 3-Period Model¶
We use a minimal consumption-savings model:
3 periods: (decisions), (terminal)
1 state: wealth
1 action: consumption
Utility:
Budget: (interest rate )
Constraint:
Terminal: (consume everything)
import jax.numpy as jnp
from lcm import (
AgeGrid,
LinSpacedGrid,
Model,
Phased,
Regime,
categorical,
)
from lcm.koopmans_aggregation import LinearAggregator
from lcm.typing import BoolND, ContinuousAction, ContinuousState, FloatND, ScalarInt@categorical(ordered=False)
class RegimeId:
working: ScalarInt
dead: ScalarInt
def utility(consumption: ContinuousAction) -> FloatND:
return jnp.log(consumption)
def terminal_utility(wealth: ContinuousState) -> FloatND:
return jnp.log(wealth)
def next_wealth(
*, wealth: ContinuousState, consumption: ContinuousAction
) -> ContinuousState:
return wealth - consumption
def borrowing_constraint(
*, consumption: ContinuousAction, wealth: ContinuousState
) -> BoolND:
return consumption <= wealth
def linear_beta_delta(
*,
utility: float,
CE: float,
beta: float,
delta: float,
) -> float:
return utility + beta * delta * CEWe build two models:
One with the shipped
LinearAggregatorfor the standard exponential agentOne with a phase-variant for the naive beta-delta agent — solving with exponential and simulating with beta-delta
WEALTH_GRID = LinSpacedGrid(start=0.5, stop=50, n_points=200)
CONSUMPTION_GRID = LinSpacedGrid(start=0.001, stop=50, n_points=500)
dead = Regime(
states={"wealth": WEALTH_GRID},
functions={"utility": terminal_utility},
)
# Standard exponential model
working_exp = Regime(
actions={"consumption": CONSUMPTION_GRID},
states={"wealth": WEALTH_GRID},
state_transitions={"wealth": next_wealth},
constraints={"borrowing_constraint": borrowing_constraint},
functions={"utility": utility},
koopmans_aggregator=LinearAggregator(),
)
# Work at ages 0 and 1, then die. One edge per age, so the graph is the whole
# transition law; `dead` has no outgoing edges and is terminal.
edges = {"working": {"working": 0, "dead": 1}}
model_exp = Model(
regimes={"working": working_exp, "dead": dead},
ages=AgeGrid(start=0, inclusive_stop=2, step="Y"),
edges=edges,
initial_nodes=((0, "working"),),
regime_id_class=RegimeId,
)
# Naive beta-delta model (a different aggregator per phase)
working_naive = Regime(
actions={"consumption": CONSUMPTION_GRID},
states={"wealth": WEALTH_GRID},
state_transitions={"wealth": next_wealth},
constraints={"borrowing_constraint": borrowing_constraint},
functions={"utility": utility},
koopmans_aggregator=Phased(
solve=LinearAggregator(),
simulate=linear_beta_delta,
),
)
model_naive = Model(
regimes={"working": working_naive, "dead": dead},
ages=AgeGrid(start=0, inclusive_stop=2, step="Y"),
edges=edges,
initial_nodes=((0, "working"),),
regime_id_class=RegimeId,
)Analytical Solution¶
With utility, the optimal consumption rule is , where is a “marginal propensity to save” denominator.
At (one period before terminal), the agent maximizes:
First-order condition: , giving .
At , the naive agent uses the perceived exponential continuation value , which has coefficient on . So the agent maximizes:
giving .
| Exponential () | ||
| Naive () |
BETA = 0.7
DELTA = 0.95
W0 = 20.0
def analytical_consumption(*, beta, delta, w0):
"""Return (c0, c1) from the closed-form solution."""
bd = beta * delta
d1 = 1 + bd
d0 = 1 + bd * (1 + delta)
c0 = w0 / d0
c1 = (w0 - c0) / d1
return c0, c1
c0_exp, c1_exp = analytical_consumption(beta=1.0, delta=DELTA, w0=W0)
c0_naive, c1_naive = analytical_consumption(beta=BETA, delta=DELTA, w0=W0)
print(f"{'Type':<15} {'c_0':>8} {'c_1':>8}")
print("-" * 33)
print(f"{'Exponential':<15} {c0_exp:8.4f} {c1_exp:8.4f}")
print(f"{'Naive':<15} {c0_naive:8.4f} {c1_naive:8.4f}")Type c_0 c_1
---------------------------------
Exponential 7.0114 6.6608
Naive 8.7080 6.7820
The naive agent consumes more at than the exponential agent — present bias pulls consumption forward. At , the naive agent also consumes more because .
initial_conditions = {
"age": jnp.array([0.0]),
"wealth": jnp.array([W0]),
"regime_id": jnp.array([RegimeId.working]),
}
result_exp = model_exp.simulate(
params={"working": {"koopmans_aggregator": {"discount_factor": DELTA}}},
initial_conditions=initial_conditions,
log_level="debug",
)
df_exp = result_exp.to_dataframe().query('regime_name == "working"')
print("Exponential agent:")
print(
f" c_0 = {df_exp.loc[df_exp['age'] == 0, 'consumption'].iloc[0]:.4f} "
f"(analytical: {c0_exp:.4f})"
)
print(
f" c_1 = {df_exp.loc[df_exp['age'] == 1, 'consumption'].iloc[0]:.4f} "
f"(analytical: {c1_exp:.4f})"
)solve call a521f0b998cb phase public_simulate begin
INFO:lcm:solve call a521f0b998cb phase public_simulate begin
solve call a521f0b998cb phase params_validation begin
INFO:lcm:solve call a521f0b998cb phase params_validation begin
solve call a521f0b998cb phase params_validation end status=ok seconds=0.066252
INFO:lcm:solve call a521f0b998cb phase params_validation end status=ok seconds=0.066252
solve call a521f0b998cb phase simulation_inputs begin
INFO:lcm:solve call a521f0b998cb phase simulation_inputs begin
solve call a521f0b998cb phase simulation_inputs end status=ok seconds=0.112160
INFO:lcm:solve call a521f0b998cb phase simulation_inputs end status=ok seconds=0.112160
solve call a521f0b998cb phase authority_fingerprint begin
INFO:lcm:solve call a521f0b998cb phase authority_fingerprint begin
solve call a521f0b998cb phase authority_fingerprint end status=ok seconds=0.004193
INFO:lcm:solve call a521f0b998cb phase authority_fingerprint end status=ok seconds=0.004193
solve call a521f0b998cb phase solver_param_checks begin
INFO:lcm:solve call a521f0b998cb phase solver_param_checks begin
solve call a521f0b998cb phase solver_param_checks end status=ok seconds=0.000036
INFO:lcm:solve call a521f0b998cb phase solver_param_checks end status=ok seconds=0.000036
solve call a521f0b998cb phase state_action_spaces begin
INFO:lcm:solve call a521f0b998cb phase state_action_spaces begin
solve call a521f0b998cb phase state_action_spaces end status=ok seconds=0.000194
INFO:lcm:solve call a521f0b998cb phase state_action_spaces end status=ok seconds=0.000194
solve call a521f0b998cb phase continuation_templates begin
INFO:lcm:solve call a521f0b998cb phase continuation_templates begin
solve call a521f0b998cb phase continuation_templates end status=ok seconds=0.001171
INFO:lcm:solve call a521f0b998cb phase continuation_templates end status=ok seconds=0.001171
solve call a521f0b998cb phase program_graphs begin
INFO:lcm:solve call a521f0b998cb phase program_graphs begin
solve call a521f0b998cb phase program_graphs end status=ok seconds=0.000499
INFO:lcm:solve call a521f0b998cb phase program_graphs end status=ok seconds=0.000499
solve call a521f0b998cb phase structural_resolution begin
INFO:lcm:solve call a521f0b998cb 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 a521f0b998cb phase structural_resolution end status=ok seconds=0.032555
INFO:lcm:solve call a521f0b998cb phase structural_resolution end status=ok seconds=0.032555
solve call a521f0b998cb phase residency_inventory begin
INFO:lcm:solve call a521f0b998cb phase residency_inventory begin
solve call a521f0b998cb phase residency_inventory end status=ok seconds=0.000335
INFO:lcm:solve call a521f0b998cb phase residency_inventory end status=ok seconds=0.000335
solve call a521f0b998cb phase compilation_waves begin
INFO:lcm:solve call a521f0b998cb phase compilation_waves begin
candidate evaluation ('working', 0, 'main') {'action_product': 64, 'cell': 128}
DEBUG:lcm:candidate evaluation ('working', 0, 'main') {'action_product': 64, 'cell': 128}
candidate evaluation ('working', 1, 'main') {'action_product': 64, 'cell': 128}
DEBUG:lcm:candidate evaluation ('working', 1, 'main') {'action_product': 64, 'cell': 128}
candidate evaluation ('dead', 2, 'main') {'cell': 128}
DEBUG:lcm:candidate evaluation ('dead', 2, 'main') {'cell': 128}
AOT compilation wave 0: 3 unique lowerings for 3 regime-period-core triples (2 workers)
INFO:lcm:AOT compilation wave 0: 3 unique lowerings for 3 regime-period-core triples (2 workers)
1/3 working main (age 0, widths={'action_product': 64, 'cell': 128})
INFO:lcm:1/3 working main (age 0, widths={'action_product': 64, 'cell': 128})
lowering working main (age 0, widths={'action_product': 64, 'cell': 128}) ...
INFO:lcm: lowering working main (age 0, widths={'action_product': 64, 'cell': 128}) ...
lowered in 448.5ms
INFO:lcm: lowered in 448.5ms
compiling working main (age 0, widths={'action_product': 64, 'cell': 128}) ...
INFO:lcm: compiling working main (age 0, widths={'action_product': 64, 'cell': 128}) ...
2/3 working main (age 1, widths={'action_product': 64, 'cell': 128})
INFO:lcm:2/3 working main (age 1, widths={'action_product': 64, 'cell': 128})
lowering working main (age 1, widths={'action_product': 64, 'cell': 128}) ...
INFO:lcm: lowering working main (age 1, widths={'action_product': 64, 'cell': 128}) ...
lowered in 283.1ms
INFO:lcm: lowered in 283.1ms
compiling working main (age 1, widths={'action_product': 64, 'cell': 128}) ...
INFO:lcm: compiling working main (age 1, widths={'action_product': 64, 'cell': 128}) ...
3/3 dead main (age 2, widths={'cell': 128})
INFO:lcm:3/3 dead main (age 2, widths={'cell': 128})
lowering dead main (age 2, widths={'cell': 128}) ...
INFO:lcm: lowering dead main (age 2, widths={'cell': 128}) ...
lowered in 48.4ms
INFO:lcm: lowered in 48.4ms
compiled working main (age 0, widths={'action_product': 64, 'cell': 128}) 828.0ms
INFO:lcm: compiled working main (age 0, widths={'action_product': 64, 'cell': 128}) 828.0ms
compiling dead main (age 2, widths={'cell': 128}) ...
INFO:lcm: compiling dead main (age 2, widths={'cell': 128}) ...
compiled dead main (age 2, widths={'cell': 128}) 185.5ms
INFO:lcm: compiled dead main (age 2, widths={'cell': 128}) 185.5ms
compiled working main (age 1, widths={'action_product': 64, 'cell': 128}) 785.4ms
INFO:lcm: compiled working main (age 1, widths={'action_product': 64, 'cell': 128}) 785.4ms
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 a521f0b998cb phase compilation_waves end status=ok seconds=1.557574
INFO:lcm:solve call a521f0b998cb phase compilation_waves end status=ok seconds=1.557574
solve call a521f0b998cb phase workspace_selection begin
INFO:lcm:solve call a521f0b998cb phase workspace_selection begin
core plan record {"active_replica_bytes":0,"axis_extents":{"action_product":500,"cell":200},"compiled_collectives":{},"compiler_peak_bytes":4523,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"action_product":"streamed","cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[200],"period":0,"period_transfer_scratch_bytes":{},"physical_value_shape":[200],"regime":"working","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":200},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"action_product":64,"cell":128}}
DEBUG:lcm:core plan record {"active_replica_bytes":0,"axis_extents":{"action_product":500,"cell":200},"compiled_collectives":{},"compiler_peak_bytes":4523,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"action_product":"streamed","cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[200],"period":0,"period_transfer_scratch_bytes":{},"physical_value_shape":[200],"regime":"working","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":200},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"action_product":64,"cell":128}}
core plan record {"active_replica_bytes":0,"axis_extents":{"action_product":500,"cell":200},"compiled_collectives":{},"compiler_peak_bytes":4523,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"action_product":"streamed","cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[200],"period":1,"period_transfer_scratch_bytes":{},"physical_value_shape":[200],"regime":"working","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":200},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"action_product":64,"cell":128}}
DEBUG:lcm:core plan record {"active_replica_bytes":0,"axis_extents":{"action_product":500,"cell":200},"compiled_collectives":{},"compiler_peak_bytes":4523,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"action_product":"streamed","cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[200],"period":1,"period_transfer_scratch_bytes":{},"physical_value_shape":[200],"regime":"working","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":200},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"action_product":64,"cell":128}}
core plan record {"active_replica_bytes":0,"axis_extents":{"cell":200},"compiled_collectives":{},"compiler_peak_bytes":1636,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[200],"period":2,"period_transfer_scratch_bytes":{},"physical_value_shape":[200],"regime":"dead","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":200},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"cell":128}}
DEBUG:lcm:core plan record {"active_replica_bytes":0,"axis_extents":{"cell":200},"compiled_collectives":{},"compiler_peak_bytes":1636,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[200],"period":2,"period_transfer_scratch_bytes":{},"physical_value_shape":[200],"regime":"dead","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":200},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"cell":128}}
solve call a521f0b998cb phase workspace_selection end status=ok seconds=0.024925
INFO:lcm:solve call a521f0b998cb phase workspace_selection end status=ok seconds=0.024925
Starting solution
INFO:lcm:Starting solution
solve call a521f0b998cb phase backward_induction begin
INFO:lcm:solve call a521f0b998cb phase backward_induction begin
Age 2 (1 regimes):
INFO:lcm:Age 2 (1 regimes):
finished in 204.0ms
INFO:lcm: finished in 204.0ms
Age 1 (1 regimes):
INFO:lcm:Age 1 (1 regimes):
finished in 26.0ms
INFO:lcm: finished in 26.0ms
Age 0 (1 regimes):
INFO:lcm:Age 0 (1 regimes):
finished in 5.9ms
INFO:lcm: finished in 5.9ms
dead age 2.0 V min=-0.693 max=3.91 mean=2.95
DEBUG:lcm: dead age 2.0 V min=-0.693 max=3.91 mean=2.95
working age 1.0 V min=-2.19 max=6.28 mean=4.41
DEBUG:lcm: working age 1.0 V min=-2.19 max=6.28 mean=4.41
working age 0.0 V min=-3.73 max=8.03 mean=5.3
DEBUG:lcm: working age 0.0 V min=-3.73 max=8.03 mean=5.3
solve call a521f0b998cb phase backward_induction end status=ok seconds=0.259792
INFO:lcm:solve call a521f0b998cb phase backward_induction end status=ok seconds=0.259792
Solution complete (261.8ms)
INFO:lcm:Solution complete (261.8ms)
solve call a521f0b998cb phase result_assembly begin
INFO:lcm:solve call a521f0b998cb phase result_assembly begin
solve call a521f0b998cb phase result_assembly end status=ok seconds=0.004334
INFO:lcm:solve call a521f0b998cb phase result_assembly end status=ok seconds=0.004334
solve call a521f0b998cb phase solution_resolution begin
INFO:lcm:solve call a521f0b998cb phase solution_resolution begin
solve call a521f0b998cb phase solution_resolution end status=ok seconds=0.001874
INFO:lcm:solve call a521f0b998cb phase solution_resolution end status=ok seconds=0.001874
solve call a521f0b998cb phase solution_handover begin
INFO:lcm:solve call a521f0b998cb phase solution_handover begin
solve call a521f0b998cb phase solution_handover end status=ok seconds=0.000004
INFO:lcm:solve call a521f0b998cb phase solution_handover end status=ok seconds=0.000004
solve call a521f0b998cb phase chunk_planning begin
INFO:lcm:solve call a521f0b998cb phase chunk_planning begin
solve call a521f0b998cb phase chunk_planning end status=ok seconds=0.000383
INFO:lcm:solve call a521f0b998cb phase chunk_planning end status=ok seconds=0.000383
solve call a521f0b998cb phase simulation_setup begin
INFO:lcm:solve call a521f0b998cb phase simulation_setup begin
Starting simulation
INFO:lcm:Starting simulation
Simulation plan: route=legacy; 1 cpu device(s) (0,); outer chunks=1 (widths=(1,)); unbudgeted.
INFO:lcm:Simulation plan: route=legacy; 1 cpu device(s) (0,); outer chunks=1 (widths=(1,)); unbudgeted.
Simulation plan detail: route=legacy; subject_devices=(cpu) (0,); axis_widths_by_regime=[working: {}; dead: {}]; outer_chunk_count=1; admitted_chunk_widths=(1,); 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: {}; dead: {}]; outer_chunk_count=1; admitted_chunk_widths=(1,); budget_mode=unbudgeted; effective_device_memory_bytes=None; subject_grouping=None.
solve call a521f0b998cb phase simulation_setup end status=ok seconds=0.104077
INFO:lcm:solve call a521f0b998cb phase simulation_setup end status=ok seconds=0.104077
solve call a521f0b998cb phase simulation_chunk begin
INFO:lcm:solve call a521f0b998cb phase simulation_chunk begin
Age 0 (1 regimes):
INFO:lcm:Age 0 (1 regimes):
solve call a521f0b998cb phase simulation_compilation begin
INFO:lcm:solve call a521f0b998cb phase simulation_compilation begin
solve call a521f0b998cb phase simulation_compilation end status=ok seconds=0.389969
INFO:lcm:solve call a521f0b998cb phase simulation_compilation end status=ok seconds=0.389969
solve call a521f0b998cb phase simulation_compilation begin
INFO:lcm:solve call a521f0b998cb phase simulation_compilation begin
solve call a521f0b998cb phase simulation_compilation end status=ok seconds=0.025406
INFO:lcm:solve call a521f0b998cb phase simulation_compilation end status=ok seconds=0.025406
solve call a521f0b998cb phase simulation_compilation begin
INFO:lcm:solve call a521f0b998cb phase simulation_compilation begin
solve call a521f0b998cb phase simulation_compilation end status=ok seconds=0.034788
INFO:lcm:solve call a521f0b998cb phase simulation_compilation end status=ok seconds=0.034788
Off-grid next states: regime='working', target='working', period=0, state='wealth', phase='realized', count=0
INFO:lcm:Off-grid next states: regime='working', target='working', period=0, state='wealth', phase='realized', count=0
transitions:
- working → working = 1
DEBUG:lcm: transitions:
- working → working = 1
finished in 862.6ms
INFO:lcm: finished in 862.6ms
Age 1 (1 regimes):
INFO:lcm:Age 1 (1 regimes):
solve call a521f0b998cb phase simulation_compilation begin
INFO:lcm:solve call a521f0b998cb phase simulation_compilation begin
solve call a521f0b998cb phase simulation_compilation end status=ok seconds=0.302570
INFO:lcm:solve call a521f0b998cb phase simulation_compilation end status=ok seconds=0.302570
solve call a521f0b998cb phase simulation_compilation begin
INFO:lcm:solve call a521f0b998cb phase simulation_compilation begin
solve call a521f0b998cb phase simulation_compilation end status=ok seconds=0.026522
INFO:lcm:solve call a521f0b998cb phase simulation_compilation end status=ok seconds=0.026522
Off-grid next states: regime='working', target='dead', period=1, state='wealth', phase='realized', count=0
INFO:lcm:Off-grid next states: regime='working', target='dead', period=1, state='wealth', phase='realized', count=0
transitions:
- working → dead = 1
DEBUG:lcm: transitions:
- working → dead = 1
finished in 429.3ms
INFO:lcm: finished in 429.3ms
Age 2 (1 regimes):
INFO:lcm:Age 2 (1 regimes):
solve call a521f0b998cb phase simulation_compilation begin
INFO:lcm:solve call a521f0b998cb phase simulation_compilation begin
solve call a521f0b998cb phase simulation_compilation end status=ok seconds=0.031791
INFO:lcm:solve call a521f0b998cb phase simulation_compilation end status=ok seconds=0.031791
transitions:
- dead → dead = 1
DEBUG:lcm: transitions:
- dead → dead = 1
finished in 42.3ms
INFO:lcm: finished in 42.3ms
solve call a521f0b998cb phase simulation_chunk end status=ok seconds=1.346742
INFO:lcm:solve call a521f0b998cb phase simulation_chunk end status=ok seconds=1.346742
solve call a521f0b998cb phase simulation_completion begin
INFO:lcm:solve call a521f0b998cb phase simulation_completion begin
Simulation complete (1.5s)
INFO:lcm:Simulation complete (1.5s)
solve call a521f0b998cb phase simulation_completion end status=ok seconds=0.000798
INFO:lcm:solve call a521f0b998cb phase simulation_completion end status=ok seconds=0.000798
solve call a521f0b998cb phase simulation_result begin
INFO:lcm:solve call a521f0b998cb phase simulation_result begin
solve call a521f0b998cb phase simulation_result end status=ok seconds=0.005843
INFO:lcm:solve call a521f0b998cb phase simulation_result end status=ok seconds=0.005843
solve call a521f0b998cb phase result_finalization begin
INFO:lcm:solve call a521f0b998cb phase result_finalization begin
solve call a521f0b998cb phase result_finalization end status=ok seconds=0.000033
INFO:lcm:solve call a521f0b998cb phase result_finalization end status=ok seconds=0.000033
solve call a521f0b998cb phase public_simulate end status=ok seconds=3.572377
INFO:lcm:solve call a521f0b998cb phase public_simulate end status=ok seconds=3.572377
Exponential agent:
c_0 = 7.0149 (analytical: 7.0114)
c_1 = 6.7143 (analytical: 6.6608)
Naive Agent¶
The naive agent uses model_naive, whose is phase-variant. During backward
induction, the solve variant (LinearAggregator) computes the perceived value
function ; during simulation, the simulate variant (linear_beta_delta)
applies present bias for action selection.
The params dict is the union of both variants’ parameters — discount_factor
for LinearAggregator, plus beta and delta for linear_beta_delta. Each variant
receives only the kwargs it expects.
result_naive = model_naive.simulate(
params={
"working": {
"koopmans_aggregator": {
"discount_factor": DELTA,
"beta": BETA,
"delta": DELTA,
},
},
},
initial_conditions=initial_conditions,
log_level="debug",
)
df_naive = result_naive.to_dataframe().query('regime_name == "working"')
print("Naive agent:")
print(
f" c_0 = {df_naive.loc[df_naive['age'] == 0, 'consumption'].iloc[0]:.4f} "
f"(analytical: {c0_naive:.4f})"
)
print(
f" c_1 = {df_naive.loc[df_naive['age'] == 1, 'consumption'].iloc[0]:.4f} "
f"(analytical: {c1_naive:.4f})"
)solve call 5fc1a637d041 phase public_simulate begin
INFO:lcm:solve call 5fc1a637d041 phase public_simulate begin
solve call 5fc1a637d041 phase params_validation begin
INFO:lcm:solve call 5fc1a637d041 phase params_validation begin
solve call 5fc1a637d041 phase params_validation end status=ok seconds=0.015690
INFO:lcm:solve call 5fc1a637d041 phase params_validation end status=ok seconds=0.015690
solve call 5fc1a637d041 phase simulation_inputs begin
INFO:lcm:solve call 5fc1a637d041 phase simulation_inputs begin
solve call 5fc1a637d041 phase simulation_inputs end status=ok seconds=0.005430
INFO:lcm:solve call 5fc1a637d041 phase simulation_inputs end status=ok seconds=0.005430
solve call 5fc1a637d041 phase authority_fingerprint begin
INFO:lcm:solve call 5fc1a637d041 phase authority_fingerprint begin
solve call 5fc1a637d041 phase authority_fingerprint end status=ok seconds=0.001785
INFO:lcm:solve call 5fc1a637d041 phase authority_fingerprint end status=ok seconds=0.001785
solve call 5fc1a637d041 phase solver_param_checks begin
INFO:lcm:solve call 5fc1a637d041 phase solver_param_checks begin
solve call 5fc1a637d041 phase solver_param_checks end status=ok seconds=0.000036
INFO:lcm:solve call 5fc1a637d041 phase solver_param_checks end status=ok seconds=0.000036
solve call 5fc1a637d041 phase state_action_spaces begin
INFO:lcm:solve call 5fc1a637d041 phase state_action_spaces begin
solve call 5fc1a637d041 phase state_action_spaces end status=ok seconds=0.000196
INFO:lcm:solve call 5fc1a637d041 phase state_action_spaces end status=ok seconds=0.000196
solve call 5fc1a637d041 phase continuation_templates begin
INFO:lcm:solve call 5fc1a637d041 phase continuation_templates begin
solve call 5fc1a637d041 phase continuation_templates end status=ok seconds=0.000327
INFO:lcm:solve call 5fc1a637d041 phase continuation_templates end status=ok seconds=0.000327
solve call 5fc1a637d041 phase program_graphs begin
INFO:lcm:solve call 5fc1a637d041 phase program_graphs begin
solve call 5fc1a637d041 phase program_graphs end status=ok seconds=0.000269
INFO:lcm:solve call 5fc1a637d041 phase program_graphs end status=ok seconds=0.000269
solve call 5fc1a637d041 phase structural_resolution begin
INFO:lcm:solve call 5fc1a637d041 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 5fc1a637d041 phase structural_resolution end status=ok seconds=0.005566
INFO:lcm:solve call 5fc1a637d041 phase structural_resolution end status=ok seconds=0.005566
solve call 5fc1a637d041 phase residency_inventory begin
INFO:lcm:solve call 5fc1a637d041 phase residency_inventory begin
solve call 5fc1a637d041 phase residency_inventory end status=ok seconds=0.000024
INFO:lcm:solve call 5fc1a637d041 phase residency_inventory end status=ok seconds=0.000024
solve call 5fc1a637d041 phase compilation_waves begin
INFO:lcm:solve call 5fc1a637d041 phase compilation_waves begin
candidate evaluation ('working', 0, 'main') {'action_product': 64, 'cell': 128}
DEBUG:lcm:candidate evaluation ('working', 0, 'main') {'action_product': 64, 'cell': 128}
candidate evaluation ('working', 1, 'main') {'action_product': 64, 'cell': 128}
DEBUG:lcm:candidate evaluation ('working', 1, 'main') {'action_product': 64, 'cell': 128}
candidate evaluation ('dead', 2, 'main') {'cell': 128}
DEBUG:lcm:candidate evaluation ('dead', 2, 'main') {'cell': 128}
AOT compilation wave 0: 3 unique lowerings for 3 regime-period-core triples (2 workers)
INFO:lcm:AOT compilation wave 0: 3 unique lowerings for 3 regime-period-core triples (2 workers)
1/3 working main (age 0, widths={'action_product': 64, 'cell': 128})
INFO:lcm:1/3 working main (age 0, widths={'action_product': 64, 'cell': 128})
lowering working main (age 0, widths={'action_product': 64, 'cell': 128}) ...
INFO:lcm: lowering working main (age 0, widths={'action_product': 64, 'cell': 128}) ...
lowered in 185.3ms
INFO:lcm: lowered in 185.3ms
compiling working main (age 0, widths={'action_product': 64, 'cell': 128}) ...
INFO:lcm: compiling working main (age 0, widths={'action_product': 64, 'cell': 128}) ...
2/3 working main (age 1, widths={'action_product': 64, 'cell': 128})
INFO:lcm:2/3 working main (age 1, widths={'action_product': 64, 'cell': 128})
lowering working main (age 1, widths={'action_product': 64, 'cell': 128}) ...
INFO:lcm: lowering working main (age 1, widths={'action_product': 64, 'cell': 128}) ...
compiled working main (age 0, widths={'action_product': 64, 'cell': 128}) 87.4ms
INFO:lcm: compiled working main (age 0, widths={'action_product': 64, 'cell': 128}) 87.4ms
lowered in 198.0ms
INFO:lcm: lowered in 198.0ms
compiling working main (age 1, widths={'action_product': 64, 'cell': 128}) ...
INFO:lcm: compiling working main (age 1, widths={'action_product': 64, 'cell': 128}) ...
3/3 dead main (age 2, widths={'cell': 128})
INFO:lcm:3/3 dead main (age 2, widths={'cell': 128})
lowering dead main (age 2, widths={'cell': 128}) ...
INFO:lcm: lowering dead main (age 2, widths={'cell': 128}) ...
lowered in 27.9ms
INFO:lcm: lowered in 27.9ms
compiling dead main (age 2, widths={'cell': 128}) ...
INFO:lcm: compiling dead main (age 2, widths={'cell': 128}) ...
compiled dead main (age 2, widths={'cell': 128}) 5.4ms
INFO:lcm: compiled dead main (age 2, widths={'cell': 128}) 5.4ms
compiled working main (age 1, widths={'action_product': 64, 'cell': 128}) 51.0ms
INFO:lcm: compiled working main (age 1, widths={'action_product': 64, 'cell': 128}) 51.0ms
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 5fc1a637d041 phase compilation_waves end status=ok seconds=0.452543
INFO:lcm:solve call 5fc1a637d041 phase compilation_waves end status=ok seconds=0.452543
solve call 5fc1a637d041 phase workspace_selection begin
INFO:lcm:solve call 5fc1a637d041 phase workspace_selection begin
core plan record {"active_replica_bytes":0,"axis_extents":{"action_product":500,"cell":200},"compiled_collectives":{},"compiler_peak_bytes":4523,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"action_product":"streamed","cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[200],"period":0,"period_transfer_scratch_bytes":{},"physical_value_shape":[200],"regime":"working","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":200},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"action_product":64,"cell":128}}
DEBUG:lcm:core plan record {"active_replica_bytes":0,"axis_extents":{"action_product":500,"cell":200},"compiled_collectives":{},"compiler_peak_bytes":4523,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"action_product":"streamed","cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[200],"period":0,"period_transfer_scratch_bytes":{},"physical_value_shape":[200],"regime":"working","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":200},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"action_product":64,"cell":128}}
core plan record {"active_replica_bytes":0,"axis_extents":{"action_product":500,"cell":200},"compiled_collectives":{},"compiler_peak_bytes":4523,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"action_product":"streamed","cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[200],"period":1,"period_transfer_scratch_bytes":{},"physical_value_shape":[200],"regime":"working","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":200},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"action_product":64,"cell":128}}
DEBUG:lcm:core plan record {"active_replica_bytes":0,"axis_extents":{"action_product":500,"cell":200},"compiled_collectives":{},"compiler_peak_bytes":4523,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"action_product":"streamed","cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[200],"period":1,"period_transfer_scratch_bytes":{},"physical_value_shape":[200],"regime":"working","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":200},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"action_product":64,"cell":128}}
core plan record {"active_replica_bytes":0,"axis_extents":{"cell":200},"compiled_collectives":{},"compiler_peak_bytes":1636,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[200],"period":2,"period_transfer_scratch_bytes":{},"physical_value_shape":[200],"regime":"dead","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":200},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"cell":128}}
DEBUG:lcm:core plan record {"active_replica_bytes":0,"axis_extents":{"cell":200},"compiled_collectives":{},"compiler_peak_bytes":1636,"compiler_reservation_bytes":null,"core":"main","device_ids":[0],"dispatch":{"cell":"streamed"},"logical_gathered_bytes":0,"logical_value_shape":[200],"period":2,"period_transfer_scratch_bytes":{},"physical_value_shape":[200],"regime":"dead","resident_bytes":null,"selected_block":null,"state_extents":{"wealth":200},"stored_owner_bytes":{},"transfer_workspace_bytes":0,"widths":{"cell":128}}
solve call 5fc1a637d041 phase workspace_selection end status=ok seconds=0.019376
INFO:lcm:solve call 5fc1a637d041 phase workspace_selection end status=ok seconds=0.019376
Starting solution
INFO:lcm:Starting solution
solve call 5fc1a637d041 phase backward_induction begin
INFO:lcm:solve call 5fc1a637d041 phase backward_induction begin
Age 2 (1 regimes):
INFO:lcm:Age 2 (1 regimes):
finished in 4.1ms
INFO:lcm: finished in 4.1ms
Age 1 (1 regimes):
INFO:lcm:Age 1 (1 regimes):
finished in 5.9ms
INFO:lcm: finished in 5.9ms
Age 0 (1 regimes):
INFO:lcm:Age 0 (1 regimes):
finished in 5.8ms
INFO:lcm: finished in 5.8ms
dead age 2.0 V min=-0.693 max=3.91 mean=2.95
DEBUG:lcm: dead age 2.0 V min=-0.693 max=3.91 mean=2.95
working age 1.0 V min=-2.19 max=6.28 mean=4.41
DEBUG:lcm: working age 1.0 V min=-2.19 max=6.28 mean=4.41
working age 0.0 V min=-3.73 max=8.03 mean=5.3
DEBUG:lcm: working age 0.0 V min=-3.73 max=8.03 mean=5.3
solve call 5fc1a637d041 phase backward_induction end status=ok seconds=0.020447
INFO:lcm:solve call 5fc1a637d041 phase backward_induction end status=ok seconds=0.020447
Solution complete (21.4ms)
INFO:lcm:Solution complete (21.4ms)
solve call 5fc1a637d041 phase result_assembly begin
INFO:lcm:solve call 5fc1a637d041 phase result_assembly begin
solve call 5fc1a637d041 phase result_assembly end status=ok seconds=0.001970
INFO:lcm:solve call 5fc1a637d041 phase result_assembly end status=ok seconds=0.001970
solve call 5fc1a637d041 phase solution_resolution begin
INFO:lcm:solve call 5fc1a637d041 phase solution_resolution begin
solve call 5fc1a637d041 phase solution_resolution end status=ok seconds=0.001045
INFO:lcm:solve call 5fc1a637d041 phase solution_resolution end status=ok seconds=0.001045
solve call 5fc1a637d041 phase solution_handover begin
INFO:lcm:solve call 5fc1a637d041 phase solution_handover begin
solve call 5fc1a637d041 phase solution_handover end status=ok seconds=0.000004
INFO:lcm:solve call 5fc1a637d041 phase solution_handover end status=ok seconds=0.000004
solve call 5fc1a637d041 phase chunk_planning begin
INFO:lcm:solve call 5fc1a637d041 phase chunk_planning begin
solve call 5fc1a637d041 phase chunk_planning end status=ok seconds=0.000366
INFO:lcm:solve call 5fc1a637d041 phase chunk_planning end status=ok seconds=0.000366
solve call 5fc1a637d041 phase simulation_setup begin
INFO:lcm:solve call 5fc1a637d041 phase simulation_setup begin
Starting simulation
INFO:lcm:Starting simulation
Simulation plan: route=legacy; 1 cpu device(s) (0,); outer chunks=1 (widths=(1,)); unbudgeted.
INFO:lcm:Simulation plan: route=legacy; 1 cpu device(s) (0,); outer chunks=1 (widths=(1,)); unbudgeted.
Simulation plan detail: route=legacy; subject_devices=(cpu) (0,); axis_widths_by_regime=[working: {}; dead: {}]; outer_chunk_count=1; admitted_chunk_widths=(1,); 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: {}; dead: {}]; outer_chunk_count=1; admitted_chunk_widths=(1,); budget_mode=unbudgeted; effective_device_memory_bytes=None; subject_grouping=None.
solve call 5fc1a637d041 phase simulation_setup end status=ok seconds=0.003990
INFO:lcm:solve call 5fc1a637d041 phase simulation_setup end status=ok seconds=0.003990
solve call 5fc1a637d041 phase simulation_chunk begin
INFO:lcm:solve call 5fc1a637d041 phase simulation_chunk begin
Age 0 (1 regimes):
INFO:lcm:Age 0 (1 regimes):
solve call 5fc1a637d041 phase simulation_compilation begin
INFO:lcm:solve call 5fc1a637d041 phase simulation_compilation begin
solve call 5fc1a637d041 phase simulation_compilation end status=ok seconds=0.314124
INFO:lcm:solve call 5fc1a637d041 phase simulation_compilation end status=ok seconds=0.314124
solve call 5fc1a637d041 phase simulation_compilation begin
INFO:lcm:solve call 5fc1a637d041 phase simulation_compilation begin
solve call 5fc1a637d041 phase simulation_compilation end status=ok seconds=0.009612
INFO:lcm:solve call 5fc1a637d041 phase simulation_compilation end status=ok seconds=0.009612
solve call 5fc1a637d041 phase simulation_compilation begin
INFO:lcm:solve call 5fc1a637d041 phase simulation_compilation begin
solve call 5fc1a637d041 phase simulation_compilation end status=ok seconds=0.012336
INFO:lcm:solve call 5fc1a637d041 phase simulation_compilation end status=ok seconds=0.012336
Off-grid next states: regime='working', target='working', period=0, state='wealth', phase='realized', count=0
INFO:lcm:Off-grid next states: regime='working', target='working', period=0, state='wealth', phase='realized', count=0
transitions:
- working → working = 1
DEBUG:lcm: transitions:
- working → working = 1
finished in 362.4ms
INFO:lcm: finished in 362.4ms
Age 1 (1 regimes):
INFO:lcm:Age 1 (1 regimes):
solve call 5fc1a637d041 phase simulation_compilation begin
INFO:lcm:solve call 5fc1a637d041 phase simulation_compilation begin
solve call 5fc1a637d041 phase simulation_compilation end status=ok seconds=0.312811
INFO:lcm:solve call 5fc1a637d041 phase simulation_compilation end status=ok seconds=0.312811
solve call 5fc1a637d041 phase simulation_compilation begin
INFO:lcm:solve call 5fc1a637d041 phase simulation_compilation begin
solve call 5fc1a637d041 phase simulation_compilation end status=ok seconds=0.012101
INFO:lcm:solve call 5fc1a637d041 phase simulation_compilation end status=ok seconds=0.012101
Off-grid next states: regime='working', target='dead', period=1, state='wealth', phase='realized', count=0
INFO:lcm:Off-grid next states: regime='working', target='dead', period=1, state='wealth', phase='realized', count=0
transitions:
- working → dead = 1
DEBUG:lcm: transitions:
- working → dead = 1
finished in 349.7ms
INFO:lcm: finished in 349.7ms
Age 2 (1 regimes):
INFO:lcm:Age 2 (1 regimes):
solve call 5fc1a637d041 phase simulation_compilation begin
INFO:lcm:solve call 5fc1a637d041 phase simulation_compilation begin
solve call 5fc1a637d041 phase simulation_compilation end status=ok seconds=0.011453
INFO:lcm:solve call 5fc1a637d041 phase simulation_compilation end status=ok seconds=0.011453
transitions:
- dead → dead = 1
DEBUG:lcm: transitions:
- dead → dead = 1
finished in 20.9ms
INFO:lcm: finished in 20.9ms
solve call 5fc1a637d041 phase simulation_chunk end status=ok seconds=0.737599
INFO:lcm:solve call 5fc1a637d041 phase simulation_chunk end status=ok seconds=0.737599
solve call 5fc1a637d041 phase simulation_completion begin
INFO:lcm:solve call 5fc1a637d041 phase simulation_completion begin
Simulation complete (744.3ms)
INFO:lcm:Simulation complete (744.3ms)
solve call 5fc1a637d041 phase simulation_completion end status=ok seconds=0.000772
INFO:lcm:solve call 5fc1a637d041 phase simulation_completion end status=ok seconds=0.000772
solve call 5fc1a637d041 phase simulation_result begin
INFO:lcm:solve call 5fc1a637d041 phase simulation_result begin
solve call 5fc1a637d041 phase simulation_result end status=ok seconds=0.000756
INFO:lcm:solve call 5fc1a637d041 phase simulation_result end status=ok seconds=0.000756
solve call 5fc1a637d041 phase result_finalization begin
INFO:lcm:solve call 5fc1a637d041 phase result_finalization begin
solve call 5fc1a637d041 phase result_finalization end status=ok seconds=0.000008
INFO:lcm:solve call 5fc1a637d041 phase result_finalization end status=ok seconds=0.000008
solve call 5fc1a637d041 phase public_simulate end status=ok seconds=1.299981
INFO:lcm:solve call 5fc1a637d041 phase public_simulate end status=ok seconds=1.299981
Naive agent:
c_0 = 8.7183 (analytical: 8.7080)
c_1 = 6.8145 (analytical: 6.7820)
Comparison¶
The small differences between numerical and analytical solutions are due to grid discretization.
import pandas as pd
comparison = pd.DataFrame(
{
"Agent": ["Exponential", "Naive"],
"c_0 (numerical)": [
df_exp.loc[df_exp["age"] == 0, "consumption"].iloc[0],
df_naive.loc[df_naive["age"] == 0, "consumption"].iloc[0],
],
"c_0 (analytical)": [c0_exp, c0_naive],
"c_1 (numerical)": [
df_exp.loc[df_exp["age"] == 1, "consumption"].iloc[0],
df_naive.loc[df_naive["age"] == 1, "consumption"].iloc[0],
],
"c_1 (analytical)": [c1_exp, c1_naive],
}
)
comparison = comparison.set_index("Agent")
comparison.style.format("{:.4f}")Sophisticated and Partially Naive Agents¶
The naive agent stores the value it chooses with: its solve phase maximizes the exponential , and the stored is that maximum. A sophisticated agent knows its future selves are present-biased. The solve then has two jobs that need different aggregators:
choose the action each future self takes, with ;
store the long-run value of that action, with , which is what the current self uses to price the future.
Decoupled(objective=..., value=...) declares exactly this. The objective ranks the
actions, the value is stored at the chosen action, and only the objective may be
Phased. One parameter , the agent’s belief about its future selves’
present bias, spans all three agent types:
: naive, it expects exponential future selves;
: sophisticated, it expects its true present bias;
: partially naive.
The acting self always chooses with its true , so the simulate variant of
the objective is the linear_beta_delta aggregator from above.
from lcm import Decoupled
def perceived_beta_delta(
*,
utility: float,
CE: float,
beta_tilde: float,
delta: float,
) -> float:
return utility + beta_tilde * delta * CE
def long_run(*, utility: float, CE: float, delta: float) -> float:
return utility + delta * CE
# The stored value is read off at the action the objective chooses, away from
# the value's own maximum, so grid error in that action enters it at first order.
# Finer grids than for the exponential agent keep it small.
FINE_WEALTH_GRID = LinSpacedGrid(start=0.5, stop=50, n_points=1000)
FINE_CONSUMPTION_GRID = LinSpacedGrid(start=0.001, stop=50, n_points=2000)
working_decoupled = Regime(
actions={"consumption": FINE_CONSUMPTION_GRID},
states={"wealth": FINE_WEALTH_GRID},
state_transitions={"wealth": next_wealth},
constraints={"borrowing_constraint": borrowing_constraint},
functions={"utility": utility},
koopmans_aggregator=Decoupled(
objective=Phased(solve=perceived_beta_delta, simulate=linear_beta_delta),
value=long_run,
),
)
model_decoupled = Model(
regimes={
"working": working_decoupled,
"dead": Regime(
states={"wealth": FINE_WEALTH_GRID},
functions={"utility": terminal_utility},
),
},
ages=AgeGrid(start=0, inclusive_stop=2, step="Y"),
edges=edges,
initial_nodes=((0, "working"),),
regime_id_class=RegimeId,
)
# Parameters shared by name across the three aggregators are one parameter.
model_decoupled.get_params_template()["working"]["koopmans_aggregator"]{'beta': 'float', 'beta_tilde': 'float', 'delta': 'float'}Analytical Solution¶
With log utility all three agents consume the same at (Pollak, 1968). At the agent expects to consume , so the long-run value it stores is
The slope in does not depend on , so neither does the choice . The beliefs show up in the acting self’s assessment at , , the value simulation publishes. The solve’s own chooses with instead.
def analytical_value_at_t0(*, beta, beta_tilde, delta, w0):
"""Return the long-run value at t=0 that an agent with belief beta_tilde stores."""
c0 = w0 / (1 + beta * delta * (1 + delta))
k = -jnp.log(1 + beta_tilde * delta) + delta * jnp.log(
beta_tilde * delta / (1 + beta_tilde * delta)
)
return float(jnp.log(c0) + delta * ((1 + delta) * jnp.log(w0 - c0) + k))
BETA_TILDE = {"naive": 1.0, "partially naive": 0.85, "sophisticated": BETA}
rows = {}
for agent, beta_tilde in BETA_TILDE.items():
result = model_decoupled.simulate(
params={
"working": {
"koopmans_aggregator": {
"beta": BETA,
"beta_tilde": beta_tilde,
"delta": DELTA,
},
},
},
initial_conditions=initial_conditions,
log_level="off",
)
first = result.to_dataframe().query('regime_name == "working" and age == 0')
rows[agent] = {
"c_0 (numerical)": first["consumption"].iloc[0],
"c_0 (analytical)": c0_naive,
"assessed V_0 (simulated)": first["value"].iloc[0],
"assessed V_0 (analytical)": analytical_value_at_t0(
beta=BETA, beta_tilde=beta_tilde, delta=DELTA, w0=W0
),
}
pd.DataFrame(rows).T.style.format("{:.4f}")Analytically all three agents consume the same, and the numbers agree up to grid error. The assessed value falls as the agent expects more present bias from its future selves. Each agent acts on the same rule at , so the long-run value every one of them actually gets is the sophisticated agent’s.
Keep in mind what the stored value is:
It is a self-assessment. For naive and partially naive agents it is the long-run value of the policy the agent expects to follow. The policy it does follow has a lower long-run value here. Welfare comparisons need that realized value, which the next section computes.
It needs finer grids. The value is read off at the objective’s choice, away from its own maximum, so grid error in the chosen action enters it at first order.
Only
GridSearchsolves it, and collective regimes and regimes whose transitions carry value-dependent gates refuse it.Taste shocks work. With
ExtremeValueTasteShocksthe objective sets the choice probabilities and the stored value is the expected value at the noisy choice.
Realized Welfare¶
The stored value is what the agent expects. The long-run value of the policy it actually follows is along its simulated path. PyLCM does not compute it by a second backward pass; simulating and discounting the realized utilities estimates it. With deterministic wealth one subject suffices here; with risk, average over many subjects.
def realized_long_run_value(*, beta_tilde):
"""Discount the utilities the agent realizes along its simulated path."""
result = model_decoupled.simulate(
params={
"working": {
"koopmans_aggregator": {
"beta": BETA,
"beta_tilde": beta_tilde,
"delta": DELTA,
},
},
},
initial_conditions=initial_conditions,
log_level="off",
)
path = result.to_dataframe(additional_targets=["utility"]).sort_values("age")
return float((DELTA ** path["age"] * path["utility"]).sum())
pd.DataFrame(
{
"assessed V_0": {
agent: rows[agent]["assessed V_0 (simulated)"] for agent in BETA_TILDE
},
"realized long-run value": {
agent: realized_long_run_value(beta_tilde=beta_tilde)
for agent, beta_tilde in BETA_TILDE.items()
},
}
).style.format("{:.4f}")The realized values agree across the three agents up to grid error, and they equal the sophisticated agent’s assessed value: only the sophisticated agent forecasts its own behavior correctly. The naive and partially naive agents overstate what they get.
Summary¶
Beta-delta discounting in PyLCM requires no core modifications:
Pair the shipped
LinearAggregatorwith a present-biased variant — only the second one has to be written:from lcm.koopmans_aggregation import LinearAggregator # utility + discount_factor * CE def linear_beta_delta(utility, CE, beta, delta): return utility + beta * delta * CEDeclare
koopmans_aggregatoras phase-variant so backward induction uses the exponential while action selection uses the beta-delta :from lcm import Phased koopmans_aggregator=Phased( solve=LinearAggregator(), simulate=linear_beta_delta, )Provide the union of both variants’ parameters:
params = { "working": { "koopmans_aggregator": { "discount_factor": delta, "beta": beta, "delta": delta, } } }For sophisticated and partially naive agents, decouple the objective from the value, with the agent’s belief as a parameter:
from lcm import Decoupled koopmans_aggregator=Decoupled( objective=Phased(solve=perceived_beta_delta, simulate=linear_beta_delta), value=long_run, )
The phase split is essential: using for both phases would compound at every recursive step, giving effective weights instead of the correct .