forked from econ-ark/HARK
-
Notifications
You must be signed in to change notification settings - Fork 1
/
HARKcore.py
1159 lines (989 loc) · 40.9 KB
/
HARKcore.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'''
High-level functions and classes for solving a wide variety of economic models.
The "core" of HARK is a framework for "microeconomic" and "macroeconomic"
models. A micro model concerns the dynamic optimization problem for some type
of agents, where agents take the inputs to their problem as exogenous. A macro
model adds an additional layer, endogenizing some of the inputs to the micro
problem by finding a general equilibrium dynamic rule.
'''
from HARKutilities import getArgNames, NullFunc
from copy import copy, deepcopy
import numpy as np
from time import clock
def distanceMetric(thing_A,thing_B):
'''
A "universal distance" metric that can be used as a default in many settings.
Parameters
----------
thing_A : object
A generic object.
thing_B : object
Another generic object.
Returns:
------------
distance : float
The "distance" between thing_A and thing_B.
'''
# Get the types of the two inputs
typeA = type(thing_A)
typeB = type(thing_B)
if typeA is list and typeB is list:
lenA = len(thing_A) # If both inputs are lists, then the distance between
lenB = len(thing_B) # them is the maximum distance between corresponding
if lenA == lenB: # elements in the lists. If they differ in length,
distance_temp = [] # the distance is the difference in lengths.
for n in range(lenA):
distance_temp.append(distanceMetric(thing_A[n],thing_B[n]))
distance = max(distance_temp)
else:
distance = float(abs(lenA - lenB))
# If both inputs are numbers, return their difference
elif (typeA is int or typeB is float) and (typeB is int or typeB is float):
distance = float(abs(thing_A - thing_B))
# If both inputs are array-like, return the maximum absolute difference b/w
# corresponding elements (if same shape); return largest difference in dimensions
# if shapes do not align.
elif hasattr(thing_A,'shape') and hasattr(thing_B,'shape'):
if thing_A.shape == thing_B.shape:
distance = np.max(abs(thing_A - thing_B))
else:
distance = np.max(abs(thing_A.shape - thing_B.shape))
# If none of the above cases, but the objects are of the same class, call
# the distance method of one on the other
elif thing_A.__class__.__name__ == thing_B.__class__.__name__:
if thing_A.__class__.__name__ == 'function':
distance = 0.0
else:
distance = thing_A.distance(thing_B)
else: # Failsafe: the inputs are very far apart
distance = 1000.0
return distance
class HARKobject():
'''
A superclass for object classes in HARK. Comes with two useful methods:
a generic/universal distance method and an attribute assignment method.
'''
def distance(self,other):
'''
A generic distance method, which requires the existence of an attribute
called distance_criteria, giving a list of strings naming the attributes
to be considered by the distance metric.
Parameters
----------
other : object
Another object to compare this instance to.
Returns
-------
(unnamed) : float
The distance between this object and another, using the "universal
distance" metric.
'''
distance_list = [0.0]
for attr_name in self.distance_criteria:
try:
obj_A = getattr(self,attr_name)
obj_B = getattr(other,attr_name)
distance_list.append(distanceMetric(obj_A,obj_B))
except:
distance_list.append(1000.0) # if either object lacks attribute, they are not the same
return max(distance_list)
def assignParameters(self,**kwds):
'''
Assign an arbitrary number of attributes to this agent.
Parameters
----------
**kwds : keyword arguments
Any number of keyword arguments of the form key=value. Each value
will be assigned to the attribute named in self.
Returns
-------
none
'''
for key in kwds:
setattr(self,key,kwds[key])
def __call__(self,**kwds):
'''
Assign an arbitrary number of attributes to this agent, as a convenience.
See assignParameters.
'''
self.assignParameters(**kwds)
def getAvg(self,varname,**kwds):
'''
Calculates the average of an attribute of this instance. Returns NaN if no such attribute.
Parameters
----------
varname : string
The name of the attribute whose average is to be calculated. This attribute must be an
np.array or other class compatible with np.mean.
Returns
-------
avg : float or np.array
The average of this attribute. Might be an array if the axis keyword is passed.
'''
if hasattr(self,varname):
return np.mean(getattr(self,varname),**kwds)
else:
return np.nan
class Solution(HARKobject):
'''
A superclass for representing the "solution" to a single period problem in a
dynamic microeconomic model.
NOTE: This can be deprecated now that HARKobject exists, but this requires
replacing each instance of Solution with HARKobject in the other modules.
'''
class AgentType(HARKobject):
'''
A superclass for economic agents in the HARK framework. Each model should
specify its own subclass of AgentType, inheriting its methods and overwriting
as necessary. Critically, every subclass of AgentType should define class-
specific static values of the attributes time_vary and time_inv as lists of
strings. Each element of time_vary is the name of a field in AgentSubType
that varies over time in the model. Each element of time_inv is the name of
a field in AgentSubType that is constant over time in the model. The string
'solveOnePeriod' should appear in exactly one of these lists, depending on
whether the same solution method is used in all periods of the model.
'''
def __init__(self,solution_terminal=None,cycles=1,time_flow=False,pseudo_terminal=True,
tolerance=0.000001,seed=0,**kwds):
'''
Initialize an instance of AgentType by setting attributes.
Parameters
----------
solution_terminal : Solution
A representation of the solution to the terminal period problem of
this AgentType instance, or an initial guess of the solution if this
is an infinite horizon problem.
cycles : int
The number of times the sequence of periods is experienced by this
AgentType in their "lifetime". cycles=1 corresponds to a lifecycle
model, with a certain sequence of one period problems experienced
once before terminating. cycles=0 corresponds to an infinite horizon
model, with a sequence of one period problems repeating indefinitely.
time_flow : boolean
Whether time is currently "flowing" forward or backward for this
instance. Used to flip between solving (using backward iteration)
and simulating (etc).
pseudo_terminal : boolean
Indicates whether solution_terminal isn't actually part of the
solution to the problem (as a known solution to the terminal period
problem), but instead represents a "scrap value"-style termination.
When True, solution_terminal is not included in the solution; when
false, solution_terminal is the last element of the solution.
tolerance : float
Maximum acceptable "distance" between successive solutions to the
one period problem in an infinite horizon (cycles=0) model in order
for the solution to be considered as having "converged". Inoperative
when cycles>0.
seed : int
A seed for this instance's random number generator.
Returns
-------
None
'''
if solution_terminal is None:
solution_terminal = NullFunc()
self.solution_terminal = solution_terminal
self.cycles = cycles
self.time_flow = time_flow
self.pseudo_terminal = pseudo_terminal
self.solveOnePeriod = NullFunc()
self.tolerance = tolerance
self.seed = seed
self.track_vars = []
self.poststate_vars = []
self.read_shocks = False
self.assignParameters(**kwds)
self.resetRNG()
def timeReport(self):
'''
Report to the user the direction that time is currently "flowing" for
this instance. Only exists as a reminder of how time_flow works.
Parameters
----------
none
Returns
-------
none
'''
if self.time_flow:
print('Time varying objects are listed in ordinary chronological order.')
else:
print('Time varying objects are listed in reverse chronological order.')
def timeFlip(self):
'''
Reverse the flow of time for this instance.
Parameters
----------
none
Returns
-------
none
'''
for name in self.time_vary:
exec('self.' + name + '.reverse()')
self.time_flow = not self.time_flow
def timeFwd(self):
'''
Make time flow forward for this instance.
Parameters
----------
none
Returns
-------
none
'''
if not self.time_flow:
self.timeFlip()
def timeRev(self):
'''
Make time flow backward for this instance.
Parameters
----------
none
Returns
-------
none
'''
if self.time_flow:
self.timeFlip()
def addToTimeVary(self,*params):
'''
Adds any number of parameters to time_vary for this instance.
Parameters
----------
params : string
Any number of strings naming attributes to be added to time_vary
Returns
-------
None
'''
for param in params:
if param not in self.time_vary:
self.time_vary.append(param)
def addToTimeInv(self,*params):
'''
Adds any number of parameters to time_inv for this instance.
Parameters
----------
params : string
Any number of strings naming attributes to be added to time_inv
Returns
-------
None
'''
for param in params:
if param not in self.time_inv:
self.time_inv.append(param)
def delFromTimeVary(self,*params):
'''
Removes any number of parameters from time_vary for this instance.
Parameters
----------
params : string
Any number of strings naming attributes to be removed from time_vary
Returns
-------
None
'''
for param in params:
if param in self.time_vary:
self.time_vary.remove(param)
def delFromTimeInv(self,*params):
'''
Removes any number of parameters from time_inv for this instance.
Parameters
----------
params : string
Any number of strings naming attributes to be removed from time_inv
Returns
-------
None
'''
for param in params:
if param in self.time_inv:
self.time_inv.remove(param)
def solve(self,verbose=False):
'''
Solve the model for this instance of an agent type by backward induction.
Loops through the sequence of one period problems, passing the solution
from period t+1 to the problem for period t.
Parameters
----------
verbose : boolean
If True, solution progress is printed to screen.
Returns
-------
none
'''
self.preSolve() # Do pre-solution stuff
self.solution = solveAgent(self,verbose) # Solve the model by backward induction
if self.time_flow: # Put the solution in chronological order if this instance's time flow runs that way
self.solution.reverse()
self.addToTimeVary('solution') # Add solution to the list of time-varying attributes
self.postSolve() # Do post-solution stuff
def resetRNG(self):
'''
Reset the random number generator for this type.
Parameters
----------
none
Returns
-------
none
'''
self.RNG = np.random.RandomState(self.seed)
def checkElementsOfTimeVaryAreLists(self):
"""
A method to check that elements of time_vary are lists.
"""
for param in self.time_vary:
assert type(getattr(self,param))==list,param + ' is not a list, but should be' + \
' because it is in time_vary'
def preSolve(self):
'''
A method that is run immediately before the model is solved, to check inputs or to prepare
the terminal solution, perhaps.
Parameters
----------
none
Returns
-------
none
'''
self.checkElementsOfTimeVaryAreLists()
return None
def postSolve(self):
'''
A method that is run immediately after the model is solved, to finalize
the solution in some way. Does nothing here.
Parameters
----------
none
Returns
-------
none
'''
return None
def initializeSim(self):
'''
Prepares this AgentType for a new simulation. Resets the internal random number generator,
makes initial states for all agents (using simBirth), clears histories of tracked variables.
Parameters
----------
None
Returns
-------
None
'''
self.resetRNG()
self.t_sim = 0
all_agents = np.ones(self.AgentCount,dtype=bool)
blank_array = np.zeros(self.AgentCount)
for var_name in self.poststate_vars:
exec('self.' + var_name + ' = copy(blank_array)')
self.t_age = np.zeros(self.AgentCount,dtype=int) # Number of periods since agent entry
self.t_cycle = np.zeros(self.AgentCount,dtype=int) # Which cycle period each agent is on
self.simBirth(all_agents)
self.clearHistory()
return None
def simOnePeriod(self):
'''
Simulates one period for this type. Calls the methods getMortality(), getShocks() or
readShocks, getStates(), getControls(), and getPostStates(). These should be defined for
AgentType subclasses, except getMortality (define its components simDeath and simBirth
instead) and readShocks.
Parameters
----------
None
Returns
-------
None
'''
self.getMortality() # Replace some agents with "newborns"
if self.read_shocks: # If shock histories have been pre-specified, use those
self.readShocks()
else: # Otherwise, draw shocks as usual according to subclass-specific method
self.getShocks()
self.getStates() # Determine each agent's state at decision time
self.getControls() # Determine each agent's choice or control variables based on states
self.getPostStates() # Determine each agent's post-decision / end-of-period states using states and controls
# Advance time for all agents
self.t_age = self.t_age + 1 # Age all consumers by one period
self.t_cycle = self.t_cycle + 1 # Age all consumers within their cycle
self.t_cycle[self.t_cycle == self.T_cycle] = 0 # Resetting to zero for those who have reached the end
def makeShockHistory(self):
'''
Makes a pre-specified history of shocks for the simulation. Shock variables should be named
in self.shock_vars, a list of strings that is subclass-specific. This method runs a subset
of the standard simulation loop by simulating only mortality and shocks; each variable named
in shock_vars is stored in a T_sim x AgentCount array in an attribute of self named X_hist.
Automatically sets self.read_shocks to True so that these pre-specified shocks are used for
all subsequent calls to simulate().
Parameters
----------
None
Returns
-------
None
'''
# Make sure time is flowing forward and re-initialize the simulation
orig_time = self.time_flow
self.timeFwd()
self.initializeSim()
# Make blank history arrays for each shock variable
for var_name in self.shock_vars:
setattr(self,var_name+'_hist',np.zeros((self.T_sim,self.AgentCount))+np.nan)
# Make and store the history of shocks for each period
for t in range(self.T_sim):
self.getMortality()
self.getShocks()
for var_name in self.shock_vars:
exec('self.' + var_name + '_hist[self.t_sim,:] = self.' + var_name)
self.t_sim += 1
self.t_age = self.t_age + 1 # Age all consumers by one period
self.t_cycle = self.t_cycle + 1 # Age all consumers within their cycle
self.t_cycle[self.t_cycle == self.T_cycle] = 0 # Resetting to zero for those who have reached the end
# Restore the flow of time and flag that shocks can be read rather than simulated
self.read_shocks = True
if not orig_time:
self.timeRev()
def getMortality(self):
'''
Simulates mortality or agent turnover according to some model-specific rules named simDeath
and simBirth (methods of an AgentType subclass). simDeath takes no arguments and returns
a Boolean array of size AgentCount, indicating which agents of this type have "died" and
must be replaced. simBirth takes such a Boolean array as an argument and generates initial
post-decision states for those agent indices.
Parameters
----------
None
Returns
-------
None
'''
who_dies = self.simDeath()
self.simBirth(who_dies)
return None
def simDeath(self):
'''
Determines which agents in the current population "die" or should be replaced. Takes no
inputs, returns a Boolean array of size self.AgentCount, which has True for agents who die
and False for those that survive. Returns all False by default, must be overwritten by a
subclass to have replacement events.
Parameters
----------
None
Returns
-------
who_dies : np.array
Boolean array of size self.AgentCount indicating which agents die and are replaced.
'''
print('AgentType subclass must define method simDeath!')
who_dies = np.ones(self.AgentCount,dtype=bool)
return who_dies
def simBirth(self,which_agents):
'''
Makes new agents for the simulation. Takes a boolean array as an input, indicating which
agent indices are to be "born". Does nothing by default, must be overwritten by a subclass.
Parameters
----------
which_agents : np.array(Bool)
Boolean array of size self.AgentCount indicating which agents should be "born".
Returns
-------
None
'''
print('AgentType subclass must define method simBirth!')
return None
def getShocks(self):
'''
Gets values of shock variables for the current period. Does nothing by default, but can
be overwritten by subclasses of AgentType.
Parameters
----------
None
Returns
-------
None
'''
return None
def readShocks(self):
'''
Reads values of shock variables for the current period from history arrays. For each var-
iable X named in self.shock_vars, this attribute of self is set to self.X_hist[self.t_sim,:].
This method is only ever called if self.read_shocks is True. This can be achieved by using
the method makeShockHistory() (or manually after storing a "handcrafted" shock history).
Parameters
----------
None
Returns
-------
None
'''
for var_name in self.shock_vars:
setattr(self,var_name,getattr(self,var_name+'_hist')[self.t_sim,:])
def getStates(self):
'''
Gets values of state variables for the current period, probably by using post-decision states
from last period, current period shocks, and maybe market-level events. Does nothing by
default, but can be overwritten by subclasses of AgentType.
Parameters
----------
None
Returns
-------
None
'''
return None
def getControls(self):
'''
Gets values of control variables for the current period, probably by using current states.
Does nothing by default, but can be overwritten by subclasses of AgentType.
Parameters
----------
None
Returns
-------
None
'''
return None
def getPostStates(self):
'''
Gets values of post-decision state variables for the current period, probably by current
states and controls and maybe market-level events or shock variables. Does nothing by
default, but can be overwritten by subclasses of AgentType.
Parameters
----------
None
Returns
-------
None
'''
return None
def simulate(self,sim_periods=None):
'''
Simulates this agent type for a given number of periods (defaults to self.T_sim if no input).
Records histories of attributes named in self.track_vars in attributes named varname_hist.
Parameters
----------
None
Returns
-------
None
'''
orig_time = self.time_flow
self.timeFwd()
if sim_periods is None:
sim_periods = self.T_sim
for t in range(sim_periods):
self.simOnePeriod()
for var_name in self.track_vars:
exec('self.' + var_name + '_hist[self.t_sim,:] = self.' + var_name)
self.t_sim += 1
if not orig_time:
self.timeRev()
def clearHistory(self):
'''
Clears the histories of the attributes named in self.track_vars.
Parameters
----------
None
Returns
-------
None
'''
for var_name in self.track_vars:
exec('self.' + var_name + '_hist = np.zeros((self.T_sim,self.AgentCount)) + np.nan')
def solveAgent(agent,verbose):
'''
Solve the dynamic model for one agent type. This function iterates on "cycles"
of an agent's model either a given number of times or until solution convergence
if an infinite horizon model is used (with agent.cycles = 0).
Parameters
----------
agent : AgentType
The microeconomic AgentType whose dynamic problem is to be solved.
verbose : boolean
If True, solution progress is printed to screen (when cycles != 1).
Returns
-------
solution : [Solution]
A list of solutions to the one period problems that the agent will
encounter in his "lifetime". Returns in reverse chronological order.
'''
# Record the flow of time when the Agent began the process, and make sure time is flowing backwards
original_time_flow = agent.time_flow
agent.timeRev()
# Check to see whether this is an (in)finite horizon problem
cycles_left = agent.cycles
infinite_horizon = cycles_left == 0
# Initialize the solution, which includes the terminal solution if it's not a pseudo-terminal period
solution = []
if not agent.pseudo_terminal:
solution.append(deepcopy(agent.solution_terminal))
# Initialize the process, then loop over cycles
solution_last = agent.solution_terminal
go = True
completed_cycles = 0
max_cycles = 5000 # escape clause
if verbose:
t_last = clock()
while go:
# Solve a cycle of the model, recording it if horizon is finite
solution_cycle = solveOneCycle(agent,solution_last)
if not infinite_horizon:
solution += solution_cycle
# Check for termination: identical solutions across cycle iterations or run out of cycles
solution_now = solution_cycle[-1]
if infinite_horizon:
if completed_cycles > 0:
solution_distance = solution_now.distance(solution_last)
go = (solution_distance > agent.tolerance and completed_cycles < max_cycles)
else: # Assume solution does not converge after only one cycle
solution_distance = 100.0
go = True
else:
cycles_left += -1
go = cycles_left > 0
# Update the "last period solution"
solution_last = solution_now
completed_cycles += 1
# Display progress if requested
if verbose:
t_now = clock()
if infinite_horizon:
print('Finished cycle #' + str(completed_cycles) + ' in ' + str(t_now-t_last) +\
' seconds, solution distance = ' + str(solution_distance))
else:
print('Finished cycle #' + str(completed_cycles) + ' of ' + str(agent.cycles) +\
' in ' + str(t_now-t_last) + ' seconds.')
t_last = t_now
# Record the last cycle if horizon is infinite (solution is still empty!)
if infinite_horizon:
solution = solution_cycle # PseudoTerminal=False impossible for infinite horizon
# Restore the direction of time to its original orientation, then return the solution
if original_time_flow:
agent.timeFwd()
return solution
def solveOneCycle(agent,solution_last):
'''
Solve one "cycle" of the dynamic model for one agent type. This function
iterates over the periods within an agent's cycle, updating the time-varying
parameters and passing them to the single period solver(s).
Parameters
----------
agent : AgentType
The microeconomic AgentType whose dynamic problem is to be solved.
solution_last : Solution
A representation of the solution of the period that comes after the
end of the sequence of one period problems. This might be the term-
inal period solution, a "pseudo terminal" solution, or simply the
solution to the earliest period from the succeeding cycle.
Returns
-------
solution_cycle : [Solution]
A list of one period solutions for one "cycle" of the AgentType's
microeconomic model. Returns in reverse chronological order.
'''
# Calculate number of periods per cycle, defaults to 1 if all variables are time invariant
if len(agent.time_vary) > 0:
name = agent.time_vary[0]
T = len(eval('agent.' + name))
else:
T = 1
# Check whether the same solution method is used in all periods
always_same_solver = 'solveOnePeriod' not in agent.time_vary
if always_same_solver:
solveOnePeriod = agent.solveOnePeriod
these_args = getArgNames(solveOnePeriod)
# Construct a dictionary to be passed to the solver
time_inv_string = ''
for name in agent.time_inv:
time_inv_string += ' \'' + name + '\' : agent.' +name + ','
time_vary_string = ''
for name in agent.time_vary:
time_vary_string += ' \'' + name + '\' : None,'
solve_dict = eval('{' + time_inv_string + time_vary_string + '}')
# Initialize the solution for this cycle, then iterate on periods
solution_cycle = []
solution_next = solution_last
for t in range(T):
# Update which single period solver to use (if it depends on time)
if not always_same_solver:
solveOnePeriod = agent.solveOnePeriod[t]
these_args = getArgNames(solveOnePeriod)
# Update time-varying single period inputs
for name in agent.time_vary:
if name in these_args:
solve_dict[name] = eval('agent.' + name + '[t]')
solve_dict['solution_next'] = solution_next
# Make a temporary dictionary for this period
temp_dict = {name: solve_dict[name] for name in these_args}
# Solve one period, add it to the solution, and move to the next period
solution_t = solveOnePeriod(**temp_dict)
solution_cycle.append(solution_t)
solution_next = solution_t
# Return the list of per-period solutions
return solution_cycle
#========================================================================
#========================================================================
class Market(HARKobject):
'''
A superclass to represent a central clearinghouse of information. Used for
dynamic general equilibrium models to solve the "macroeconomic" model as a
layer on top of the "microeconomic" models of one or more AgentTypes.
'''
def __init__(self,agents=[],sow_vars=[],reap_vars=[],const_vars=[],track_vars=[],dyn_vars=[],
millRule=None,calcDynamics=None,act_T=1000,tolerance=0.000001):
'''
Make a new instance of the Market class.
Parameters
----------
agents : [AgentType]
A list of all the AgentTypes in this market.
sow_vars : [string]
Names of variables generated by the "aggregate market process" that should
be "sown" to the agents in the market. Aggregate state, etc.
reap_vars : [string]
Names of variables to be collected ("reaped") from agents in the market
to be used in the "aggregate market process".
const_vars : [string]
Names of attributes of the Market instance that are used in the "aggregate
market process" but do not come from agents-- they are constant or simply
parameters inherent to the process.
track_vars : [string]
Names of variables generated by the "aggregate market process" that should
be tracked as a "history" so that a new dynamic rule can be calculated.
This is often a subset of sow_vars.
dyn_vars : [string]
Names of variables that constitute a "dynamic rule".
millRule : function
A function that takes inputs named in reap_vars and returns an object
with attributes named in sow_vars. The "aggregate market process" that
transforms individual agent actions/states/data into aggregate data to
be sent back to agents.
calcDynamics : function
A function that takes inputs named in track_vars and returns an object
with attributes named in dyn_vars. Looks at histories of aggregate
variables and generates a new "dynamic rule" for agents to believe and
act on.
act_T : int
The number of times that the "aggregate market process" should be run
in order to generate a history of aggregate variables.
tolerance: float
Minimum acceptable distance between "dynamic rules" to consider the
Market solution process converged. Distance is a user-defined metric.
Returns
-------
None
'''
self.agents = agents
self.reap_vars = reap_vars
self.sow_vars = sow_vars
self.const_vars = const_vars
self.track_vars = track_vars
self.dyn_vars = dyn_vars
if millRule is not None: # To prevent overwriting of method-based millRules
self.millRule = millRule
if calcDynamics is not None: # Ditto for calcDynamics
self.calcDynamics = calcDynamics
self.act_T = act_T
self.tolerance = tolerance
self.max_loops = 1000
def solveAgents(self):
'''
Solves the microeconomic problem for all AgentTypes in this market.
Parameters
----------
None
Returns
-------
None
'''
for this_type in self.agents:
this_type.solve()
def solve(self):
'''
"Solves" the market by finding a "dynamic rule" that governs the aggregate
market state such that when agents believe in these dynamics, their actions
collectively generate the same dynamic rule.
Parameters
----------
None
Returns
-------
None
'''
go = True
max_loops = self.max_loops # Failsafe against infinite solution loop
completed_loops = 0
old_dynamics = None
while go: # Loop until the dynamic process converges or we hit the loop cap
self.solveAgents() # Solve each AgentType's micro problem
self.makeHistory() # "Run" the model while tracking aggregate variables
new_dynamics = self.updateDynamics() # Find a new aggregate dynamic rule
# Check to see if the dynamic rule has converged (if this is not the first loop)
if completed_loops > 0:
distance = new_dynamics.distance(old_dynamics)
else:
distance = 1000000.0
# Move to the next loop if the terminal conditions are not met
old_dynamics = new_dynamics
completed_loops += 1
go = distance >= self.tolerance and completed_loops < max_loops
self.dynamics = new_dynamics # Store the final dynamic rule in self
def reap(self):
'''
Collects attributes named in reap_vars from each AgentType in the market,
storing them in respectively named attributes of self.
Parameters
----------
none
Returns
-------
none
'''
for var_name in self.reap_vars:
harvest = []
for this_type in self.agents:
harvest.append(getattr(this_type,var_name))
setattr(self,var_name,harvest)
def sow(self):
'''
Distributes attrributes named in sow_vars from self to each AgentType