Skip to main content

Profiling and debugging

🧠8% 🤖92%

A model can go wrong in three ways: it stops with an error, it runs but gives an answer that looks wrong, or it runs too slowly. This page goes through Starsim’s tools for each, and the mistakes that commonly catch people out.

Where to look first, by symptom (the sections below cover each in turn):

Symptom First checks
An error before the run starts The message: it names the parameter, state, or module, and usually what to do
No epidemic R0 in the first lines printed; where the initial infections are (init_prev seeds the first infected state, not necessarily an infectious one); a rate written where a duration was meant
An epidemic far too fast or too slow Units of rates and durations; β per contact multiplied by a number of contacts; the time step (sim.check_dt())
The wrong population size total_pop and n_agents; births, deaths, and migration; the number of people each agent stands for
A fit that looks wrong Which result each data column is compared with, and over what period
Different results from run to run The seed, the solver, changes to the model, versions of Starsim
A slow run The number of agents, steps, and runs; then profiling

The examples on this page use the measles model from the tutorials (β = 1.9 per day, infectious for 8 days, so R0 ≈ 15):

import starsim as ss

S, I, R = ss.states('S I R')
infection = S >> I @ 1.9*I        # β = 1.9 per day
recovery  = I >> R @ ss.days(8)   # Infectious for 8 days on average
measles = ss.Disease(infection, recovery, init_prev=0.001)
sim = ss.Sim(measles, total_pop=100_000, stop=ss.days(50))
sim.run()
Sim: measles, 100,000 people, as an ODE, days 0 to 50
  measles: R0 = 15.2, with 100 infectious at the start
  Done in 0.0025 s; measles: 99,900 infections; at most 74,112 infectious, on day 6
Sim(n=100000; 0—50; diseases=measles)

Errors and what a run prints

When Starsim can’t build or run a model, it raises an error that names what is wrong in the terms of the model, and says what to do about it. Here the parameter β is misspelled:

try:
    ss.Sim(measles, pars=dict(betta=2.0), total_pop=100_000, stop=ss.days(50)).run()
except KeyError as error:
    print(error)
pars= has a value for "betta", which matches no parameter. Parameters are measles.init_prev, measles.beta, measles.dur_I; did you mean measles.beta?

Most errors are Python’s own (KeyError, TypeError, ValueError); the few that code may want to catch by kind, such as ss.CapabilityError (the solver asked for can’t run part of the model), are classes of their own, listed in the appendix. Most mistakes are caught when the sim is built (sim.init(), which sim.run() calls first), so an error usually comes at once rather than after a long run.

A run prints what it is running and how, the R0 of each disease, any notes, and a closing line with the size and timing of the peak. Read these first: most surprises are explained there. A note is something Starsim assumed or noticed that is worth knowing, and doesn’t stop the run. For example, in an SEIR model, init_prev puts people in E, where a new infection starts, so nobody is infectious at first (to start people infectious, give init=dict(I=0.001) or init_n=dict(I=100)):

S, E, I, R = ss.states('S E I R')
infection = S >> E @ 1.9*I
latency   = E >> I @ ss.days(10)
recovery  = I >> R @ ss.days(8)
seir = ss.Disease(infection, latency, recovery, init_prev=0.001, name='measles')
seir_sim = ss.Sim(seir, total_pop=100_000, stop=ss.days(100))
seir_sim.run()
Sim: measles, 100,000 people, as an ODE, days 0 to 100
  measles: R0 = 15.2, with 0 infectious at the start
  Note: init_prev seeds E of "measles", where a new infection starts, so nobody is infectious at the start; to seed infectious people, use init=dict(I=...) (a fraction) or init_n= (a number)
  Done in 0.0034 s; measles: 99,900 infections; at most 29,177 infectious, on day 33
Sim(n=100000; 0—100; diseases=measles)

The notes are kept in sim.notes, even when nothing is printed (verbose=0):

seir_sim.notes
["unit='day' (from the typed values in the model)",
 'init_prev seeds E of "measles", where a new infection starts, so nobody is infectious at the start; to seed infectious people, use init=dict(I=...) (a fraction) or init_n= (a number)']

Some messages are warnings rather than notes: the run continues, but something probably needs attention (for example, a fitted parameter that ended at one of its bounds). To stop at the first warning instead, so that a script can’t carry on past one unnoticed, turn warnings into errors:

ss.options(warnings='error')

Checking the model

Starsim fills in whatever a model leaves out: the population size, the distribution of each duration, how people mix, the unit of time, and which solver to use. describe() lists the whole model as it was built, with each value that Starsim chose itself marked as guessed. If a result looks wrong, check here that the model is the one you meant:

sim.describe()
Sim 1 module, 100,000 people, days 0 to 50
solver ode guessed: every part of the model has a compartmental form
unit day from the typed values in the model
Disease 'measles' 3 states, 2 transitions
states S, I, R
I infectious
init: init_prev = 0.001; the rest in S
infection S -> I transmission, from I
beta = 1.9 per day
rate = 1.9 × I.n / N homogeneous mixing
recovery I -> R spontaneous
time in I ~ exponential(mean = 8 days) guessed: scalar duration
Mixing and interventions
mixing homogeneous, 1 contact per day guessed: no network given
Results and running
results S, I, R, and one flow per transition; also infected, prevalence
plan measles: as compiled code, the whole run in one call
schedule every day: measles
Notes
note unit='day' (from the typed values in the model)
Per step of measles 1 day
infection beta = 1.9 per day → 1.9 per step
recovery rate 0.125 per day → p = 1 - exp(-0.125 × 1) = 0.1175 per step

equations() gives the same model as the equations of an ODE, which is often the quickest way to check that each rate is what you meant (for example, that a transmission is frequency-dependent, β S I / N, and not density-dependent, β S I):

measles.equations()
dS/dt = -beta * S * I / N
dI/dt =  beta * S * I / N - I / dur_I
dR/dt =  I / dur_I

Runs that should match but don’t

When two sims that should agree don’t, compare what each says about itself: describe() gives the text of each (a string), and a diff of the two shows only the lines where their states, transitions, parameters, and settings differ. Here a second sim has a lower β and a shorter step:

import difflib

S, I, R = ss.states('S I R')
infection = S >> I @ 1.5*I
recovery  = I >> R @ ss.days(8)
other = ss.Disease(infection, recovery, init_prev=0.001, name='measles')
other_sim = ss.Sim(other, total_pop=100_000, stop=ss.days(50), dt=ss.days(0.5), verbose=0)

before = sim.describe().splitlines()
after = other_sim.describe().splitlines()
print('\n'.join(difflib.unified_diff(before, after, 'sim', 'other_sim', lineterm='', n=0)))
--- sim
+++ other_sim
@@ -11,2 +11,2 @@
-               beta = 1.9 per day
-               rate = 1.9 × I.n / N                            [homogeneous mixing]
+               beta = 1.5 per day
+               rate = 1.5 × I.n / N                            [homogeneous mixing]
@@ -22 +22 @@
-  schedule     every day: measles
+  schedule     every 0.5 days: measles
@@ -27,3 +27,3 @@
-Per step of measles: 1 day
-  infection    beta = 1.9 per day → 1.9 per step
-  recovery     rate 0.125 per day → p = 1 - exp(-0.125 × 1) = 0.1175 per step
+Per step of measles: 0.5 days
+  infection    beta = 1.5 per day → 0.75 per step
+  recovery     rate 0.125 per day → p = 1 - exp(-0.125 × 0.5) = 0.0606 per step

The lines marked - are the first sim’s and those marked + the second’s. To compare the results of two runs, ss.debugtools.diff_sims() lists each summary result that differs, with the ratio of the two, and ss.debugtools.check_sims_match() gives True if every result matches (see debugtools):

other_sim.run()
ss.debugtools.diff_sims(sim, other_sim);

The following values differ between the two simulations:
                         sim1       sim2    diff   ratio change
measles_S               0.026      0.642   0.615  24.437   ↑↑↑↑
measles_I             317.166    368.459  51.294   1.162     ↑↑
measles_R           99682.808  99630.899 -51.909   0.999      ≈
measles_recovery    99720.076  99653.223 -66.853   0.999      ≈
measles_infected      317.166    368.459  51.294   1.162     ↑↑
measles_prevalence      0.156      0.158   0.001   1.009      ↑

Where two runs of what should be the same model differ only from some step on (after a change to code written as a module, say), sim.set_diagnostics() before the run records every random draw and every agent’s states at every step, in sim.diagnostics, so the step where the two runs diverge can be found. It slows the run a great deal, so use it on a small sim.

Checking the numbers

A few numbers can be checked by hand. The run prints R0, which for an SIR is β × the infectious period; if it is far from what you expected, a rate or a duration is wrong, often in its units (see Common pitfalls). Another check is to run the same model in another way, such as agents against the ODE: a large difference that doesn’t shrink with more agents points to the time step, or to something only one of them does (Solvers and backends says when to expect them to differ). For the time step, sim.check_dt() runs the model with its step and with one half as long, and shows the largest difference in each result at any time, as a fraction of the result’s largest value:

agent_sim = ss.Sim(measles, n_agents=20_000, stop=ss.days(50), verbose=0)
agent_sim.check_dt()
Change in each result when the timestep goes from 1 to 0.5 days (largest difference, as a fraction of the largest value):
  measles.infection             64.1%   (peak 5365 → 6500)
  measles.recovery              55.5%   (peak 1677 → 1789)
  measles.prevalence            38.7%   (peak 0.7844 → 0.7603)
  measles.I                     38.7%   (peak 15688 → 15206)
  measles.infected              38.7%   (peak 15688 → 15206)
  measles.S                     34.3%   (peak 19980 → 19980)
  measles.R                     14.2%   (peak 19884 → 19909)

If the result you care about changes by more than a few percent, use a smaller step (dt=ss.days(0.5)) and check again. For a stochastic run, part of each change is chance.

Finding what changes a parameter

Interventions and connectors change parameters while the model runs. When a parameter doesn’t seem to have the value you expect, sim.what_modifies() lists everything that changes it:

masks = ss.modify(measles.pars.beta, multiply=0.6, start=ss.days(10))
masked = ss.Sim(measles, total_pop=100_000, stop=ss.days(50), interventions=masks, verbose=0)
masked.what_modifies(measles.pars.beta)
measles.pars.beta
  modifiers
    masks            multiply=0.6   when=ss.schedule(start=days(10))

Common pitfalls

These are mistakes that have caught people out. Most don’t raise an error: the model runs and gives a wrong answer, though Starsim prints a note for many of them. The commonest is a number after @, which is a rate, not a duration: I >> R @ 8 means leaving I at a rate of 8 per day (after an eighth of a day on average), not after 8 days. Starsim notes a duration shorter than the step, which is almost always this:

S, I, R = ss.states('S I R')
wrong = ss.Disease(S >> I @ 1.9*I, I >> R @ 8, init_prev=0.001, name='measles') # Meant: ss.days(8)
ss.Sim(wrong, total_pop=100_000, stop=ss.days(50)).run()
Sim: measles, 100,000 people, as an ODE, days 0 to 50
  measles: R0 = 0.238, with 100 infectious at the start
  Note: unit='day' (from start and stop)
  Note: I_to_R has a mean dwell of 0.12 days, shorter than the step; a duration is written dur=, or ss.days(10)
  Done in 0.0015 s; measles: 31 infections; at most 100 infectious, on day 0
Sim(n=100000; 0—50; diseases=measles)

R0 comes out at 0.24 instead of 15. Give a duration with its unit (ss.days(8)), or as dur= in ss.Transition(I, R, dur=ss.days(8)). The others, each covered in full on its own page:

  • A plain number is in the sim’s unit of time. If the model’s unit is days, stop=2, meant as two years, is two days. Give times with their units or as dates (Time).
  • A number as a time is a calendar year if the sim has a calendar. With start='2025-01-01', when=2026 is the start of 2026, whatever the unit; with start=0, it is 2026 units from 0. A time outside the sim is an error that says how it was read (Time).
  • A rate times a duration is not a rate. The product itself is fine: ss.peryear(0.1)*ss.days(3) is a plain number, the chance of an event over 3 days. Using it as the rate of a transition raises a TypeError that shows the product and asks for the rate itself (Time).
  • An ODE always takes off. With R0 above 1 it takes off from any number infected, while a real outbreak started by a few cases often dies out by chance; for the chance of an outbreak, run a stochastic solver many times (Solvers and backends).
  • The median of many runs can hide that some died out. When some runs die out and the rest take off, the median or mean describes neither; count the runs with an outbreak, and summarize those (Scenarios).
  • n_agents= runs the model as agents. To set the size of a compartmental model, use total_pop= (Running sims).
  • With contacts, β is per contact. Adding a network or contact matrix multiplies R0 by about the number of contacts, so scale β down to keep the same R0; the sim prints the new R0 (Mixing).
  • stratify() changes the disease itself. Stratifying the same disease again in another way is an error; build the disease again each time (Stratification).
  • Vaccines are leaky unless told otherwise. efficacy=0.9 reduces everyone’s chance of infection by 90%; leaky=False fully protects 90% instead, which for a fast disease such as measles gives far fewer infections (Products).
  • Data is matched to results by name. A column called reported is not compared with the result cases (Starsim notes it, and sim.fit() stops); rename it, or use ss.SimData(data, aliases=dict(cases='reported')) (Fitting a model).
  • Counts over a period are compared with totals over the same period. Infections are summed between data points, while the number infectious is compared at each; check which kind each column is (Fitting a model).
  • A method that forgot to call super(). A module written as code that overrides init_pre(), init_post(), finish_step(), and the like without calling the base class’s version leaves part of it unset; Starsim warns at the end of the run (Custom modules).
  • len(sim.people) still counts those who died this step. For a fraction worked out in an analyzer, use sim.people.n, the number alive.
  • A date between two steps never equals sim.now. if sim.now == ss.date('2025-03-01') with weekly steps does nothing; compare with >=, or give the module start=.

More are in the pitfalls of Custom modules and Interventions.

Looking inside a run

To look at a run partway through, sim.run(until=...) stops after the step that starts at that time, with the people and their states as they are at its end (sim.people.to_df()); a later sim.run() carries on (see Running sims).

A model run as agents, or one with modules written as code, is run one step at a time, and within each step the modules are run in a fixed order: the loop. When a sim does something it shouldn’t, or doesn’t do something it should, sim.loop is your friend. sim.loop.to_df() lists every call in it, in order, for the whole run; the first rows are the first step:

agent_sim.run()
agent_sim.loop.to_df().head(13)
time ti func_order label module func_name cpu_time
0 0.0 0 1 sim.start_step sim start_step NaN
1 0.0 0 2 measles.start_step measles start_step NaN
2 0.0 0 3 people.record_states people record_states NaN
3 0.0 0 4 measles.record_states measles record_states NaN
4 0.0 0 5 measles.step_state measles step_state NaN
5 0.0 0 6 measles.step measles step NaN
6 0.0 0 7 people.step_die people step_die NaN
7 0.0 0 8 people.record_events people record_events NaN
8 0.0 0 9 measles.record_events measles record_events NaN
9 0.0 0 10 measles.finish_step measles finish_step NaN
10 0.0 0 11 people.finish_step people finish_step NaN
11 0.0 0 12 sim.finish_step sim finish_step NaN
12 1.0 1 0 sim.advance sim advance NaN

To see what happens during a run, a function of the sim can be inserted into the loop, after (or, with before=True, before) any call in it. Here it prints the number infectious every ten days, just after the disease has taken its step:

def show_infectious(sim):
    if sim.ti % 10 == 0:
        print('Day', sim.ti, 'infectious:', sim.diseases.measles.I.sum())

probed = ss.Sim(measles, n_agents=20_000, stop=ss.days(50), verbose=0)
probed.init()
probed.loop.insert(show_infectious, label='measles.step')
probed.run()
Day 0 infectious: 46
Day 10 infectious: 13552
Day 20 infectious: 3951
Day 30 infectious: 1162
Day 40 infectious: 307
Day 50 infectious: 102
Sim(n=20000; 0—50; diseases=measles)

A function given as an analyzer (analyzers=show_infectious) does the same, but always runs last in the step; for more than a quick look, write an analyzer that keeps results of its own. A compartmental model that Starsim runs whole has no loop to look into; inserting a function makes it run one step at a time, with the same results.

sim.loop.plot() draws the loop as a diagram, which is most useful when modules have time steps of their own (demographics once a year and a disease once a week, say), to see which runs when. sim.run_one_step() takes a single step, for stepping through a run by hand in a debugger.

Profiling

Compartmental models run in well under a second, so profiling is mostly for models run as agents, especially with networks or modules written as code. Start with how long each part of each step takes: sim.profile(kind='loop') runs the sim recording the time of each call of the loop, and plots the time spent in each (sim.loop.plot_cpu() plots it again):

big = ss.Sim(measles, n_agents=50_000, networks=ss.RandomNet(n_contacts=10), stop=ss.days(50), verbose=0)
big.profile(kind='loop');

To see which lines of a function take the time, follow it with sim.profile() (kind='line', the default), which runs a copy of the sim with a line-by-line profiler on the functions given:

big = ss.Sim(measles, n_agents=50_000, networks=ss.RandomNet(n_contacts=10), stop=ss.days(50), verbose=0)
prof = big.profile(follow=ss.RandomNet.add_pairs, plot=False)
prof.disp(maxentries=5)
Profiling 1 function(s):
 <function RandomNet.add_pairs at 0x77b4643d9300> 

Elapsed time: 0.722 s


——————————————————————————————————————————————————————————————
Profile of networks.RandomNet.add_pairs: 0.246455 s (34.1092%)
——————————————————————————————————————————————————————————————

Total time: 0.246455 s
File: /home/cliffk/idm/ssv4/starsim/networks.py
Function: RandomNet.add_pairs at line 1144

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
  1144                                               def add_pairs(self):
  1145                                                   """ Add the new edges needed to bring each agent up to their number of contacts """
  1146        51     138235.0   2710.5      0.1          p = self.pars
  1147        51      86663.0   1699.3      0.0          people = self.sim.people
  1148        51   19396465.0 380322.8      7.9          uids = self.eligible(p.who).uids
  1149        51     278936.0   5469.3      0.1          current = len(self)
  1150        51    1607825.0  31526.0      0.7          n_conn = self.same_contacts(len(uids), current)
  1151        51      40596.0    796.0      0.0          if n_conn is None:
  1152                                                       if isinstance(p.n_contacts, ss.Dist):
  1153                                                           n_conn = p.n_contacts.rvs(uids)
  1154                                                       else:
  1155                                                           n_conn = np.full(len(people), p.n_contacts)
  1156                                           
  1157                                                       # See how many new edges we need
  1158                                                       orig = n_conn.sum()
  1159                                                       target = orig / 2 # Divide by 2 since bi-directional
  1160                                                       needed = target - current
  1161                                                       if needed <= 0:
  1162                                                           return
  1163                                                       n_conn = n_conn*needed/orig
  1164                                                       n_conn = p.n_contacts.randround(n_conn) # CRN-safe stochastic integer rounding
  1165                                           
  1166        51  208787550.0 4.09e+06     84.7          p1, p2 = self.pair(uids, n_conn)
  1167        51     118242.0   2318.5      0.0          if p.remove_self_loops:
  1168                                                       keep = p1 != p2 # One pass over the new edges; finding duplicates would need a sort of all of them
  1169                                                       p1, p2 = p1[keep], p2[keep]
  1170        51      85329.0   1673.1      0.0          n = len(p1)
  1171        51    7804428.0 153028.0      3.2          weight = np.ones(n, dtype=ss_float)
  1172        51    7060272.0 138436.7      2.9          dur = self.edge_steps(p1)
  1173                                           
  1174        51     200982.0   3940.8      0.1          p1 = p1.view(ss.uids) # No copy
  1175        51     100746.0   1975.4      0.0          p2 = p2.view(ss.uids)
  1176        51      45543.0    893.0      0.0          if current == 0: # Nothing to append to
  1177        51     257173.0   5042.6      0.1              self.edges.p1 = p1
  1178        51     135763.0   2662.0      0.1              self.edges.p2 = p2
  1179        51     153123.0   3002.4      0.1              self.edges.weight = weight
  1180        51     115964.0   2273.8      0.0              self.edges.dur = dur
  1181                                                   else:
  1182                                                       self.append(p1=p1, p2=p2, weight=weight, dur=dur)
  1183        51      41451.0    812.8      0.0          return



——————————————————————————————————————————————————————————————
Profile of networks.RandomNet.add_pairs: 0.246455 s (34.1092%)
——————————————————————————————————————————————————————————————

Total time: 0.246455 s
File: /home/cliffk/idm/ssv4/starsim/networks.py
Function: RandomNet.add_pairs at line 1144

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
  1144                                               def add_pairs(self):
  1145                                                   """ Add the new edges needed to bring each agent up to their number of contacts """
  1146        51     138235.0   2710.5      0.1          p = self.pars
  1147        51      86663.0   1699.3      0.0          people = self.sim.people
  1148        51   19396465.0 380322.8      7.9          uids = self.eligible(p.who).uids
  1149        51     278936.0   5469.3      0.1          current = len(self)
  1150        51    1607825.0  31526.0      0.7          n_conn = self.same_contacts(len(uids), current)
  1151        51      40596.0    796.0      0.0          if n_conn is None:
  1152                                                       if isinstance(p.n_contacts, ss.Dist):
  1153                                                           n_conn = p.n_contacts.rvs(uids)
  1154                                                       else:
  1155                                                           n_conn = np.full(len(people), p.n_contacts)
  1156                                           
  1157                                                       # See how many new edges we need
  1158                                                       orig = n_conn.sum()
  1159                                                       target = orig / 2 # Divide by 2 since bi-directional
  1160                                                       needed = target - current
  1161                                                       if needed <= 0:
  1162                                                           return
  1163                                                       n_conn = n_conn*needed/orig
  1164                                                       n_conn = p.n_contacts.randround(n_conn) # CRN-safe stochastic integer rounding
  1165                                           
  1166        51  208787550.0 4.09e+06     84.7          p1, p2 = self.pair(uids, n_conn)
  1167        51     118242.0   2318.5      0.0          if p.remove_self_loops:
  1168                                                       keep = p1 != p2 # One pass over the new edges; finding duplicates would need a sort of all of them
  1169                                                       p1, p2 = p1[keep], p2[keep]
  1170        51      85329.0   1673.1      0.0          n = len(p1)
  1171        51    7804428.0 153028.0      3.2          weight = np.ones(n, dtype=ss_float)
  1172        51    7060272.0 138436.7      2.9          dur = self.edge_steps(p1)
  1173                                           
  1174        51     200982.0   3940.8      0.1          p1 = p1.view(ss.uids) # No copy
  1175        51     100746.0   1975.4      0.0          p2 = p2.view(ss.uids)
  1176        51      45543.0    893.0      0.0          if current == 0: # Nothing to append to
  1177        51     257173.0   5042.6      0.1              self.edges.p1 = p1
  1178        51     135763.0   2662.0      0.1              self.edges.p2 = p2
  1179        51     153123.0   3002.4      0.1              self.edges.weight = weight
  1180        51     115964.0   2273.8      0.0              self.edges.dur = dur
  1181                                                   else:
  1182                                                       self.append(p1=p1, p2=p2, weight=weight, dur=dur)
  1183        51      41451.0    812.8      0.0          return

sim.profile(kind='function') profiles every function the run calls, for when you don’t yet know where to look. Only functions called during the run can be followed this way; for anything else, such as building the sim, use sc.profile() from Sciris directly. When the time is in your own code, Speed and scale has the remedies. A fit runs the model thousands of times, so profile the model before fitting it.

Further reading

If none of this finds the problem, cut it down to the smallest script that shows it (a small population, a short run, one disease, no data files) and ask on the Starsim GitHub issues, with the script, what it printed, and what you expected; see How to contribute.

Appendix: every diagnostic tool

The tools on this page and elsewhere, by the question each answers. “Builds” means it calls sim.init() if the sim hasn’t been built, without running it.

Tool What it answers Runs the sim?
The lines a run prints, and sim.notes What Starsim assumed, approximated, or wants checked Notes are made as the sim is built and run
sim.describe() The model as built: every state, transition, parameter, and setting, with each guess marked Builds
disease.equations() The model as the equations of an ODE No
disease.diagram() The states and transitions, drawn No
sim.solver, sim.solver_reason Which solver was chosen, and why Builds
sim.describe(), require_kernels=True Why a compartmental run is taken a step at a time rather than in one call (see Speed and scale) Builds
ss.solvers.solver_table() Every rule that decides which engine runs a model (see Solvers and backends) No
sim.check_dt() How much each result changes with a step half as long Runs two copies
sim.what_modifies() Everything that changes a parameter during the run Builds
describe() with difflib Where two models differ Builds
ss.debugtools.diff_sims(), check_sims_match() Which results of two runs differ, and by how much No (give it sims that have run)
sim.set_diagnostics() Every random draw and agent state at every step, to find where two runs diverge Records during the next run
ss.options(warnings='error') Stops at the first warning No
sim.run(until=...), sim.people.to_df() The people and their states partway through a run Runs part of the way
sim.run_one_step() One step at a time, for a debugger Runs one step
sim.loop.to_df(), sim.loop.plot() Every call in every step, in order Builds
sim.loop.insert() Runs a function of your own at any point in the step No (it runs with the sim)
sim.profile(kind='loop'), sim.loop.plot_cpu() How long each call of the loop takes Runs
sim.profile(follow=...) How long each line of the functions given takes Runs a copy
sim.profile(kind='function') How long every function the run calls takes Runs
sim.elapsed The seconds the last run took No
STARSIM_CHECK_COUNTS=1 (environment variable) Checks, at every step, the counts the agent engine keeps against a new count (for testing the engine) Applies to every run as agents

Appendix: errors and warnings

Most errors are Python’s own (KeyError, TypeError, ValueError); these are the ones that are classes of their own, in starsim/internals/errors.py, so that code can catch or filter them by kind (see Model execution for the policy behind them):

import inspect
import pandas as pd
pd.set_option('display.max_colwidth', None) # Show each entry in full

rows = []
for name, cls in inspect.getmembers(ss.errors, inspect.isclass):
    if issubclass(cls, (Exception, Warning)) and cls.__module__ == ss.errors.__name__:
        rows.append(dict(
            name = ('ss.' if name in ss.errors.__all__ else 'ss.errors.') + name,
            kind = 'warning' if issubclass(cls, Warning) else 'error',
            subclass_of = ', '.join(base.__name__ for base in cls.__bases__),
            what = ' '.join(inspect.getdoc(cls).split()),
        ))
pd.DataFrame(rows).set_index('name')
kind subclass_of what
name
ss.AlreadyRunError error RuntimeError A sim that has already run (or been finalized) is stepped, finalized, or added to again; `sim.run()` runs it again from the start
ss.errors.AtBound warning RuntimeWarning A best-fit value is at or near one of its bounds
ss.errors.BooleanOperationError error NotImplementedError A logical operation (and, or, xor, not) is applied to an array of people's values that is not Boolean, e.g. `ppl.age & ppl.female`
ss.CapabilityError error ValueError The requested solver cannot run part of the model. Three cases: there is no sensible approximation (contact tracing, transmission trees, agent-only custom code); a function or condition asked for something that exists only per agent in a compartmental run; or an approximation would be needed and `approximations="error"`. Names the part, what it asked for, and what would fix it
ss.errors.DistNotInitializedError error RuntimeError A distribution made with `strict=True` is sampled before it is initialized (by the sim, or with `dist.init()` or `dist.mock()`)
ss.errors.DistSeedRepeatError error RuntimeError Two distributions of a sim have the same seed, or a distribution is jumped back to a state it has passed, so that it would draw the same random numbers twice
ss.errors.ImportIncomplete warning RuntimeWarning An import could not translate something in the source. Lists each item by name, says what to ask an AI assistant with the `convert-model` skill to finish it, and warns again when the sim is first run
ss.errors.LossyExport warning RuntimeWarning An export to another format left something out or changed it. Lists each item
ss.errors.NoAgentAttribute error CapabilityError, AttributeError An attribute of people that exists only per agent, asked for in a compartmental run: also an `AttributeError`, so `hasattr()` gives False
ss.errors.NotConverged warning RuntimeWarning The chains of a posterior disagree (R-hat above 1.01), or some draws diverged