Profiling and debugging

Profiling

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.

Let’s look at a simple simulation:

import sciris as sc
import starsim as ss
ss.options(jupyter=True)
sc.options(jupyter=True)

pars = dict(
    start = '2000-01-01',
    stop = '2020-01-01',
    diseases = 'sis',
    networks = 'random'
)

# Profile sim
sim = ss.Sim(pars)
prof = sim.profile()
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:

prof = sim.profile(follow=ss.SIS.infect, plot=False)
prof.disp()
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:

import starsim as ss

sim = ss.Sim(
    start = 2000,
    stop = 2002,
    diseases = 'sis',
    networks = 'random',
    verbose = 0,
)
sim.run()
sim.loop.df.disp()
# %%
    time  ti  func_order                     label     module       func_name    cpu_time
0   2000   0           0            sim.start_step        sim      start_step  3.1995e-04
1   2000   0           1      randomnet.start_step  randomnet      start_step  1.8924e-04
2   2000   0           2            sis.start_step        sis      start_step  2.2888e-04
3   2000   0           3            sis.step_state        sis      step_state  1.0473e-04
4   2000   0           4            randomnet.step  randomnet            step  1.3678e-03
5   2000   0           5                  sis.step        sis            step  4.5852e-03
6   2000   0           6           people.step_die     people        step_die  5.7878e-05
7   2000   0           7     people.update_results     people  update_results  8.9998e-05
8   2000   0           8  randomnet.update_results  randomnet  update_results  1.5439e-05
9   2000   0           9        sis.update_results        sis  update_results  1.0008e-04
10  2000   0          10     randomnet.finish_step  randomnet     finish_step  8.0360e-06
11  2000   0          11           sis.finish_step        sis     finish_step  2.7950e-06
12  2000   0          12        people.finish_step     people     finish_step  2.2893e-05
13  2000   0          13           sim.finish_step        sim     finish_step  3.8670e-06
14  2001   1           0            sim.start_step        sim      start_step  6.9810e-05
15  2001   1           1      randomnet.start_step  randomnet      start_step  1.4306e-04
16  2001   1           2            sis.start_step        sis      start_step  2.6257e-04
17  2001   1           3            sis.step_state        sis      step_state  9.1211e-05
18  2001   1           4            randomnet.step  randomnet            step  1.2047e-03
19  2001   1           5                  sis.step        sis            step  2.4146e-03
20  2001   1           6           people.step_die     people        step_die  4.6918e-05
21  2001   1           7     people.update_results     people  update_results  7.1133e-05
22  2001   1           8  randomnet.update_results  randomnet  update_results  1.1882e-05
23  2001   1           9        sis.update_results        sis  update_results  1.0593e-04
24  2001   1          10     randomnet.finish_step  randomnet     finish_step  7.7350e-06
25  2001   1          11           sis.finish_step        sis     finish_step  2.5850e-06
26  2001   1          12        people.finish_step     people     finish_step  1.8184e-05
27  2001   1          13           sim.finish_step        sim     finish_step  2.6950e-06
28  2002   2           0            sim.start_step        sim      start_step  6.5883e-05
29  2002   2           1      randomnet.start_step  randomnet      start_step  1.4364e-04
30  2002   2           2            sis.start_step        sis      start_step  2.1245e-04
31  2002   2           3            sis.step_state        sis      step_state  8.6553e-05
32  2002   2           4            randomnet.step  randomnet            step  1.2150e-03
33  2002   2           5                  sis.step        sis            step  2.4088e-03
34  2002   2           6           people.step_die     people        step_die  4.6326e-05
35  2002   2           7     people.update_results     people  update_results  6.9751e-05
36  2002   2           8  randomnet.update_results  randomnet  update_results  1.1622e-05
37  2002   2           9        sis.update_results        sis  update_results  7.7615e-05
38  2002   2          10     randomnet.finish_step  randomnet     finish_step  6.3220e-06
39  2002   2          11           sis.finish_step        sis     finish_step  2.5340e-06
40  2002   2          12        people.finish_step     people     finish_step  1.7203e-05
41  2002   2          13           sim.finish_step        sim     finish_step  2.4940e-06

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:

def check_pop_size(sim):
    print(f'Population size is {len(sim.people)}')

sim = ss.Sim(diseases='sir', networks='random', demographics=True, dur=10)
sim.init()
sim.loop.insert(check_pop_size, label='people.finish_step')
sim.run()
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
Sim(n=10000; 2000—2010.0; demographics=births, deaths; networks=randomnet; diseases=sir)

In this case, you get the same output as using an analyzer:

def check_pop_size(sim):
    print(f'Population size is {len(sim.people)}')

sim = ss.Sim(diseases='sir', networks='random', demographics=True, dur=10, analyzers=check_pop_size)
sim.run()
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
Sim(n=10000; 2000—2010.0; demographics=births, deaths; networks=randomnet; diseases=sir; analyzers=check_pop_size)

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.:

sis = ss.SIS(dt=0.1)
net = ss.RandomNet(dt=0.5)
births = ss.Births(dt=1)
sim = ss.Sim(dt=0.1, dur=5, diseases=sis, networks=net, demographics=births)
sim.init()
sim.loop.plot_step_order()
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.)