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 0x7f21d56d7a60>
<function Module.start_step at 0x7f21d56d7a60>
<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 0x7f21d5085a80>
<function Module.finish_step at 0x7f21d56d7c40>
<function Module.finish_step at 0x7f21d56d7c40>
<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.77 s) ••••••••••—————————— 52%
Running 2020.01.01 (20/21) (0.85 s) •••••••••••••••••••• 100%
Elapsed time: 0.876 s
——————————————————————————————————————————————————————————————————————
Profile of networks.Network.update_results: 0.000519656 s (0.0593361%)
——————————————————————————————————————————————————————————————————————
Total time: 0.000519656 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 107323.0 5110.6 20.7 super().update_results()
259 21 403245.0 19202.1 77.6 self.results['n_edges'][self.ti] = len(self)
260 21 9088.0 432.8 1.7 return
———————————————————————————————————————————————————————————————
Profile of people.People.finish_step: 0.000878717 s (0.100335%)
———————————————————————————————————————————————————————————————
Total time: 0.000878717 s
File: /home/runner/work/starsim/starsim/starsim/people.py
Function: People.finish_step at line 497
Line # Hits Time Per Hit % Time Line Contents
==============================================================
497 def finish_step(self):
498 21 572794.0 27275.9 65.2 self.remove_dead()
499 21 297578.0 14170.4 33.9 self.update_post()
500 21 8345.0 397.4 0.9 return
———————————————————————————————————————————————————————————
Profile of people.People.step_die: 0.00202804 s (0.231569%)
———————————————————————————————————————————————————————————
Total time: 0.00202804 s
File: /home/runner/work/starsim/starsim/starsim/people.py
Function: People.step_die at line 459
Line # Hits Time Per Hit % Time Line Contents
==============================================================
459 def step_die(self):
460 """ Carry out any deaths or removals that took place this timestep """
461 21 1720269.0 81917.6 84.8 death_uids = ((self.ti_dead <= self.sim.ti) | (self.ti_removed <= self.sim.ti)).uids
462 21 84718.0 4034.2 4.2 self.alive[death_uids] = False # Whilst not dead, removed agents should not be included in alive totals
463
464 # Execute deaths that took place this timestep (i.e., changing the `alive` state of the agents). This is executed
465 # before analyzers have run so that analyzers are able to inspect and record outcomes for agents that died this timestep
466 42 163823.0 3900.5 8.1 for disease in self.sim.diseases():
467 21 16720.0 796.2 0.8 if isinstance(disease, ss.Disease):
468 21 33370.0 1589.0 1.6 disease.step_die(death_uids)
469
470 21 9144.0 435.4 0.5 return death_uids
————————————————————————————————————————————————————————————————
Profile of diseases.SIS.update_results: 0.00361768 s (0.413079%)
————————————————————————————————————————————————————————————————
Total time: 0.00361768 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: SIS.update_results at line 779
Line # Hits Time Per Hit % Time Line Contents
==============================================================
779 @ss.required()
780 def update_results(self):
781 """ Store the population immunity (susceptibility) """
782 21 2622979.0 124903.8 72.5 super().update_results()
783 21 985467.0 46927.0 27.2 self.results['rel_sus'][self.ti] = self.rel_sus.mean()
784 21 9238.0 439.9 0.3 return
———————————————————————————————————————————————————————————————
Profile of people.People.update_results: 0.0036641 s (0.41838%)
———————————————————————————————————————————————————————————————
Total time: 0.0036641 s
File: /home/runner/work/starsim/starsim/starsim/people.py
Function: People.update_results at line 488
Line # Hits Time Per Hit % Time Line Contents
==============================================================
488 def update_results(self):
489 21 32993.0 1571.1 0.9 ti = self.sim.ti
490 21 11332.0 539.6 0.3 res = self.sim.results
491 21 711430.0 33877.6 19.4 res.n_alive[ti] = np.count_nonzero(self.alive)
492 21 999178.0 47579.9 27.3 res.new_deaths[ti] = np.count_nonzero(self.ti_dead == ti)
493 21 921926.0 43901.2 25.2 res.new_emigrants[ti] = np.count_nonzero(self.ti_removed == ti)
494 21 976717.0 46510.3 26.7 res.cum_deaths[ti] = np.sum(res.new_deaths[:ti]) # TODO: inefficient to compute the cumulative sum on every timestep!
495 21 10527.0 501.3 0.3 return
———————————————————————————————————————————————————————
Profile of sim.Sim.start_step: 0.00536926 s (0.613081%)
———————————————————————————————————————————————————————
Total time: 0.00536926 s
File: /home/runner/work/starsim/starsim/starsim/sim.py
Function: Sim.start_step at line 450
Line # Hits Time Per Hit % Time Line Contents
==============================================================
450 def start_step(self):
451 """ Start the step -- only print progress; all actual changes happen in the modules """
452
453 # Set the time and if we have reached the end of the simulation, then do nothing
454 21 11681.0 556.2 0.2 if self.complete:
455 errormsg = 'Simulation already complete (call sim.init() to re-run)'
456 raise AlreadyRunError(errormsg)
457
458 # Print out progress if needed
459 21 3709102.0 176623.9 69.1 self.elapsed = self.timer.toc(output=True)
460 21 13867.0 660.3 0.3 if self.verbose: # Print progress
461 21 9246.0 440.3 0.2 t = self.t
462 21 281943.0 13425.9 5.3 simlabel = f'"{self.label}": ' if self.label else ''
463 21 860629.0 40982.3 16.0 string = f' Running {simlabel}{t.now("str")} ({t.ti:2.0f}/{t.npts}) ({self.elapsed:0.2f} s) '
464 21 16843.0 802.0 0.3 if self.verbose >= 1:
465 sc.heading(string)
466 21 12896.0 614.1 0.2 elif self.verbose > 0:
467 21 22713.0 1081.6 0.4 if not (t.ti % int(1.0 / self.verbose)):
468 3 420217.0 140072.3 7.8 sc.progressbar(t.ti + 1, t.npts, label=string, length=20, newline=True)
469 21 10119.0 481.9 0.2 return
——————————————————————————————————————————————————————————
Profile of diseases.SIS.step_state: 0.00581783 s (0.6643%)
——————————————————————————————————————————————————————————
Total time: 0.00581783 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: SIS.step_state at line 739
Line # Hits Time Per Hit % Time Line Contents
==============================================================
739 def step_state(self):
740 """ Progress infectious -> recovered """
741 21 1839776.0 87608.4 31.6 recovered = (self.infected & (self.ti_recovered <= self.ti)).uids
742 21 101320.0 4824.8 1.7 self.infected[recovered] = False
743 21 67152.0 3197.7 1.2 self.susceptible[recovered] = True
744 21 3800053.0 180954.9 65.3 self.update_immunity()
745 21 9527.0 453.7 0.2 return
————————————————————————————————————————————————————————————
Profile of modules.Module.start_step: 0.0138157 s (1.57752%)
————————————————————————————————————————————————————————————
Total time: 0.0138157 s
File: /home/runner/work/starsim/starsim/starsim/modules.py
Function: Module.start_step at line 679
Line # Hits Time Per Hit % Time Line Contents
==============================================================
679 @required()
680 def start_step(self):
681 """ Tasks to perform at the beginning of the step """
682 42 37942.0 903.4 0.3 if self.finalized:
683 errormsg = f'The module {self._debug_name} has already been run. Did you mean to copy it before running it?'
684 raise RuntimeError(errormsg)
685 42 22830.0 543.6 0.2 if self.dists is not None: # Will be None if no distributions are defined
686 42 13733894.0 326997.5 99.4 self.dists.jump_dt() # Advance random number generators forward for calls on this step
687 42 21037.0 500.9 0.2 return
——————————————————————————————————————————————————————————————
Profile of networks.DynamicNetwork.step: 0.0453814 s (5.1818%)
——————————————————————————————————————————————————————————————
Total time: 0.0453814 s
File: /home/runner/work/starsim/starsim/starsim/networks.py
Function: DynamicNetwork.step at line 414
Line # Hits Time Per Hit % Time Line Contents
==============================================================
414 def step(self):
415 21 15920336.0 758111.2 35.1 self.end_pairs()
416 21 29448957.0 1.4e+06 64.9 self.add_pairs()
417 21 12114.0 576.9 0.0 return
—————————————————————————————————————————————————————————
Profile of diseases.Infection.step: 0.770294 s (87.9548%)
—————————————————————————————————————————————————————————
Total time: 0.770294 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: Infection.step at line 215
Line # Hits Time Per Hit % Time Line Contents
==============================================================
215 def step(self):
216 """
217 Perform key infection updates, including infection and setting prognoses
218 """
219 # Create new cases
220 21 753832388.0 3.59e+07 97.9 new_cases, sources, networks = self.infect() # TODO: store outputs in self or use objdict rather than 3 returns
221
222 # Set prognoses
223 21 15480.0 737.1 0.0 if len(new_cases):
224 21 16433534.0 782549.2 2.1 self.set_outcomes(new_cases, sources)
225
226 21 12983.0 618.2 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.00536926 s (0.613081%)
———————————————————————————————————————————————————————
Total time: 0.00536926 s
File: /home/runner/work/starsim/starsim/starsim/sim.py
Function: Sim.start_step at line 450
Line # Hits Time Per Hit % Time Line Contents
==============================================================
450 def start_step(self):
451 """ Start the step -- only print progress; all actual changes happen in the modules """
452
453 # Set the time and if we have reached the end of the simulation, then do nothing
454 21 11681.0 556.2 0.2 if self.complete:
455 errormsg = 'Simulation already complete (call sim.init() to re-run)'
456 raise AlreadyRunError(errormsg)
457
458 # Print out progress if needed
459 21 3709102.0 176623.9 69.1 self.elapsed = self.timer.toc(output=True)
460 21 13867.0 660.3 0.3 if self.verbose: # Print progress
461 21 9246.0 440.3 0.2 t = self.t
462 21 281943.0 13425.9 5.3 simlabel = f'"{self.label}": ' if self.label else ''
463 21 860629.0 40982.3 16.0 string = f' Running {simlabel}{t.now("str")} ({t.ti:2.0f}/{t.npts}) ({self.elapsed:0.2f} s) '
464 21 16843.0 802.0 0.3 if self.verbose >= 1:
465 sc.heading(string)
466 21 12896.0 614.1 0.2 elif self.verbose > 0:
467 21 22713.0 1081.6 0.4 if not (t.ti % int(1.0 / self.verbose)):
468 3 420217.0 140072.3 7.8 sc.progressbar(t.ti + 1, t.npts, label=string, length=20, newline=True)
469 21 10119.0 481.9 0.2 return
——————————————————————————————————————————————————————————
Profile of diseases.SIS.step_state: 0.00581783 s (0.6643%)
——————————————————————————————————————————————————————————
Total time: 0.00581783 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: SIS.step_state at line 739
Line # Hits Time Per Hit % Time Line Contents
==============================================================
739 def step_state(self):
740 """ Progress infectious -> recovered """
741 21 1839776.0 87608.4 31.6 recovered = (self.infected & (self.ti_recovered <= self.ti)).uids
742 21 101320.0 4824.8 1.7 self.infected[recovered] = False
743 21 67152.0 3197.7 1.2 self.susceptible[recovered] = True
744 21 3800053.0 180954.9 65.3 self.update_immunity()
745 21 9527.0 453.7 0.2 return
————————————————————————————————————————————————————————————
Profile of modules.Module.start_step: 0.0138157 s (1.57752%)
————————————————————————————————————————————————————————————
Total time: 0.0138157 s
File: /home/runner/work/starsim/starsim/starsim/modules.py
Function: Module.start_step at line 679
Line # Hits Time Per Hit % Time Line Contents
==============================================================
679 @required()
680 def start_step(self):
681 """ Tasks to perform at the beginning of the step """
682 42 37942.0 903.4 0.3 if self.finalized:
683 errormsg = f'The module {self._debug_name} has already been run. Did you mean to copy it before running it?'
684 raise RuntimeError(errormsg)
685 42 22830.0 543.6 0.2 if self.dists is not None: # Will be None if no distributions are defined
686 42 13733894.0 326997.5 99.4 self.dists.jump_dt() # Advance random number generators forward for calls on this step
687 42 21037.0 500.9 0.2 return
——————————————————————————————————————————————————————————————
Profile of networks.DynamicNetwork.step: 0.0453814 s (5.1818%)
——————————————————————————————————————————————————————————————
Total time: 0.0453814 s
File: /home/runner/work/starsim/starsim/starsim/networks.py
Function: DynamicNetwork.step at line 414
Line # Hits Time Per Hit % Time Line Contents
==============================================================
414 def step(self):
415 21 15920336.0 758111.2 35.1 self.end_pairs()
416 21 29448957.0 1.4e+06 64.9 self.add_pairs()
417 21 12114.0 576.9 0.0 return
—————————————————————————————————————————————————————————
Profile of diseases.Infection.step: 0.770294 s (87.9548%)
—————————————————————————————————————————————————————————
Total time: 0.770294 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: Infection.step at line 215
Line # Hits Time Per Hit % Time Line Contents
==============================================================
215 def step(self):
216 """
217 Perform key infection updates, including infection and setting prognoses
218 """
219 # Create new cases
220 21 753832388.0 3.59e+07 97.9 new_cases, sources, networks = self.infect() # TODO: store outputs in self or use objdict rather than 3 returns
221
222 # Set prognoses
223 21 15480.0 737.1 0.0 if len(new_cases):
224 21 16433534.0 782549.2 2.1 self.set_outcomes(new_cases, sources)
225
226 21 12983.0 618.2 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 0x7f21d5084540>
Running 2000.01.01 ( 0/21) (0.00 s) ———————————————————— 5%
Running 2010.01.01 (10/21) (0.08 s) ••••••••••—————————— 52%
Running 2020.01.01 (20/21) (0.15 s) •••••••••••••••••••• 100%
Elapsed time: 0.179 s
———————————————————————————————————————————————————————————
Profile of diseases.Infection.infect: 0.070177 s (39.1298%)
———————————————————————————————————————————————————————————
Total time: 0.070177 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: Infection.infect at line 237
Line # Hits Time Per Hit % Time Line Contents
==============================================================
237 def infect(self):
238 """ Determine who gets infected on this timestep via transmission on the network """
239 21 15937.0 758.9 0.0 new_cases = []
240 21 8487.0 404.1 0.0 sources = []
241 21 8035.0 382.6 0.0 networks = []
242 21 642435.0 30592.1 0.9 betamap = self.validate_beta()
243
244 21 2546098.0 121242.8 3.6 rel_trans = self.rel_trans.asnew(self.infectious * self.rel_trans)
245 21 2197454.0 104640.7 3.1 rel_sus = self.rel_sus.asnew(self.susceptible * self.rel_sus)
246
247 42 182759.0 4351.4 0.3 for i, (nkey,route) in enumerate(self.sim.networks.items()):
248 21 38459.0 1831.4 0.1 nk = ss.standardize_netkey(nkey)
249
250 # Main use case: networks
251 21 14112.0 672.0 0.0 if isinstance(route, ss.Network):
252 21 268178.0 12770.4 0.4 if len(route): # Skip networks with no edges
253 21 9958.0 474.2 0.0 edges = route.edges
254 21 366206.0 17438.4 0.5 p1p2b0 = [edges.p1, edges.p2, betamap[nk][0]] # Person 1, person 2, beta 0
255 21 348010.0 16571.9 0.5 p2p1b1 = [edges.p2, edges.p1, betamap[nk][1]] # Person 2, person 1, beta 1
256 63 43723.0 694.0 0.1 for src, trg, beta in [p1p2b0, p2p1b1]:
257 42 70912.0 1688.4 0.1 if beta: # Skip networks with no transmission
258 42 1203423.0 28652.9 1.7 disease_beta = beta.to_prob(self.t.dt) if isinstance(beta, ss.Rate) else beta
259 42 1723868.0 41044.5 2.5 beta_per_dt = route.net_beta(disease_beta=disease_beta, disease=self) # Compute beta for this network and timestep
260 42 50218897.0 1.2e+06 71.6 randvals = self.trans_rng.rvs(src, trg) # Generate a new random number based on the two other random numbers
261 42 37602.0 895.3 0.1 args = (src, trg, rel_trans, rel_sus, beta_per_dt, randvals) # Set up the arguments to calculate transmission
262 42 7662195.0 182433.2 10.9 target_uids, source_uids = self.compute_transmission(*args) # Actually calculate it
263 42 29096.0 692.8 0.0 new_cases.append(target_uids)
264 42 17608.0 419.2 0.0 sources.append(source_uids)
265 42 453336.0 10793.7 0.6 networks.append(np.full(len(target_uids), dtype=ss_int, fill_value=i))
266
267 # Handle everything else: mixing pools, environmental transmission, etc.
268 elif isinstance(route, ss.Route):
269 # Mixing pools are unidirectional, only use the first beta value
270 disease_beta = betamap[nk][0].to_prob(self.t.dt) if isinstance(betamap[nk][0], ss.Rate) else betamap[nk][0]
271 target_uids = route.compute_transmission(rel_sus, rel_trans, disease_beta, disease=self)
272 new_cases.append(target_uids)
273 sources.append(np.full(len(target_uids), dtype=ss_int, fill_value=ss.dtypes.int_nan))
274 networks.append(np.full(len(target_uids), dtype=ss_int, fill_value=i))
275 else:
276 errormsg = f'Cannot compute transmission via route {type(route)}; please subclass ss.Route and define a compute_transmission() method'
277 raise TypeError(errormsg)
278
279 # Finalize
280 21 15278.0 727.5 0.0 if len(new_cases) and len(sources):
281 21 344082.0 16384.9 0.5 new_cases = ss.uids.concatenate(new_cases)
282 21 1301528.0 61977.5 1.9 new_cases, inds = new_cases.unique(return_index=True)
283 21 297037.0 14144.6 0.4 sources = ss.uids.concatenate(sources)[inds]
284 21 100418.0 4781.8 0.1 networks = np.concatenate(networks)[inds]
285 else:
286 new_cases = ss.uids()
287 sources = ss.uids()
288 networks = np.empty(0, dtype=ss_int)
289
290 21 11887.0 566.0 0.0 return new_cases, sources, networks
———————————————————————————————————————————————————————————
Profile of diseases.Infection.infect: 0.070177 s (39.1298%)
———————————————————————————————————————————————————————————
Total time: 0.070177 s
File: /home/runner/work/starsim/starsim/starsim/diseases.py
Function: Infection.infect at line 237
Line # Hits Time Per Hit % Time Line Contents
==============================================================
237 def infect(self):
238 """ Determine who gets infected on this timestep via transmission on the network """
239 21 15937.0 758.9 0.0 new_cases = []
240 21 8487.0 404.1 0.0 sources = []
241 21 8035.0 382.6 0.0 networks = []
242 21 642435.0 30592.1 0.9 betamap = self.validate_beta()
243
244 21 2546098.0 121242.8 3.6 rel_trans = self.rel_trans.asnew(self.infectious * self.rel_trans)
245 21 2197454.0 104640.7 3.1 rel_sus = self.rel_sus.asnew(self.susceptible * self.rel_sus)
246
247 42 182759.0 4351.4 0.3 for i, (nkey,route) in enumerate(self.sim.networks.items()):
248 21 38459.0 1831.4 0.1 nk = ss.standardize_netkey(nkey)
249
250 # Main use case: networks
251 21 14112.0 672.0 0.0 if isinstance(route, ss.Network):
252 21 268178.0 12770.4 0.4 if len(route): # Skip networks with no edges
253 21 9958.0 474.2 0.0 edges = route.edges
254 21 366206.0 17438.4 0.5 p1p2b0 = [edges.p1, edges.p2, betamap[nk][0]] # Person 1, person 2, beta 0
255 21 348010.0 16571.9 0.5 p2p1b1 = [edges.p2, edges.p1, betamap[nk][1]] # Person 2, person 1, beta 1
256 63 43723.0 694.0 0.1 for src, trg, beta in [p1p2b0, p2p1b1]:
257 42 70912.0 1688.4 0.1 if beta: # Skip networks with no transmission
258 42 1203423.0 28652.9 1.7 disease_beta = beta.to_prob(self.t.dt) if isinstance(beta, ss.Rate) else beta
259 42 1723868.0 41044.5 2.5 beta_per_dt = route.net_beta(disease_beta=disease_beta, disease=self) # Compute beta for this network and timestep
260 42 50218897.0 1.2e+06 71.6 randvals = self.trans_rng.rvs(src, trg) # Generate a new random number based on the two other random numbers
261 42 37602.0 895.3 0.1 args = (src, trg, rel_trans, rel_sus, beta_per_dt, randvals) # Set up the arguments to calculate transmission
262 42 7662195.0 182433.2 10.9 target_uids, source_uids = self.compute_transmission(*args) # Actually calculate it
263 42 29096.0 692.8 0.0 new_cases.append(target_uids)
264 42 17608.0 419.2 0.0 sources.append(source_uids)
265 42 453336.0 10793.7 0.6 networks.append(np.full(len(target_uids), dtype=ss_int, fill_value=i))
266
267 # Handle everything else: mixing pools, environmental transmission, etc.
268 elif isinstance(route, ss.Route):
269 # Mixing pools are unidirectional, only use the first beta value
270 disease_beta = betamap[nk][0].to_prob(self.t.dt) if isinstance(betamap[nk][0], ss.Rate) else betamap[nk][0]
271 target_uids = route.compute_transmission(rel_sus, rel_trans, disease_beta, disease=self)
272 new_cases.append(target_uids)
273 sources.append(np.full(len(target_uids), dtype=ss_int, fill_value=ss.dtypes.int_nan))
274 networks.append(np.full(len(target_uids), dtype=ss_int, fill_value=i))
275 else:
276 errormsg = f'Cannot compute transmission via route {type(route)}; please subclass ss.Route and define a compute_transmission() method'
277 raise TypeError(errormsg)
278
279 # Finalize
280 21 15278.0 727.5 0.0 if len(new_cases) and len(sources):
281 21 344082.0 16384.9 0.5 new_cases = ss.uids.concatenate(new_cases)
282 21 1301528.0 61977.5 1.9 new_cases, inds = new_cases.unique(return_index=True)
283 21 297037.0 14144.6 0.4 sources = ss.uids.concatenate(sources)[inds]
284 21 100418.0 4781.8 0.1 networks = np.concatenate(networks)[inds]
285 else:
286 new_cases = ss.uids()
287 sources = ss.uids()
288 networks = np.empty(0, dtype=ss_int)
289
290 21 11887.0 566.0 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.08 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.08 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.)