Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V0.5 fixes #527

Merged
merged 33 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d3c008b
Ensure unborn agents cannot become pregnant.
daniel-klein May 24, 2024
b430360
update set_prognoses
cliffckerr May 27, 2024
d85a2ad
fix line list
cliffckerr May 27, 2024
3c9d061
fix requirements message
cliffckerr May 27, 2024
2cd5b17
rename delta to const
cliffckerr May 27, 2024
4e525ce
fix init
cliffckerr May 27, 2024
7232664
Merge branch 'v0.5-fixes-3' into v0.5-fixes-4
cliffckerr May 27, 2024
819e5ec
add static network fix
cliffckerr May 27, 2024
c49ccbb
working on simplifying convert_key
cliffckerr May 27, 2024
67c3073
done simplifying convert_key
cliffckerr May 27, 2024
85e6fd8
last hiv benchmark
cliffckerr May 27, 2024
093b615
new benchmark with sir and sis
cliffckerr May 27, 2024
f62dff5
renamed initialize to init_pre
cliffckerr May 27, 2024
a819426
renamed contacts to edges
cliffckerr May 28, 2024
abfe6ea
added test showing error
cliffckerr May 28, 2024
38b2eae
fixed callable lognorm
cliffckerr May 28, 2024
4b1892c
Add option of sir_vaccine to be leaky or all_or_nothing
roberthinch May 28, 2024
c284947
update version and changelog
cliffckerr May 28, 2024
dffe7d2
Merge branch 'main' into v0.5-fixes
cliffckerr May 28, 2024
82480ed
Update README.rst
kaa2102 May 31, 2024
2db7c19
Update README.rst
kaa2102 May 31, 2024
31aae8d
Update README.rst
kaa2102 May 31, 2024
3adf66b
Update README.rst
kaa2102 May 31, 2024
121f745
Update README.rst
kaa2102 May 31, 2024
b227481
Merge pull request #530 from starsimhub/update-read-me-for-pypi
cliffckerr Jun 3, 2024
630dfa9
Merge pull request #526 from starsimhub/522-extend-sssir_vaccine-to-s…
cliffckerr Jun 3, 2024
a214c76
Merge pull request #525 from starsimhub/ensure_unborn_agents_cannot_b…
cliffckerr Jun 3, 2024
6e4e715
update readme
cliffckerr Jun 3, 2024
141c450
renamed const to constant
cliffckerr Jun 3, 2024
7a62e36
update sir vaccine
cliffckerr Jun 3, 2024
b7e0cf0
add min and max pregnancy age
cliffckerr Jun 3, 2024
aa3fc84
add docstring
cliffckerr Jun 3, 2024
230c41d
update version and changelog
cliffckerr Jun 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,25 @@ What's new
All notable changes to the codebase are documented in this file. Changes that may result in differences in model output, or are required in order to run an old parameter set with the current version, are flagged with the term "Regression information".


Version 0.5.2 (2024-06-04)
--------------------------
- Renames ``network.contacts`` to ``network.edges``.
- For modules (including diseases, networks, etc.), renames ``initialize()`` to ``init_pre()`` and ``init_vals()`` to ``init_post()``.
- Renames ``ss.delta()`` to ``ss.constant()``.
- Allows ``Arr`` objects to be indexed by integer (which are assumed to be UIDs).
- Fixes bug when using callable parameters with ``ss.lognorm_ex()`` and ``ss.lognorm_im()``.
- Fixes bug when initializing ``ss.StaticNet()``.
- Updates default birth rate from 0 to 30 (so ``demographics=True`` is meaningful).
- Adds ``min_age`` and ``max_age`` parameters to the ``Pregnancy`` module (with defaults 15 and 50 years).
- Adds an option for the ``sir_vaccine`` to be all-or-nothing instead of leaky.
- Updates baseline test from HIV to SIR + SIS.
- Fixes issue with infection log not being populated.
- *GitHub info*: PR `527 <https://github.com/starsimhub/starsim/pull/527>`_


Version 0.5.1 (2024-05-15)
--------------------------
- Separates maternal transmission into prenatal and postnatal modules
- Separates maternal transmission into prenatal and postnatal modules.
- *GitHub info*: PR `509 <https://github.com/starsimhub/starsim/pull/509>`_


Expand Down
75 changes: 69 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@ Starsim

**Warning! Starsim is still in the early stages of development. It is being shared solely for transparency and to facilitate collaborative development. It is not ready to be used for real research or policy questions.**

Starsim is an agent-based disease modeling framework in which users can design and configure simulations of pathogens that progress over time within each agent and pass from one agent to the next along dynamic transmission networks. The framework explicitly supports co-transmission of multiple pathogens, allowing users to concurrently simulate several diseases while capturing behavioral and biological interactions. Non-communicable diseases can easily be included as well, either as a co-factor for transmissible pathogens or as an independent exploration. Detailed modeling of mother-child relationships can be simulated from the timepoint of conception, enabling study of congenital diseases and associated birth outcomes. Finally, Starsim facilitates the comparison of one or more intervention scenarios to a baseline scenario in evaluating the impact of various products like vaccines, therapeutics, and novel diagnostics delivered via flexible routes including mass campaigns, screen and treat, and targeted outreach.
Starsim is an agent-based modeling framework in which users can design and configure simulations of diseases (or other health states) that progress over time within each agent and pass from one agent to the next along dynamic transmission networks. The framework explicitly supports co-transmission of multiple pathogens, allowing users to concurrently simulate several diseases while capturing behavioral and biological interactions. Non-communicable diseases can be included as well, either as a co-factor for transmissible pathogens or as an independent exploration. Detailed modeling of mother-child relationships can be simulated from the timepoint of conception, enabling study of congenital diseases and associated birth outcomes. Finally, Starsim facilitates the comparison of one or more intervention scenarios to a baseline scenario in evaluating the impact of various products like vaccines, therapeutics, and novel diagnostics delivered via flexible routes including mass campaigns, screen and treat, and targeted outreach.

The framework is appropriate for simulating one or more sexually transmitted infections (including syphilis, gonorrhea, chlamydia, HPV, and HIV), respiratory infections (like RSV and tuberculosis), and other diseases and underlying determinants (such as Ebola, diabetes, and malnutrition).
The framework is appropriate for simulating sexually transmitted infections (including syphilis, gonorrhea, chlamydia, HPV, and HIV, including co-transmission), respiratory infections (like RSV and tuberculosis), and other diseases and underlying determinants (such as Ebola, diabetes, and malnutrition).

Starsim is a general-purpose modeling framework that is part of the same suite of tools as `Covasim <https://covasim.org>`_, `HPVsim <https://hpvsim.org>`_, and `FPsim <https://fpsim.org>`_.


Requirements
------------

Python 3.9-3.12.

We recommend, but do not require, installing Starsim in a virtual environment, such as `Anaconda <https://www.anaconda.com/products>`__.


Installation
Expand All @@ -19,15 +29,68 @@ Starsim can also be installed locally. To do this, clone first this repository,
Usage and documentation
-----------------------

Documentation is available at https://docs.starsim.org.

Usage examples are available in the ``tests`` folder.
Documentation, including tutorials and an API reference, is available at https://docs.starsim.org.

If everything is working, the following Python commands will run a simulation with the simplest version of a Starsim model. We'll make a version of a classic SIR model::

import starsim as ss

# Define the parameters
pars = dict(
n_agents = 5_000, # Number of agents to simulate
networks = dict( # *Networks* add detail on how agents interact w/ each other
type = 'random', # Here, we use a 'random' network
n_contacts = 10 # Each person has an average of 10 contacts w/ other people
),
diseases = dict( # *Diseases* add detail on what diseases to model
type = 'sir', # Here, we're creating an SIR disease
init_prev = 0.1, # Proportion of the population initially infected
beta = 0.5, # Probability of transmission between contacts
)
)

# Make the sim, run and plot
sim = ss.Sim(pars)
sim.run()
sim.plot()

More usage examples are available in the ``tests`` folder.


Model structure
---------------

All core model code is located in the ``starsim`` subfolder; standard usage is ``import starsim as ss``.

The model consists of core classes including Sim, Run, People, State, Network, Connectors, Analyzers, Interventions, Results, and more. These classes contain methods for running, building simple or dynamic networks, generating random numbers, calculating results, plotting, etc.

The structure of the starsim folder is as follows, roughly in the order in which the modules are imported, building from most fundamental to most complex:

• ``demographics.py``: Classes to transform initial condition input parameters for use in building and utilizing networks.
• ``disease.py``: Classes to manage infection rate of spread, prevalence, waning effects, and other parameters for specific diseases.
• ``distributions.py``: Classes that handle statistical distributions used throughout Starsim.
• ``interventions.py``: The Intervention class, for adding interventions and dynamically modifying parameters, and classes for each of the specific interventions derived from it. The Analyzers class (for performing analyses on the sim while it's running), and other classes and functions for analyzing simulations.
• ``modules.py``: Class to handle "module" logic, such as updates (diseases, networks, etc).
• ``network.py``: Classes for creating simple and dynamic networks of people based on input parameters.
• ``parameters.py``: Classes for creating the simulation parameters.
• ``people.py``: The People class, for handling updates of state for each person.
• ``products.py``: Classes to manage the deployment of vaccines and treatments.
• ``results.py``: Classes to analyze and save results from simulations.
• ``run.py``: Classes for running simulations (e.g. parallel runs and the Scenarios and MultiSim classes).
• ``samples.py``: Class to store data from a large number of simulations.
• ``settings.py``: User-customizable options for Starsim (e.g. default font size).
• ``sim.py``: The Sim class, which performs most of the heavy lifting: initializing the model, running, and plotting.
• ``states.py``: Classes to handle store and update states for people in networks in the simulation including living, mother, child, susceptible, infected, inoculated, recovered, etc.
• ``utils.py``: Helper functions.
• ``version.py``: Version, date, and license information.

The ``diseases`` folder within the Starsim package contains loading scripts for the epidemiological data specific to each respective disease.


Contributing
------------

If you wish to contribute, please see the code of conduct and contributing documents.
Questions or comments can be directed to `info@starsim.org <mailto:info@starsim.org>`__ , or on this project’s `GitHub <https://github.com/starsimhub/starsim>`__ page. Full information about Starsim is provided in the `documentation <https://docs.starsim.org>`__.


Disclaimer
Expand Down
6 changes: 4 additions & 2 deletions starsim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@
print(__license__)

# Double-check key requirements -- should match setup.py
sc.require(['sciris>=3.1.6', 'pandas>=2.0.0', 'scipy', 'numba', 'networkx'], message=f'The following dependencies for Starsim {__version__} were not met: <MISSING>.')
del sc # Don't keep this in the module
reqs = ['sciris>=3.1.6', 'pandas>=2.0.0', 'scipy', 'numba', 'networkx']
msg = f'The following dependencies for Starsim {__version__} were not met: <MISSING>.'
sc.require(reqs, message=msg)
del sc, reqs, msg # Don't keep this in the module
27 changes: 16 additions & 11 deletions starsim/demographics.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ class Demographics(ss.Module):
place at the start of the timestep, before networks are updated and before
any disease modules are executed.
"""
def initialize(self, sim):
super().initialize(sim)
def init_pre(self, sim):
super().init_pre(sim)
self.init_results()
return

Expand All @@ -36,7 +36,7 @@ class Births(Demographics):
def __init__(self, pars=None, metadata=None, **kwargs):
super().__init__()
self.default_pars(
birth_rate = 0,
birth_rate = 30,
rel_birth = 1,
units = 1e-3, # assumes birth rates are per 1000. If using percentages, switch this to 1
)
Expand All @@ -54,9 +54,9 @@ def __init__(self, pars=None, metadata=None, **kwargs):
self.n_births = 0 # For results tracking
return

def initialize(self, sim):
def init_pre(self, sim):
""" Initialize with sim information """
super().initialize(sim)
super().init_pre(sim)
if isinstance(self.pars.birth_rate, pd.DataFrame):
br_year = self.pars.birth_rate[self.metadata.data_cols['year']]
br_val = self.pars.birth_rate[self.metadata.data_cols['cbr']]
Expand Down Expand Up @@ -265,6 +265,8 @@ def __init__(self, pars=None, metadata=None, **kwargs):
rel_fertility = 1,
maternal_death_prob = ss.bernoulli(0),
sex_ratio = ss.bernoulli(0.5), # Ratio of babies born female
min_age = 15, # Minimum age to become pregnant
max_age = 50, # Maximum age to become pregnant
units = 1e-3, # Assumes fertility rates are per 1000. If using percentages, switch this to 1
)
self.update_pars(pars, **kwargs)
Expand Down Expand Up @@ -342,9 +344,12 @@ def make_fertility_prob_fn(self, sim, uids):
fertility_rate = pd.Series(index=uids)
fertility_rate[uids] = new_percent[age_inds]

# Scale from rate to probability. Consider an exponential here.
# Scale from rate to probability
age = self.sim.people.age[uids]
invalid_age = (age < self.pars.min_age) | (age > self.pars.max_age)
fertility_prob = fertility_rate * (self.pars.units * self.pars.rel_fertility * sim.pars.dt)
fertility_prob[self.pregnant.uids] = 0 # Currently pregnant women cannot become pregnant again
fertility_prob[uids[invalid_age]] = 0 # Women too young or old cannot become pregnant
fertility_prob = np.clip(fertility_prob, a_min=0, a_max=1)

return fertility_prob
Expand All @@ -354,8 +359,8 @@ def standardize_fertility_data(self):
fertility_rate = ss.standardize_data(data=self.pars.fertility_rate, metadata=self.metadata)
return fertility_rate

def initialize(self, sim):
super().initialize(sim)
def init_pre(self, sim):
super().init_pre(sim)
low = sim.pars.n_agents + 1
high = int(sim.pars.slot_scale*sim.pars.n_agents)
self.choose_slots = ss.randint(low=low, high=high, sim=sim, module=self)
Expand Down Expand Up @@ -402,9 +407,9 @@ def update_states(self):
prenatalnet = [nw for nw in self.sim.networks.values() if nw.prenatal][0]

# Find the prenatal connections that are ending
prenatal_ending = prenatalnet.contacts.end<=self.sim.ti
new_mother_uids = prenatalnet.contacts.p1[prenatal_ending]
new_infant_uids = prenatalnet.contacts.p2[prenatal_ending]
prenatal_ending = prenatalnet.edges.end<=self.sim.ti
new_mother_uids = prenatalnet.edges.p1[prenatal_ending]
new_infant_uids = prenatalnet.edges.p2[prenatal_ending]

# Validation
if not np.array_equal(new_mother_uids, deliveries.uids):
Expand Down
38 changes: 20 additions & 18 deletions starsim/disease.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
class Disease(ss.Module):
""" Base module class for diseases """

def __init__(self, *args, **kwargs):
def __init__(self, log=True, *args, **kwargs):
super().__init__(*args, **kwargs)
self.results = ss.Results(self.name)
self.log = InfectionLog() # See below for definition
self.log = InfectionLog() if log else None # See below for definition
return

@property
Expand All @@ -34,8 +34,9 @@ def _boolean_states(self):
yield state
return

def initialize(self, sim):
super().initialize(sim)
def init_pre(self, sim):
""" Link the disease to the sim, create objects, and initialize results; see Module.init_pre() for details """
super().init_pre(sim)
self.init_results()
return

Expand Down Expand Up @@ -88,7 +89,7 @@ def make_new_cases(self):
"""
pass

def set_prognoses(self, target_uids, source_uids=None):
def set_prognoses(self, uids, source_uids=None):
"""
Set prognoses upon infection/acquisition

Expand All @@ -105,13 +106,14 @@ def set_prognoses(self, target_uids, source_uids=None):
uids (array): UIDs for agents to assign disease progoses to
from_uids (array): Optionally specify the infecting agent
"""
sim = self.sim
if source_uids is None:
for target in target_uids:
self.log.append(np.nan, target, sim.year)
else:
for target, source in zip(target_uids, source_uids):
self.log.append(source, target, sim.year)
if self.log is not None:
sim = self.sim
if source_uids is None:
for target in uids:
self.log.append(np.nan, target, sim.year)
else:
for target, source in zip(uids, source_uids):
self.log.append(source, target, sim.year)
return

def update_results(self):
Expand Down Expand Up @@ -152,8 +154,8 @@ def __init__(self, *args, **kwargs):
self.rng_source = ss.random(name='source')
return

def initialize(self, sim):
super().initialize(sim)
def init_pre(self, sim):
super().init_pre(sim)
self.validate_beta()
return

Expand Down Expand Up @@ -188,7 +190,7 @@ def infectious(self):
"""
return self.infected

def init_vals(self):
def init_post(self):
"""
Set initial values for states. This could involve passing in a full set of initial conditions,
or using init_prev, or other. Note that this is different to initialization of the Arr objects
Expand Down Expand Up @@ -256,12 +258,12 @@ def make_new_cases(self):
break

nbetas = betamap[nkey]
contacts = net.contacts
edges = net.edges

rel_trans = self.rel_trans.asnew(self.infectious * self.rel_trans)
rel_sus = self.rel_sus.asnew(self.susceptible * self.rel_sus)
p1p2b0 = [contacts.p1, contacts.p2, nbetas[0]]
p2p1b1 = [contacts.p2, contacts.p1, nbetas[1]]
p1p2b0 = [edges.p1, edges.p2, nbetas[0]]
p2p1b1 = [edges.p2, edges.p1, nbetas[1]]
for src, trg, beta in [p1p2b0, p2p1b1]:

# Skip networks with no transmission
Expand Down
27 changes: 10 additions & 17 deletions starsim/diseases/gonorrhea.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ def __init__(self, pars=None, *args, **kwargs):
return

def init_results(self):
"""
Initialize results
"""
""" Initialize results """
super().init_results()
self.results += ss.Result(self.name, 'new_clearances', self.sim.npts, dtype=int)
return
Expand All @@ -49,10 +47,7 @@ def update_results(self):
return

def update_pre(self):
# What if something in here should depend on another module?
# I guess we could just check for it e.g., 'if HIV in sim.modules' or
# 'if 'hiv' in sim.people' or something
# Natural clearance
""" Natural clearance """
clearances = self.ti_clearance <= self.sim.ti
self.susceptible[clearances] = True
self.infected[clearances] = False
Expand All @@ -61,24 +56,22 @@ def update_pre(self):

return

def set_prognoses(self, target_uids, source_uids=None):
"""
Natural history of gonorrhea for adult infection
"""
super().set_prognoses(target_uids, source_uids)
def set_prognoses(self, uids, source_uids=None):
""" Natural history of gonorrhea for adult infection """
super().set_prognoses(uids, source_uids)
ti = self.sim.ti

# Set infection status
self.susceptible[target_uids] = False
self.infected[target_uids] = True
self.ti_infected[target_uids] = ti
self.susceptible[uids] = False
self.infected[uids] = True
self.ti_infected[uids] = ti

# Set infection status
symp_uids = self.pars.p_symp.filter(target_uids)
symp_uids = self.pars.p_symp.filter(uids)
self.symptomatic[symp_uids] = True

# Set natural clearance
clear_uids = self.pars.p_clear.filter(target_uids)
clear_uids = self.pars.p_clear.filter(uids)
dur = ti + self.pars.dur_inf_in_days.rvs(clear_uids)/365/self.sim.dt # Convert from days to years and then adjust for dt
self.ti_clearance[clear_uids] = dur
return
12 changes: 6 additions & 6 deletions starsim/diseases/hiv.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ def set_prognoses(self, uids, source_uids=None):
self.ti_infected[uids] = self.sim.ti
return

def set_congenital(self, target_uids, source_uids):
return self.set_prognoses(target_uids, source_uids)
def set_congenital(self, uids, source_uids):
return self.set_prognoses(uids, source_uids)


# %% HIV-related interventions
Expand All @@ -94,8 +94,8 @@ def __init__(self, year: np.array, coverage: np.array, **kwargs):
self.prob_art_at_infection = ss.bernoulli(p=lambda self, sim, uids: np.interp(sim.year, self.year, self.coverage))
return

def initialize(self, sim):
super().initialize(sim)
def init_pre(self, sim):
super().init_pre(sim)
self.results += ss.Result(self.name, 'n_art', sim.npts, dtype=int)
self.initialized = True
return
Expand Down Expand Up @@ -129,8 +129,8 @@ def __init__(self):
self.cd4 = None
return

def initialize(self, sim):
super().initialize(sim)
def init_pre(self, sim):
super().init_pre(sim)
self.cd4 = np.zeros((sim.npts, sim.people.n), dtype=int)
return

Expand Down
Loading
Loading