CellModules
MultiSimu.py
Go to the documentation of this file.
1from multiprocessing import Pool
2import pandas as pd
3from tqdm import tqdm
4import itertools
5from copy import deepcopy
6from typing import Callable, Dict, List, Any, Union, Iterable
7
8class MultiSimu():
9 r"""
10 Initializes the MultiSimu instance for running multiple simulations with various parameters, conditions, and replicates.
11
12 Parameters
13 ----------
14 : **simuRunner** : callable
15 The function to run the simulation. It should accept the parameters and conditions as input.
16
17 : **params** : dict or list of dict
18 The parameters for the simulation. If a single dictionary is provided, it is wrapped in a list. Each dictionary represents a distinct set of parameters for a simulation run.
19
20 : **replicat** : int, optional, default=1
21 The number of replicates to run for each parameter and condition combination.
22
23 : **conditions** : list, optional, default=[0]
24 A list of conditions to apply to each simulation run.
25
26 : **batch_size_level** : int or str, optional, default=None
27 Determines the size of the batches returned by the iterator. It can be an integer specifying the number of batches, or a string indicating the batch level ('param', 'replicat', or 'condition').
28
29 : **cacheSize** : int or None, optional, default=200
30 The size of the cache for the number of simulations to pre-fetch in parallel.
31
32 : **withTqdm** : bool, optional, default=False
33 If True, progress bars will be displayed using `tqdm` to show the progress of parameter, condition, and replicate iterations.
34
35 : **parallel** : bool, optional, default=True
36 If True, simulations will be run in parallel using multiprocessing. If False, simulations will be run sequentially.
37
38 : **autoIndex** : bool, optional, default=True
39 If True, adds index columns (ID_PARAMETER, ID_CONDITION, ID_REPLICAT) to the output data for identification.
40
41 : **autoConcat** : bool, optional, default=True
42 If True, concatenates the results from different simulations into a single pandas DataFrame.
43
44 Raises
45 ------
46 : **KeyError**
47 - If `params` is neither a list of dictionaries nor a single dictionary.
48 - If `conditions` is not a list.
49
50 Example
51 -------
52 ```python
53 def dataCatcherOnStep(simu):
54 return simu.cells.countState()
55
56 def run_simu(params,condition,replicat):
57 steps = list(range(0,2501,10))
58 MOI, confluence = condition
59 params['input']['Scenario']['infection'] = [[0,MOI]]
60 params['input']['Scenario']['initConfluence'] = confluence
61 data = Simu(params).compute(dataCatcherOnStep,steps)
62 data.index *= params['input']['Scenario']['dt']
63 return data
64
65
66 paramsTest = [params1,params2]
67 conditions = [(0,0.1),
68 (5,0.15),
69 (10,0.2)]
70
71 def storeData(datas):
72 pass
73
74 for d in MultiSimu(run_simu,paramsTest,replicat = 5, conditions=conditions, batch_size_level='param', withTqdm=True):
75 storeData(d)
76
77 # or simple usage for replicate :
78
79 def dataCatcherOnStep(simu):
80 return simu.cells.countState()
81
82 def run_simu(params):
83 steps = list(range(0,2501,10))
84 data = Simu(params).compute(dataCatcherOnStep,steps)
85 data.index *= params['input']['Scenario']['dt']
86 return data
87
88 data = MultiSimu(run_simu,params,replicat=5).get()
89 ```
90 """
91 def __init__(self,
92 simuRunner: Callable,
93 params: dict | list[dict],
94 replicat: int = 1,
95 conditions: list[int] = [0],
96 batch_size_level: int | str | None = None,
97 cacheSize: int | None = 200,
98 withTqdm: bool = False,
99 parallel: bool = True,
100 autoIndex: bool = True,
101 autoConcat: bool = True):
102 if isinstance(params,dict): params = [params]
103 elif not isinstance(params,Iterable): raise KeyError("params must a list of dict or dict")
104 if not isinstance(conditions,list): raise KeyError("conditions must be a list")
105 self.simuRunner = simuRunner
106 self.parallel = parallel
107 self.autoConcat = autoConcat
108 self.autoIndex = autoIndex
109 nbParams = len(params)
110 nbCondition = len(conditions)
111 self.WithParam = nbParams > 1
112 self.WithCondition = nbCondition > 1
113 self.WithReplicat = replicat > 1
114 self.withTqdm = withTqdm
115 if withTqdm: self._init_pbars(nbParams,nbCondition,replicat)
116 if isinstance(batch_size_level,int):realBatchSize=replicat*nbCondition*batch_size_level
117 elif (batch_size_level=='param'): realBatchSize=replicat*nbCondition
118 elif batch_size_level=='condition': realBatchSize=replicat
119 elif batch_size_level=='replicat': realBatchSize=1
120 else: realBatchSize = replicat*nbCondition*nbParams
121 self.realBatchSize = realBatchSize
122 if self.parallel:
123 self.argsIter = self._combine(params,conditions,range(replicat))
124 else:
125 self.argsIter = self._batched(self._combine(params,conditions,range(replicat)),realBatchSize)
126 self.i = 0
127 self.end = False
128 self.cacheSize = cacheSize
129
130 def _feedProcess(self,pool,futures,n=1):
131 if not self.end:
132 for _ in range(n):
133 try:
134 ids, params = next(self.argsIter)
135 futures.append((ids,pool.apply_async(self.simuRunner,params)))
136 except StopIteration:
137 self.end = True
138 break
139
140
141 def __iter__(self):
142 if self.parallel:
143 with Pool() as pool:
144 data = []
145 futures = []
146 self._feedProcess(pool,futures,self.cacheSize)
147 while len(futures)>0:
148 self._feedProcess(pool,futures)
149 ids, simu = futures.pop(0)
150 data.append(self._postProcess(simu.get(), *ids))
151 if len(data) == self.realBatchSize:
152 yield self._autoConcat(data)
153 data = []
154 if len(data)>0:
155 yield self._autoConcat(data)
156 data = []
157 else:
158 for batch in self.argsIter:
159 yield self._autoConcat([self._postProcess(self.simuRunner(*params),*ids) for ids,params in batch])
160 self._close_pbars()
161
162 def get(self) -> Union[pd.DataFrame, List[pd.DataFrame]]:
163 """
164 Retrieve all simulation results at once.
165
166 Returns
167 -------
168 : Union[pd.DataFrame, List[pd.DataFrame]]
169 Returns a single pandas DataFrame if there is only one batch of results.
170 Returns a list of pandas DataFrames if there are multiple batches of results.
171 """
172 ret = list(self)
173 if len(ret)==1: return ret[0]
174 else: return ret
175
176 def _autoConcat(self,datas):
177 nbOutput = len(datas[0]) if (isinstance(datas[0],list) or isinstance(datas[0],tuple)) else 0
178 if nbOutput==1: datas=datas[0]
179 if nbOutput>1:
180 datas = list(zip(*datas))
181 if self.autoConcat:
182 for i in range(len(datas)):
183 datas[i] = pd.concat(datas[i],copy=False)
184 elif self.autoConcat:
185 datas = pd.concat(datas,copy=False)
186 return datas
187
188 def _init_pbars(self,nbParam,nbCondition,nbReplicat):
189 i= 0
190 self.pbars={}
191 if self.WithParam:
192 self.pbars['param']=tqdm(total=nbParam, ncols=80, position=i ,desc='Parameters')
193 self.pbars['param'].updateEach = nbReplicat*nbCondition
194 i+=1
195 if self.WithCondition:
196 self.pbars['condition']=tqdm(total=nbCondition, ncols=80, position=i,desc='Conditions')
197 self.pbars['condition'].updateEach = nbReplicat
198 i+=1
199 if self.WithReplicat:
200 self.pbars['replicat']=tqdm(total=nbReplicat, ncols=80, position=i ,desc=' Replicats')
201 self.pbars['replicat'].updateEach = 1
202 self.total = nbReplicat*nbParam*nbCondition
203
204
205 def _updateBar(self):
206 if self.withTqdm:
207 for pbar in self.pbars.values():
208 if pbar.n >= pbar.total:
209 pbar.n=0
210 pbar.refresh()
211 elif (self.i%pbar.updateEach)==0:
212 pbar.update()
213 pbar.refresh()
214
215 def _postProcess(self,datas,id_param,id_cond,replicat):
216 self.i+=1
217 if self.autoIndex:
218 if isinstance(datas,list) or isinstance(datas,tuple):
219 for data in datas:
220 if isinstance(data,pd.DataFrame):
221 if self.WithParam: data['ID_PARAMETER'] = id_param
222 if self.WithCondition: data['ID_CONDITION'] = id_cond
223 if self.WithReplicat: data['ID_REPLICAT'] = replicat
224 elif isinstance(datas,pd.DataFrame):
225 if self.WithParam: datas['ID_PARAMETER'] = id_param
226 if self.WithCondition: datas['ID_CONDITION'] = id_cond
227 if self.WithReplicat: datas['ID_REPLICAT'] = replicat
228
229 self._updateBar()
230 return datas
231
232 def _combine(self,params,conditions,replicat):
233 for idP,param in enumerate(params):
234 if self.WithCondition:
235 for idC,condition in enumerate(conditions):
236 if self.WithReplicat:
237 for idR in replicat:
238 yield ((idP,idC,idR),(deepcopy(param),condition))
239 else:
240 yield ((idP,idC,None),(deepcopy(param),condition))
241 else:
242 if self.WithReplicat:
243 for idR in replicat:
244 yield ((idP,None,idR),(deepcopy(param),))
245 else:
246 yield ((idP,None,None),(deepcopy(param),))
247
248 def _batched(self,iterable, n):
249 if n < 1: raise ValueError('n must be at least one')
250 iterator = iter(iterable)
251 while batch := tuple(itertools.islice(iterator, n)):
252 yield batch
253
254 def _close_pbars(self):
255 if self.withTqdm:
256 for pbar in self.pbars.values():
257 pbar.close()
Definition: simu.cpp:29
def _batched(self, iterable, n)
Definition: MultiSimu.py:248
def _postProcess(self, datas, id_param, id_cond, replicat)
Definition: MultiSimu.py:215
def _feedProcess(self, pool, futures, n=1)
Definition: MultiSimu.py:130
def _autoConcat(self, datas)
Definition: MultiSimu.py:176
def __init__(self, Callable simuRunner, dict|list[dict] params, int replicat=1, list[int] conditions=[0], int|str|None batch_size_level=None, int|None cacheSize=200, bool withTqdm=False, bool parallel=True, bool autoIndex=True, bool autoConcat=True)
Definition: MultiSimu.py:101
def _init_pbars(self, nbParam, nbCondition, nbReplicat)
Definition: MultiSimu.py:188
Union[pd.DataFrame, List[pd.DataFrame]] get(self)
Definition: MultiSimu.py:162
def _combine(self, params, conditions, replicat)
Definition: MultiSimu.py:232
CompositeGenerator< T > values(T val1, T val2)
Definition: catch.hpp:1968