10
10
from itertools import product , count
11
11
import pandas as pd
12
12
from tqdm import tqdm
13
+
14
+ import random
15
+
13
16
try :
14
17
from pathos .multiprocessing import ProcessPool
15
18
except ImportError :
18
21
pathos_support = True
19
22
20
23
21
- class VariableParameterError (TypeError ):
22
- MESSAGE = ('variable_parameters must map a name to a sequence of values . '
23
- 'These parameters were given with non-sequence values : {}' )
24
+ class ParameterError (TypeError ):
25
+ MESSAGE = ('parameters must map a name to a value . '
26
+ 'These names did not match paramerets : {}' )
24
27
25
28
def __init__ (self , bad_names ):
26
29
self .bad_names = bad_names
@@ -29,7 +32,15 @@ def __str__(self):
29
32
return self .MESSAGE .format (self .bad_names )
30
33
31
34
32
- class BatchRunner :
35
+ class VariableParameterError (ParameterError ):
36
+ MESSAGE = ('variable_parameters must map a name to a sequence of values. '
37
+ 'These parameters were given with non-sequence values: {}' )
38
+
39
+ def __init__ (self , bad_names ):
40
+ super ().__init__ (bad_names )
41
+
42
+
43
+ class FixedBatchRunner :
33
44
""" This class is instantiated with a model class, and model parameters
34
45
associated with one or more values. It is also instantiated with model and
35
46
agent-level reporters, dictionaries mapping a variable name to a function
@@ -39,9 +50,8 @@ class BatchRunner:
39
50
Note that by default, the reporters only collect data at the *end* of the
40
51
run. To get step by step data, simply have a reporter store the model's
41
52
entire DataCollector object.
42
-
43
53
"""
44
- def __init__ (self , model_cls , variable_parameters = None ,
54
+ def __init__ (self , model_cls , parameters_list = None ,
45
55
fixed_parameters = None , iterations = 1 , max_steps = 1000 ,
46
56
model_reporters = None , agent_reporters = None ,
47
57
display_progress = True ):
@@ -50,20 +60,20 @@ def __init__(self, model_cls, variable_parameters=None,
50
60
51
61
Args:
52
62
model_cls: The class of model to batch-run.
53
- variable_parameters: Dictionary of parameters to lists of values .
54
- The model will be run with every combo of these paramters.
55
- For example, given variable_parameters of
56
- {"param_1 ": range(5) ,
57
- "param_2 ": [1, 5, 10]}
58
- models will be run with {param_1=1, param_2=1},
59
- {param_1=2, param_2=1}, ..., {param_1=4, param_2=10} .
63
+ parameters_list: A list of dictionaries of parameter sets .
64
+ The model will be run with dictionary of paramters.
65
+ For example, given parameters_list of
66
+ [{"homophily ": 3, "density": 0.8, "minority_pc": 0.2} ,
67
+ {"homophily ": 2, "density": 0.9, "minority_pc": 0.1},
68
+ {"homophily": 4, "density": 0.6, "minority_pc": 0.5}]
69
+ 3 models will be run, one for each provided set of parameters .
60
70
fixed_parameters: Dictionary of parameters that stay same through
61
71
all batch runs. For example, given fixed_parameters of
62
72
{"constant_parameter": 3},
63
73
every instantiated model will be passed constant_parameter=3
64
74
as a kwarg.
65
- iterations: The total number of times to run the model for each
66
- combination of parameters.
75
+ iterations: The total number of times to run the model for each set
76
+ of parameters.
67
77
max_steps: Upper limit of steps above which each run will be halted
68
78
if it hasn't halted on its own.
69
79
model_reporters: The dictionary of variables to collect on each run
@@ -77,9 +87,9 @@ def __init__(self, model_cls, variable_parameters=None,
77
87
78
88
"""
79
89
self .model_cls = model_cls
80
- if variable_parameters is None :
81
- variable_parameters = {}
82
- self .variable_parameters = self . _process_parameters ( variable_parameters )
90
+ if parameters_list is None :
91
+ parameters_list = []
92
+ self .parameters_list = list ( parameters_list )
83
93
self .fixed_parameters = fixed_parameters or {}
84
94
self ._include_fixed = len (self .fixed_parameters .keys ()) > 0
85
95
self .iterations = iterations
@@ -96,16 +106,6 @@ def __init__(self, model_cls, variable_parameters=None,
96
106
97
107
self .display_progress = display_progress
98
108
99
- def _process_parameters (self , params ):
100
- params = copy .deepcopy (params )
101
- bad_names = []
102
- for name , values in params .items ():
103
- if (isinstance (values , str ) or not hasattr (values , "__iter__" )):
104
- bad_names .append (name )
105
- if bad_names :
106
- raise VariableParameterError (bad_names )
107
- return params
108
-
109
109
def _make_model_args (self ):
110
110
"""Prepare all combinations of parameter values for `run_all`
111
111
@@ -117,21 +117,20 @@ def _make_model_args(self):
117
117
all_kwargs = []
118
118
all_param_values = []
119
119
120
- if len (self .variable_parameters ) > 0 :
121
- param_names , param_ranges = zip (* self .variable_parameters .items ())
122
- for param_range in param_ranges :
123
- total_iterations *= len (param_range )
124
-
125
- for param_values in product (* param_ranges ):
126
- kwargs = dict (zip (param_names , param_values ))
120
+ count = len (self .parameters_list )
121
+ if count :
122
+ for params in self .parameters_list :
123
+ kwargs = params .copy ()
127
124
kwargs .update (self .fixed_parameters )
128
125
all_kwargs .append (kwargs )
129
- all_param_values .append (param_values )
130
- else :
131
- kwargs = self .fixed_parameters
132
- param_values = None
133
- all_kwargs = [kwargs ]
134
- all_param_values = [None ]
126
+ all_param_values .append (params .values ())
127
+ elif len (self .fixed_parameters ):
128
+ count = 1
129
+ kwargs = self .fixed_parameters .copy ()
130
+ all_kwargs .append (kwargs )
131
+ all_param_values .append (kwargs .values ())
132
+
133
+ total_iterations *= count
135
134
136
135
return (total_iterations , all_kwargs , all_param_values )
137
136
@@ -154,7 +153,7 @@ def run_iteration(self, kwargs, param_values, run_count):
154
153
155
154
# Collect and store results:
156
155
if param_values is not None :
157
- model_key = param_values + (run_count ,)
156
+ model_key = tuple ( param_values ) + (run_count ,)
158
157
else :
159
158
model_key = (run_count ,)
160
159
@@ -215,7 +214,10 @@ def _prepare_report_table(self, vars_dict, extra_cols=None):
215
214
column as a key.
216
215
"""
217
216
extra_cols = ['Run' ] + (extra_cols or [])
218
- index_cols = list (self .variable_parameters .keys ()) + extra_cols
217
+ index_cols = set ()
218
+ for params in self .parameters_list :
219
+ index_cols |= params .keys ()
220
+ index_cols = list (index_cols ) + extra_cols
219
221
220
222
records = []
221
223
for param_key , values in vars_dict .items ():
@@ -237,6 +239,98 @@ def _prepare_report_table(self, vars_dict, extra_cols=None):
237
239
return ordered
238
240
239
241
242
+ # This is kind of a useless class, but it does carry the 'source' parameters with it
243
+ class ParameterProduct :
244
+ def __init__ (self , variable_parameters ):
245
+ self .param_names , self .param_lists = \
246
+ zip (* (copy .deepcopy (variable_parameters )).items ())
247
+ self ._product = product (* self .param_lists )
248
+
249
+ def __iter__ (self ):
250
+ return self
251
+
252
+ def __next__ (self ):
253
+ return dict (zip (self .param_names , next (self ._product )))
254
+
255
+
256
+ # Roughly inspired by sklearn.model_selection.ParameterSampler. Does not handle
257
+ # distributions, only lists.
258
+ class ParameterSampler :
259
+ def __init__ (self , parameter_lists , n , random_state = None ):
260
+ self .param_names , self .param_lists = \
261
+ zip (* (copy .deepcopy (parameter_lists )).items ())
262
+ self .n = n
263
+ if random_state is None :
264
+ self .random_state = random .Random ()
265
+ elif isinstance (random_state , int ):
266
+ self .random_state = random .Random (random_state )
267
+ else :
268
+ self .random_state = random_state
269
+ self .count = 0
270
+
271
+ def __iter__ (self ):
272
+ return self
273
+
274
+ def __next__ (self ):
275
+ self .count += 1
276
+ if self .count <= self .n :
277
+ return dict (zip (self .param_names , [self .random_state .choice (l ) for l in self .param_lists ]))
278
+ raise StopIteration ()
279
+
280
+
281
+ class BatchRunner (FixedBatchRunner ):
282
+ """ This class is instantiated with a model class, and model parameters
283
+ associated with one or more values. It is also instantiated with model and
284
+ agent-level reporters, dictionaries mapping a variable name to a function
285
+ which collects some data from the model or its agents at the end of the run
286
+ and stores it.
287
+
288
+ Note that by default, the reporters only collect data at the *end* of the
289
+ run. To get step by step data, simply have a reporter store the model's
290
+ entire DataCollector object.
291
+
292
+ """
293
+ def __init__ (self , model_cls , variable_parameters = None ,
294
+ fixed_parameters = None , iterations = 1 , max_steps = 1000 ,
295
+ model_reporters = None , agent_reporters = None ,
296
+ display_progress = True ):
297
+ """ Create a new BatchRunner for a given model with the given
298
+ parameters.
299
+
300
+ Args:
301
+ model_cls: The class of model to batch-run.
302
+ variable_parameters: Dictionary of parameters to lists of values.
303
+ The model will be run with every combo of these paramters.
304
+ For example, given variable_parameters of
305
+ {"param_1": range(5),
306
+ "param_2": [1, 5, 10]}
307
+ models will be run with {param_1=1, param_2=1},
308
+ {param_1=2, param_2=1}, ..., {param_1=4, param_2=10}.
309
+ fixed_parameters: Dictionary of parameters that stay same through
310
+ all batch runs. For example, given fixed_parameters of
311
+ {"constant_parameter": 3},
312
+ every instantiated model will be passed constant_parameter=3
313
+ as a kwarg.
314
+ iterations: The total number of times to run the model for each
315
+ combination of parameters.
316
+ max_steps: Upper limit of steps above which each run will be halted
317
+ if it hasn't halted on its own.
318
+ model_reporters: The dictionary of variables to collect on each run
319
+ at the end, with variable names mapped to a function to collect
320
+ them. For example:
321
+ {"agent_count": lambda m: m.schedule.get_agent_count()}
322
+ agent_reporters: Like model_reporters, but each variable is now
323
+ collected at the level of each agent present in the model at
324
+ the end of the run.
325
+ display_progress: Display progresss bar with time estimation?
326
+
327
+ """
328
+ super ().__init__ (model_cls , ParameterProduct (variable_parameters ),
329
+ fixed_parameters , iterations , max_steps ,
330
+ model_reporters , agent_reporters ,
331
+ display_progress )
332
+
333
+
240
334
class MPSupport (Exception ):
241
335
def __str__ (self ):
242
336
return ("BatchRunnerMP depends on pathos, which is either not "
0 commit comments