Starsim v3.0 to v3.1 migration guide

Overview

This guide describes the breaking changes introduced in Starsim v3.1.0 and the steps required to migrate code from Starsim v3.0.x. It was mostly written by an LLM, and is mostly written for other LLMs.

Most of the changes are in the Pregnancy module and maternal networks. If your code does not use ss.Pregnancy, ss.MaternalNet, ss.PrenatalNet, or ss.PostnatalNet, you may only need to check the general changes in sections 1-3.

Note: If you are an LLM reading this, perform each of the following migrations one at a time and confirm with the user after each step.

General changes

1. People.(module_name) now returns the module, not a state container

Previously, people.sir returned a proxy object that exposed only module states (e.g. people.sir.infected). Now it returns a direct reference to the module itself, so all attributes – including dynamically computed @property values – are accessible.

What to change: In most cases, no change is needed – people.sir.infected still works. However, if your code relied on people.sir being a state-only namespace, be aware that it is now the full module object.

# v3.0 -- worked for states, but not for properties
people.sir.infected      # OK
people.sir.infectious    # AttributeError

# v3.1 -- both work
people.sir.infected      # OK (state)
people.sir.infectious    # OK (property)

2. Network.prenatal and Network.postnatal attributes removed

These boolean attributes have been removed. Type checking is used instead.

What to change: Replace attribute checks with isinstance() checks.

# v3.0
for nw in sim.networks.values():
    if nw.prenatal:
        ...
    if nw.postnatal:
        ...

# v3.1
for nw in sim.networks.values():
    if isinstance(nw, ss.PrenatalNet):
        ...
    if isinstance(nw, ss.PostnatalNet):
        ...

3. MaternalNet.add_pairs() argument names and signature changed

The arguments mother_inds and unborn_inds have been renamed to mother_uids and unborn_uids. The dur, start, and end arguments have been removed – edge lifespans are now managed by the Pregnancy module.

What to change: Update argument names and remove dur/start/end arguments.

# v3.0
net.add_pairs(mother_inds=mothers, unborn_inds=babies, dur=dur_preg, start=ti)

# v3.1
net.add_pairs(mother_uids=mothers, unborn_uids=babies)

4. PostnatalNet is now a DynamicNetwork with a dur parameter

PostnatalNet is no longer a subclass of MaternalNet. It is now a DynamicNetwork that accepts an optional dur parameter for the postnatal period. Its add_pairs() method now takes mother_uids and infant_uids (not unborn_uids).

What to change: Update how you construct and interact with PostnatalNet.

# v3.0
postnatal = ss.PostnatalNet()
# Pairs were added by Pregnancy with dur/start/end

# v3.1
postnatal = ss.PostnatalNet(dur=ss.lognorm_ex(mean=ss.years(0.5), std=ss.years(0.5)))
# Pairs are automatically added at delivery by Pregnancy

5. New BreastfeedingNet network

A new ss.BreastfeedingNet is available for modeling breastfeeding transmission. It automatically tracks breastfeeding status from the Pregnancy module. This is additive – no migration is required – but if you were previously using PostnatalNet to model breastfeeding transmission, consider switching.

# v3.1
sim = ss.Sim(
    demographics=ss.Pregnancy(),
    networks=[ss.PrenatalNet(), ss.BreastfeedingNet()],
    diseases=ss.SIR(),
)

Pregnancy module changes

6. fecund is now a computed property, not a BoolState

Previously fecund was a BoolState that was manually set to True/False. It is now a read-only @property that dynamically computes fecundity based on age and sex.

What to change: Remove any code that sets fecund directly.

# v3.0
pregnancy.fecund[uids] = False  # Manually mark as infecund
pregnancy.fecund[uids] = True   # Manually mark as fecund

# v3.1
# fecund is now read-only, computed from age, sex, and min_age/max_age
# Use pregnancy.infertile to mark agents as unable to conceive
pregnancy.infertile[uids] = True

7. postpartum state removed; breastfeeding states added

The postpartum BoolState and associated dur_postpartum / ti_postpartum states have been removed. Breastfeeding is now tracked separately.

What to change: Replace postpartum references with breastfeeding or use PostnatalNet with a dur parameter.

# v3.0
pregnancy.postpartum         # BoolState
pregnancy.dur_postpartum     # FloatArr
pregnancy.ti_postpartum      # FloatArr

# v3.1
pregnancy.breastfeeding      # BoolState (mothers currently breastfeeding)
pregnancy.dur_breastfeed     # FloatArr (duration of breastfeeding)
pregnancy.ti_stop_breastfeed # FloatArr (time breastfeeding stops)
pregnancy.breastfed          # BoolState (newborns who were breastfed)

8. child_uid state removed

The child_uid FloatArr has been removed. Unborn children are now found dynamically via people.parent.

What to change: Use pregnancy.find_unborn_children(parent_uids) instead.

# v3.0
child = pregnancy.child_uid[mother_uid]

# v3.1
children = pregnancy.find_unborn_children(mother_uids)

9. make_p_fertility() renamed to make_p_conceive()

The method and associated distribution have been renamed.

What to change: Rename method calls and references.

# v3.0
p = pregnancy.make_p_fertility(eligible_uids)
pregnancy.pars.p_fertility  # Bernoulli distribution

# v3.1
p = pregnancy.make_p_conceive(eligible_uids)
pregnancy._p_conceive  # Bernoulli distribution (now private)

10. p_neonatal_death replaced by p_survive_maternal_death

The semantics have been inverted and the name clarified. Despite its name, p_neonatal_death in v3.0 was only applied when a mother dies during pregnancy – it was the probability that the neonate also dies conditional on maternal death. It was never a general neonatal mortality parameter.

In v3.1, p_survive_maternal_death makes this conditional relationship explicit: it is the probability that an unborn child survives the mother’s death (default 0, meaning unborn children do not survive maternal death).

Note: Neither v3.0 nor v3.1 includes a built-in parameter for general neonatal mortality (e.g. from prematurity or birth complications). To model that, override the process_newborns() hook in a Pregnancy subclass.

What to change: Rename the parameter and invert the probability value.

# v3.0
pregnancy = ss.Pregnancy(p_neonatal_death=ss.bernoulli(0.8))  # 80% chance neonate dies given maternal death

# v3.1
pregnancy = ss.Pregnancy(p_survive_maternal_death=ss.bernoulli(0.2))  # 20% chance unborn survives maternal death

11. dur_pregnancy is now a distribution, not a fixed duration

Previously, dur_pregnancy was a fixed value (default ss.years(0.75)). It is now an ss.choice distribution that draws from gestational weeks 32-42, and duration is assigned per-agent.

What to change: If you were relying on a fixed pregnancy duration or accessing self.pars.dur_pregnancy.years, update accordingly.

# v3.0
pregnancy = ss.Pregnancy(dur_pregnancy=ss.years(0.75))
# Internally: self.pars.dur_pregnancy.years was a single float

# v3.1
# Default draws from weeks 32-42; to override with a fixed duration:
pregnancy = ss.Pregnancy(dur_pregnancy=ss.choice(a=ss.weeks(np.arange(38, 43))))
# Per-agent durations are stored in: pregnancy.dur_pregnancy[uids]  (a FloatArr)

12. set_prognoses() removed; logic split across new methods

The set_prognoses() method has been removed. Its functionality is now split across make_pregnancies(), process_delivery(), and set_breastfeeding().

What to change: If you had a subclass that overrode set_prognoses(), move the logic to the appropriate new method.

# v3.0
class MyPregnancy(ss.Pregnancy):
    def set_prognoses(self, uids):
        super().set_prognoses(uids)
        # Custom logic for new pregnancies

# v3.1
class MyPregnancy(ss.Pregnancy):
    def make_pregnancies(self, uids):
        embryo_counts = super().make_pregnancies(uids)
        # Custom logic for new pregnancies
        return embryo_counts

13. do_step() and update_states() removed; step() restructured

The step() method no longer delegates to do_step() and update_states(). The burn-in logic has also been moved from step() to init_post(). The new step() calls a sequence of smaller, overridable methods.

What to change: If you had a subclass that overrode do_step() or update_states(), restructure to override the new methods.

# v3.0 step flow
step() -> do_step() -> update_states() + make_pregnancies() + make_embryos()

# v3.1 step flow
init_post()           # Burn-in happens here now
step():
    updates_pre()           # Set base states, check breastfeeding
    progress_pregnancies()  # Advance gestational clock
    process_delivery()      # Handle births
    set_rel_sus()           # Update susceptibility
    select_conceivers()     # Find who conceives
    make_pregnancies()      # Set pregnancy states
    make_embryos()          # Create embryo agents
    update_maternal_deaths()# Handle maternal deaths

Use the _post_delivery() hook for custom post-delivery logic:

# v3.1
class MyPregnancy(ss.Pregnancy):
    def _post_delivery(self, mother_uids, newborn_uids):
        # Custom post-delivery logic here
        pass

14. make_embryos() return value changed

make_embryos() now returns a tuple (conceive_uids_with_repeats, new_uids) instead of just new_uids. The first element accounts for multiple embryos per pregnancy (e.g. twins).

What to change: Update any code that captures the return value.

# v3.0
new_uids = pregnancy.make_embryos(conceive_uids)

# v3.1
conceive_uids_with_repeats, new_uids = pregnancy.make_embryos(conceive_uids, embryo_counts)

15. make_pregnancies() renamed to select_conceivers() for selection; make_pregnancies() now sets pregnancy states

The old make_pregnancies() both selected who conceives and set prognoses. These are now separate: select_conceivers() returns UIDs of women who will conceive, and make_pregnancies() sets pregnancy states and durations for those UIDs.

What to change: If you overrode make_pregnancies() in a subclass, split the logic.

# v3.0
class MyPregnancy(ss.Pregnancy):
    def make_pregnancies(self):
        # Custom selection + prognosis logic

# v3.1
class MyPregnancy(ss.Pregnancy):
    def select_conceivers(self, uids=None):
        # Custom selection logic
        return conceive_uids

    def make_pregnancies(self, uids):
        # Custom prognosis logic
        embryo_counts = super().make_pregnancies(uids)
        return embryo_counts

16. New parameters added to Pregnancy

The following new parameters are available via PregnancyPars. No migration is required, but you may want to use them:

Parameter Default Description
p_infertile ss.bernoulli(p=0) Primary infertility probability
dur_breastfeed ss.lognorm_ex(mean=ss.years(0.75), std=ss.years(0.5)) Breastfeeding duration
p_breastfeed ss.bernoulli(p=1) Probability of breastfeeding
embryos_per_pregnancy ss.choice(a=[1,2], p=[1.0, 0.0]) Number of embryos (twins support)
rr_ptb ss.normal(loc=1, scale=0.1) Baseline relative risk of pre-term birth
rr_ptb_age Age-dependent array Relative risk of pre-term birth by age
p_survive_maternal_death ss.bernoulli(0) Probability unborn survives mother’s death
trimesters [ss.weeks(13), ss.weeks(26)] Trimester boundaries

17. New results added to Pregnancy

The following results are now available. No migration is required.

Result Description
maternal_deaths Number of maternal deaths per timestep
mmr Maternal mortality rate (per 100,000 births)
tfr Total fertility rate
n_fecund Number of fecund women
n_fertile Number of fertile women
n_susceptible Number of susceptible (non-pregnant fertile) women

Age-specific fertility rates are available via pregnancy.asfr (a 2D array, not a standard Result).