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 0x7f60e7a494e0>
<function Module.start_step at 0x7f60e7a494e0>
<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 0x7f60e738efc0>
<function Module.finish_step at 0x7f60e7a496c0>
<function Module.finish_step at 0x7f60e7a496c0>
<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.94 s) ••••••••••—————————— 52%
Running 2020.01.01 (20/21) (1.03 s) •••••••••••••••••••• 100%
Elapsed time: 1.05 s
———————————————————————————————————————————————————————
Profile of networks.py:None: 0.000451329 s (0.0427846%)
———————————————————————————————————————————————————————
Total time: 0.000451329 s
File: /home/runner/work/starsim/starsim/starsim/networks.py
Function: Network.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 439446.0 20926.0 97.4 self.results['n_edges'][self.ti] = len(self)
259 21 11883.0 565.9 2.6 return
—————————————————————————————————————————————————————
Profile of people.py:None_2: 0.00131547 s (0.124702%)
—————————————————————————————————————————————————————
Total time: 0.00131547 s
File: /home/runner/work/starsim/starsim/starsim/people.py
Function: People.finish_step at line 422
Line # Hits Time Per Hit % Time Line Contents
==============================================================
422 def finish_step(self):
423 21 972613.0 46314.9 73.9 self.remove_dead()
424 21 330697.0 15747.5 25.1 self.update_post()
425 21 12162.0 579.1 0.9 return
——————————————————————————————————————————————————
Profile of people.py:None: 0.00234048 s (0.22187%)
——————————————————————————————————————————————————
Total time: 0.00234048 s
File: /home/runner/work/starsim/starsim/starsim/people.py
Function: People.step_die at line 384
Line # Hits Time Per Hit % Time Line Contents
==============================================================
384 def step_die(self):
385 """ Carry out any deaths or removals that took place this timestep """
386 21 1972493.0 93928.2 84.3 death_uids = ((self.ti_dead <= self.sim.ti) | (self.ti_removed <= self.sim.ti)).uids
387 21 105997.0 5047.5 4.5 self.alive[death_uids] = False # Whilst not dead, removed agents should not be included in alive totals
388
389 # Execute deaths that took place this timestep (i.e., changing the `alive` state of the agents). This is executed
390 # before analyzers have run so that analyzers are able to inspect and record outcomes for agents that died this timestep
391 42 181378.0 4318.5 7.7 for disease in self.sim.diseases():
392 21 26010.0 1238.6 1.1 if isinstance(disease, ss.Disease):
393 21 41951.0 1997.7 1.8 disease.step_die(death_uids)
394
395 21 12646.0 602.2 0.5 return death_uids
———————————————————————————————————————————————————————
Profile of diseases.py:None_2: 0.00404944 s (0.383874%)
———————————————————————————————————————————————————————
Total time: 0.00404944 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: SIS.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 2922453.0 139164.4 72.2 super().update_results()
674 21 1113378.0 53018.0 27.5 self.results['rel_sus'][self.ti] = self.rel_sus.mean()
675 21 13604.0 647.8 0.3 return
———————————————————————————————————————————————————
Profile of people.py:None_1: 0.004075 s (0.386297%)
———————————————————————————————————————————————————
Total time: 0.004075 s
File: /home/runner/work/starsim/starsim/starsim/people.py
Function: People.update_results at line 413
Line # Hits Time Per Hit % Time Line Contents
==============================================================
413 def update_results(self):
414 21 38804.0 1847.8 1.0 ti = self.sim.ti
415 21 16142.0 768.7 0.4 res = self.sim.results
416 21 818097.0 38957.0 20.1 res.n_alive[ti] = np.count_nonzero(self.alive)
417 21 1121660.0 53412.4 27.5 res.new_deaths[ti] = np.count_nonzero(self.ti_dead == ti)
418 21 1000043.0 47621.1 24.5 res.new_emigrants[ti] = np.count_nonzero(self.ti_removed == ti)
419 21 1065898.0 50757.0 26.2 res.cum_deaths[ti] = np.sum(res.new_deaths[:ti]) # TODO: inefficient to compute the cumulative sum on every timestep!
420 21 14353.0 683.5 0.4 return
—————————————————————————————————————————————————————
Profile of diseases.py:None_1: 0.0060924 s (0.57754%)
—————————————————————————————————————————————————————
Total time: 0.0060924 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: SIS.step_state at line 631
Line # Hits Time Per Hit % Time Line Contents
==============================================================
631 def step_state(self):
632 """ Progress infectious -> recovered """
633 21 2123045.0 101097.4 34.8 recovered = (self.infected & (self.ti_recovered <= self.ti)).uids
634 21 120234.0 5725.4 2.0 self.infected[recovered] = False
635 21 77316.0 3681.7 1.3 self.susceptible[recovered] = True
636 21 3756993.0 178904.4 61.7 self.update_immunity()
637 21 14808.0 705.1 0.2 return
————————————————————————————————————————————————
Profile of sim.py:None: 0.00688712 s (0.652877%)
————————————————————————————————————————————————
Total time: 0.00688712 s
File: /home/runner/work/starsim/starsim/starsim/sim.py
Function: Sim.start_step at line 306
Line # Hits Time Per Hit % Time Line Contents
==============================================================
306 def start_step(self):
307 """ Start the step -- only print progress; all actual changes happen in the modules """
308
309 # Set the time and if we have reached the end of the simulation, then do nothing
310 21 14918.0 710.4 0.2 if self.complete:
311 errormsg = 'Simulation already complete (call sim.init() to re-run)'
312 raise AlreadyRunError(errormsg)
313
314 # Print out progress if needed
315 21 4852435.0 231068.3 70.5 self.elapsed = self.timer.toc(output=True)
316 21 18357.0 874.1 0.3 if self.verbose: # Print progress
317 21 28434.0 1354.0 0.4 t = self.t
318 21 298125.0 14196.4 4.3 simlabel = f'"{self.label}": ' if self.label else ''
319 21 960922.0 45758.2 14.0 string = f' Running {simlabel}{t.now("str")} ({t.ti:2.0f}/{t.npts}) ({self.elapsed:0.2f} s) '
320 21 20278.0 965.6 0.3 if self.verbose >= 1:
321 sc.heading(string)
322 21 14829.0 706.1 0.2 elif self.verbose > 0:
323 21 27727.0 1320.3 0.4 if not (t.ti % int(1.0 / self.verbose)):
324 3 635780.0 211926.7 9.2 sc.progressbar(t.ti + 1, t.npts, label=string, length=20, newline=True)
325 21 15319.0 729.5 0.2 return
——————————————————————————————————————————————————
Profile of modules.py:None: 0.0152785 s (1.44835%)
——————————————————————————————————————————————————
Total time: 0.0152785 s
File: /home/runner/work/starsim/starsim/starsim/modules.py
Function: Module.start_step at line 670
Line # Hits Time Per Hit % Time Line Contents
==============================================================
670 @required()
671 def start_step(self):
672 """ Tasks to perform at the beginning of the step """
673 42 37999.0 904.7 0.2 if self.finalized:
674 errormsg = f'The module {self._debug_name} has already been run. Did you mean to copy it before running it?'
675 raise RuntimeError(errormsg)
676 42 33582.0 799.6 0.2 if self.dists is not None: # Will be None if no distributions are defined
677 42 15171830.0 361234.0 99.3 self.dists.jump_dt() # Advance random number generators forward for calls on this step
678 42 35108.0 835.9 0.2 return
—————————————————————————————————————————————————————
Profile of networks.py:None_1: 0.0632225 s (5.99329%)
—————————————————————————————————————————————————————
Total time: 0.0632225 s
File: /home/runner/work/starsim/starsim/starsim/networks.py
Function: DynamicNetwork.step at line 413
Line # Hits Time Per Hit % Time Line Contents
==============================================================
413 def step(self):
414 21 17797946.0 847521.2 28.2 self.end_pairs()
415 21 45392561.0 2.16e+06 71.8 self.add_pairs()
416 21 32020.0 1524.8 0.1 return
——————————————————————————————————————————————————
Profile of diseases.py:None: 0.932284 s (88.3776%)
——————————————————————————————————————————————————
Total time: 0.932284 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: Infection.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 913941045.0 4.35e+07 98.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 23323.0 1110.6 0.0 if len(new_cases):
217 21 18299245.0 871392.6 2.0 self.set_outcomes(new_cases, sources)
218
219 21 19896.0 947.4 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 diseases.py:None_1: 0.0060924 s (0.57754%)
—————————————————————————————————————————————————————
Total time: 0.0060924 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: SIS.step_state at line 631
Line # Hits Time Per Hit % Time Line Contents
==============================================================
631 def step_state(self):
632 """ Progress infectious -> recovered """
633 21 2123045.0 101097.4 34.8 recovered = (self.infected & (self.ti_recovered <= self.ti)).uids
634 21 120234.0 5725.4 2.0 self.infected[recovered] = False
635 21 77316.0 3681.7 1.3 self.susceptible[recovered] = True
636 21 3756993.0 178904.4 61.7 self.update_immunity()
637 21 14808.0 705.1 0.2 return
————————————————————————————————————————————————
Profile of sim.py:None: 0.00688712 s (0.652877%)
————————————————————————————————————————————————
Total time: 0.00688712 s
File: /home/runner/work/starsim/starsim/starsim/sim.py
Function: Sim.start_step at line 306
Line # Hits Time Per Hit % Time Line Contents
==============================================================
306 def start_step(self):
307 """ Start the step -- only print progress; all actual changes happen in the modules """
308
309 # Set the time and if we have reached the end of the simulation, then do nothing
310 21 14918.0 710.4 0.2 if self.complete:
311 errormsg = 'Simulation already complete (call sim.init() to re-run)'
312 raise AlreadyRunError(errormsg)
313
314 # Print out progress if needed
315 21 4852435.0 231068.3 70.5 self.elapsed = self.timer.toc(output=True)
316 21 18357.0 874.1 0.3 if self.verbose: # Print progress
317 21 28434.0 1354.0 0.4 t = self.t
318 21 298125.0 14196.4 4.3 simlabel = f'"{self.label}": ' if self.label else ''
319 21 960922.0 45758.2 14.0 string = f' Running {simlabel}{t.now("str")} ({t.ti:2.0f}/{t.npts}) ({self.elapsed:0.2f} s) '
320 21 20278.0 965.6 0.3 if self.verbose >= 1:
321 sc.heading(string)
322 21 14829.0 706.1 0.2 elif self.verbose > 0:
323 21 27727.0 1320.3 0.4 if not (t.ti % int(1.0 / self.verbose)):
324 3 635780.0 211926.7 9.2 sc.progressbar(t.ti + 1, t.npts, label=string, length=20, newline=True)
325 21 15319.0 729.5 0.2 return
——————————————————————————————————————————————————
Profile of modules.py:None: 0.0152785 s (1.44835%)
——————————————————————————————————————————————————
Total time: 0.0152785 s
File: /home/runner/work/starsim/starsim/starsim/modules.py
Function: Module.start_step at line 670
Line # Hits Time Per Hit % Time Line Contents
==============================================================
670 @required()
671 def start_step(self):
672 """ Tasks to perform at the beginning of the step """
673 42 37999.0 904.7 0.2 if self.finalized:
674 errormsg = f'The module {self._debug_name} has already been run. Did you mean to copy it before running it?'
675 raise RuntimeError(errormsg)
676 42 33582.0 799.6 0.2 if self.dists is not None: # Will be None if no distributions are defined
677 42 15171830.0 361234.0 99.3 self.dists.jump_dt() # Advance random number generators forward for calls on this step
678 42 35108.0 835.9 0.2 return
—————————————————————————————————————————————————————
Profile of networks.py:None_1: 0.0632225 s (5.99329%)
—————————————————————————————————————————————————————
Total time: 0.0632225 s
File: /home/runner/work/starsim/starsim/starsim/networks.py
Function: DynamicNetwork.step at line 413
Line # Hits Time Per Hit % Time Line Contents
==============================================================
413 def step(self):
414 21 17797946.0 847521.2 28.2 self.end_pairs()
415 21 45392561.0 2.16e+06 71.8 self.add_pairs()
416 21 32020.0 1524.8 0.1 return
——————————————————————————————————————————————————
Profile of diseases.py:None: 0.932284 s (88.3776%)
——————————————————————————————————————————————————
Total time: 0.932284 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: Infection.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 913941045.0 4.35e+07 98.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 23323.0 1110.6 0.0 if len(new_cases):
217 21 18299245.0 871392.6 2.0 self.set_outcomes(new_cases, sources)
218
219 21 19896.0 947.4 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 0x7f60e738dbc0>
Running 2000.01.01 ( 0/21) (0.00 s) ———————————————————— 5%
Running 2010.01.01 (10/21) (0.09 s) ••••••••••—————————— 52%
Running 2020.01.01 (20/21) (0.18 s) •••••••••••••••••••• 100%
Elapsed time: 0.208 s
——————————————————————————————————————————————————
Profile of diseases.py:None: 0.083936 s (40.3421%)
——————————————————————————————————————————————————
Total time: 0.083936 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: Infection.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 21758.0 1036.1 0.0 new_cases = []
233 21 22893.0 1090.1 0.0 sources = []
234 21 20216.0 962.7 0.0 networks = []
235 21 702040.0 33430.5 0.8 betamap = self.validate_beta()
236
237 21 1582566.0 75360.3 1.9 rel_trans = self.rel_trans.asnew(self.infectious * self.rel_trans)
238 21 1186561.0 56502.9 1.4 rel_sus = self.rel_sus.asnew(self.susceptible * self.rel_sus)
239
240 42 195385.0 4652.0 0.2 for i, (nkey,route) in enumerate(self.sim.networks.items()):
241 21 50577.0 2408.4 0.1 nk = ss.standardize_netkey(nkey)
242
243 # Main use case: networks
244 21 26230.0 1249.0 0.0 if isinstance(route, ss.Network):
245 21 305567.0 14550.8 0.4 if len(route): # Skip networks with no edges
246 21 20686.0 985.0 0.0 edges = route.edges
247 21 425887.0 20280.3 0.5 p1p2b0 = [edges.p1, edges.p2, betamap[nk][0]] # Person 1, person 2, beta 0
248 21 390586.0 18599.3 0.5 p2p1b1 = [edges.p2, edges.p1, betamap[nk][1]] # Person 2, person 1, beta 1
249 63 79479.0 1261.6 0.1 for src, trg, beta in [p1p2b0, p2p1b1]:
250 42 103302.0 2459.6 0.1 if beta: # Skip networks with no transmission
251 42 1464074.0 34858.9 1.7 disease_beta = beta.to_prob(self.t.dt) if isinstance(beta, ss.Rate) else beta
252 42 1837851.0 43758.4 2.2 beta_per_dt = route.net_beta(disease_beta=disease_beta, disease=self) # Compute beta for this network and timestep
253 42 63152292.0 1.5e+06 75.2 randvals = self.trans_rng.rvs(src, trg) # Generate a new random number based on the two other random numbers
254 42 77679.0 1849.5 0.1 args = (src, trg, rel_trans, rel_sus, beta_per_dt, randvals) # Set up the arguments to calculate transmission
255 42 9511086.0 226454.4 11.3 target_uids, source_uids = self.compute_transmission(*args) # Actually calculate it
256 42 55398.0 1319.0 0.1 new_cases.append(target_uids)
257 42 42079.0 1001.9 0.1 sources.append(source_uids)
258 42 516387.0 12294.9 0.6 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 28703.0 1366.8 0.0 if len(new_cases) and len(sources):
274 21 275329.0 13110.9 0.3 new_cases = ss.uids.cat(new_cases)
275 21 1454408.0 69257.5 1.7 new_cases, inds = new_cases.unique(return_index=True)
276 21 243062.0 11574.4 0.3 sources = ss.uids.cat(sources)[inds]
277 21 120974.0 5760.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 22993.0 1094.9 0.0 return new_cases, sources, networks
——————————————————————————————————————————————————
Profile of diseases.py:None: 0.083936 s (40.3421%)
——————————————————————————————————————————————————
Total time: 0.083936 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: Infection.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 21758.0 1036.1 0.0 new_cases = []
233 21 22893.0 1090.1 0.0 sources = []
234 21 20216.0 962.7 0.0 networks = []
235 21 702040.0 33430.5 0.8 betamap = self.validate_beta()
236
237 21 1582566.0 75360.3 1.9 rel_trans = self.rel_trans.asnew(self.infectious * self.rel_trans)
238 21 1186561.0 56502.9 1.4 rel_sus = self.rel_sus.asnew(self.susceptible * self.rel_sus)
239
240 42 195385.0 4652.0 0.2 for i, (nkey,route) in enumerate(self.sim.networks.items()):
241 21 50577.0 2408.4 0.1 nk = ss.standardize_netkey(nkey)
242
243 # Main use case: networks
244 21 26230.0 1249.0 0.0 if isinstance(route, ss.Network):
245 21 305567.0 14550.8 0.4 if len(route): # Skip networks with no edges
246 21 20686.0 985.0 0.0 edges = route.edges
247 21 425887.0 20280.3 0.5 p1p2b0 = [edges.p1, edges.p2, betamap[nk][0]] # Person 1, person 2, beta 0
248 21 390586.0 18599.3 0.5 p2p1b1 = [edges.p2, edges.p1, betamap[nk][1]] # Person 2, person 1, beta 1
249 63 79479.0 1261.6 0.1 for src, trg, beta in [p1p2b0, p2p1b1]:
250 42 103302.0 2459.6 0.1 if beta: # Skip networks with no transmission
251 42 1464074.0 34858.9 1.7 disease_beta = beta.to_prob(self.t.dt) if isinstance(beta, ss.Rate) else beta
252 42 1837851.0 43758.4 2.2 beta_per_dt = route.net_beta(disease_beta=disease_beta, disease=self) # Compute beta for this network and timestep
253 42 63152292.0 1.5e+06 75.2 randvals = self.trans_rng.rvs(src, trg) # Generate a new random number based on the two other random numbers
254 42 77679.0 1849.5 0.1 args = (src, trg, rel_trans, rel_sus, beta_per_dt, randvals) # Set up the arguments to calculate transmission
255 42 9511086.0 226454.4 11.3 target_uids, source_uids = self.compute_transmission(*args) # Actually calculate it
256 42 55398.0 1319.0 0.1 new_cases.append(target_uids)
257 42 42079.0 1001.9 0.1 sources.append(source_uids)
258 42 516387.0 12294.9 0.6 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 28703.0 1366.8 0.0 if len(new_cases) and len(sources):
274 21 275329.0 13110.9 0.3 new_cases = ss.uids.cat(new_cases)
275 21 1454408.0 69257.5 1.7 new_cases, inds = new_cases.unique(return_index=True)
276 21 243062.0 11574.4 0.3 sources = ss.uids.cat(sources)[inds]
277 21 120974.0 5760.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 22993.0 1094.9 0.0 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 10264
Population size is 10357
Population size is 10447
Population size is 10557
Population size is 10664
Population size is 10739
Population size is 10817
Population size is 10917
Population size is 11033
Running 2010.01.01 (10/11) (0.15 s) •••••••••••••••••••• 100%
Population size is 11124
Initializing sim with 10000 agents
Running 2000.01.01 ( 0/11) (0.00 s) •——————————————————— 9%
Population size is 10191
Population size is 10264
Population size is 10357
Population size is 10447
Population size is 10557
Population size is 10664
Population size is 10739
Population size is 10817
Population size is 10917
Population size is 11033
Running 2010.01.01 (10/11) (0.15 s) •••••••••••••••••••• 100%
Population size is 11124
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.)