Migrating from Covasim v3 to v4
Covasim v4 is built on the Starsim framework: cv.Sim is a subclass of ss.Sim, the population is an ss.People, contact layers are ss.Networks, and the COVID disease logic is in a single cv.COVID module (an ss.Infection). The v3 API is preserved, so most v3 code runs on v4 without changes. This guide lists what does need to change, and how to make those changes.
What stays the same
- Creating and running a sim:
cv.Sim(pars),cv.Sim(pop_size=20e3, ...),sim.initialize(),sim.run(),sim.plot(),sim.save()andcv.load(). - Parameters: the v3 parameter names (
pop_size,pop_infected,pop_type,n_days,start_day,beta,pop_scale,rescale,use_waning,locationetc.), read and set withsim['key']. - Results:
sim.results['cum_infections'],sim.summary['cum_infections'], and per-agent states such assim.people.exposed. - Interventions:
cv.test_prob,cv.test_num,cv.contact_tracing,cv.vaccinate_prob,cv.vaccinate_num,cv.vaccinate,cv.simple_vaccine,cv.change_beta,cv.clip_edges,cv.dynamic_parsandcv.sequence, as well as custom interventions with anapply(self, sim)method. - Analyzers:
cv.snapshot,cv.age_histogram,cv.daily_age_stats,cv.nab_histogramandcv.TransTree, as well as custom analyzers. - Variants and immunity:
cv.variant, waning immunity and cross-immunity (use_waning, which is on by default as in v3). - Multiple runs and fitting:
cv.MultiSim,cv.Scenarios,cv.parallel,cv.Fitandcv.Calibration.
What is supported for backwards compatibility is in covasim/compat.py.
How to migrate
The rules below are the changes that v3 code needs in order to run on v4, found by running the v3 test suite, tutorials and examples against v4. Rules marked mechanical are applied by the migration script; rules marked judgment can change results without raising an error, so need checking by hand.
covasim-migrate3to4 my_script.py # Show the changes for one file
covasim-migrate3to4 my_folder --apply # Change every .py file and notebook in a folderThe script also lists the lines that may need one of the other rules. The same functions are available in Python as cv.migrate3to4, e.g. cv.migrate3to4.migrate('my_folder'). For an AI assistant doing a migration, docs/migrate3to4/SKILL.md has instructions.
Results are not identical to v3 for the same rand_seed, since v4 uses Starsim’s random number streams (one for each distribution) rather than a single global stream. To check that a migrated script gives the same answers, compare the means over several seeds rather than single runs.
Removed
cv.ParsObj,cv.BaseSim,cv.BasePeople,cv.Person: the v3 base classes are gone;cv.Simandcv.Peopleare now based onss.Simandss.People. There is no v4 equivalent; code that subclasses these needs rewriting.pop_type='synthpops': SynthPops is not supported; usepop_type='hybrid'.popfile,cv.make_people(),cv.make_randpop(),cv.make_synthpop(): v3 populations can’t be loaded or created; letcv.Simcreate the population. To reuse a saved v3 (e.g. SynthPops) population, export each agent’s age and sex and each layer’sp1,p2andbetaarrays with v3; then in v4, createpeople = cv.People(n), setpeople.age.defaultandpeople.female.defaultto functions that return the stored values (e.g.def get_ages(n): return ages[:n]), and passpeople=people, networks=[cv.Layer(name='h', edges=dict(p1=p1, p2=p2, beta=beta)), ...]tocv.Sim().cv.migrate3to4(): v3 sims can be loaded withcv.load()and their results, parameters (sim['key']), dates (sim.day(),sim.datevec), interventions and analyzers read, but they can’t be migrated, rerun or plotted, and v3 sims saved with their people (or v3.pplfiles) can’t be loaded.cv.options(numba_parallel=...)and theCOVASIM_NUMBA_PARALLELenvironment variable: Covasim no longer has multithreaded Numba functions; delete the line.- Parameters from Covasim versions before 2.1.0 (
cv.Sim(version=...)). Versions before 2.1.0 differ from 2.1.0 only in their duration distributions, whosepar2is the square root of the 2.1.0 value, so e.g. useversion='2.1.0'withdurfromcv.make_pars(version='2.1.0')['dur']with eachpar2replaced by its square root.
Changed
sim.pars['key']→sim['key'](mechanical).sim.parsnow holds the Starsim sim parameters (e.g.n_agentsrather thanpop_size); the COVID parameters are insim.diseases.covid.pars.sim['key']gets and sets the v3 parameters wherever they are stored. Unmigrated, setting a COVID parameter withsim.pars['key'] = valueraises an error once the sim is initialized (Key "key" not found); before then it has no effect, without an error.- Agents who have died are removed from the per-agent arrays (judgment). As in Starsim,
sim.people.age,sim.people.exposed,sim.people.date_exposedetc.,len(sim.people)andsim.nonly include agents who are alive, so their length shrinks as agents die, and a position in an array is not a UID. The exceptions aresim.people.deadandsim.people.date_dead, which cover every agent ever created (sosim.people.dead.sum()is the number of deaths, andcv.true(sim.people.dead)is their UIDs). Indexing by UID works for any agent, alive or dead, sopeople.age[cv.true(people.dead)]needs no change. What needs migrating is code that assumes a fixed length or combines arrays elementwise withdead: use.rawfor the values for every agent, e.g.people.age[people.dead]→people.age.raw[people.dead],np.arange(len(sim.people))→sim.people.indices()(the UIDs of agents alive),np.zeros(len(sim.people))indexed by UID →np.zeros(sim.people.n_uids). Use.uidsorcv.true()to get UIDs rather thansc.findinds()ornp.nonzero(), which return positions. people.get(),people.person_keys(),people.dur_keys(),people.to_arr(),people.to_list(),people.from_list(),people._resize_arrays(),people.make_edgelist(): v3 internals with no v4 equivalent. Usepeople.keys(),people.to_df(), andpeople.person(uid)instead.cv.Result(...)→ss.Result(...):cv.Resultis now Starsim’sss.Result, which has a different signature.sim.validate_pars(),sim.init_*()(e.g.init_people,init_immunity): v3 internals of initialization. Parameters are validated when the sim is created or initialized.- Sims are saved with their people by default (
sim.save(); usekeep_people=Falseto remove them), so a loaded sim that has been run needssim.initialize(reset=True)before it can be rerun. sim.run(reset_seed=True)andsim.set_seed()part-way through a run have no effect on the results: v4 uses a separate random number stream for each distribution (common random numbers), so results only depend onrand_seed.sim.people.t = sim.t(to continue running a sim after re-initializing it): v3 internal; usesim.initialize(reset=True)instead.daily_age.results[date]→daily_age.age_results[date](ordaily_age.to_df()): Starsim reservesresultsfor time series.snapshotvalues arecv.PeopleSnapshotobjects (full-length arrays indexed by UID, supporting indexing,true(),false(),defined()andcount()) rather than copies ofPeople; otherPeoplemethods (e.g. contacts) aren’t available on them.sim.results['gen_time']→sim.gen_time, andsim.results.transtree/agehist→sim.transtree/sim.agehist(withoutput=False) (mechanical): Starsim results can only hold time series.TransTree.detailedcolumns fordate_known_contactare NaN, since v4 doesn’t record that date. Likewise,cv.PeopleSnapshotdoesn’t includeknown_dead,date_known_dead,date_known_contact,n_infectionsor the durations (dur_*).sim.summarize()with no arguments returns the summary rather than printing it, since Starsim uses it that way; useprint(sim.summary)instead, orprint(sim.summarize(output=True))for the v3 table.fit.to_json():cv.Fitis no longer an analyzer and has noto_json(); usefit.mismatch,fit.mismatchesandfit.pairdirectly.contacts.pop_layer()andcontacts[key] = layeron a sim’s contacts raise an error, since networks can’t be removed from or replaced in a sim; create the sim with the layers you want, e.g.cv.Sim(networks=...). Adding a layer withcontacts.add_layer()still works.people.vaccination_dates(a list of dates per person) →people.date_vaccinated(the date of the most recent dose).cv.itrue(arr, inds)and the related functions needindsto have one entry per living agent, socv.itrue(people.symptomatic, np.arange(pop_size))gives positions rather than UIDs once agents have died; usecv.itrue(people.symptomatic, people.uid), orpeople.symptomatic.uids.- With
frac_susceptible, all the seed infections are kept (v3 dropped about half of them), so results are slightly higher than in v3. msim.combine()doesn’t merge people; the combined sim keeps the first sim’s people.Scenarios(sim=<an initialized or already-run sim>)resets the sim to its state before initialization, so changes made after initializing it are not used. Likewise,cv.MultiSim(sim)on an initialized sim that hasn’t been run discards changes made after initialization (with a warning); make such changes in an intervention, or pass a list of sims withreseed=False.- Scenario results by variant (
scens.results) have time as the first axis, i.e. shape(npts, n_variants), as for sims. - Scenarios can’t change parameters that determine how the sim is built (e.g.
use_waning,pop_type); set these on the base sim. cv.Calibration(..., n_workers=...)defaults to the number of CPUs, and the calibration database is stored in a temporary folder unlessnameordb_nameis given.- Plot titles use the v4 result labels (e.g. “New infections” rather than “Number of new infections”), so
log_scale=[title]lists may need updating. sim.t→sim.ti(mechanical) in custom interventions and analyzers, e.g.if sim.t == 10:→if sim.ti == 10:,self.results[key][sim.t]→...[sim.ti], andsim.t % 7→sim.ti % 7. In v4,sim.tis the Starsim timeline (ss.Timeline) rather than the integer day, so comparing it with a number or using it as an index raises an error.- Custom
finalize(self)methods in the Starsim form (without thesimargument) must callsuper().finalize(); v3-stylefinalize(self, sim)methods don’t need to. - In custom interventions and analyzers, attributes Starsim reserves (
t,pars,sim,dists,results) can’t be set (e.g.self.t = []→self.tvec = []). Module names must be unique across interventions, analyzers, and networks: a module with a clashing default name (e.g. a classA, namedalike the random network) is renamed with a warning, but explicitly set names that clash raise an error. - Transmission is 1–2% lower than in v3 for the same parameters (judgment), because of a correction to the viral load.
viral_distgives a high viral load for the firstfrac_time(30%) of the infectious period, up tohigh_cap(4) days. In v3, a rounding error (comparing a double-precision fraction with a single-precision threshold) made this a day longer for people infectious for exactly 10 days (4 days rather than 3) and for most people infectious for 14 days or more (5 rather than 4); v4 uses the intended durations. The difference compounds while an epidemic is growing: e.g. about 10% fewer infections after four months in one calibrated model. Models calibrated with v3 should be recalibrated, usually by increasingbetaby 1–2%; there is no option to reproduce the v3 behavior. - Results by variant:
new_symptomatic_by_variantandnew_severe_by_variant(and their cumulative versions) are counted on the day people become symptomatic or severe, as for the results that aren’t by variant; v3 counted them on the day of infection.prevalence_by_variantis the number infected with each variant divided by the number alive, so it sums toprevalence; v3 used the number of new infections. No changes to code are needed, but the values differ from v3. cv.historical_wave()doesn’t include the seed infections in the wave. v3 did, then infected them again on day 0, which counted them twice innew_infectionsandcum_infectionson day 0 (so v3’s values are higher bypop_infected), and listed them twice in the transmission log.- Results by variant have time as the first axis, i.e. shape
(npts, n_variants)rather than(n_variants, npts): e.g.sim.results['variant']['new_infections_by_variant'][1,:]→[:,1](or.values.T), or use the variant name, e.g....['new_infections_by_variant'].delta.
Where things are in v4
None of this is needed to run v3 code, but it helps when reading v4 code or writing new code:
- The COVID parameters are in
sim.diseases.covid.pars, and the sim parameters (with the Starsim names, e.g.n_agents) are insim.pars;sim['key']reads and sets both. - The COVID results are in
sim.diseases.covid.results, and are also available assim.results[key]. Results by variant are insim.results['variant'].sim.summaryandsim.to_df()use the same names assim.results(e.g.cum_infections). sim.init()is the Starsim name forsim.initialize(); both work.
See the Starsim documentation for more on the framework.