One of the main reasons people don’t use ABMs is because they can be very slow. While “vanilla Starsim” is quite fast (10,000 agents running for 100 timesteps should take about a second), custom modules, if not properly written, can be quite slow.
The first step of fixing a slow module is to identify the problem. To do this, Starsim includes some built-in profiling tools.
Initializing sim with 10000 agents
Profiling 15 function(s):
<bound method Sim.run of Sim(n=10000; 2000.01.01—2020.01.01; networks=randomnet; diseases=sis)>
<bound method Sim.start_step of Sim(n=10000; 2000.01.01—2020.01.01; networks=randomnet; diseases=sis [...]
<function Module.start_step at 0x734dd522b9c0>
<function Module.start_step at 0x734dd522b9c0>
<bound method SIS.step_state of sis(pars=[init_prev, beta, dur_inf, waning, imm_boost, _n_initial_ca [...]
<bound method DynamicNetwork.step of randomnet(n_edges=50000; pars=[n_contacts, dur, beta]; states=[ [...]
<bound method Infection.step of sis(pars=[init_prev, beta, dur_inf, waning, imm_boost, _n_initial_ca [...]
<bound method People.step_die of People(n=10000; age=30.0±17.3)>
<bound method People.update_results of People(n=10000; age=30.0±17.3)>
<bound method Network.update_results of randomnet(n_edges=50000; pars=[n_contacts, dur, beta]; state [...]
<function SIS.update_results at 0x734dd4bf0860>
<function Module.finish_step at 0x734dd522bba0>
<function Module.finish_step at 0x734dd522bba0>
<bound method People.finish_step of People(n=10000; age=30.0±17.3)>
<bound method Sim.finish_step of Sim(n=10000; 2000.01.01—2020.01.01; networks=randomnet; diseases=si [...]
Running 2000.01.01 ( 0/21) (0.00 s) ———————————————————— 5%
Running 2010.01.01 (10/21) (0.23 s) ••••••••••—————————— 52%
Running 2020.01.01 (20/21) (0.27 s) •••••••••••••••••••• 100%
Elapsed time: 0.280 s
——————————————————————————————————————————————————————————————————————
Profile of networks.Network.update_results: 0.000231578 s (0.0827615%)
——————————————————————————————————————————————————————————————————————
Total time: 0.000231578 s
File: /home/cliffk/idm/starsim/starsim/networks.py
Function: update_results at line 256
Line # Hits Time Per Hit % Time Line Contents
==============================================================
256 def update_results(self):
257 """ Store the number of edges in the network """
258 21 227181.0 10818.1 98.1 self.results['n_edges'][self.ti] = len(self)
259 21 4397.0 209.4 1.9 return
———————————————————————————————————————————————————————————————
Profile of people.People.finish_step: 0.000352856 s (0.126104%)
———————————————————————————————————————————————————————————————
Total time: 0.000352856 s
File: /home/cliffk/idm/starsim/starsim/people.py
Function: finish_step at line 412
Line # Hits Time Per Hit % Time Line Contents
==============================================================
412 def finish_step(self):
413 21 285623.0 13601.1 80.9 self.remove_dead()
414 21 63322.0 3015.3 17.9 self.update_post()
415 21 3911.0 186.2 1.1 return
———————————————————————————————————————————————————————————
Profile of people.People.step_die: 0.000493424 s (0.17634%)
———————————————————————————————————————————————————————————
Total time: 0.000493424 s
File: /home/cliffk/idm/starsim/starsim/people.py
Function: step_die at line 375
Line # Hits Time Per Hit % Time Line Contents
==============================================================
375 def step_die(self):
376 """ Carry out any deaths that took place this timestep """
377 21 375549.0 17883.3 76.1 death_uids = (self.ti_dead <= self.sim.ti).uids
378 21 34331.0 1634.8 7.0 self.alive[death_uids] = False
379
380 # Execute deaths that took place this timestep (i.e., changing the `alive` state of the agents). This is executed
381 # before analyzers have run so that analyzers are able to inspect and record outcomes for agents that died this timestep
382 42 56941.0 1355.7 11.5 for disease in self.sim.diseases():
383 21 7886.0 375.5 1.6 if isinstance(disease, ss.Disease):
384 21 11104.0 528.8 2.3 disease.step_die(death_uids)
385
386 21 7613.0 362.5 1.5 return death_uids
——————————————————————————————————————————————————————————————————
Profile of people.People.update_results: 0.000978376 s (0.349653%)
——————————————————————————————————————————————————————————————————
Total time: 0.000978376 s
File: /home/cliffk/idm/starsim/starsim/people.py
Function: update_results at line 404
Line # Hits Time Per Hit % Time Line Contents
==============================================================
404 def update_results(self):
405 21 10157.0 483.7 1.0 ti = self.sim.ti
406 21 4219.0 200.9 0.4 res = self.sim.results
407 21 271126.0 12910.8 27.7 res.n_alive[ti] = np.count_nonzero(self.alive)
408 21 308582.0 14694.4 31.5 res.new_deaths[ti] = np.count_nonzero(self.ti_dead == ti)
409 21 373968.0 17808.0 38.2 res.cum_deaths[ti] = np.sum(res.new_deaths[:ti]) # TODO: inefficient to compute the cumulative sum on every timestep!
410 21 10324.0 491.6 1.1 return
————————————————————————————————————————————————————
Profile of diseases.py:670: 0.00132272 s (0.472715%)
————————————————————————————————————————————————————
Total time: 0.00132272 s
File: /home/cliffk/idm/starsim/starsim/diseases.py
Function: update_results at line 670
Line # Hits Time Per Hit % Time Line Contents
==============================================================
670 @ss.required()
671 def update_results(self):
672 """ Store the population immunity (susceptibility) """
673 21 904294.0 43061.6 68.4 super().update_results()
674 21 412859.0 19660.0 31.2 self.results['rel_sus'][self.ti] = self.rel_sus.mean()
675 21 5563.0 264.9 0.4 return
——————————————————————————————————————————————————————
Profile of sim.Sim.start_step: 0.00210333 s (0.75169%)
——————————————————————————————————————————————————————
Total time: 0.00210333 s
File: /home/cliffk/idm/starsim/starsim/sim.py
Function: start_step at line 305
Line # Hits Time Per Hit % Time Line Contents
==============================================================
305 def start_step(self):
306 """ Start the step -- only print progress; all actual changes happen in the modules """
307
308 # Set the time and if we have reached the end of the simulation, then do nothing
309 21 3404.0 162.1 0.2 if self.complete:
310 errormsg = 'Simulation already complete (call sim.init() to re-run)'
311 raise AlreadyRunError(errormsg)
312
313 # Print out progress if needed
314 21 1308341.0 62302.0 62.2 self.elapsed = self.timer.toc(output=True)
315 21 5647.0 268.9 0.3 if self.verbose: # Print progress
316 21 2410.0 114.8 0.1 t = self.t
317 21 57381.0 2732.4 2.7 simlabel = f'"{self.label}": ' if self.label else ''
318 21 571023.0 27191.6 27.1 string = f' Running {simlabel}{t.now("str")} ({t.ti:2.0f}/{t.npts}) ({self.elapsed:0.2f} s) '
319 21 4793.0 228.2 0.2 if self.verbose >= 1:
320 sc.heading(string)
321 21 3321.0 158.1 0.2 elif self.verbose > 0:
322 21 12749.0 607.1 0.6 if not (t.ti % int(1.0 / self.verbose)):
323 3 120894.0 40298.0 5.7 sc.progressbar(t.ti + 1, t.npts, label=string, length=20, newline=True)
324 21 13369.0 636.6 0.6 return
————————————————————————————————————————————————————————————
Profile of diseases.SIS.step_state: 0.00240363 s (0.859011%)
————————————————————————————————————————————————————————————
Total time: 0.00240363 s
File: /home/cliffk/idm/starsim/starsim/diseases.py
Function: step_state at line 631
Line # Hits Time Per Hit % Time Line Contents
==============================================================
631 def step_state(self):
632 """ Progress infectious -> recovered """
633 21 553840.0 26373.3 23.0 recovered = (self.infected & (self.ti_recovered <= self.ti)).uids
634 21 46091.0 2194.8 1.9 self.infected[recovered] = False
635 21 23852.0 1135.8 1.0 self.susceptible[recovered] = True
636 21 1772016.0 84381.7 73.7 self.update_immunity()
637 21 7830.0 372.9 0.3 return
——————————————————————————————————————————————————
Profile of modules.py:665: 0.00423679 s (1.51415%)
——————————————————————————————————————————————————
Total time: 0.00423679 s
File: /home/cliffk/idm/starsim/starsim/modules.py
Function: start_step at line 665
Line # Hits Time Per Hit % Time Line Contents
==============================================================
665 @required()
666 def start_step(self):
667 """ Tasks to perform at the beginning of the step """
668 42 9779.0 232.8 0.2 if self.finalized:
669 errormsg = f'The module {self._debug_name} has already been run. Did you mean to copy it before running it?'
670 raise RuntimeError(errormsg)
671 42 8051.0 191.7 0.2 if self.dists is not None: # Will be None if no distributions are defined
672 42 4202803.0 100066.7 99.2 self.dists.jump_dt() # Advance random number generators forward for calls on this step
673 42 16157.0 384.7 0.4 return
———————————————————————————————————————————————————————————————
Profile of networks.DynamicNetwork.step: 0.0253281 s (9.05178%)
———————————————————————————————————————————————————————————————
Total time: 0.0253281 s
File: /home/cliffk/idm/starsim/starsim/networks.py
Function: step at line 413
Line # Hits Time Per Hit % Time Line Contents
==============================================================
413 def step(self):
414 21 5178577.0 246598.9 20.4 self.end_pairs()
415 21 20143364.0 959207.8 79.5 self.add_pairs()
416 21 6194.0 295.0 0.0 return
—————————————————————————————————————————————————————————
Profile of diseases.Infection.step: 0.236692 s (84.5892%)
—————————————————————————————————————————————————————————
Total time: 0.236692 s
File: /home/cliffk/idm/starsim/starsim/diseases.py
Function: step at line 208
Line # Hits Time Per Hit % Time Line Contents
==============================================================
208 def step(self):
209 """
210 Perform key infection updates, including infection and setting prognoses
211 """
212 # Create new cases
213 21 229577971.0 1e+07 97.0 new_cases, sources, networks = self.infect() # TODO: store outputs in self or use objdict rather than 3 returns
214
215 # Set prognoses
216 21 7046.0 335.5 0.0 if len(new_cases):
217 21 7098351.0 338016.7 3.0 self.set_outcomes(new_cases, sources)
218
219 21 8470.0 403.3 0.0 return new_cases, sources, networks
Figure(672x480)
This graph (which is a shortcut to sim.loop.plot_cpu()) shows us how much time each step in the integration loop takes. We can get line-by-line detail of where each function is taking time, though:
prof.disp(maxentries=5)
——————————————————————————————————————————————————————
Profile of sim.Sim.start_step: 0.00210333 s (0.75169%)
——————————————————————————————————————————————————————
Total time: 0.00210333 s
File: /home/cliffk/idm/starsim/starsim/sim.py
Function: start_step at line 305
Line # Hits Time Per Hit % Time Line Contents
==============================================================
305 def start_step(self):
306 """ Start the step -- only print progress; all actual changes happen in the modules """
307
308 # Set the time and if we have reached the end of the simulation, then do nothing
309 21 3404.0 162.1 0.2 if self.complete:
310 errormsg = 'Simulation already complete (call sim.init() to re-run)'
311 raise AlreadyRunError(errormsg)
312
313 # Print out progress if needed
314 21 1308341.0 62302.0 62.2 self.elapsed = self.timer.toc(output=True)
315 21 5647.0 268.9 0.3 if self.verbose: # Print progress
316 21 2410.0 114.8 0.1 t = self.t
317 21 57381.0 2732.4 2.7 simlabel = f'"{self.label}": ' if self.label else ''
318 21 571023.0 27191.6 27.1 string = f' Running {simlabel}{t.now("str")} ({t.ti:2.0f}/{t.npts}) ({self.elapsed:0.2f} s) '
319 21 4793.0 228.2 0.2 if self.verbose >= 1:
320 sc.heading(string)
321 21 3321.0 158.1 0.2 elif self.verbose > 0:
322 21 12749.0 607.1 0.6 if not (t.ti % int(1.0 / self.verbose)):
323 3 120894.0 40298.0 5.7 sc.progressbar(t.ti + 1, t.npts, label=string, length=20, newline=True)
324 21 13369.0 636.6 0.6 return
————————————————————————————————————————————————————————————
Profile of diseases.SIS.step_state: 0.00240363 s (0.859011%)
————————————————————————————————————————————————————————————
Total time: 0.00240363 s
File: /home/cliffk/idm/starsim/starsim/diseases.py
Function: step_state at line 631
Line # Hits Time Per Hit % Time Line Contents
==============================================================
631 def step_state(self):
632 """ Progress infectious -> recovered """
633 21 553840.0 26373.3 23.0 recovered = (self.infected & (self.ti_recovered <= self.ti)).uids
634 21 46091.0 2194.8 1.9 self.infected[recovered] = False
635 21 23852.0 1135.8 1.0 self.susceptible[recovered] = True
636 21 1772016.0 84381.7 73.7 self.update_immunity()
637 21 7830.0 372.9 0.3 return
——————————————————————————————————————————————————
Profile of modules.py:665: 0.00423679 s (1.51415%)
——————————————————————————————————————————————————
Total time: 0.00423679 s
File: /home/cliffk/idm/starsim/starsim/modules.py
Function: start_step at line 665
Line # Hits Time Per Hit % Time Line Contents
==============================================================
665 @required()
666 def start_step(self):
667 """ Tasks to perform at the beginning of the step """
668 42 9779.0 232.8 0.2 if self.finalized:
669 errormsg = f'The module {self._debug_name} has already been run. Did you mean to copy it before running it?'
670 raise RuntimeError(errormsg)
671 42 8051.0 191.7 0.2 if self.dists is not None: # Will be None if no distributions are defined
672 42 4202803.0 100066.7 99.2 self.dists.jump_dt() # Advance random number generators forward for calls on this step
673 42 16157.0 384.7 0.4 return
———————————————————————————————————————————————————————————————
Profile of networks.DynamicNetwork.step: 0.0253281 s (9.05178%)
———————————————————————————————————————————————————————————————
Total time: 0.0253281 s
File: /home/cliffk/idm/starsim/starsim/networks.py
Function: step at line 413
Line # Hits Time Per Hit % Time Line Contents
==============================================================
413 def step(self):
414 21 5178577.0 246598.9 20.4 self.end_pairs()
415 21 20143364.0 959207.8 79.5 self.add_pairs()
416 21 6194.0 295.0 0.0 return
—————————————————————————————————————————————————————————
Profile of diseases.Infection.step: 0.236692 s (84.5892%)
—————————————————————————————————————————————————————————
Total time: 0.236692 s
File: /home/cliffk/idm/starsim/starsim/diseases.py
Function: step at line 208
Line # Hits Time Per Hit % Time Line Contents
==============================================================
208 def step(self):
209 """
210 Perform key infection updates, including infection and setting prognoses
211 """
212 # Create new cases
213 21 229577971.0 1e+07 97.0 new_cases, sources, networks = self.infect() # TODO: store outputs in self or use objdict rather than 3 returns
214
215 # Set prognoses
216 21 7046.0 335.5 0.0 if len(new_cases):
217 21 7098351.0 338016.7 3.0 self.set_outcomes(new_cases, sources)
218
219 21 8470.0 403.3 0.0 return new_cases, sources, networks
(Note that the names of the functions here refer to the actual functions called, which may not match the graph above. That’s because, for example, ss.SIS does not define its own step() method, but instead inherits step() from Infection. In the graph, this is shown as sis.step(), but is listed in the table as Infection.step(). This is because it’s referring to the actual code being run, so refers to where those lines of code exist in the codebase; there is no code corresponding to SIS.step() since it’s just inherited from Infection.step().)
If you want more detail, you can also define custom functions to follow. For example, we can see that ss.SIS.infect() takes the most time in ss.SIS.step(), so let’s profile that:
Initializing sim with 10000 agents
Profiling 1 function(s):
<function Infection.infect at 0x734dd4be3420>
Running 2000.01.01 ( 0/21) (0.00 s) ———————————————————— 5%
Running 2010.01.01 (10/21) (0.04 s) ••••••••••—————————— 52%
Running 2020.01.01 (20/21) (0.07 s) •••••••••••••••••••• 100%
Elapsed time: 82.0 ms
————————————————————————————————————————————————————————————
Profile of diseases.Infection.infect: 0.0366014 s (44.6268%)
————————————————————————————————————————————————————————————
Total time: 0.0366014 s
File: /home/cliffk/idm/starsim/starsim/diseases.py
Function: infect at line 230
Line # Hits Time Per Hit % Time Line Contents
==============================================================
230 def infect(self):
231 """ Determine who gets infected on this timestep via transmission on the network """
232 21 7646.0 364.1 0.0 new_cases = []
233 21 3833.0 182.5 0.0 sources = []
234 21 1635.0 77.9 0.0 networks = []
235 21 212085.0 10099.3 0.6 betamap = self.validate_beta()
236
237 21 578626.0 27553.6 1.6 rel_trans = self.rel_trans.asnew(self.infectious * self.rel_trans)
238 21 303527.0 14453.7 0.8 rel_sus = self.rel_sus.asnew(self.susceptible * self.rel_sus)
239
240 42 52155.0 1241.8 0.1 for i, (nkey,route) in enumerate(self.sim.networks.items()):
241 21 10358.0 493.2 0.0 nk = ss.standardize_netkey(nkey)
242
243 # Main use case: networks
244 21 5025.0 239.3 0.0 if isinstance(route, ss.Network):
245 21 61713.0 2938.7 0.2 if len(route): # Skip networks with no edges
246 21 3179.0 151.4 0.0 edges = route.edges
247 21 61276.0 2917.9 0.2 p1p2b0 = [edges.p1, edges.p2, betamap[nk][0]] # Person 1, person 2, beta 0
248 21 53037.0 2525.6 0.1 p2p1b1 = [edges.p2, edges.p1, betamap[nk][1]] # Person 2, person 1, beta 1
249 63 23561.0 374.0 0.1 for src, trg, beta in [p1p2b0, p2p1b1]:
250 42 28792.0 685.5 0.1 if beta: # Skip networks with no transmission
251 42 487177.0 11599.5 1.3 disease_beta = beta.to_prob(self.t.dt) if isinstance(beta, ss.Rate) else beta
252 42 1220398.0 29057.1 3.3 beta_per_dt = route.net_beta(disease_beta=disease_beta, disease=self) # Compute beta for this network and timestep
253 42 26205771.0 623946.9 71.6 randvals = self.trans_rng.rvs(src, trg) # Generate a new random number based on the two other random numbers
254 42 17873.0 425.5 0.0 args = (src, trg, rel_trans, rel_sus, beta_per_dt, randvals) # Set up the arguments to calculate transmission
255 42 5888228.0 140195.9 16.1 target_uids, source_uids = self.compute_transmission(*args) # Actually calculate it
256 42 7603.0 181.0 0.0 new_cases.append(target_uids)
257 42 4283.0 102.0 0.0 sources.append(source_uids)
258 42 247614.0 5895.6 0.7 networks.append(np.full(len(target_uids), dtype=ss_int, fill_value=i))
259
260 # Handle everything else: mixing pools, environmental transmission, etc.
261 elif isinstance(route, ss.Route):
262 # Mixing pools are unidirectional, only use the first beta value
263 disease_beta = betamap[nk][0].to_prob(self.t.dt) if isinstance(betamap[nk][0], ss.Rate) else betamap[nk][0]
264 target_uids = route.compute_transmission(rel_sus, rel_trans, disease_beta, disease=self)
265 new_cases.append(target_uids)
266 sources.append(np.full(len(target_uids), dtype=ss_float, fill_value=np.nan))
267 networks.append(np.full(len(target_uids), dtype=ss_int, fill_value=i))
268 else:
269 errormsg = f'Cannot compute transmission via route {type(route)}; please subclass ss.Route and define a compute_transmission() method'
270 raise TypeError(errormsg)
271
272 # Finalize
273 21 7544.0 359.2 0.0 if len(new_cases) and len(sources):
274 21 132806.0 6324.1 0.4 new_cases = ss.uids.cat(new_cases)
275 21 791759.0 37702.8 2.2 new_cases, inds = new_cases.unique(return_index=True)
276 21 94972.0 4522.5 0.3 sources = ss.uids.cat(sources)[inds]
277 21 49364.0 2350.7 0.1 networks = np.concatenate(networks)[inds]
278 else:
279 new_cases = ss.uids()
280 sources = ss.uids()
281 networks = np.empty(0, dtype=ss_int)
282
283 21 39545.0 1883.1 0.1 return new_cases, sources, networks
————————————————————————————————————————————————————————————
Profile of diseases.Infection.infect: 0.0366014 s (44.6268%)
————————————————————————————————————————————————————————————
Total time: 0.0366014 s
File: /home/cliffk/idm/starsim/starsim/diseases.py
Function: infect at line 230
Line # Hits Time Per Hit % Time Line Contents
==============================================================
230 def infect(self):
231 """ Determine who gets infected on this timestep via transmission on the network """
232 21 7646.0 364.1 0.0 new_cases = []
233 21 3833.0 182.5 0.0 sources = []
234 21 1635.0 77.9 0.0 networks = []
235 21 212085.0 10099.3 0.6 betamap = self.validate_beta()
236
237 21 578626.0 27553.6 1.6 rel_trans = self.rel_trans.asnew(self.infectious * self.rel_trans)
238 21 303527.0 14453.7 0.8 rel_sus = self.rel_sus.asnew(self.susceptible * self.rel_sus)
239
240 42 52155.0 1241.8 0.1 for i, (nkey,route) in enumerate(self.sim.networks.items()):
241 21 10358.0 493.2 0.0 nk = ss.standardize_netkey(nkey)
242
243 # Main use case: networks
244 21 5025.0 239.3 0.0 if isinstance(route, ss.Network):
245 21 61713.0 2938.7 0.2 if len(route): # Skip networks with no edges
246 21 3179.0 151.4 0.0 edges = route.edges
247 21 61276.0 2917.9 0.2 p1p2b0 = [edges.p1, edges.p2, betamap[nk][0]] # Person 1, person 2, beta 0
248 21 53037.0 2525.6 0.1 p2p1b1 = [edges.p2, edges.p1, betamap[nk][1]] # Person 2, person 1, beta 1
249 63 23561.0 374.0 0.1 for src, trg, beta in [p1p2b0, p2p1b1]:
250 42 28792.0 685.5 0.1 if beta: # Skip networks with no transmission
251 42 487177.0 11599.5 1.3 disease_beta = beta.to_prob(self.t.dt) if isinstance(beta, ss.Rate) else beta
252 42 1220398.0 29057.1 3.3 beta_per_dt = route.net_beta(disease_beta=disease_beta, disease=self) # Compute beta for this network and timestep
253 42 26205771.0 623946.9 71.6 randvals = self.trans_rng.rvs(src, trg) # Generate a new random number based on the two other random numbers
254 42 17873.0 425.5 0.0 args = (src, trg, rel_trans, rel_sus, beta_per_dt, randvals) # Set up the arguments to calculate transmission
255 42 5888228.0 140195.9 16.1 target_uids, source_uids = self.compute_transmission(*args) # Actually calculate it
256 42 7603.0 181.0 0.0 new_cases.append(target_uids)
257 42 4283.0 102.0 0.0 sources.append(source_uids)
258 42 247614.0 5895.6 0.7 networks.append(np.full(len(target_uids), dtype=ss_int, fill_value=i))
259
260 # Handle everything else: mixing pools, environmental transmission, etc.
261 elif isinstance(route, ss.Route):
262 # Mixing pools are unidirectional, only use the first beta value
263 disease_beta = betamap[nk][0].to_prob(self.t.dt) if isinstance(betamap[nk][0], ss.Rate) else betamap[nk][0]
264 target_uids = route.compute_transmission(rel_sus, rel_trans, disease_beta, disease=self)
265 new_cases.append(target_uids)
266 sources.append(np.full(len(target_uids), dtype=ss_float, fill_value=np.nan))
267 networks.append(np.full(len(target_uids), dtype=ss_int, fill_value=i))
268 else:
269 errormsg = f'Cannot compute transmission via route {type(route)}; please subclass ss.Route and define a compute_transmission() method'
270 raise TypeError(errormsg)
271
272 # Finalize
273 21 7544.0 359.2 0.0 if len(new_cases) and len(sources):
274 21 132806.0 6324.1 0.4 new_cases = ss.uids.cat(new_cases)
275 21 791759.0 37702.8 2.2 new_cases, inds = new_cases.unique(return_index=True)
276 21 94972.0 4522.5 0.3 sources = ss.uids.cat(sources)[inds]
277 21 49364.0 2350.7 0.1 networks = np.concatenate(networks)[inds]
278 else:
279 new_cases = ss.uids()
280 sources = ss.uids()
281 networks = np.empty(0, dtype=ss_int)
282
283 21 39545.0 1883.1 0.1 return new_cases, sources, networks
(Note: you can only follow functions that are called as part of sim.run() this way. To follow other functions, such as those run by sim.init(), you can use sc.profile() directly.)
Debugging
When figuring out what your sim is doing – whether it’s doing something it shouldn’t be, or not doing something it should – sim.loop is your friend. It shows everything that will happen in the sim, and in what order:
As you can see, it’s a lot – this is only three timesteps and two modules, and it’s already 41 steps.
The typical way to do debugging is to insert breakpoints or print statements into your modules for custom debugging (e.g., to print a value), or to use analyzers for heavier-lift debugging. Starsim also lets you manually modify the loop by inserting “probes” or other arbitrary functions. For example, if you wanted to check the population size after each time the People object is updated:
Initializing sim with 10000 agents
Running 2000.01.01 ( 0/11) (0.00 s) •——————————————————— 9%
Population size is 10191
Population size is 10279
Population size is 10377
Population size is 10485
Population size is 10563
Population size is 10651
Population size is 10728
Population size is 10803
Population size is 10901
Population size is 11008
Running 2010.01.01 (10/11) (0.06 s) •••••••••••••••••••• 100%
Population size is 11099
Initializing sim with 10000 agents
Running 2000.01.01 ( 0/11) (0.00 s) •——————————————————— 9%
Population size is 10191
Population size is 10279
Population size is 10377
Population size is 10485
Population size is 10563
Population size is 10651
Population size is 10728
Population size is 10803
Population size is 10901
Population size is 11008
Running 2010.01.01 (10/11) (0.06 s) •••••••••••••••••••• 100%
Population size is 11099
However, inserting functions directly in the loop gives you more control over their exact placement, whereas analyzers are always executed last in the timestep.
The loop also has methods for visualizing itself. You can get a simple representation of the loop with loop.plot():
sim.loop.plot()
Figure(672x480)
Or a slightly more detailed one with loop.plot_step_order():
sim.loop.plot_step_order()
Figure(672x480)
This is especially useful if your simulation has modules with different timesteps, e.g.:
Initializing sim with 10000 agents
Figure(672x480)
(Note: this is a 3D plot, so it helps if you can plot it in a separate window interactively to be able to move it around, rather than just in a notebook.)