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 0x7f9c9e5478a0>
<function Module.start_step at 0x7f9c9e5478a0>
<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 0x7f9c9ded8f60>
<function Module.finish_step at 0x7f9c9e547ab0>
<function Module.finish_step at 0x7f9c9e547ab0>
<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) (1.01 s) ••••••••••—————————— 52%
Running 2020.01.01 (20/21) (1.13 s) •••••••••••••••••••• 100%
Elapsed time: 1.16 s
———————————————————————————————————————————————————————
Profile of networks.py:None: 0.000543662 s (0.0470365%)
———————————————————————————————————————————————————————
Total time: 0.000543662 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 512750.0 24416.7 94.3 self.results['n_edges'][self.ti] = len(self)
259 21 30912.0 1472.0 5.7 return
—————————————————————————————————————————————————————
Profile of people.py:None_2: 0.00136984 s (0.118516%)
—————————————————————————————————————————————————————
Total time: 0.00136984 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 1001267.0 47679.4 73.1 self.remove_dead()
424 21 355525.0 16929.8 26.0 self.update_post()
425 21 13043.0 621.1 1.0 return
———————————————————————————————————————————————————
Profile of people.py:None: 0.00268903 s (0.232649%)
———————————————————————————————————————————————————
Total time: 0.00268903 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 2240725.0 106701.2 83.3 death_uids = ((self.ti_dead <= self.sim.ti) | (self.ti_removed <= self.sim.ti)).uids
387 21 121737.0 5797.0 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 220604.0 5252.5 8.2 for disease in self.sim.diseases():
392 21 37217.0 1772.2 1.4 if isinstance(disease, ss.Disease):
393 21 54636.0 2601.7 2.0 disease.step_die(death_uids)
394
395 21 14107.0 671.8 0.5 return death_uids
—————————————————————————————————————————————————————
Profile of people.py:None_1: 0.00440807 s (0.381377%)
—————————————————————————————————————————————————————
Total time: 0.00440807 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 44655.0 2126.4 1.0 ti = self.sim.ti
415 21 19496.0 928.4 0.4 res = self.sim.results
416 21 931164.0 44341.1 21.1 res.n_alive[ti] = np.count_nonzero(self.alive)
417 21 1169227.0 55677.5 26.5 res.new_deaths[ti] = np.count_nonzero(self.ti_dead == ti)
418 21 1029616.0 49029.3 23.4 res.new_emigrants[ti] = np.count_nonzero(self.ti_removed == ti)
419 21 1197904.0 57043.0 27.2 res.cum_deaths[ti] = np.sum(res.new_deaths[:ti]) # TODO: inefficient to compute the cumulative sum on every timestep!
420 21 16004.0 762.1 0.4 return
———————————————————————————————————————————————————————
Profile of diseases.py:None_2: 0.00456052 s (0.394566%)
———————————————————————————————————————————————————————
Total time: 0.00456052 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 3252883.0 154899.2 71.3 super().update_results()
674 21 1292684.0 61556.4 28.3 self.results['rel_sus'][self.ti] = self.rel_sus.mean()
675 21 14951.0 712.0 0.3 return
————————————————————————————————————————————————
Profile of sim.py:None: 0.00689206 s (0.596286%)
————————————————————————————————————————————————
Total time: 0.00689206 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 19976.0 951.2 0.3 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 4692358.0 223445.6 68.1 self.elapsed = self.timer.toc(output=True)
316 21 26392.0 1256.8 0.4 if self.verbose: # Print progress
317 21 16129.0 768.0 0.2 t = self.t
318 21 329145.0 15673.6 4.8 simlabel = f'"{self.label}": ' if self.label else ''
319 21 1158410.0 55162.4 16.8 string = f' Running {simlabel}{t.now("str")} ({t.ti:2.0f}/{t.npts}) ({self.elapsed:0.2f} s) '
320 21 21783.0 1037.3 0.3 if self.verbose >= 1:
321 sc.heading(string)
322 21 17544.0 835.4 0.3 elif self.verbose > 0:
323 21 32360.0 1541.0 0.5 if not (t.ti % int(1.0 / self.verbose)):
324 3 561451.0 187150.3 8.1 sc.progressbar(t.ti + 1, t.npts, label=string, length=20, newline=True)
325 21 16510.0 786.2 0.2 return
———————————————————————————————————————————————————————
Profile of diseases.py:None_1: 0.00707561 s (0.612167%)
———————————————————————————————————————————————————————
Total time: 0.00707561 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 2338818.0 111372.3 33.1 recovered = (self.infected & (self.ti_recovered <= self.ti)).uids
634 21 128038.0 6097.0 1.8 self.infected[recovered] = False
635 21 81453.0 3878.7 1.2 self.susceptible[recovered] = True
636 21 4511793.0 214847.3 63.8 self.update_immunity()
637 21 15511.0 738.6 0.2 return
——————————————————————————————————————————————————
Profile of modules.py:None: 0.0154357 s (1.33546%)
——————————————————————————————————————————————————
Total time: 0.0154357 s
File: /home/runner/work/starsim/starsim/starsim/modules.py
Function: Module.start_step at line 673
Line # Hits Time Per Hit % Time Line Contents
==============================================================
673 @required()
674 def start_step(self):
675 """ Tasks to perform at the beginning of the step """
676 42 49890.0 1187.9 0.3 if self.finalized:
677 errormsg = f'The module {self._debug_name} has already been run. Did you mean to copy it before running it?'
678 raise RuntimeError(errormsg)
679 42 50983.0 1213.9 0.3 if self.dists is not None: # Will be None if no distributions are defined
680 42 15295746.0 364184.4 99.1 self.dists.jump_dt() # Advance random number generators forward for calls on this step
681 42 39112.0 931.2 0.3 return
————————————————————————————————————————————————————
Profile of networks.py:None_1: 0.072403 s (6.26415%)
————————————————————————————————————————————————————
Total time: 0.072403 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 17671123.0 841482.0 24.4 self.end_pairs()
415 21 54712193.0 2.61e+06 75.6 self.add_pairs()
416 21 19664.0 936.4 0.0 return
—————————————————————————————————————————————————
Profile of diseases.py:None: 1.02071 s (88.3096%)
—————————————————————————————————————————————————
Total time: 1.02071 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 1001171720.0 4.77e+07 98.1 new_cases, sources, networks = self.infect() # TODO: store outputs in self or use objdict rather than 3 returns
214
215 # Set prognoses
216 21 28112.0 1338.7 0.0 if len(new_cases):
217 21 19491256.0 928155.0 1.9 self.set_outcomes(new_cases, sources)
218
219 21 22112.0 1053.0 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.py:None: 0.00689206 s (0.596286%)
————————————————————————————————————————————————
Total time: 0.00689206 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 19976.0 951.2 0.3 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 4692358.0 223445.6 68.1 self.elapsed = self.timer.toc(output=True)
316 21 26392.0 1256.8 0.4 if self.verbose: # Print progress
317 21 16129.0 768.0 0.2 t = self.t
318 21 329145.0 15673.6 4.8 simlabel = f'"{self.label}": ' if self.label else ''
319 21 1158410.0 55162.4 16.8 string = f' Running {simlabel}{t.now("str")} ({t.ti:2.0f}/{t.npts}) ({self.elapsed:0.2f} s) '
320 21 21783.0 1037.3 0.3 if self.verbose >= 1:
321 sc.heading(string)
322 21 17544.0 835.4 0.3 elif self.verbose > 0:
323 21 32360.0 1541.0 0.5 if not (t.ti % int(1.0 / self.verbose)):
324 3 561451.0 187150.3 8.1 sc.progressbar(t.ti + 1, t.npts, label=string, length=20, newline=True)
325 21 16510.0 786.2 0.2 return
———————————————————————————————————————————————————————
Profile of diseases.py:None_1: 0.00707561 s (0.612167%)
———————————————————————————————————————————————————————
Total time: 0.00707561 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 2338818.0 111372.3 33.1 recovered = (self.infected & (self.ti_recovered <= self.ti)).uids
634 21 128038.0 6097.0 1.8 self.infected[recovered] = False
635 21 81453.0 3878.7 1.2 self.susceptible[recovered] = True
636 21 4511793.0 214847.3 63.8 self.update_immunity()
637 21 15511.0 738.6 0.2 return
——————————————————————————————————————————————————
Profile of modules.py:None: 0.0154357 s (1.33546%)
——————————————————————————————————————————————————
Total time: 0.0154357 s
File: /home/runner/work/starsim/starsim/starsim/modules.py
Function: Module.start_step at line 673
Line # Hits Time Per Hit % Time Line Contents
==============================================================
673 @required()
674 def start_step(self):
675 """ Tasks to perform at the beginning of the step """
676 42 49890.0 1187.9 0.3 if self.finalized:
677 errormsg = f'The module {self._debug_name} has already been run. Did you mean to copy it before running it?'
678 raise RuntimeError(errormsg)
679 42 50983.0 1213.9 0.3 if self.dists is not None: # Will be None if no distributions are defined
680 42 15295746.0 364184.4 99.1 self.dists.jump_dt() # Advance random number generators forward for calls on this step
681 42 39112.0 931.2 0.3 return
————————————————————————————————————————————————————
Profile of networks.py:None_1: 0.072403 s (6.26415%)
————————————————————————————————————————————————————
Total time: 0.072403 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 17671123.0 841482.0 24.4 self.end_pairs()
415 21 54712193.0 2.61e+06 75.6 self.add_pairs()
416 21 19664.0 936.4 0.0 return
—————————————————————————————————————————————————
Profile of diseases.py:None: 1.02071 s (88.3096%)
—————————————————————————————————————————————————
Total time: 1.02071 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 1001171720.0 4.77e+07 98.1 new_cases, sources, networks = self.infect() # TODO: store outputs in self or use objdict rather than 3 returns
214
215 # Set prognoses
216 21 28112.0 1338.7 0.0 if len(new_cases):
217 21 19491256.0 928155.0 1.9 self.set_outcomes(new_cases, sources)
218
219 21 22112.0 1053.0 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 0x7f9c9deb3ab0>
Running 2000.01.01 ( 0/21) (0.00 s) ———————————————————— 5%
Running 2010.01.01 (10/21) (0.12 s) ••••••••••—————————— 52%
Running 2020.01.01 (20/21) (0.24 s) •••••••••••••••••••• 100%
Elapsed time: 0.271 s
————————————————————————————————————————————————
Profile of diseases.py:None: 0.1243 s (45.7883%)
————————————————————————————————————————————————
Total time: 0.1243 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 31709.0 1510.0 0.0 new_cases = []
233 21 22050.0 1050.0 0.0 sources = []
234 21 22402.0 1066.8 0.0 networks = []
235 21 855040.0 40716.2 0.7 betamap = self.validate_beta()
236
237 21 1856747.0 88416.5 1.5 rel_trans = self.rel_trans.asnew(self.infectious * self.rel_trans)
238 21 1297277.0 61775.1 1.0 rel_sus = self.rel_sus.asnew(self.susceptible * self.rel_sus)
239
240 42 220737.0 5255.6 0.2 for i, (nkey,route) in enumerate(self.sim.networks.items()):
241 21 62685.0 2985.0 0.1 nk = ss.standardize_netkey(nkey)
242
243 # Main use case: networks
244 21 31588.0 1504.2 0.0 if isinstance(route, ss.Network):
245 21 337970.0 16093.8 0.3 if len(route): # Skip networks with no edges
246 21 20689.0 985.2 0.0 edges = route.edges
247 21 406292.0 19347.2 0.3 p1p2b0 = [edges.p1, edges.p2, betamap[nk][0]] # Person 1, person 2, beta 0
248 21 382566.0 18217.4 0.3 p2p1b1 = [edges.p2, edges.p1, betamap[nk][1]] # Person 2, person 1, beta 1
249 63 111924.0 1776.6 0.1 for src, trg, beta in [p1p2b0, p2p1b1]:
250 42 129102.0 3073.9 0.1 if beta: # Skip networks with no transmission
251 42 1709489.0 40702.1 1.4 disease_beta = beta.to_prob(self.t.dt) if isinstance(beta, ss.Rate) else beta
252 42 2157692.0 51373.6 1.7 beta_per_dt = route.net_beta(disease_beta=disease_beta, disease=self) # Compute beta for this network and timestep
253 42 92639053.0 2.21e+06 74.5 randvals = self.trans_rng.rvs(src, trg) # Generate a new random number based on the two other random numbers
254 42 89998.0 2142.8 0.1 args = (src, trg, rel_trans, rel_sus, beta_per_dt, randvals) # Set up the arguments to calculate transmission
255 42 18670956.0 444546.6 15.0 target_uids, source_uids = self.compute_transmission(*args) # Actually calculate it
256 42 65952.0 1570.3 0.1 new_cases.append(target_uids)
257 42 42895.0 1021.3 0.0 sources.append(source_uids)
258 42 609887.0 14521.1 0.5 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 29319.0 1396.1 0.0 if len(new_cases) and len(sources):
274 21 346548.0 16502.3 0.3 new_cases = ss.uids.cat(new_cases)
275 21 1719220.0 81867.6 1.4 new_cases, inds = new_cases.unique(return_index=True)
276 21 257645.0 12268.8 0.2 sources = ss.uids.cat(sources)[inds]
277 21 145926.0 6948.9 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 26449.0 1259.5 0.0 return new_cases, sources, networks
————————————————————————————————————————————————
Profile of diseases.py:None: 0.1243 s (45.7883%)
————————————————————————————————————————————————
Total time: 0.1243 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 31709.0 1510.0 0.0 new_cases = []
233 21 22050.0 1050.0 0.0 sources = []
234 21 22402.0 1066.8 0.0 networks = []
235 21 855040.0 40716.2 0.7 betamap = self.validate_beta()
236
237 21 1856747.0 88416.5 1.5 rel_trans = self.rel_trans.asnew(self.infectious * self.rel_trans)
238 21 1297277.0 61775.1 1.0 rel_sus = self.rel_sus.asnew(self.susceptible * self.rel_sus)
239
240 42 220737.0 5255.6 0.2 for i, (nkey,route) in enumerate(self.sim.networks.items()):
241 21 62685.0 2985.0 0.1 nk = ss.standardize_netkey(nkey)
242
243 # Main use case: networks
244 21 31588.0 1504.2 0.0 if isinstance(route, ss.Network):
245 21 337970.0 16093.8 0.3 if len(route): # Skip networks with no edges
246 21 20689.0 985.2 0.0 edges = route.edges
247 21 406292.0 19347.2 0.3 p1p2b0 = [edges.p1, edges.p2, betamap[nk][0]] # Person 1, person 2, beta 0
248 21 382566.0 18217.4 0.3 p2p1b1 = [edges.p2, edges.p1, betamap[nk][1]] # Person 2, person 1, beta 1
249 63 111924.0 1776.6 0.1 for src, trg, beta in [p1p2b0, p2p1b1]:
250 42 129102.0 3073.9 0.1 if beta: # Skip networks with no transmission
251 42 1709489.0 40702.1 1.4 disease_beta = beta.to_prob(self.t.dt) if isinstance(beta, ss.Rate) else beta
252 42 2157692.0 51373.6 1.7 beta_per_dt = route.net_beta(disease_beta=disease_beta, disease=self) # Compute beta for this network and timestep
253 42 92639053.0 2.21e+06 74.5 randvals = self.trans_rng.rvs(src, trg) # Generate a new random number based on the two other random numbers
254 42 89998.0 2142.8 0.1 args = (src, trg, rel_trans, rel_sus, beta_per_dt, randvals) # Set up the arguments to calculate transmission
255 42 18670956.0 444546.6 15.0 target_uids, source_uids = self.compute_transmission(*args) # Actually calculate it
256 42 65952.0 1570.3 0.1 new_cases.append(target_uids)
257 42 42895.0 1021.3 0.0 sources.append(source_uids)
258 42 609887.0 14521.1 0.5 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 29319.0 1396.1 0.0 if len(new_cases) and len(sources):
274 21 346548.0 16502.3 0.3 new_cases = ss.uids.cat(new_cases)
275 21 1719220.0 81867.6 1.4 new_cases, inds = new_cases.unique(return_index=True)
276 21 257645.0 12268.8 0.2 sources = ss.uids.cat(sources)[inds]
277 21 145926.0 6948.9 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 26449.0 1259.5 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.18 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.18 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.)