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

```python
# 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.

```python
# 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.

```python
# 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`.

```python
# 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.

```python
# 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.

```python
# 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.

```python
# 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.

```python
# 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.

```python
# 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.

```python
# 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.

```python
# 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.

```python
# 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.

```python
# 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:

```python
# 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.

```python
# 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.

```python
# 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`).
