Skip to content

Commit ea4931f

Browse files
authored
Merge pull request #1499 from sbenthall/i1471-a
Some expansion to the MonteCarloSimulator functionality
2 parents 189b452 + b8d5623 commit ea4931f

File tree

4 files changed

+313
-1
lines changed

4 files changed

+313
-1
lines changed

HARK/model.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,11 @@ def get_dynamics(self):
270270
return self.dynamics
271271

272272
def get_vars(self):
273-
return list(self.shocks.keys()) + list(self.dynamics.keys())
273+
return (
274+
list(self.shocks.keys())
275+
+ list(self.dynamics.keys())
276+
+ list(self.reward.keys())
277+
)
274278

275279
def transition(self, pre, dr):
276280
"""
@@ -314,6 +318,9 @@ def get_decision_value_function(self, dr, continuation):
314318
Given a decision rule and a continuation value function,
315319
return a function for the value at the decision step/tac,
316320
after the shock have been realized.
321+
322+
## TODO: it would be better to systematize these value functions per block
323+
## better, then construct them with 'partial' methods
317324
"""
318325
srvf = self.get_state_rule_value_function_from_continuation(continuation)
319326

HARK/simulation/monte_carlo.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from HARK.model import DBlock
1717
from HARK.model import construct_shocks, simulate_dynamics
1818

19+
from HARK.utilities import apply_fun_to_vals
20+
1921

2022
def draw_shocks(shocks: Mapping[str, Distribution], conditions: Sequence[int]):
2123
"""
@@ -29,6 +31,7 @@ def draw_shocks(shocks: Mapping[str, Distribution], conditions: Sequence[int]):
2931
conditions: Sequence[int]
3032
An array of conditions, one for each agent.
3133
Typically these will be agent ages.
34+
# TODO: generalize this to wider range of inputs.
3235
3336
Parameters
3437
------------
@@ -51,6 +54,7 @@ def draw_shocks(shocks: Mapping[str, Distribution], conditions: Sequence[int]):
5154
draws[shock_var] = shock.draw(conditions)
5255
else:
5356
draws[shock_var] = shock.draw(len(conditions))
57+
# this is hacky if there are no conditions.
5458

5559
return draws
5660

@@ -396,3 +400,240 @@ def clear_history(self):
396400
for var_name in self.vars:
397401
self.history[var_name] = np.empty((self.T_sim, self.agent_count))
398402
self.history[var_name].fill(np.nan)
403+
404+
405+
class MonteCarloSimulator(Simulator):
406+
"""
407+
A Monte Carlo simulation engine based.
408+
409+
Unlike the AgentTypeMonteCarloSimulator HARK.core.AgentType,
410+
this class does make any assumptions about aging or mortality.
411+
It operates only on model information passed in as blocks.
412+
413+
It also does not have read_shocks functionality;
414+
it is a strict subset of the AgentTypeMonteCarloSimulator functionality.
415+
416+
Parameters
417+
------------
418+
419+
calibration: Mapping[str, Any]
420+
421+
block : DBlock
422+
Has shocks, dynamics, and rewards
423+
424+
dr: Mapping[str, Callable]
425+
426+
initial: dict
427+
428+
seed : int
429+
A seed for this instance's random number generator.
430+
431+
Attributes
432+
----------
433+
agent_count : int
434+
The number of agents of this type to use in simulation.
435+
436+
T_sim : int
437+
The number of periods to simulate.
438+
"""
439+
440+
state_vars = []
441+
442+
def __init__(
443+
self, calibration, block: DBlock, dr, initial, seed=0, agent_count=1, T_sim=10
444+
):
445+
super().__init__()
446+
447+
self.calibration = calibration
448+
self.block = block
449+
450+
# shocks are exogenous (but for age) but can depend on calibration
451+
raw_shocks = block.get_shocks()
452+
self.shocks = construct_shocks(raw_shocks, calibration)
453+
454+
self.dynamics = block.get_dynamics()
455+
self.dr = dr
456+
self.initial = initial
457+
458+
self.seed = seed # NOQA
459+
self.agent_count = agent_count # TODO: pass this in at block level
460+
self.T_sim = T_sim
461+
462+
# changes here from HARK.core.AgentType
463+
self.vars = block.get_vars()
464+
465+
self.vars_now = {v: None for v in self.vars}
466+
self.vars_prev = self.vars_now.copy()
467+
468+
self.shock_history = {}
469+
self.newborn_init_history = {}
470+
self.history = {}
471+
472+
self.reset_rng() # NOQA
473+
474+
def reset_rng(self):
475+
"""
476+
Reset the random number generator for this type.
477+
"""
478+
self.RNG = np.random.default_rng(self.seed)
479+
480+
def initialize_sim(self):
481+
"""
482+
Prepares for a new simulation. Resets the internal random number generator,
483+
makes initial states for all agents (using sim_birth), clears histories of tracked variables.
484+
"""
485+
if self.T_sim <= 0:
486+
raise Exception(
487+
"T_sim represents the largest number of observations "
488+
+ "that can be simulated for an agent, and must be a positive number."
489+
)
490+
491+
self.reset_rng()
492+
self.t_sim = 0
493+
all_agents = np.ones(self.agent_count, dtype=bool)
494+
blank_array = np.empty(self.agent_count)
495+
blank_array[:] = np.nan
496+
for var in self.vars:
497+
if self.vars_now[var] is None:
498+
self.vars_now[var] = copy(blank_array)
499+
500+
self.t_cycle = np.zeros(
501+
self.agent_count, dtype=int
502+
) # Which cycle period each agent is on
503+
504+
for var_name in self.initial:
505+
self.newborn_init_history[var_name] = (
506+
np.zeros((self.T_sim, self.agent_count)) + np.nan
507+
)
508+
509+
self.sim_birth(all_agents)
510+
511+
self.clear_history()
512+
return None
513+
514+
def sim_one_period(self):
515+
"""
516+
Simulates one period for this type. Calls the methods get_mortality(), get_shocks() or
517+
read_shocks, get_states(), get_controls(), and get_poststates(). These should be defined for
518+
AgentType subclasses, except get_mortality (define its components sim_death and sim_birth
519+
instead) and read_shocks.
520+
"""
521+
522+
# state_{t-1}
523+
for var in self.vars:
524+
self.vars_prev[var] = self.vars_now[var]
525+
526+
if isinstance(self.vars_now[var], np.ndarray):
527+
self.vars_now[var] = np.empty(self.agent_count)
528+
self.vars_now[var][:] = np.nan
529+
else:
530+
# Probably an aggregate variable. It may be getting set by the Market.
531+
pass
532+
533+
shocks_now = {}
534+
535+
shocks_now = draw_shocks(
536+
self.shocks,
537+
np.zeros(self.agent_count), # TODO: stupid hack to remove age calculations.
538+
# this needs a little more thought
539+
)
540+
541+
pre = self.calibration # for AgentTypeMC, this is conditional on age
542+
# TODO: generalize indexing into calibration.
543+
544+
pre.update(self.vars_prev)
545+
pre.update(shocks_now)
546+
547+
# Won't work for 3.8: self.parameters | self.vars_prev | shocks_now
548+
549+
dr = self.dr # AgentTypeMC chooses rule by age;
550+
# that generalizes to age as a DR argument?
551+
552+
post = simulate_dynamics(self.dynamics, pre, dr)
553+
554+
for r in self.block.reward:
555+
post[r] = apply_fun_to_vals(self.block.reward[r], post)
556+
557+
self.vars_now = post
558+
559+
def sim_birth(self, which_agents):
560+
"""
561+
Makes new agents for the simulation. Takes a boolean array as an input, indicating which
562+
agent indices are to be "born". Does nothing by default, must be overwritten by a subclass.
563+
564+
Parameters
565+
----------
566+
which_agents : np.array(Bool)
567+
Boolean array of size self.agent_count indicating which agents should be "born".
568+
569+
Returns
570+
-------
571+
None
572+
"""
573+
574+
initial_vals = draw_shocks(self.initial, np.zeros(which_agents.sum()))
575+
576+
if np.sum(which_agents) > 0:
577+
for varn in initial_vals:
578+
self.vars_now[varn][which_agents] = initial_vals[varn]
579+
self.newborn_init_history[varn][self.t_sim, which_agents] = (
580+
initial_vals[varn]
581+
)
582+
583+
def simulate(self, sim_periods=None):
584+
"""
585+
Simulates this agent type for a given number of periods. Defaults to
586+
self.T_sim if no input.
587+
588+
Records histories of attributes named in self.track_vars in
589+
self.history[varname].
590+
591+
Parameters
592+
----------
593+
sim_periods : int
594+
Number of periods to simulate.
595+
596+
Returns
597+
-------
598+
history : dict
599+
The history tracked during the simulation.
600+
"""
601+
if not hasattr(self, "t_sim"):
602+
raise Exception(
603+
"It seems that the simulation variables were not initialize before calling "
604+
+ "simulate(). Call initialize_sim() to initialize the variables before calling simulate() again."
605+
)
606+
if sim_periods is not None and self.T_sim < sim_periods:
607+
raise Exception(
608+
"To simulate, sim_periods has to be larger than the maximum data set size "
609+
+ "T_sim. Either increase the attribute T_sim of this agent type instance "
610+
+ "and call the initialize_sim() method again, or set sim_periods <= T_sim."
611+
)
612+
613+
# Ignore floating point "errors". Numpy calls it "errors", but really it's excep-
614+
# tions with well-defined answers such as 1.0/0.0 that is np.inf, -1.0/0.0 that is
615+
# -np.inf, np.inf/np.inf is np.nan and so on.
616+
with np.errstate(
617+
divide="ignore", over="ignore", under="ignore", invalid="ignore"
618+
):
619+
if sim_periods is None:
620+
sim_periods = self.T_sim
621+
622+
for t in range(sim_periods):
623+
self.sim_one_period()
624+
625+
# track all the vars -- shocks and dynamics
626+
for var_name in self.vars:
627+
self.history[var_name][self.t_sim, :] = self.vars_now[var_name]
628+
629+
self.t_sim += 1
630+
631+
return self.history
632+
633+
def clear_history(self):
634+
"""
635+
Clears the histories.
636+
"""
637+
for var_name in self.vars:
638+
self.history[var_name] = np.empty((self.T_sim, self.agent_count))
639+
self.history[var_name].fill(np.nan)

HARK/simulation/test_monte_carlo.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,49 @@ def test_simulate(self):
159159
b1 = history["m"][1] - self.dr["c"][1](history["m"][1])
160160

161161
self.assertTrue((a1 == b1).all())
162+
163+
164+
class test_MonteCarloSimulator(unittest.TestCase):
165+
def setUp(self):
166+
self.calibration = { # TODO
167+
"G": 1.05,
168+
}
169+
self.block = DBlock(
170+
**{
171+
"shocks": {
172+
"theta": MeanOneLogNormal(1),
173+
"agg_R": Aggregate(MeanOneLogNormal(1)),
174+
},
175+
"dynamics": {
176+
"b": lambda agg_R, G, a: agg_R * G * a,
177+
"m": lambda b, theta: b + theta,
178+
"c": Control(["m"]),
179+
"a": lambda m, c: m - c,
180+
},
181+
}
182+
)
183+
184+
self.initial = {"a": MeanOneLogNormal(1)}
185+
186+
self.dr = {"c": lambda m: m / 2}
187+
188+
def test_simulate(self):
189+
self.simulator = MonteCarloSimulator(
190+
self.calibration,
191+
self.block,
192+
self.dr,
193+
self.initial,
194+
agent_count=3,
195+
)
196+
197+
self.simulator.initialize_sim()
198+
history = self.simulator.simulate()
199+
200+
a1 = history["a"][5]
201+
b1 = (
202+
history["a"][4] * history["agg_R"][5] * self.calibration["G"]
203+
+ history["theta"][5]
204+
- history["c"][5]
205+
)
206+
207+
self.assertTrue((a1 == b1).all())

HARK/utilities.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import numpy as np # Python's numeric library, abbreviated "np"
1515
from scipy.interpolate import interp1d
1616

17+
from inspect import signature
18+
1719
# try:
1820
# import matplotlib.pyplot as plt # Python's plotting library
1921
# except ImportError:
@@ -112,6 +114,22 @@ def distance(self, other):
112114
return 10000.0
113115

114116

117+
def apply_fun_to_vals(fun, vals):
118+
"""
119+
Applies a function to the arguments defined in `vals`.
120+
This is equivalent to `fun(**vals)`, except
121+
that `vals` may contain keys that are not named arguments
122+
of `fun`.
123+
124+
Parameters
125+
----------
126+
fun: callable
127+
128+
vals: dict
129+
"""
130+
return fun(*[vals[var] for var in signature(fun).parameters])
131+
132+
115133
# =======================================================
116134
# ================ Other useful functions ===============
117135
# =======================================================

0 commit comments

Comments
 (0)