From de5bb5938cd56b0aa09424313b494e05d54e7acf Mon Sep 17 00:00:00 2001 From: Katherine Rosenfeld Date: Fri, 10 Jul 2020 15:02:41 -0700 Subject: [PATCH 001/569] add daily_age_analyzer --- covasim/analysis.py | 81 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 57ba2e936..95e82d12d 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -12,7 +12,7 @@ from . import interventions as cvi -__all__ = ['Analyzer', 'snapshot', 'age_histogram', 'Fit', 'TransTree'] +__all__ = ['Analyzer', 'snapshot', 'age_histogram', 'daily_age_analyzer', 'Fit', 'TransTree'] class Analyzer(sc.prettyobj): @@ -310,6 +310,85 @@ def plot(self, windows=False, width=0.8, color='#F8A493', font_size=18, fig_args return figs +class daily_age_analyzer(Analyzer): + ''' Calculate daily counts by age, saving for each day of the simulation + + Args: + states (list): which states of people to record (default: exposed, tested, diagnosed, dead) + edges (list): edges of age bins to use (default: 10 year bins from 0 to 100) + kwargs (dict): passed to Analyzer() + + **Examples**:: + + sim = cv.Sim(analyzers=cv.daily_age_analyzer()) + sim.run() + agehist = sim['analyzers'][0].make_df() + + ''' + + def __init__(self, states=None, edges=None, **kwargs): + super().__init__(**kwargs) + self.edges = edges + self.bins = None # Age bins, calculated from edges + self.states = states + self.results = sc.odict() + + def initialize(self, sim): + + if self.states is None: + self.states = ['diagnoses', 'deaths', 'tests', 'severe'] + + # Handle edges and age bins + if self.edges is None: # Default age bins + self.edges = np.linspace(0, 100, 11) + self.bins = self.edges[:-1] # Don't include the last edge in the bins + + self.initialized = True + return + + def apply(self, sim): + mapper = {'diagnoses': 'diagnosed', 'deaths': 'dead', 'tests': 'tested', 'severe': 'severe'} + df_entry = sc.odict() + for state in self.states: + inds = sc.findinds(sim.people['date_{}'.format(mapper[state])], sim.t) + b, _ = np.histogram(sim.people.age[inds], self.edges) + df_entry.update({state: b * sim.rescale_vec[sim.t]}) + df_entry.update({'age': self.bins}) + self.results.update({sim.date(sim.t): df_entry}) + + def make_df(self): + '''Create dataframe totals for each day''' + mapper = {f'{k}': f'new_{k}' for k in ['diagnoses', 'deaths', 'tests', 'severe']} + df = pd.DataFrame() + for date, k in self.results.items(): + df_ = pd.DataFrame(k) + df_['date'] = date + df_.rename(mapper, inplace=True, axis=1) + df = pd.concat((df, df_)) + cols = list(df.columns.values) + cols = [cols[-1]] + [cols[-2]] + cols[:-2] + df = df[cols] + return df + + def make_total_df(self): + '''Create dataframe totals''' + df = self.make_df() + cols = list(df.columns) + cum_cols = [c for c in cols if c.split('_')[0] == 'new'] + mapper = {'new_{}'.format(c.split('_')[1]): 'cum_{}'.format(c.split('_')[1]) for c in cum_cols} + df_dict = {'age': []} + df_dict.update({c: [] for c in mapper.values()}) + for age, group in df.groupby('age'): + cum_vals = group.sum() + df_dict['age'].append(age) + for k, v in mapper.items(): + df_dict[v].append(cum_vals[k]) + df = pd.DataFrame(df_dict) + if ('cum_diagnoses' in df.columns) and ('cum_tests' in df.columns): + df['yield'] = df['cum_diagnoses'] / df['cum_tests'] + return df + + class Fit(sc.prettyobj): ''' A class for calculating the fit between the model and the data. Note the From ce56392629cdbd280d0acd0269a0f5fea3a7b86e Mon Sep 17 00:00:00 2001 From: Katherine Rosenfeld Date: Fri, 10 Jul 2020 20:49:02 -0700 Subject: [PATCH 002/569] add option for data --- covasim/analysis.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 95e82d12d..95739b642 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -316,6 +316,7 @@ class daily_age_analyzer(Analyzer): Args: states (list): which states of people to record (default: exposed, tested, diagnosed, dead) edges (list): edges of age bins to use (default: 10 year bins from 0 to 100) + datafile (str): the name of the data file to load in for comparison, or a dataframe of data (optional) kwargs (dict): passed to Analyzer() **Examples**:: @@ -326,12 +327,14 @@ class daily_age_analyzer(Analyzer): ''' - def __init__(self, states=None, edges=None, **kwargs): + def __init__(self, states=None, edges=None, datafile=None, **kwargs): super().__init__(**kwargs) self.edges = edges self.bins = None # Age bins, calculated from edges self.states = states self.results = sc.odict() + self.datafile = datafile + self.data = None def initialize(self, sim): @@ -343,6 +346,13 @@ def initialize(self, sim): self.edges = np.linspace(0, 100, 11) self.bins = self.edges[:-1] # Don't include the last edge in the bins + # Handle datafile + if sc.isstring(self.datafile): + self.data = cvm.load_data(self.datafile, check_date=False) + else: + self.data = self.datafile # Use it directly + self.datafile = None + self.initialized = True return From f884c203a8584ff6c1b3b4edc37f263991cd3deb Mon Sep 17 00:00:00 2001 From: Katherine Rosenfeld Date: Fri, 10 Jul 2020 20:49:38 -0700 Subject: [PATCH 003/569] update info --- covasim/analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 95739b642..936b12d40 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -314,7 +314,7 @@ class daily_age_analyzer(Analyzer): ''' Calculate daily counts by age, saving for each day of the simulation Args: - states (list): which states of people to record (default: exposed, tested, diagnosed, dead) + states (list): which states of people to record (default: ['diagnoses', 'deaths', 'tests', 'severe']) edges (list): edges of age bins to use (default: 10 year bins from 0 to 100) datafile (str): the name of the data file to load in for comparison, or a dataframe of data (optional) kwargs (dict): passed to Analyzer() From f659218b0f2d9fbac6e0cc0571ab3e0449ebd14e Mon Sep 17 00:00:00 2001 From: Katherine Rosenfeld Date: Fri, 10 Jul 2020 21:16:55 -0700 Subject: [PATCH 004/569] option to make total df from data --- covasim/analysis.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 936b12d40..204819291 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -380,9 +380,11 @@ def make_df(self): df = df[cols] return df - def make_total_df(self): + def make_total_df(self, df=None): '''Create dataframe totals''' - df = self.make_df() + + if df is None: + df = self.make_df() cols = list(df.columns) cum_cols = [c for c in cols if c.split('_')[0] == 'new'] mapper = {'new_{}'.format(c.split('_')[1]): 'cum_{}'.format(c.split('_')[1]) for c in cum_cols} From c4db721fc4293d743fb558df756810f2aac2b624 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 3 Feb 2021 20:56:23 -0500 Subject: [PATCH 005/569] adding variant functionality --- covasim/base.py | 3 ++ covasim/defaults.py | 3 ++ covasim/parameters.py | 3 +- covasim/people.py | 19 +++++++--- covasim/sim.py | 83 ++++++++++++++++++++++++++----------------- covasim/utils.py | 1 + 6 files changed, 74 insertions(+), 38 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 5621234ef..6f12ac80f 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -115,6 +115,9 @@ def update_pars(self, pars=None, create=False): errormsg = f'Key(s) {mismatches} not found; available keys are {available_keys}' raise sc.KeyNotFoundError(errormsg) self.pars.update(pars) + + if 'n_strains' in pars.keys(): + self.pars['beta'] = np.resize(self.pars['beta'], pars['n_strains']) return diff --git a/covasim/defaults.py b/covasim/defaults.py index 124bef548..520e5115e 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -48,6 +48,7 @@ class PeopleMeta(sc.prettyobj): 'death_prob', # Float 'rel_trans', # Float 'rel_sus', # Float + 'time_of_last_inf', # Int ] # Set the states that a person can be in: these are all booleans per person -- used in people.py @@ -55,6 +56,8 @@ class PeopleMeta(sc.prettyobj): 'susceptible', 'exposed', 'infectious', + 'exposed_by_strain', + 'infectious_by_strain', 'symptomatic', 'severe', 'critical', diff --git a/covasim/parameters.py b/covasim/parameters.py index 5b71306bf..7de53e6d9 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -48,13 +48,14 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['rescale_factor'] = 1.2 # Factor by which the population is rescaled on each step # Basic disease transmission - pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated + pars['beta'] = np.full(1, 0.016) # Beta per symptomatic contact; absolute value, calibrated pars['contacts'] = None # The number of contacts per layer; set by reset_layer_pars() below pars['dynam_layer'] = None # Which layers are dynamic; set by reset_layer_pars() below pars['beta_layer'] = None # Transmissibility per layer; set by reset_layer_pars() below pars['n_imports'] = 0 # Average daily number of imported cases (actual number is drawn from Poisson distribution) pars['beta_dist'] = dict(dist='neg_binomial', par1=1.0, par2=0.45, step=0.01) # Distribution to draw individual level transmissibility; dispersion from https://www.researchsquare.com/article/rs-29548/v1 pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 + pars['n_strains'] = 1 # The number of strains currently circulating in the population # Efficacy of protection measures pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 diff --git a/covasim/people.py b/covasim/people.py index 87a3af34a..c9800e16b 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -58,13 +58,17 @@ def __init__(self, pars, strict=True, **kwargs): for key in self.meta.person: if key == 'uid': self[key] = np.arange(self.pop_size, dtype=cvd.default_int) + elif key == 'rel_trans' or key == 'rel_sus' or key == 'time_of_last_inf': + self[key] = np.full((self.pop_size, self.pars['n_strains']), np.nan, dtype=cvd.default_float) else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) - # Set health states -- only susceptible is true by default -- booleans + # Set health states -- only susceptible is true by default -- booleans except exposed by strain which should return the strain that ind is exposed to for key in self.meta.states: if key == 'susceptible': self[key] = np.full(self.pop_size, True, dtype=bool) + elif key == 'exposed_by_strain' or key == 'infectious_by_strain': + self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) else: self[key] = np.full(self.pop_size, False, dtype=bool) @@ -133,8 +137,9 @@ def find_cutoff(age_cutoffs, age): self.severe_prob[:] = progs['severe_probs'][inds]*progs['comorbidities'][inds] # Severe disease probability is modified by comorbidities self.crit_prob[:] = progs['crit_probs'][inds] # Probability of developing critical disease self.death_prob[:] = progs['death_probs'][inds] # Probability of death - self.rel_sus[:] = progs['sus_ORs'][inds] # Default susceptibilities - self.rel_trans[:] = progs['trans_ORs'][inds]*cvu.sample(**self.pars['beta_dist'], size=len(inds)) # Default transmissibilities, with viral load drawn from a distribution + for strain in range(self.pars['n_strains']): + self.rel_sus[:, strain] = progs['sus_ORs'][inds] # Default susceptibilities + self.rel_trans[:, strain] = progs['trans_ORs'][inds]*cvu.sample(**self.pars['beta_dist'], size=len(inds)) # Default transmissibilities, with viral load drawn from a distribution return @@ -212,6 +217,7 @@ def check_infectious(self): ''' Check if they become infectious ''' inds = self.check_inds(self.infectious, self.date_infectious, filter_inds=self.is_exp) self.infectious[inds] = True + self.infectious_by_strain[inds] = self.exposed_by_strain[inds] return len(inds) @@ -245,6 +251,7 @@ def check_recovery(self): self.severe[inds] = False self.critical[inds] = False self.recovered[inds] = True + self.infectious_by_strain[inds] = np.nan return len(inds) @@ -322,7 +329,7 @@ def make_susceptible(self, inds): return - def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None): + def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, strain=0): ''' Infect people and determine their eventual outcomes. * Every infected person can infect other people, regardless of whether they develop symptoms @@ -359,6 +366,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None): # Set states self.susceptible[inds] = False self.exposed[inds] = True + self.exposed_by_strain[inds] = strain self.date_exposed[inds] = self.t self.flows['new_infections'] += len(inds) @@ -366,6 +374,9 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None): for i, target in enumerate(inds): self.infection_log.append(dict(source=source[i] if source is not None else None, target=target, date=self.t, layer=layer)) + # Record time of infection + self.time_of_last_inf[inds, strain] = self.t + # Calculate how long before this person can infect other people self.dur_exp2inf[inds] = cvu.sample(**durpars['exp2inf'], size=n_infections) self.date_infectious[inds] = self.dur_exp2inf[inds] + self.t diff --git a/covasim/sim.py b/covasim/sim.py index 0c405cf3f..23c9c29b2 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -480,6 +480,9 @@ def step(self): n_imports = cvu.poisson(self['n_imports']) # Imported cases if n_imports>0: importation_inds = cvu.choose(max_n=len(people), n=n_imports) + + # TODO -- iterate through n_strains, using value as index for beta, etc. + people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation') # Apply interventions @@ -494,39 +497,53 @@ def step(self): people.update_states_post() # Check for state changes after interventions - # Compute the probability of transmission - beta = cvd.default_float(self['beta']) - asymp_factor = cvd.default_float(self['asymp_factor']) - frac_time = cvd.default_float(self['viral_dist']['frac_time']) - load_ratio = cvd.default_float(self['viral_dist']['load_ratio']) - high_cap = cvd.default_float(self['viral_dist']['high_cap']) - date_inf = people.date_infectious - date_rec = people.date_recovered - date_dead = people.date_dead - viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) - - for lkey,layer in contacts.items(): - p1 = layer['p1'] - p2 = layer['p2'] - betas = layer['beta'] - - # Compute relative transmission and susceptibility - rel_trans = people.rel_trans - rel_sus = people.rel_sus - inf = people.infectious - sus = people.susceptible - symp = people.symptomatic - diag = people.diagnosed - quar = people.quarantined - iso_factor = cvd.default_float(self['iso_factor'][lkey]) - quar_factor = cvd.default_float(self['quar_factor'][lkey]) - beta_layer = cvd.default_float(self['beta_layer'][lkey]) - rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor) - - # Calculate actual transmission - for sources,targets in [[p1,p2], [p2,p1]]: # Loop over the contact network from p1->p2 and p2->p1 - source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! - people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey) # Actually infect people + # TODO -- iterate through n_strains, using value as index for beta, etc. + + for strain in range(self['n_strains']): + # Compute the probability of transmission + beta = cvd.default_float(self['beta'][strain]) + asymp_factor = cvd.default_float(self['asymp_factor']) + frac_time = cvd.default_float(self['viral_dist']['frac_time']) + load_ratio = cvd.default_float(self['viral_dist']['load_ratio']) + high_cap = cvd.default_float(self['viral_dist']['high_cap']) + date_inf = people.date_infectious + date_rec = people.date_recovered + date_dead = people.date_dead + viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) + + for lkey, layer in contacts.items(): + p1 = layer['p1'] + p2 = layer['p2'] + betas = layer['beta'] + + # Compute relative transmission and susceptibility + rel_trans = people.rel_trans[:,strain] + rel_sus = people.rel_sus[:,strain] + + inf = people.infectious_by_strain + for person, value in enumerate(inf): + if value == strain: + inf[person] = True + else: + inf[person] = False + + #TODO-- write a function that returns an array which is TRUE if infectious_by_strain == strain and otherwise false + sus = people.susceptible + symp = people.symptomatic + diag = people.diagnosed + quar = people.quarantined + iso_factor = cvd.default_float(self['iso_factor'][lkey]) + quar_factor = cvd.default_float(self['quar_factor'][lkey]) + beta_layer = cvd.default_float(self['beta_layer'][lkey]) + rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, + diag, quar, asymp_factor, iso_factor, quar_factor) + + # Calculate actual transmission + for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 + source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, + rel_sus) # Calculate transmission! + people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, + layer=lkey, strain=strain) # Actually infect people # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): diff --git a/covasim/utils.py b/covasim/utils.py index 364e15442..4c390e375 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -82,6 +82,7 @@ def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, @nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=True, parallel=parallel) def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): + #TODO -- make this specific to strain -- use rel_trans for this -- need to set it to 0 if not infected with strain and otherwise whatever the value is ''' The heaviest step of the model -- figure out who gets infected on this timestep ''' betas = beta * layer_betas * rel_trans[sources] * rel_sus[targets] # Calculate the raw transmission probabilities nonzero_inds = betas.nonzero()[0] # Find nonzero entries From b7b7697bc8c3c608ad015e529d022f331df0d6ae Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 3 Feb 2021 21:16:58 -0500 Subject: [PATCH 006/569] runs! --- covasim/base.py | 6 +++++- covasim/parameters.py | 3 ++- covasim/people.py | 3 ++- covasim/sim.py | 4 ++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 6f12ac80f..c40704c5a 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -117,7 +117,11 @@ def update_pars(self, pars=None, create=False): self.pars.update(pars) if 'n_strains' in pars.keys(): - self.pars['beta'] = np.resize(self.pars['beta'], pars['n_strains']) + # check that length of beta is same as length of strains (there is a beta for each strain) + if 'beta' not in pars.keys(): + raise ValueError(f'You supplied strains without betas for each strain') + else: + self.pars['beta'] = pars['beta'] return diff --git a/covasim/parameters.py b/covasim/parameters.py index 7de53e6d9..f8d146bc1 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -48,7 +48,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['rescale_factor'] = 1.2 # Factor by which the population is rescaled on each step # Basic disease transmission - pars['beta'] = np.full(1, 0.016) # Beta per symptomatic contact; absolute value, calibrated + pars['beta'] = [0.016] # Beta per symptomatic contact; absolute value, calibrated pars['contacts'] = None # The number of contacts per layer; set by reset_layer_pars() below pars['dynam_layer'] = None # Which layers are dynamic; set by reset_layer_pars() below pars['beta_layer'] = None # Transmissibility per layer; set by reset_layer_pars() below @@ -56,6 +56,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['beta_dist'] = dict(dist='neg_binomial', par1=1.0, par2=0.45, step=0.01) # Distribution to draw individual level transmissibility; dispersion from https://www.researchsquare.com/article/rs-29548/v1 pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 pars['n_strains'] = 1 # The number of strains currently circulating in the population + pars['max_strains'] = 10 # For allocating memory with numpy arrays # Efficacy of protection measures pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 diff --git a/covasim/people.py b/covasim/people.py index c9800e16b..3baf04556 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -59,7 +59,7 @@ def __init__(self, pars, strict=True, **kwargs): if key == 'uid': self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif key == 'rel_trans' or key == 'rel_sus' or key == 'time_of_last_inf': - self[key] = np.full((self.pop_size, self.pars['n_strains']), np.nan, dtype=cvd.default_float) + self[key] = np.full((self.pop_size, self.pars['max_strains']), np.nan, dtype=cvd.default_float) else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -138,6 +138,7 @@ def find_cutoff(age_cutoffs, age): self.crit_prob[:] = progs['crit_probs'][inds] # Probability of developing critical disease self.death_prob[:] = progs['death_probs'][inds] # Probability of death for strain in range(self.pars['n_strains']): + #TODO -- make this strain specific in inputs self.rel_sus[:, strain] = progs['sus_ORs'][inds] # Default susceptibilities self.rel_trans[:, strain] = progs['trans_ORs'][inds]*cvu.sample(**self.pars['beta_dist'], size=len(inds)) # Default transmissibilities, with viral load drawn from a distribution diff --git a/covasim/sim.py b/covasim/sim.py index 23c9c29b2..8f0e20b43 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -520,8 +520,8 @@ def step(self): rel_trans = people.rel_trans[:,strain] rel_sus = people.rel_sus[:,strain] - inf = people.infectious_by_strain - for person, value in enumerate(inf): + inf = people.infectious + for person, value in enumerate(people.infectious_by_strain): if value == strain: inf[person] = True else: From eba5fa74743d37871b354aa6124bc9753af4539f Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 4 Feb 2021 12:59:47 -0500 Subject: [PATCH 007/569] runs with added strain-specific functionality in results. Still have to think on prev/inc --- covasim/base.py | 11 ++++++++-- covasim/defaults.py | 11 +++++++++- covasim/people.py | 23 +++++++++++++++++---- covasim/sim.py | 50 +++++++++++++++++++++++++++++++-------------- 4 files changed, 73 insertions(+), 22 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index c40704c5a..60b3f6ad3 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -143,7 +143,7 @@ class Result(object): print(r1.values) ''' - def __init__(self, name=None, npts=None, scale=True, color=None): + def __init__(self, name=None, npts=None, scale=True, color=None, max_strains=10): self.name = name # Name of this result self.scale = scale # Whether or not to scale the result by the scale factor if color is None: @@ -151,7 +151,10 @@ def __init__(self, name=None, npts=None, scale=True, color=None): self.color = color # Default color if npts is None: npts = 0 - self.values = np.array(np.zeros(int(npts)), dtype=cvd.result_float) + if 'by_strain' in self.name or 'by strain' in self.name: + self.values = np.full((npts, max_strains), 0, dtype=cvd.result_float) + else: + self.values = np.array(np.zeros(int(npts)), dtype=cvd.result_float) self.low = None self.high = None return @@ -911,6 +914,10 @@ def count(self, key): ''' Count the number of people for a given key ''' return (self[key]>0).sum() + def count_by_strain(self, key, strain): + ''' Count the number of people for a given key ''' + return (self[key][:,strain]>0).sum() + def count_not(self, key): ''' Count the number of people who do not have a property for a given key ''' diff --git a/covasim/defaults.py b/covasim/defaults.py index 520e5115e..d5f785ef3 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -56,7 +56,8 @@ class PeopleMeta(sc.prettyobj): 'susceptible', 'exposed', 'infectious', - 'exposed_by_strain', + 'exposed_strain', + 'infectious_strain', 'infectious_by_strain', 'symptomatic', 'severe', @@ -93,6 +94,7 @@ class PeopleMeta(sc.prettyobj): 'susceptible': 'Number susceptible', 'exposed': 'Number exposed', 'infectious': 'Number infectious', + 'infectious_by_strain': 'Number infectious by strain', 'symptomatic': 'Number symptomatic', 'severe': 'Number of severe cases', 'critical': 'Number of critical cases', @@ -102,7 +104,9 @@ class PeopleMeta(sc.prettyobj): # The types of result that are counted as flows -- used in sim.py; value is the label suffix result_flows = {'infections': 'infections', + 'infections_by_strain': 'infections_by_strain', 'infectious': 'infectious', + 'infectious_by_strain': 'infectious_by_strain', 'tests': 'tests', 'diagnoses': 'diagnoses', 'recoveries': 'recoveries', @@ -150,7 +154,9 @@ def get_colors(): colors = sc.objdict( susceptible = '#5e7544', infectious = '#c78f65', + infectious_by_strain ='#c78f65', infections = '#c75649', + infections_by_strain='#c78f65', exposed = '#c75649', # Duplicate tests = '#aaa8ff', diagnoses = '#8886cc', @@ -170,16 +176,19 @@ def get_colors(): # Define the 'overview plots', i.e. the most useful set of plots to explore different aspects of a simulation overview_plots = [ 'cum_infections', + 'cum_infections_by_strain', 'cum_severe', 'cum_critical', 'cum_deaths', 'cum_diagnoses', 'new_infections', + 'new_infections_by_strain', 'new_severe', 'new_critical', 'new_deaths', 'new_diagnoses', 'n_infectious', + 'n_infectious_by_strain', 'n_severe', 'n_critical', 'n_susceptible', diff --git a/covasim/people.py b/covasim/people.py index 3baf04556..07b02e5b2 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -67,8 +67,10 @@ def __init__(self, pars, strict=True, **kwargs): for key in self.meta.states: if key == 'susceptible': self[key] = np.full(self.pop_size, True, dtype=bool) - elif key == 'exposed_by_strain' or key == 'infectious_by_strain': + elif key == 'exposed_strain' or key == 'infectious_strain': self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) + elif key == 'infectious_by_strain': + self[key] = np.full((self.pop_size, self.pars['max_strains']), False, dtype=bool) else: self[key] = np.full(self.pop_size, False, dtype=bool) @@ -82,6 +84,10 @@ def __init__(self, pars, strict=True, **kwargs): # Store flows to be computed during simulation self.flows = {key:0 for key in cvd.new_result_flows} + for key in cvd.new_result_flows: + if 'by_strain' in key: + self.flows[key] = np.full(self.pars['n_strains'], 0, dtype=cvd.default_float) + # Although we have called init(), we still need to call initialize() self.initialized = False @@ -154,6 +160,9 @@ def update_states_pre(self, t): # Perform updates self.flows = {key:0 for key in cvd.new_result_flows} + for key in cvd.new_result_flows: + if 'by_strain' in key: + self.flows[key] += np.full(self.pars['n_strains'], 0, dtype=cvd.default_float) self.flows['new_infectious'] += self.check_infectious() # For people who are exposed and not infectious, check if they begin being infectious self.flows['new_symptomatic'] += self.check_symptomatic() self.flows['new_severe'] += self.check_severe() @@ -218,7 +227,12 @@ def check_infectious(self): ''' Check if they become infectious ''' inds = self.check_inds(self.infectious, self.date_infectious, filter_inds=self.is_exp) self.infectious[inds] = True - self.infectious_by_strain[inds] = self.exposed_by_strain[inds] + self.infectious_strain[inds] = self.exposed_strain[inds] + for strain in range(self.pars['n_strains']): + inds_strain = [index for index, value in enumerate(self.infectious_strain) if value == strain] + self.infectious_by_strain[inds_strain, strain] = True + self.flows['new_infectious_by_strain'][strain] += len(inds_strain) + return len(inds) @@ -252,7 +266,7 @@ def check_recovery(self): self.severe[inds] = False self.critical[inds] = False self.recovered[inds] = True - self.infectious_by_strain[inds] = np.nan + self.infectious_strain[inds] = np.nan return len(inds) @@ -367,9 +381,10 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str # Set states self.susceptible[inds] = False self.exposed[inds] = True - self.exposed_by_strain[inds] = strain + self.exposed_strain[inds] = strain self.date_exposed[inds] = self.t self.flows['new_infections'] += len(inds) + self.flows['new_infections_by_strain'][strain] += len(inds) # Record transmissions for i, target in enumerate(inds): diff --git a/covasim/sim.py b/covasim/sim.py index 8f0e20b43..ce2391c88 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -286,7 +286,9 @@ def init_res(*args, **kwargs): # Other variables self.results['n_alive'] = init_res('Number of people alive', scale=False) self.results['prevalence'] = init_res('Prevalence', scale=False) + self.results['prevalence_by_strain'] = init_res('Prevalence by strain', scale=False) self.results['incidence'] = init_res('Incidence', scale=False) + self.results['incidence_by_strain'] = init_res('Incidence by strain', scale=False) self.results['r_eff'] = init_res('Effective reproduction number', scale=False) self.results['doubling_time'] = init_res('Doubling time', scale=False) self.results['test_yield'] = init_res('Testing yield', scale=False) @@ -385,8 +387,9 @@ def init_people(self, save_pop=False, load_pop=False, popfile=None, reset=False, self.people.initialize() # Fully initialize the people # Create the seed infections - inds = cvu.choose(self['pop_size'], self['pop_infected']) - self.people.infect(inds=inds, layer='seed_infection') + for strain in range(self['n_strains']): + inds = cvu.choose(self['pop_size'], self['pop_infected']) + self.people.infect(inds=inds, layer='seed_infection', strain=strain) return @@ -477,13 +480,12 @@ def step(self): icu_max = people.count('critical') > self['n_beds_icu'] if self['n_beds_icu'] else False # Check for ICU bed constraint # Randomly infect some people (imported infections) - n_imports = cvu.poisson(self['n_imports']) # Imported cases - if n_imports>0: - importation_inds = cvu.choose(max_n=len(people), n=n_imports) - - # TODO -- iterate through n_strains, using value as index for beta, etc. - - people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation') + imports = cvu.n_poisson(self['n_imports'], self['n_strains']) # Imported cases + # TODO -- calculate imports per strain. + for strain, n_imports in enumerate(imports): + if n_imports>0: + importation_inds = cvu.choose(max_n=len(people), n=n_imports) + people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation', strain=strain) # Apply interventions for intervention in self['interventions']: @@ -521,7 +523,7 @@ def step(self): rel_sus = people.rel_sus[:,strain] inf = people.infectious - for person, value in enumerate(people.infectious_by_strain): + for person, value in enumerate(people.infectious_strain): if value == strain: inf[person] = True else: @@ -547,11 +549,19 @@ def step(self): # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): - self.results[f'n_{key}'][t] = people.count(key) + if 'by_strain' in key or 'by strain' in key: + for strain in range(self['n_strains']): + self.results[f'n_{key}'][t][strain] = people.count_by_strain(key, strain) + else: + self.results[f'n_{key}'][t] = people.count(key) # Update counts for this time step: flows for key,count in people.flows.items(): - self.results[key][t] += count + if 'by_strain' in key or 'by strain' in key: + for strain in range(self['n_strains']): + self.results[key][t][strain] += count[strain] + else: + self.results[key][t] += count # Apply analyzers -- same syntax as interventions for analyzer in self['analyzers']: @@ -663,12 +673,22 @@ def finalize(self, verbose=None, restore_pars=True): # Scale the results for reskey in self.result_keys(): if self.results[reskey].scale: # Scale the result dynamically - self.results[reskey].values *= self.rescale_vec + if 'by_strain' in reskey: + for strain in range(self['n_strains']): + self.results[reskey].values[:,strain] *= self.rescale_vec + else: + self.results[reskey].values *= self.rescale_vec # Calculate cumulative results for key in cvd.result_flows.keys(): - self.results[f'cum_{key}'][:] = np.cumsum(self.results[f'new_{key}'][:]) + if 'by_strain' in key: + for strain in range(self['n_strains']): + self.results[f'cum_{key}'][:,strain] = np.cumsum(self.results[f'new_{key}'][:,strain]) + else: + self.results[f'cum_{key}'][:] = np.cumsum(self.results[f'new_{key}'][:]) self.results['cum_infections'].values += self['pop_infected']*self.rescale_vec[0] # Include initially infected people + for strain in range(self['n_strains']): + self.results['cum_infections_by_strain'].values[:, strain] += self['pop_infected']*self.rescale_vec[0] # Include initially infected people # Final settings self.results_ready = True # Set this first so self.summary() knows to print the results @@ -957,7 +977,7 @@ def summarize(self, full=False, t=None, output=False): labelstr = f' "{self.label}"' if self.label else '' string = f'Simulation{labelstr} summary:\n' for key in self.result_keys(): - if full or key.startswith('cum_'): + if full or key.startswith('cum_') and 'by_strain' not in key: string += f' {summary[key]:5.0f} {self.results[key].name.lower()}\n' # Print or return string From 090ff9cbd0925034745e4d726c8abc4c48a8ac32 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 4 Feb 2021 15:04:22 -0500 Subject: [PATCH 008/569] adding in prevalence and incidence by strain --- covasim/defaults.py | 4 ++++ covasim/people.py | 10 +++++++++- covasim/sim.py | 11 +++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index d5f785ef3..a5ddd5c4e 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -49,6 +49,7 @@ class PeopleMeta(sc.prettyobj): 'rel_trans', # Float 'rel_sus', # Float 'time_of_last_inf', # Int + # 'immune_factor_by_strain', # Float ] # Set the states that a person can be in: these are all booleans per person -- used in people.py @@ -57,6 +58,7 @@ class PeopleMeta(sc.prettyobj): 'exposed', 'infectious', 'exposed_strain', + 'exposed_by_strain', 'infectious_strain', 'infectious_by_strain', 'symptomatic', @@ -93,6 +95,7 @@ class PeopleMeta(sc.prettyobj): result_stocks = { 'susceptible': 'Number susceptible', 'exposed': 'Number exposed', + 'exposed_by_strain': 'Number exposed by strain', 'infectious': 'Number infectious', 'infectious_by_strain': 'Number infectious by strain', 'symptomatic': 'Number symptomatic', @@ -158,6 +161,7 @@ def get_colors(): infections = '#c75649', infections_by_strain='#c78f65', exposed = '#c75649', # Duplicate + exposed_by_strain ='#c75649', # Duplicate tests = '#aaa8ff', diagnoses = '#8886cc', diagnosed = '#8886cc', # Duplicate diff --git a/covasim/people.py b/covasim/people.py index 07b02e5b2..bd76cd0dd 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -60,6 +60,8 @@ def __init__(self, pars, strict=True, **kwargs): self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif key == 'rel_trans' or key == 'rel_sus' or key == 'time_of_last_inf': self[key] = np.full((self.pop_size, self.pars['max_strains']), np.nan, dtype=cvd.default_float) + elif key == 'immune_factor_by_strain': # everyone starts out with no immunity to either strain. + self[key] = np.full((self.pop_size, self.pars['max_strains']), 1, dtype=cvd.default_float) else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -69,7 +71,7 @@ def __init__(self, pars, strict=True, **kwargs): self[key] = np.full(self.pop_size, True, dtype=bool) elif key == 'exposed_strain' or key == 'infectious_strain': self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) - elif key == 'infectious_by_strain': + elif key == 'infectious_by_strain' or key == 'exposed_by_strain': self[key] = np.full((self.pop_size, self.pars['max_strains']), False, dtype=bool) else: self[key] = np.full(self.pop_size, False, dtype=bool) @@ -282,6 +284,11 @@ def check_death(self): self.dead[inds] = True return len(inds) + # TODO-- add in immunity instead of recovery + # def check_immunity(self): + # '''Update immunity by strain based on time since recovery''' + # just_recovered_inds = self.check_inds(self.recovered, self.date_recovered, filter_inds=self.is_exp) + def check_diagnosed(self): ''' @@ -382,6 +389,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.susceptible[inds] = False self.exposed[inds] = True self.exposed_strain[inds] = strain + self.exposed_by_strain[inds, strain] = True self.date_exposed[inds] = self.t self.flows['new_infections'] += len(inds) self.flows['new_infections_by_strain'][strain] += len(inds) diff --git a/covasim/sim.py b/covasim/sim.py index ce2391c88..3fa969174 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -487,6 +487,8 @@ def step(self): importation_inds = cvu.choose(max_n=len(people), n=n_imports) people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation', strain=strain) + # TODO -- Randomly introduce new strain + # Apply interventions for intervention in self['interventions']: if isinstance(intervention, cvi.Intervention): @@ -672,6 +674,10 @@ def finalize(self, verbose=None, restore_pars=True): # Scale the results for reskey in self.result_keys(): + if 'by_strain' in reskey: + # resize results to include only active strains + self.results[reskey].values = np.delete(self.results[reskey], + slice(self['n_strains'], self['max_strains'] - 1, 1), 1) if self.results[reskey].scale: # Scale the result dynamically if 'by_strain' in reskey: for strain in range(self['n_strains']): @@ -737,6 +743,11 @@ def compute_prev_inci(self): self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence + + for strain in range(self['n_strains']): + self.results['incidence_by_strain'][:,strain] = res['new_infections_by_strain'][:,strain]/res['n_susceptible'][:] # Calculate the incidence + self.results['prevalence_by_strain'][:, strain] = res['n_exposed_by_strain'][:, strain] / res['n_alive'][:] # Calculate the prevalence + return From 756c413e72d7e3f8a5445c430b8e10bbd9e57912 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 4 Feb 2021 17:32:59 -0500 Subject: [PATCH 009/569] removed list comprehension, cut run-time in half --- covasim/people.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index bd76cd0dd..e6467afeb 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -231,9 +231,11 @@ def check_infectious(self): self.infectious[inds] = True self.infectious_strain[inds] = self.exposed_strain[inds] for strain in range(self.pars['n_strains']): - inds_strain = [index for index, value in enumerate(self.infectious_strain) if value == strain] - self.infectious_by_strain[inds_strain, strain] = True - self.flows['new_infectious_by_strain'][strain] += len(inds_strain) + inf_strain = self.infectious_strain == strain + inf_strain = inf_strain[inf_strain == True] + # inds_strain = [index for index, value in enumerate(self.infectious_strain) if value == strain] + # self.infectious_by_strain[inds_strain, strain] = True + self.flows['new_infectious_by_strain'][strain] += len(inf_strain) return len(inds) From 662c086b7713889b82bae6d6736e3bb84a8c472f Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 4 Feb 2021 19:31:19 -0500 Subject: [PATCH 010/569] example script for running variants --- examples/t10_variants.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 examples/t10_variants.py diff --git a/examples/t10_variants.py b/examples/t10_variants.py new file mode 100644 index 000000000..f679ff27d --- /dev/null +++ b/examples/t10_variants.py @@ -0,0 +1,15 @@ +import covasim as cv + + +pars = {'n_strains': 1, + 'beta': [0.016]} +sim = cv.Sim(pars=pars) +sim.run() + +sim2 = cv.Sim() +sim2.run() +# sim.plot_result('cum_infections_by_strain', do_show=True) +# sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True) +# sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True) + +print('done') \ No newline at end of file From dc9e85e39bc7b7f3dd4ec8c898dccd9c0ace91fa Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 4 Feb 2021 19:33:03 -0500 Subject: [PATCH 011/569] example script for running variants --- examples/t10_variants.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/t10_variants.py b/examples/t10_variants.py index f679ff27d..628286b6a 100644 --- a/examples/t10_variants.py +++ b/examples/t10_variants.py @@ -1,15 +1,13 @@ import covasim as cv -pars = {'n_strains': 1, - 'beta': [0.016]} +pars = {'n_strains': 2, + 'beta': [0.016, 0.035]} sim = cv.Sim(pars=pars) sim.run() -sim2 = cv.Sim() -sim2.run() -# sim.plot_result('cum_infections_by_strain', do_show=True) -# sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True) -# sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True) +sim.plot_result('cum_infections_by_strain', do_show=True) +sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True) +sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True) print('done') \ No newline at end of file From 176fed582f62f8cc1b27010aad0c9bfa3ed12b80 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 4 Feb 2021 21:15:33 -0500 Subject: [PATCH 012/569] added an intervention for newly imported strains! --- covasim/interventions.py | 53 ++++++++++++++++++++++++++++++++++++++++ covasim/people.py | 4 +-- examples/t10_variants.py | 24 ++++++++++++------ 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 9832388c2..3824a4f1e 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1116,3 +1116,56 @@ def apply(self, sim): self.vaccination_dates[v_ind].append(sim.t) return + + +#%% Multi-strain interventions + +__all__+= ['import_strain'] + +class import_strain(Intervention): + ''' + Introduce a new variant(s) to the population through an importation at a given time point. + + Args: + day (int): the day to apply the interventions + n_imports (list of ints): the number of imports of strain(s) + beta (list of floats): per contact transmission of strain(s) + rel_sus (list of floats): relative change in susceptibility of strain(s); 0 = perfect, 1 = no effect + rel_trans (list of floats): relative change in transmission of strain(s); 0 = perfect, 1 = no effect + kwargs (dict): passed to Intervention() + + **Examples**:: + + interv = cv.import_strain(days=50, beta=0.3, rel_sus=0.5, rel_symp=0.1) + ''' + + def __init__(self, day=None, n_imports=None, beta=None, rel_sus=None, rel_trans=None, **kwargs): + # Do standard initialization + super().__init__(**kwargs) # Initialize the Intervention object + self._store_args() # Store the input arguments so the intervention can be recreated + + len_imports = len(n_imports) + len_betas = len(beta) + if len_imports != len_betas: + raise ValueError( + f'Number of different imports ({len_imports} does not match the number of betas ({len_betas})') + else: + self.new_strains = len_imports + + self.day = day + self.n_imports = n_imports + self.beta = beta + self.rel_sus = rel_sus + self.rel_trans = rel_trans + return + + def apply(self, sim): + t = sim.t + if t == self.day: + for strain in range(self.new_strains): + sim['beta'].append(self.beta[strain]) + sim.people['rel_sus'][:,strain+self.new_strains] = self.rel_sus[strain] + sim.people['rel_trans'][:,strain+self.new_strains] = self.rel_trans[strain] + importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[strain]) + sim.people.infect(inds=importation_inds, layer='importation', strain=strain+self.new_strains) + sim['n_strains'] += self.new_strains \ No newline at end of file diff --git a/covasim/people.py b/covasim/people.py index e6467afeb..f8b864ec5 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -88,7 +88,7 @@ def __init__(self, pars, strict=True, **kwargs): self.flows = {key:0 for key in cvd.new_result_flows} for key in cvd.new_result_flows: if 'by_strain' in key: - self.flows[key] = np.full(self.pars['n_strains'], 0, dtype=cvd.default_float) + self.flows[key] = np.full(self.pars['max_strains'], 0, dtype=cvd.default_float) # Although we have called init(), we still need to call initialize() @@ -164,7 +164,7 @@ def update_states_pre(self, t): self.flows = {key:0 for key in cvd.new_result_flows} for key in cvd.new_result_flows: if 'by_strain' in key: - self.flows[key] += np.full(self.pars['n_strains'], 0, dtype=cvd.default_float) + self.flows[key] += np.full(self.pars['max_strains'], 0, dtype=cvd.default_float) self.flows['new_infectious'] += self.check_infectious() # For people who are exposed and not infectious, check if they begin being infectious self.flows['new_symptomatic'] += self.check_symptomatic() self.flows['new_severe'] += self.check_severe() diff --git a/examples/t10_variants.py b/examples/t10_variants.py index 628286b6a..249795111 100644 --- a/examples/t10_variants.py +++ b/examples/t10_variants.py @@ -1,13 +1,21 @@ import covasim as cv -pars = {'n_strains': 2, - 'beta': [0.016, 0.035]} -sim = cv.Sim(pars=pars) -sim.run() +pars = {'n_strains': 1, + 'beta': [0.016]} -sim.plot_result('cum_infections_by_strain', do_show=True) -sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True) -sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True) +imports = cv.import_strain(day=10, n_imports=[5], beta=[0.05], rel_trans=[1], rel_sus=[1]) +# sim = cv.Sim(pars=pars) +# sim.run() +# +# sim.plot_result('cum_infections_by_strain', do_show=True) +# sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True) +# sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True) + +sim2 = cv.Sim(pars=pars, interventions=imports, label='With imported infections') +sim2.run() + +sim2.plot_result('cum_infections_by_strain', do_show=True) +sim2.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True) +sim2.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True) -print('done') \ No newline at end of file From 287455390997c283b86414ad04b98fa74d99c9b7 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 4 Feb 2021 21:17:48 -0500 Subject: [PATCH 013/569] updated test script --- examples/t10_variants.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/examples/t10_variants.py b/examples/t10_variants.py index 249795111..2052607d8 100644 --- a/examples/t10_variants.py +++ b/examples/t10_variants.py @@ -1,17 +1,19 @@ import covasim as cv -pars = {'n_strains': 1, - 'beta': [0.016]} +pars = {'n_strains': 2, + 'beta': [0.016, 0.035]} -imports = cv.import_strain(day=10, n_imports=[5], beta=[0.05], rel_trans=[1], rel_sus=[1]) -# sim = cv.Sim(pars=pars) -# sim.run() -# -# sim.plot_result('cum_infections_by_strain', do_show=True) -# sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True) -# sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True) +sim = cv.Sim(pars=pars) +sim.run() + +sim.plot_result('cum_infections_by_strain', do_show=True) +sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True) +sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True) + +## run sim with newly imported strain that's more transmissible on day 10 +imports = cv.import_strain(day=10, n_imports=[5], beta=[0.05], rel_trans=[1], rel_sus=[1]) sim2 = cv.Sim(pars=pars, interventions=imports, label='With imported infections') sim2.run() From 2db61ed109e04bcf1cf8a6fe788cfd3449a45152 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 5 Feb 2021 08:35:33 +0100 Subject: [PATCH 014/569] speed up --- covasim/sim.py | 9 +++------ examples/t10_variants.py | 12 ++++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 3fa969174..fa8f7a6c8 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -525,11 +525,8 @@ def step(self): rel_sus = people.rel_sus[:,strain] inf = people.infectious - for person, value in enumerate(people.infectious_strain): - if value == strain: - inf[person] = True - else: - inf[person] = False + inf_by_this_strain = sc.dcp(inf) + inf_by_this_strain[cvu.false(people.infectious_strain==strain)] = False #TODO-- write a function that returns an array which is TRUE if infectious_by_strain == strain and otherwise false sus = people.susceptible @@ -539,7 +536,7 @@ def step(self): iso_factor = cvd.default_float(self['iso_factor'][lkey]) quar_factor = cvd.default_float(self['quar_factor'][lkey]) beta_layer = cvd.default_float(self['beta_layer'][lkey]) - rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, + rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor) # Calculate actual transmission diff --git a/examples/t10_variants.py b/examples/t10_variants.py index 2052607d8..5aff1fb7e 100644 --- a/examples/t10_variants.py +++ b/examples/t10_variants.py @@ -8,16 +8,16 @@ sim = cv.Sim(pars=pars) sim.run() -sim.plot_result('cum_infections_by_strain', do_show=True) -sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True) -sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True) +sim.plot_result('cum_infections_by_strain', do_show=False, do_save=True, fig_path='results/sim1_cum_infections_by_strain') +sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True, do_save=True, fig_path='results/sim1_incidence_by_strain') +sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True, do_save=True, fig_path='results/sim1_prevalence_by_strain') ## run sim with newly imported strain that's more transmissible on day 10 imports = cv.import_strain(day=10, n_imports=[5], beta=[0.05], rel_trans=[1], rel_sus=[1]) sim2 = cv.Sim(pars=pars, interventions=imports, label='With imported infections') sim2.run() -sim2.plot_result('cum_infections_by_strain', do_show=True) -sim2.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True) -sim2.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True) +sim2.plot_result('cum_infections_by_strain', do_show=False, do_save=True, fig_path='results/sim2_cum_infections_by_strain') +sim2.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True, do_save=True, fig_path='results/sim2_incidence_by_strain') +sim2.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True, do_save=True, fig_path='results/sim2_prevalence_by_strain') From e68eef0f2444104339d15ea2993740fe9e38056a Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 5 Feb 2021 08:45:03 +0100 Subject: [PATCH 015/569] exploring intervention --- covasim/interventions.py | 8 +++++++- examples/t10_variants.py | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 3824a4f1e..fa18a2d85 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1136,7 +1136,7 @@ class import_strain(Intervention): **Examples**:: - interv = cv.import_strain(days=50, beta=0.3, rel_sus=0.5, rel_symp=0.1) + interv = cv.import_strain(days=50, beta=0.3, rel_sus=0.5, rel_trans=0.1) ''' def __init__(self, day=None, n_imports=None, beta=None, rel_sus=None, rel_trans=None, **kwargs): @@ -1144,6 +1144,11 @@ def __init__(self, day=None, n_imports=None, beta=None, rel_sus=None, rel_trans= super().__init__(**kwargs) # Initialize the Intervention object self._store_args() # Store the input arguments so the intervention can be recreated + # Handle inputs + n_imports = sc.promotetolist(n_imports) + beta = sc.promotetolist(beta) + rel_sus = sc.promotetolist(rel_sus) + rel_trans = sc.promotetolist(rel_trans) len_imports = len(n_imports) len_betas = len(beta) if len_imports != len_betas: @@ -1152,6 +1157,7 @@ def __init__(self, day=None, n_imports=None, beta=None, rel_sus=None, rel_trans= else: self.new_strains = len_imports + # Set attributes self.day = day self.n_imports = n_imports self.beta = beta diff --git a/examples/t10_variants.py b/examples/t10_variants.py index 5aff1fb7e..561a75032 100644 --- a/examples/t10_variants.py +++ b/examples/t10_variants.py @@ -9,15 +9,15 @@ sim.run() sim.plot_result('cum_infections_by_strain', do_show=False, do_save=True, fig_path='results/sim1_cum_infections_by_strain') -sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True, do_save=True, fig_path='results/sim1_incidence_by_strain') -sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True, do_save=True, fig_path='results/sim1_prevalence_by_strain') +sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim1_incidence_by_strain') +sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim1_prevalence_by_strain') ## run sim with newly imported strain that's more transmissible on day 10 -imports = cv.import_strain(day=10, n_imports=[5], beta=[0.05], rel_trans=[1], rel_sus=[1]) +imports = cv.import_strain(day=10, n_imports=5, beta=0.05, rel_trans=1, rel_sus=1) sim2 = cv.Sim(pars=pars, interventions=imports, label='With imported infections') sim2.run() sim2.plot_result('cum_infections_by_strain', do_show=False, do_save=True, fig_path='results/sim2_cum_infections_by_strain') -sim2.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=True, do_save=True, fig_path='results/sim2_incidence_by_strain') -sim2.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=True, do_save=True, fig_path='results/sim2_prevalence_by_strain') +sim2.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim2_incidence_by_strain') +sim2.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim2_prevalence_by_strain') From 2820a827a3355e4abe2ee2981d92a351fd64189d Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 5 Feb 2021 08:53:04 +0100 Subject: [PATCH 016/569] looking for the change --- covasim/people.py | 2 +- examples/t10_variants.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index f8b864ec5..87d38d0d2 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -394,7 +394,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.exposed_by_strain[inds, strain] = True self.date_exposed[inds] = self.t self.flows['new_infections'] += len(inds) - self.flows['new_infections_by_strain'][strain] += len(inds) + self.flows['new_infections_by_strain'][strain] += len(inds) # TODO: is this working as it should? # Record transmissions for i, target in enumerate(inds): diff --git a/examples/t10_variants.py b/examples/t10_variants.py index 561a75032..f0db6b845 100644 --- a/examples/t10_variants.py +++ b/examples/t10_variants.py @@ -13,11 +13,11 @@ sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim1_prevalence_by_strain') ## run sim with newly imported strain that's more transmissible on day 10 -imports = cv.import_strain(day=10, n_imports=5, beta=0.05, rel_trans=1, rel_sus=1) -sim2 = cv.Sim(pars=pars, interventions=imports, label='With imported infections') -sim2.run() +#imports = cv.import_strain(day=10, n_imports=5, beta=0.05, rel_trans=1, rel_sus=1) +#sim2 = cv.Sim(pars=pars, interventions=imports, label='With imported infections') +#sim2.run() -sim2.plot_result('cum_infections_by_strain', do_show=False, do_save=True, fig_path='results/sim2_cum_infections_by_strain') -sim2.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim2_incidence_by_strain') -sim2.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim2_prevalence_by_strain') +#sim2.plot_result('cum_infections_by_strain', do_show=False, do_save=True, fig_path='results/sim2_cum_infections_by_strain') +#sim2.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim2_incidence_by_strain') +#sim2.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim2_prevalence_by_strain') From 8f4ac81cbe1ca7044e36d3f4e03b25474b1e3c3a Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 5 Feb 2021 09:21:21 +0100 Subject: [PATCH 017/569] looking at results --- covasim/sim.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index fa8f7a6c8..8443636c3 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -673,12 +673,10 @@ def finalize(self, verbose=None, restore_pars=True): for reskey in self.result_keys(): if 'by_strain' in reskey: # resize results to include only active strains - self.results[reskey].values = np.delete(self.results[reskey], - slice(self['n_strains'], self['max_strains'] - 1, 1), 1) + self.results[reskey].values = self.results[reskey].values[:, :self['n_strains']] if self.results[reskey].scale: # Scale the result dynamically if 'by_strain' in reskey: - for strain in range(self['n_strains']): - self.results[reskey].values[:,strain] *= self.rescale_vec + self.results[reskey].values = np.einsum('ij,i->ij',self.results[reskey].values,self.rescale_vec) else: self.results[reskey].values *= self.rescale_vec From ef8b462dc3eaf5485f24c6dd4ed8ae38e518180f Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 5 Feb 2021 09:34:27 +0100 Subject: [PATCH 018/569] arrayify a few things --- covasim/sim.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 8443636c3..369d187c5 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -682,14 +682,9 @@ def finalize(self, verbose=None, restore_pars=True): # Calculate cumulative results for key in cvd.result_flows.keys(): - if 'by_strain' in key: - for strain in range(self['n_strains']): - self.results[f'cum_{key}'][:,strain] = np.cumsum(self.results[f'new_{key}'][:,strain]) - else: - self.results[f'cum_{key}'][:] = np.cumsum(self.results[f'new_{key}'][:]) - self.results['cum_infections'].values += self['pop_infected']*self.rescale_vec[0] # Include initially infected people - for strain in range(self['n_strains']): - self.results['cum_infections_by_strain'].values[:, strain] += self['pop_infected']*self.rescale_vec[0] # Include initially infected people + self.results[f'cum_{key}'][:] = np.cumsum(self.results[f'new_{key}'][:],axis=0) + for key in ['cum_infections','cum_infections_by_strain']: + self.results[key].values += self['pop_infected']*self.rescale_vec[0] # Include initially infected people # Final settings self.results_ready = True # Set this first so self.summary() knows to print the results From 7c1c074b5ec904c3b6f0634f548642e790b8260d Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 5 Feb 2021 10:22:52 +0100 Subject: [PATCH 019/569] attempted to arrayify main calcs, but numba doesnt support it --- covasim/sim.py | 135 ++++++++++++++++++++++++++++++++--------------- covasim/utils.py | 10 ++-- 2 files changed, 97 insertions(+), 48 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 369d187c5..4411f6725 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -502,49 +502,98 @@ def step(self): people.update_states_post() # Check for state changes after interventions # TODO -- iterate through n_strains, using value as index for beta, etc. - - for strain in range(self['n_strains']): - # Compute the probability of transmission - beta = cvd.default_float(self['beta'][strain]) - asymp_factor = cvd.default_float(self['asymp_factor']) - frac_time = cvd.default_float(self['viral_dist']['frac_time']) - load_ratio = cvd.default_float(self['viral_dist']['load_ratio']) - high_cap = cvd.default_float(self['viral_dist']['high_cap']) - date_inf = people.date_infectious - date_rec = people.date_recovered - date_dead = people.date_dead - viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) - - for lkey, layer in contacts.items(): - p1 = layer['p1'] - p2 = layer['p2'] - betas = layer['beta'] - - # Compute relative transmission and susceptibility - rel_trans = people.rel_trans[:,strain] - rel_sus = people.rel_sus[:,strain] - - inf = people.infectious - inf_by_this_strain = sc.dcp(inf) - inf_by_this_strain[cvu.false(people.infectious_strain==strain)] = False - - #TODO-- write a function that returns an array which is TRUE if infectious_by_strain == strain and otherwise false - sus = people.susceptible - symp = people.symptomatic - diag = people.diagnosed - quar = people.quarantined - iso_factor = cvd.default_float(self['iso_factor'][lkey]) - quar_factor = cvd.default_float(self['quar_factor'][lkey]) - beta_layer = cvd.default_float(self['beta_layer'][lkey]) - rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, - diag, quar, asymp_factor, iso_factor, quar_factor) - - # Calculate actual transmission - for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 - source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, - rel_sus) # Calculate transmission! - people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, - layer=lkey, strain=strain) # Actually infect people + # Compute the probability of transmission + beta = cvd.default_float(self['beta']) + asymp_factor = cvd.default_float(self['asymp_factor']) + frac_time = cvd.default_float(self['viral_dist']['frac_time']) + load_ratio = cvd.default_float(self['viral_dist']['load_ratio']) + high_cap = cvd.default_float(self['viral_dist']['high_cap']) + date_inf = people.date_infectious + date_rec = people.date_recovered + date_dead = people.date_dead + viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) + + # Initialise an array to store infections by strain on this time step + inf_by_strain = np.full((self['pop_size'], self['max_strains']), np.nan, dtype=cvd.default_float) + + for lkey, layer in contacts.items(): + p1 = layer['p1'] + p2 = layer['p2'] + betas = layer['beta'] + + # Compute relative transmission and susceptibility + rel_trans = people.rel_trans + rel_sus = people.rel_sus + + inf = people.infectious +# inf_by_this_strain[cvu.false(people.infectious_strain==strain)] = False + for strain in range(self['n_strains']): + inf_by_strain[cvu.true(people.infectious_strain == strain), strain] = True + + sus = people.susceptible + symp = people.symptomatic + diag = people.diagnosed + quar = people.quarantined + iso_factor = cvd.default_float(self['iso_factor'][lkey]) + quar_factor = cvd.default_float(self['quar_factor'][lkey]) + beta_layer = cvd.default_float(self['beta_layer'][lkey]) + rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_strain, sus, beta_layer, viral_load, symp, + diag, quar, asymp_factor, iso_factor, quar_factor) + rel_trans = np.float32(rel_trans) + rel_sus = np.float32(rel_sus) + + # Calculate actual transmission + for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 + source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, + rel_sus) # Calculate transmission! + people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, + layer=lkey, strain=strain) # Actually infect people + + # for strain in range(self['n_strains']): + # # Compute the probability of transmission + # beta = cvd.default_float(self['beta'][strain]) + # asymp_factor = cvd.default_float(self['asymp_factor']) + # frac_time = cvd.default_float(self['viral_dist']['frac_time']) + # load_ratio = cvd.default_float(self['viral_dist']['load_ratio']) + # high_cap = cvd.default_float(self['viral_dist']['high_cap']) + # date_inf = people.date_infectious + # date_rec = people.date_recovered + # date_dead = people.date_dead + # viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) + # + # for lkey, layer in contacts.items(): + # p1 = layer['p1'] + # p2 = layer['p2'] + # betas = layer['beta'] + # + # # Compute relative transmission and susceptibility + # import traceback; + # traceback.print_exc(); + # import pdb; + # pdb.set_trace() + # rel_trans = people.rel_trans[:,strain] + # rel_sus = people.rel_sus[:,strain] + # + # inf = people.infectious + # inf_by_this_strain = sc.dcp(inf) + # inf_by_this_strain[cvu.false(people.infectious_strain==strain)] = False + # + # sus = people.susceptible + # symp = people.symptomatic + # diag = people.diagnosed + # quar = people.quarantined + # iso_factor = cvd.default_float(self['iso_factor'][lkey]) + # quar_factor = cvd.default_float(self['quar_factor'][lkey]) + # beta_layer = cvd.default_float(self['beta_layer'][lkey]) + # rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, + # diag, quar, asymp_factor, iso_factor, quar_factor) + # + # # Calculate actual transmission + # for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 + # source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, + # rel_sus) # Calculate transmission! + # people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, + # layer=lkey, strain=strain) # Actually infect people # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): diff --git a/covasim/utils.py b/covasim/utils.py index 4c390e375..aff85367a 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -69,19 +69,19 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) +#@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] f_iso = ~diag + diag * iso_factor # Isolation factor, changes e.g. [0,1] with a factor of 0.2 to [1,0.2] f_quar = ~quar + quar * quar_factor # Quarantine, changes e.g. [0,1] with a factor of 0.5 to [1,0.5] - rel_trans = rel_trans * inf * f_quar * f_asymp * f_iso * beta_layer * viral_load # Recalculate transmissibility - rel_sus = rel_sus * sus * f_quar # Recalculate susceptibility + rel_trans = beta_layer * np.einsum('ij,ij,i,i,i,i->ij', rel_trans, inf, f_quar, f_asymp, f_iso, viral_load) # Recalculate transmissibility + rel_sus = np.einsum('ij,i,i->ij', rel_sus, sus, f_quar) # Recalculate susceptibility return rel_trans, rel_sus -@nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=True, parallel=parallel) -def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): +@nb.njit( (nbfloat[:], nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=True, parallel=parallel) +def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): #TODO -- make this specific to strain -- use rel_trans for this -- need to set it to 0 if not infected with strain and otherwise whatever the value is ''' The heaviest step of the model -- figure out who gets infected on this timestep ''' betas = beta * layer_betas * rel_trans[sources] * rel_sus[targets] # Calculate the raw transmission probabilities From b6388477861bb9caa3d0b8c019db19ae30f2c89c Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 5 Feb 2021 10:25:26 +0100 Subject: [PATCH 020/569] revert changes --- covasim/sim.py | 133 +++++++++++++++-------------------------------- covasim/utils.py | 10 ++-- 2 files changed, 46 insertions(+), 97 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 4411f6725..267505485 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -502,98 +502,47 @@ def step(self): people.update_states_post() # Check for state changes after interventions # TODO -- iterate through n_strains, using value as index for beta, etc. - # Compute the probability of transmission - beta = cvd.default_float(self['beta']) - asymp_factor = cvd.default_float(self['asymp_factor']) - frac_time = cvd.default_float(self['viral_dist']['frac_time']) - load_ratio = cvd.default_float(self['viral_dist']['load_ratio']) - high_cap = cvd.default_float(self['viral_dist']['high_cap']) - date_inf = people.date_infectious - date_rec = people.date_recovered - date_dead = people.date_dead - viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) - - # Initialise an array to store infections by strain on this time step - inf_by_strain = np.full((self['pop_size'], self['max_strains']), np.nan, dtype=cvd.default_float) - - for lkey, layer in contacts.items(): - p1 = layer['p1'] - p2 = layer['p2'] - betas = layer['beta'] - - # Compute relative transmission and susceptibility - rel_trans = people.rel_trans - rel_sus = people.rel_sus - - inf = people.infectious -# inf_by_this_strain[cvu.false(people.infectious_strain==strain)] = False - for strain in range(self['n_strains']): - inf_by_strain[cvu.true(people.infectious_strain == strain), strain] = True - - sus = people.susceptible - symp = people.symptomatic - diag = people.diagnosed - quar = people.quarantined - iso_factor = cvd.default_float(self['iso_factor'][lkey]) - quar_factor = cvd.default_float(self['quar_factor'][lkey]) - beta_layer = cvd.default_float(self['beta_layer'][lkey]) - rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_strain, sus, beta_layer, viral_load, symp, - diag, quar, asymp_factor, iso_factor, quar_factor) - rel_trans = np.float32(rel_trans) - rel_sus = np.float32(rel_sus) - - # Calculate actual transmission - for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 - source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, - rel_sus) # Calculate transmission! - people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, - layer=lkey, strain=strain) # Actually infect people - - # for strain in range(self['n_strains']): - # # Compute the probability of transmission - # beta = cvd.default_float(self['beta'][strain]) - # asymp_factor = cvd.default_float(self['asymp_factor']) - # frac_time = cvd.default_float(self['viral_dist']['frac_time']) - # load_ratio = cvd.default_float(self['viral_dist']['load_ratio']) - # high_cap = cvd.default_float(self['viral_dist']['high_cap']) - # date_inf = people.date_infectious - # date_rec = people.date_recovered - # date_dead = people.date_dead - # viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) - # - # for lkey, layer in contacts.items(): - # p1 = layer['p1'] - # p2 = layer['p2'] - # betas = layer['beta'] - # - # # Compute relative transmission and susceptibility - # import traceback; - # traceback.print_exc(); - # import pdb; - # pdb.set_trace() - # rel_trans = people.rel_trans[:,strain] - # rel_sus = people.rel_sus[:,strain] - # - # inf = people.infectious - # inf_by_this_strain = sc.dcp(inf) - # inf_by_this_strain[cvu.false(people.infectious_strain==strain)] = False - # - # sus = people.susceptible - # symp = people.symptomatic - # diag = people.diagnosed - # quar = people.quarantined - # iso_factor = cvd.default_float(self['iso_factor'][lkey]) - # quar_factor = cvd.default_float(self['quar_factor'][lkey]) - # beta_layer = cvd.default_float(self['beta_layer'][lkey]) - # rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, - # diag, quar, asymp_factor, iso_factor, quar_factor) - # - # # Calculate actual transmission - # for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 - # source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, - # rel_sus) # Calculate transmission! - # people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, - # layer=lkey, strain=strain) # Actually infect people + for strain in range(self['n_strains']): + # Compute the probability of transmission + beta = cvd.default_float(self['beta'][strain]) + asymp_factor = cvd.default_float(self['asymp_factor']) + frac_time = cvd.default_float(self['viral_dist']['frac_time']) + load_ratio = cvd.default_float(self['viral_dist']['load_ratio']) + high_cap = cvd.default_float(self['viral_dist']['high_cap']) + date_inf = people.date_infectious + date_rec = people.date_recovered + date_dead = people.date_dead + viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) + + for lkey, layer in contacts.items(): + p1 = layer['p1'] + p2 = layer['p2'] + betas = layer['beta'] + + # Compute relative transmission and susceptibility + rel_trans = people.rel_trans[:,strain] + rel_sus = people.rel_sus[:,strain] + + inf = people.infectious + inf_by_this_strain = sc.dcp(inf) + inf_by_this_strain[cvu.false(people.infectious_strain==strain)] = False + + sus = people.susceptible + symp = people.symptomatic + diag = people.diagnosed + quar = people.quarantined + iso_factor = cvd.default_float(self['iso_factor'][lkey]) + quar_factor = cvd.default_float(self['quar_factor'][lkey]) + beta_layer = cvd.default_float(self['beta_layer'][lkey]) + rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, + diag, quar, asymp_factor, iso_factor, quar_factor) + + # Calculate actual transmission + for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 + source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, + rel_sus) # Calculate transmission! + people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, + layer=lkey, strain=strain) # Actually infect people # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): diff --git a/covasim/utils.py b/covasim/utils.py index aff85367a..78eec329d 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -69,19 +69,19 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -#@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) +@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] f_iso = ~diag + diag * iso_factor # Isolation factor, changes e.g. [0,1] with a factor of 0.2 to [1,0.2] f_quar = ~quar + quar * quar_factor # Quarantine, changes e.g. [0,1] with a factor of 0.5 to [1,0.5] - rel_trans = beta_layer * np.einsum('ij,ij,i,i,i,i->ij', rel_trans, inf, f_quar, f_asymp, f_iso, viral_load) # Recalculate transmissibility - rel_sus = np.einsum('ij,i,i->ij', rel_sus, sus, f_quar) # Recalculate susceptibility + rel_trans = beta_layer * rel_trans * inf * f_quar * f_asymp * f_iso * viral_load # Recalculate transmissibility + rel_sus = rel_sus * sus * f_quar # Recalculate susceptibility return rel_trans, rel_sus -@nb.njit( (nbfloat[:], nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=True, parallel=parallel) -def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): +@nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=True, parallel=parallel) +def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): #TODO -- make this specific to strain -- use rel_trans for this -- need to set it to 0 if not infected with strain and otherwise whatever the value is ''' The heaviest step of the model -- figure out who gets infected on this timestep ''' betas = beta * layer_betas * rel_trans[sources] * rel_sus[targets] # Calculate the raw transmission probabilities From 58d42e40773e624a8e5e9cff63a74287d99e1ec6 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 5 Feb 2021 12:14:23 +0100 Subject: [PATCH 021/569] add to devtests --- covasim/interventions.py | 18 +++++++--- covasim/sim.py | 16 ++++----- examples/t10_variants.py | 17 +++++----- tests/devtests/test_variants.py | 58 +++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 22 deletions(-) create mode 100644 tests/devtests/test_variants.py diff --git a/covasim/interventions.py b/covasim/interventions.py index fa18a2d85..38ca3a90f 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1155,7 +1155,7 @@ def __init__(self, day=None, n_imports=None, beta=None, rel_sus=None, rel_trans= raise ValueError( f'Number of different imports ({len_imports} does not match the number of betas ({len_betas})') else: - self.new_strains = len_imports + self.new_strains = len_imports # Number of new strains being introduced # Set attributes self.day = day @@ -1163,15 +1163,23 @@ def __init__(self, day=None, n_imports=None, beta=None, rel_sus=None, rel_trans= self.beta = beta self.rel_sus = rel_sus self.rel_trans = rel_trans + return def apply(self, sim): t = sim.t if t == self.day: + + # Check number of strains + prev_strains = sim['n_strains'] + if prev_strains + self.new_strains > sim['max_strains']: + errormsg = f"Number of existing strains ({sim['n_strains']}) plus number of new strains ({self.new_strains}) exceeds the maximal allowable ({sim['max_strains']}. Increase pars['max_strains'])." + raise ValueError(errormsg) + for strain in range(self.new_strains): sim['beta'].append(self.beta[strain]) - sim.people['rel_sus'][:,strain+self.new_strains] = self.rel_sus[strain] - sim.people['rel_trans'][:,strain+self.new_strains] = self.rel_trans[strain] - importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[strain]) - sim.people.infect(inds=importation_inds, layer='importation', strain=strain+self.new_strains) + sim.people['rel_sus'][:, prev_strains+strain] = self.rel_sus[strain] + sim.people['rel_trans'][:, prev_strains+strain] = self.rel_trans[strain] + importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely + sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains+strain) sim['n_strains'] += self.new_strains \ No newline at end of file diff --git a/covasim/sim.py b/covasim/sim.py index 267505485..5e00d67c2 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -274,21 +274,21 @@ def init_res(*args, **kwargs): # Flows and cumulative flows for key,label in cvd.result_flows.items(): - self.results[f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key]) # Cumulative variables -- e.g. "Cumulative infections" + self.results[f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key], max_strains=self['max_strains']) # Cumulative variables -- e.g. "Cumulative infections" for key,label in cvd.result_flows.items(): # Repeat to keep all the cumulative keys together - self.results[f'new_{key}'] = init_res(f'Number of new {label}', color=dcols[key]) # Flow variables -- e.g. "Number of new infections" + self.results[f'new_{key}'] = init_res(f'Number of new {label}', color=dcols[key], max_strains=self['max_strains']) # Flow variables -- e.g. "Number of new infections" # Stock variables for key,label in cvd.result_stocks.items(): - self.results[f'n_{key}'] = init_res(label, color=dcols[key]) + self.results[f'n_{key}'] = init_res(label, color=dcols[key], max_strains=self['max_strains']) # Other variables self.results['n_alive'] = init_res('Number of people alive', scale=False) self.results['prevalence'] = init_res('Prevalence', scale=False) - self.results['prevalence_by_strain'] = init_res('Prevalence by strain', scale=False) + self.results['prevalence_by_strain'] = init_res('Prevalence by strain', scale=False, max_strains=self['max_strains']) self.results['incidence'] = init_res('Incidence', scale=False) - self.results['incidence_by_strain'] = init_res('Incidence by strain', scale=False) + self.results['incidence_by_strain'] = init_res('Incidence by strain', scale=False, max_strains=self['max_strains']) self.results['r_eff'] = init_res('Effective reproduction number', scale=False) self.results['doubling_time'] = init_res('Doubling time', scale=False) self.results['test_yield'] = init_res('Testing yield', scale=False) @@ -731,10 +731,8 @@ def compute_prev_inci(self): self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence - - for strain in range(self['n_strains']): - self.results['incidence_by_strain'][:,strain] = res['new_infections_by_strain'][:,strain]/res['n_susceptible'][:] # Calculate the incidence - self.results['prevalence_by_strain'][:, strain] = res['n_exposed_by_strain'][:, strain] / res['n_alive'][:] # Calculate the prevalence + self.results['incidence_by_strain'][:] = np.einsum('ij,i->ij',res['new_infections_by_strain'][:],1/res['n_susceptible'][:]) # Calculate the incidence + self.results['prevalence_by_strain'][:] = np.einsum('ij,i->ij',res['n_exposed_by_strain'][:], 1/res['n_alive'][:]) # Calculate the prevalence return diff --git a/examples/t10_variants.py b/examples/t10_variants.py index f0db6b845..f5284f77c 100644 --- a/examples/t10_variants.py +++ b/examples/t10_variants.py @@ -12,12 +12,13 @@ sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim1_incidence_by_strain') sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim1_prevalence_by_strain') -## run sim with newly imported strain that's more transmissible on day 10 -#imports = cv.import_strain(day=10, n_imports=5, beta=0.05, rel_trans=1, rel_sus=1) -#sim2 = cv.Sim(pars=pars, interventions=imports, label='With imported infections') -#sim2.run() - -#sim2.plot_result('cum_infections_by_strain', do_show=False, do_save=True, fig_path='results/sim2_cum_infections_by_strain') -#sim2.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim2_incidence_by_strain') -#sim2.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim2_prevalence_by_strain') +# Run sim with a single strain initially, then introduce a new strain that's more transmissible on day 10 +pars = {'n_strains': 10, 'beta': [0.016]*10, 'max_strains': 11} +imports = cv.import_strain(day=10, n_imports=20, beta=0.05, rel_trans=1, rel_sus=1) +sim2 = cv.Sim(pars=pars, interventions=imports, label='With imported infections') +sim2.run() + +sim2.plot_result('cum_infections_by_strain', do_show=False, do_save=True, fig_path='results/sim2_cum_infections_by_strain') +sim2.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim2_incidence_by_strain') +sim2.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim2_prevalence_by_strain') diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py new file mode 100644 index 000000000..d2fd46455 --- /dev/null +++ b/tests/devtests/test_variants.py @@ -0,0 +1,58 @@ +import covasim as cv +import sciris as sc + +do_plot = 1 +do_show = 0 +do_save = 1 + + +def test_multistrains(do_plot=False, do_show=True, do_save=False, fig_path=None): + sc.heading('Run basic sim with multiple strains') + + sc.heading('Setting up...') + + pars = {'n_strains': 2, + 'beta': [0.016, 0.035]} + + sim = cv.Sim(pars=pars) + sim.run() + + if do_plot: + sim.plot_result('cum_infections_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim1_cum_infections_by_strain') + sim.plot_result('incidence_by_strain', label=['strain1', 'strain2'], do_show=do_show, do_save=do_save, fig_path='results/sim1_incidence_by_strain') + sim.plot_result('prevalence_by_strain', label=['strain1', 'strain2'], do_show=do_show, do_save=do_save, fig_path='results/sim1_prevalence_by_strain') + + return sim + + +def test_importstrain(do_plot=False, do_show=True, do_save=False, fig_path=None): + sc.heading('Test introducing a new strain partway through a sim') + + sc.heading('Setting up...') + + # Run sim with several strains initially, then introduce a new strain that's more transmissible on day 10 + pars = {'n_strains': 10, 'beta': [0.016] * 10, 'max_strains': 11} # Checking here that increasing max_strains works + imports = cv.import_strain(day=10, n_imports=20, beta=0.05, rel_trans=1, rel_sus=1) + sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') + sim.run() + + if do_plot: + sim.plot_result('cum_infections_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim2_cum_infections_by_strain') + sim.plot_result('incidence_by_strain', label=['strain1', 'strain2'], do_show=do_show, do_save=do_save, fig_path='results/sim2_incidence_by_strain') + sim.plot_result('prevalence_by_strain', label=['strain1', 'strain2'], do_show=do_show, do_save=do_save, fig_path='results/sim2_prevalence_by_strain') + + return sim + + + +#%% Run as a script +if __name__ == '__main__': + sc.tic() + + sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + sim2 = test_importstrain(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + + sc.toc() + + +print('Done.') From a8dc0ca683f5c00954023d0eebff25adc10b381d Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 5 Feb 2021 13:05:35 +0100 Subject: [PATCH 022/569] starting with immunity --- covasim/parameters.py | 1 + covasim/people.py | 2 +- covasim/sim.py | 15 ++++++++++++--- covasim/utils.py | 5 +++-- tests/devtests/test_variants.py | 2 +- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index f8d146bc1..9b012e523 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -57,6 +57,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 pars['n_strains'] = 1 # The number of strains currently circulating in the population pars['max_strains'] = 10 # For allocating memory with numpy arrays + pars['immunity'] = dict(form='exponential', par1=180, par2=1.) # Protection from immunity. Options: constant immunity (use a value from 0-1), or a decay function. Currently only expnential decay supported, with par1=half_life, par2=initial immunity # Efficacy of protection measures pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 diff --git a/covasim/people.py b/covasim/people.py index 87d38d0d2..f8b864ec5 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -394,7 +394,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.exposed_by_strain[inds, strain] = True self.date_exposed[inds] = self.t self.flows['new_infections'] += len(inds) - self.flows['new_infections_by_strain'][strain] += len(inds) # TODO: is this working as it should? + self.flows['new_infections_by_strain'][strain] += len(inds) # Record transmissions for i, target in enumerate(inds): diff --git a/covasim/sim.py b/covasim/sim.py index 5e00d67c2..9f9b30b47 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -514,6 +514,9 @@ def step(self): date_dead = people.date_dead viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) + # Compute immunity + immunity = cvu.compute_immunity(immunity_factors, date_rec) + for lkey, layer in contacts.items(): p1 = layer['p1'] p2 = layer['p2'] @@ -527,15 +530,21 @@ def step(self): inf_by_this_strain = sc.dcp(inf) inf_by_this_strain[cvu.false(people.infectious_strain==strain)] = False - sus = people.susceptible + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() + + sus = people.susceptible + rec = people.recovered # Both susceptible and recovered people can get reinfected symp = people.symptomatic diag = people.diagnosed quar = people.quarantined iso_factor = cvd.default_float(self['iso_factor'][lkey]) quar_factor = cvd.default_float(self['quar_factor'][lkey]) beta_layer = cvd.default_float(self['beta_layer'][lkey]) - rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, - diag, quar, asymp_factor, iso_factor, quar_factor) + rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, rec, beta_layer, viral_load, symp, + diag, quar, asymp_factor, iso_factor, quar_factor, immune_factor) # Calculate actual transmission for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 diff --git a/covasim/utils.py b/covasim/utils.py index 78eec329d..73a70396b 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -69,12 +69,13 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) -def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor): # pragma: no cover +#@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) +def compute_trans_sus(rel_trans, rel_sus, inf, sus, rec, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immune_factor): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] f_iso = ~diag + diag * iso_factor # Isolation factor, changes e.g. [0,1] with a factor of 0.2 to [1,0.2] f_quar = ~quar + quar * quar_factor # Quarantine, changes e.g. [0,1] with a factor of 0.5 to [1,0.5] + f_rec = ~rec + rec * immune_factor # Immunity rel_trans = beta_layer * rel_trans * inf * f_quar * f_asymp * f_iso * viral_load # Recalculate transmissibility rel_sus = rel_sus * sus * f_quar # Recalculate susceptibility return rel_trans, rel_sus diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index d2fd46455..ea12495fd 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -50,7 +50,7 @@ def test_importstrain(do_plot=False, do_show=True, do_save=False, fig_path=None) sc.tic() sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) - sim2 = test_importstrain(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + #sim2 = test_importstrain(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) sc.toc() From d46e55634a4b897a9b9295f17710d1077cce453f Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 5 Feb 2021 15:30:07 +0100 Subject: [PATCH 023/569] bring in reinfection --- covasim/parameters.py | 2 +- covasim/sim.py | 40 ++++++++++++++++++++++------------------ covasim/utils.py | 27 +++++++++++++++++++++++---- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 9b012e523..9d76c305e 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -57,7 +57,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 pars['n_strains'] = 1 # The number of strains currently circulating in the population pars['max_strains'] = 10 # For allocating memory with numpy arrays - pars['immunity'] = dict(form='exponential', par1=180, par2=1.) # Protection from immunity. Options: constant immunity (use a value from 0-1), or a decay function. Currently only expnential decay supported, with par1=half_life, par2=initial immunity + pars['immunity'] = dict(init_immunity=1., half_life=180) # Protection from immunity. If half_life is None immunity is constant; if it's a number it decays exponentially. TODO: improve this with data, e.g. https://www.nejm.org/doi/full/10.1056/nejmc2025179 # Efficacy of protection measures pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 diff --git a/covasim/sim.py b/covasim/sim.py index 9f9b30b47..98f0d28f7 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -501,21 +501,30 @@ def step(self): people.update_states_post() # Check for state changes after interventions - # TODO -- iterate through n_strains, using value as index for beta, etc. + # Compute viral loads + frac_time = cvd.default_float(self['viral_dist']['frac_time']) + load_ratio = cvd.default_float(self['viral_dist']['load_ratio']) + high_cap = cvd.default_float(self['viral_dist']['high_cap']) + date_inf = people.date_infectious + date_rec = people.date_recovered + date_dead = people.date_dead + viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) + + # Extract additional parameters + asymp_factor = cvd.default_float(self['asymp_factor']) + init_immunity = cvd.default_float(self['immunity']['init_immunity']) + half_life = cvd.default_float(self['immunity']['half_life']) + decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. + immunity_factors = np.full(self['pop_size'], 1., dtype=cvd.default_float) # TODO: initialise this somewhere else + + # Compute immunity factors + immunity_factors = cvu.compute_immunity(immunity_factors, t, date_rec, init_immunity, decay_rate) + + # Iterate through n_strains to calculate infections for strain in range(self['n_strains']): + # Compute the probability of transmission beta = cvd.default_float(self['beta'][strain]) - asymp_factor = cvd.default_float(self['asymp_factor']) - frac_time = cvd.default_float(self['viral_dist']['frac_time']) - load_ratio = cvd.default_float(self['viral_dist']['load_ratio']) - high_cap = cvd.default_float(self['viral_dist']['high_cap']) - date_inf = people.date_infectious - date_rec = people.date_recovered - date_dead = people.date_dead - viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) - - # Compute immunity - immunity = cvu.compute_immunity(immunity_factors, date_rec) for lkey, layer in contacts.items(): p1 = layer['p1'] @@ -530,11 +539,6 @@ def step(self): inf_by_this_strain = sc.dcp(inf) inf_by_this_strain[cvu.false(people.infectious_strain==strain)] = False - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() - sus = people.susceptible rec = people.recovered # Both susceptible and recovered people can get reinfected symp = people.symptomatic @@ -544,7 +548,7 @@ def step(self): quar_factor = cvd.default_float(self['quar_factor'][lkey]) beta_layer = cvd.default_float(self['beta_layer'][lkey]) rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, rec, beta_layer, viral_load, symp, - diag, quar, asymp_factor, iso_factor, quar_factor, immune_factor) + diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors) # Calculate actual transmission for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 diff --git a/covasim/utils.py b/covasim/utils.py index 73a70396b..1a889be86 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -69,15 +69,34 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -#@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) -def compute_trans_sus(rel_trans, rel_sus, inf, sus, rec, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immune_factor): # pragma: no cover +#@nb.njit( (nbfloat[:], nbint, nbfloat[:], nbfloat, nbfloat), cache=True, parallel=parallel) +def compute_immunity(immunity_factors, t, date_rec, init_immunity, decay_rate): # pragma: no cover + ''' + Calculate immunity factors for time t + + Args: + t: (int) timestep + date_rec: (float[]) recovery dates + init_immunity: (float) initial immunity protection (1=perfect protection, 0=no protection) + decay_rate: (float) decay rate of immunity. If 0, immunity stays constant + + Returns: + immunity_factors (float[]): immunity factors + ''' + time_since_rec = t - date_rec # Time since recovery + inds = true(time_since_rec>0) # Extract people who have recovered + immunity_factors[inds] = init_immunity * np.exp(-decay_rate * time_since_rec[inds]) # Calculate their immunity factors + return immunity_factors + + +#@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=parallel) +def compute_trans_sus(rel_trans, rel_sus, inf, sus, rec, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] f_iso = ~diag + diag * iso_factor # Isolation factor, changes e.g. [0,1] with a factor of 0.2 to [1,0.2] f_quar = ~quar + quar * quar_factor # Quarantine, changes e.g. [0,1] with a factor of 0.5 to [1,0.5] - f_rec = ~rec + rec * immune_factor # Immunity rel_trans = beta_layer * rel_trans * inf * f_quar * f_asymp * f_iso * viral_load # Recalculate transmissibility - rel_sus = rel_sus * sus * f_quar # Recalculate susceptibility + rel_sus = rel_sus * sus * f_quar + rec * (1.-immunity_factors) # Recalculate susceptibility return rel_trans, rel_sus From ecaf84599a627b483c7b25144cafe0a8af768f0e Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 5 Feb 2021 15:46:40 +0100 Subject: [PATCH 024/569] something wrong with numba --- covasim/sim.py | 4 ++-- covasim/utils.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 98f0d28f7..8acaa9662 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -549,11 +549,11 @@ def step(self): beta_layer = cvd.default_float(self['beta_layer'][lkey]) rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, rec, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors) + rel_sus = np.float32(rel_sus) # TODO: why doesn't this get returned in this format already? # Calculate actual transmission for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 - source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, - rel_sus) # Calculate transmission! + source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey, strain=strain) # Actually infect people diff --git a/covasim/utils.py b/covasim/utils.py index 1a889be86..d9ebda4bc 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -69,7 +69,7 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -#@nb.njit( (nbfloat[:], nbint, nbfloat[:], nbfloat, nbfloat), cache=True, parallel=parallel) +@nb.njit( (nbfloat[:], nbint, nbfloat[:], nbfloat, nbfloat), cache=True, parallel=parallel) def compute_immunity(immunity_factors, t, date_rec, init_immunity, decay_rate): # pragma: no cover ''' Calculate immunity factors for time t @@ -84,12 +84,12 @@ def compute_immunity(immunity_factors, t, date_rec, init_immunity, decay_r immunity_factors (float[]): immunity factors ''' time_since_rec = t - date_rec # Time since recovery - inds = true(time_since_rec>0) # Extract people who have recovered + inds = (time_since_rec>0).nonzero()[0] # Extract people who have recovered immunity_factors[inds] = init_immunity * np.exp(-decay_rate * time_since_rec[inds]) # Calculate their immunity factors return immunity_factors -#@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=parallel) +@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=parallel) def compute_trans_sus(rel_trans, rel_sus, inf, sus, rec, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] From 06ab357ad9632fe0a1c070e71343987bec6374fb Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 5 Feb 2021 15:48:28 +0100 Subject: [PATCH 025/569] stopping for now --- tests/devtests/test_variants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index ea12495fd..d2fd46455 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -50,7 +50,7 @@ def test_importstrain(do_plot=False, do_show=True, do_save=False, fig_path=None) sc.tic() sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) - #sim2 = test_importstrain(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + sim2 = test_importstrain(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) sc.toc() From 470a1cce94c93dfad583ff851e76111febc90478 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 6 Feb 2021 23:06:03 -0800 Subject: [PATCH 026/569] update version --- covasim/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/version.py b/covasim/version.py index d1febf74f..7c501be4c 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,6 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '2.0.2' -__versiondate__ = '2020-02-01' +__version__ = '2.0.3' +__versiondate__ = '2021-02-01' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From 7ee4854e862747070fd37df271b0133bf46f6204 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 6 Feb 2021 23:26:39 -0800 Subject: [PATCH 027/569] update date --- covasim/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/version.py b/covasim/version.py index 7c501be4c..a965358cb 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -5,5 +5,5 @@ __all__ = ['__version__', '__versiondate__', '__license__'] __version__ = '2.0.3' -__versiondate__ = '2021-02-01' +__versiondate__ = '2021-02-06' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From 9cdb8ac8b29154b83c912681a4433dc0fbcc52ba Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Sun, 7 Feb 2021 20:20:38 -0500 Subject: [PATCH 028/569] making immunity strain-specific. defaults to same values unless parameters provided --- covasim/defaults.py | 1 + covasim/parameters.py | 3 ++- covasim/people.py | 4 ++-- covasim/sim.py | 10 ++++++---- tests/devtests/test_variants.py | 16 ++++++++-------- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index a5ddd5c4e..41f3b792d 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -49,6 +49,7 @@ class PeopleMeta(sc.prettyobj): 'rel_trans', # Float 'rel_sus', # Float 'time_of_last_inf', # Int + 'immunity_factors', # Float # 'immune_factor_by_strain', # Float ] diff --git a/covasim/parameters.py b/covasim/parameters.py index 9d76c305e..bc11e7284 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -57,7 +57,8 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 pars['n_strains'] = 1 # The number of strains currently circulating in the population pars['max_strains'] = 10 # For allocating memory with numpy arrays - pars['immunity'] = dict(init_immunity=1., half_life=180) # Protection from immunity. If half_life is None immunity is constant; if it's a number it decays exponentially. TODO: improve this with data, e.g. https://www.nejm.org/doi/full/10.1056/nejmc2025179 + # pars['immunity'] = dict(init_immunity=1., half_life=180) # Protection from immunity. If half_life is None immunity is constant; if it's a number it decays exponentially. TODO: improve this with data, e.g. https://www.nejm.org/doi/full/10.1056/nejmc2025179 + pars['immunity'] = [dict(init_immunity=1., half_life=180) for _ in range(pars['max_strains'])] # Efficacy of protection measures pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 diff --git a/covasim/people.py b/covasim/people.py index f8b864ec5..f07b8a4a4 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -60,8 +60,8 @@ def __init__(self, pars, strict=True, **kwargs): self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif key == 'rel_trans' or key == 'rel_sus' or key == 'time_of_last_inf': self[key] = np.full((self.pop_size, self.pars['max_strains']), np.nan, dtype=cvd.default_float) - elif key == 'immune_factor_by_strain': # everyone starts out with no immunity to either strain. - self[key] = np.full((self.pop_size, self.pars['max_strains']), 1, dtype=cvd.default_float) + elif key == 'immunity_factors': # everyone starts out with no immunity to either strain. + self[key] = np.full((self.pop_size, self.pars['max_strains']), 0, dtype=cvd.default_float) else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) diff --git a/covasim/sim.py b/covasim/sim.py index 8acaa9662..03e03871d 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -512,19 +512,21 @@ def step(self): # Extract additional parameters asymp_factor = cvd.default_float(self['asymp_factor']) - init_immunity = cvd.default_float(self['immunity']['init_immunity']) - half_life = cvd.default_float(self['immunity']['half_life']) + init_immunity = cvd.default_float(self['immunity'][strain]['init_immunity']) + half_life = cvd.default_float(self['immunity'][strain]['half_life']) decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. - immunity_factors = np.full(self['pop_size'], 1., dtype=cvd.default_float) # TODO: initialise this somewhere else + # immunity_factors = np.full(self['pop_size'], 1., dtype=cvd.default_float) # TODO: initialise this somewhere else # Compute immunity factors - immunity_factors = cvu.compute_immunity(immunity_factors, t, date_rec, init_immunity, decay_rate) + # immunity_factors = cvu.compute_immunity(immunity_factors, t, date_rec, init_immunity, decay_rate) # Iterate through n_strains to calculate infections for strain in range(self['n_strains']): # Compute the probability of transmission beta = cvd.default_float(self['beta'][strain]) + # Compute immunity factors + immunity_factors = cvu.compute_immunity(people.immunity_factors[:,strain], t, date_rec, init_immunity, decay_rate) for lkey, layer in contacts.items(): p1 = layer['p1'] diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index d2fd46455..8c4102d23 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -2,8 +2,8 @@ import sciris as sc do_plot = 1 -do_show = 0 -do_save = 1 +do_show = 1 +do_save = 0 def test_multistrains(do_plot=False, do_show=True, do_save=False, fig_path=None): @@ -19,8 +19,8 @@ def test_multistrains(do_plot=False, do_show=True, do_save=False, fig_path=None) if do_plot: sim.plot_result('cum_infections_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim1_cum_infections_by_strain') - sim.plot_result('incidence_by_strain', label=['strain1', 'strain2'], do_show=do_show, do_save=do_save, fig_path='results/sim1_incidence_by_strain') - sim.plot_result('prevalence_by_strain', label=['strain1', 'strain2'], do_show=do_show, do_save=do_save, fig_path='results/sim1_prevalence_by_strain') + sim.plot_result('incidence_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim1_incidence_by_strain') + sim.plot_result('prevalence_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim1_prevalence_by_strain') return sim @@ -32,14 +32,14 @@ def test_importstrain(do_plot=False, do_show=True, do_save=False, fig_path=None) # Run sim with several strains initially, then introduce a new strain that's more transmissible on day 10 pars = {'n_strains': 10, 'beta': [0.016] * 10, 'max_strains': 11} # Checking here that increasing max_strains works - imports = cv.import_strain(day=10, n_imports=20, beta=0.05, rel_trans=1, rel_sus=1) + imports = cv.import_strain(day=30, n_imports=20, beta=0.05, rel_trans=1, rel_sus=1) sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') sim.run() if do_plot: sim.plot_result('cum_infections_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim2_cum_infections_by_strain') - sim.plot_result('incidence_by_strain', label=['strain1', 'strain2'], do_show=do_show, do_save=do_save, fig_path='results/sim2_incidence_by_strain') - sim.plot_result('prevalence_by_strain', label=['strain1', 'strain2'], do_show=do_show, do_save=do_save, fig_path='results/sim2_prevalence_by_strain') + sim.plot_result('incidence_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim2_incidence_by_strain') + sim.plot_result('prevalence_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim2_prevalence_by_strain') return sim @@ -49,7 +49,7 @@ def test_importstrain(do_plot=False, do_show=True, do_save=False, fig_path=None) if __name__ == '__main__': sc.tic() - sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + # sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) sim2 = test_importstrain(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) sc.toc() From 6f2995e39864533cecd327e8ba2c4c8b538f2724 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 8 Feb 2021 10:38:57 -0500 Subject: [PATCH 029/569] fixed intervention. made immunity strain-specific. still need to add in cross-immunity --- covasim/interventions.py | 51 +++++++++++++++++++-------------- covasim/parameters.py | 3 +- covasim/people.py | 4 +-- covasim/sim.py | 10 ++----- tests/devtests/test_variants.py | 16 ++++++++--- 5 files changed, 48 insertions(+), 36 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 38ca3a90f..84762b0da 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1127,28 +1127,30 @@ class import_strain(Intervention): Introduce a new variant(s) to the population through an importation at a given time point. Args: - day (int): the day to apply the interventions + days (int or list of ints): the day(s) to apply the interventions n_imports (list of ints): the number of imports of strain(s) beta (list of floats): per contact transmission of strain(s) - rel_sus (list of floats): relative change in susceptibility of strain(s); 0 = perfect, 1 = no effect - rel_trans (list of floats): relative change in transmission of strain(s); 0 = perfect, 1 = no effect + init_immunity (list of floats): initial immunity against strain(s) once recovered; 1 = perfect, 0 = no immunity + half_life (list of floats): determines decay rate of immunity against strain(s); If half_life is None immunity is constant kwargs (dict): passed to Intervention() **Examples**:: - interv = cv.import_strain(days=50, beta=0.3, rel_sus=0.5, rel_trans=0.1) + interv = cv.import_strain(day=50, beta=0.3, init_immunity=1, half_life=180) + interv = cv.import_strain(day=50, beta=[0.3, 0.5], init_immunity=1, half_life=180) ''' - def __init__(self, day=None, n_imports=None, beta=None, rel_sus=None, rel_trans=None, **kwargs): + def __init__(self, days=None, n_imports=None, beta=None, init_immunity=None, half_life=None, **kwargs): # Do standard initialization super().__init__(**kwargs) # Initialize the Intervention object self._store_args() # Store the input arguments so the intervention can be recreated # Handle inputs + days = sc.promotetolist(days) n_imports = sc.promotetolist(n_imports) beta = sc.promotetolist(beta) - rel_sus = sc.promotetolist(rel_sus) - rel_trans = sc.promotetolist(rel_trans) + init_immunity = sc.promotetolist(init_immunity) + half_life = sc.promotetolist(half_life) len_imports = len(n_imports) len_betas = len(beta) if len_imports != len_betas: @@ -1158,28 +1160,33 @@ def __init__(self, day=None, n_imports=None, beta=None, rel_sus=None, rel_trans= self.new_strains = len_imports # Number of new strains being introduced # Set attributes - self.day = day + self.days = days self.n_imports = n_imports self.beta = beta - self.rel_sus = rel_sus - self.rel_trans = rel_trans - + self.init_immunity = init_immunity + self.half_life = half_life return + def initialize(self, sim): + self.days = process_days(sim, self.days) + self.max_strains = sim['max_strains'] + self.initialized = True + def apply(self, sim): - t = sim.t - if t == self.day: + for strain in find_day(self.days, sim.t): # Check number of strains prev_strains = sim['n_strains'] - if prev_strains + self.new_strains > sim['max_strains']: - errormsg = f"Number of existing strains ({sim['n_strains']}) plus number of new strains ({self.new_strains}) exceeds the maximal allowable ({sim['max_strains']}. Increase pars['max_strains'])." + if prev_strains + 1 > self.max_strains: + errormsg = f"Number of existing strains ({sim['n_strains']}) plus new strain exceeds the maximal allowable ({sim['max_strains']}. Increase pars['max_strains'])." raise ValueError(errormsg) - for strain in range(self.new_strains): - sim['beta'].append(self.beta[strain]) - sim.people['rel_sus'][:, prev_strains+strain] = self.rel_sus[strain] - sim.people['rel_trans'][:, prev_strains+strain] = self.rel_trans[strain] - importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely - sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains+strain) - sim['n_strains'] += self.new_strains \ No newline at end of file + sim['beta'].append(self.beta[strain]) + sim['immunity'][prev_strains + strain]['init_immunity'] = self.init_immunity[strain] + sim['immunity'][prev_strains + strain]['half_life'] = self.half_life[strain] + importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[ + strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely + sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains + strain) + sim['n_strains'] += 1 + + return \ No newline at end of file diff --git a/covasim/parameters.py b/covasim/parameters.py index bc11e7284..0a0075876 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -56,7 +56,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['beta_dist'] = dict(dist='neg_binomial', par1=1.0, par2=0.45, step=0.01) # Distribution to draw individual level transmissibility; dispersion from https://www.researchsquare.com/article/rs-29548/v1 pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 pars['n_strains'] = 1 # The number of strains currently circulating in the population - pars['max_strains'] = 10 # For allocating memory with numpy arrays + pars['max_strains'] = 30 # For allocating memory with numpy arrays # pars['immunity'] = dict(init_immunity=1., half_life=180) # Protection from immunity. If half_life is None immunity is constant; if it's a number it decays exponentially. TODO: improve this with data, e.g. https://www.nejm.org/doi/full/10.1056/nejmc2025179 pars['immunity'] = [dict(init_immunity=1., half_life=180) for _ in range(pars['max_strains'])] @@ -106,6 +106,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses + # If version is specified, load old parameters if version is not None: version_pars = cvm.get_version_pars(version, verbose=pars['verbose']) diff --git a/covasim/people.py b/covasim/people.py index f07b8a4a4..4e5ce96af 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -145,8 +145,8 @@ def find_cutoff(age_cutoffs, age): self.severe_prob[:] = progs['severe_probs'][inds]*progs['comorbidities'][inds] # Severe disease probability is modified by comorbidities self.crit_prob[:] = progs['crit_probs'][inds] # Probability of developing critical disease self.death_prob[:] = progs['death_probs'][inds] # Probability of death - for strain in range(self.pars['n_strains']): - #TODO -- make this strain specific in inputs + for strain in range(self.pars['max_strains']): + #TODO -- make this strain specific in inputs if needed? self.rel_sus[:, strain] = progs['sus_ORs'][inds] # Default susceptibilities self.rel_trans[:, strain] = progs['trans_ORs'][inds]*cvu.sample(**self.pars['beta_dist'], size=len(inds)) # Default transmissibilities, with viral load drawn from a distribution diff --git a/covasim/sim.py b/covasim/sim.py index 03e03871d..e3155eb8b 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -512,13 +512,6 @@ def step(self): # Extract additional parameters asymp_factor = cvd.default_float(self['asymp_factor']) - init_immunity = cvd.default_float(self['immunity'][strain]['init_immunity']) - half_life = cvd.default_float(self['immunity'][strain]['half_life']) - decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. - # immunity_factors = np.full(self['pop_size'], 1., dtype=cvd.default_float) # TODO: initialise this somewhere else - - # Compute immunity factors - # immunity_factors = cvu.compute_immunity(immunity_factors, t, date_rec, init_immunity, decay_rate) # Iterate through n_strains to calculate infections for strain in range(self['n_strains']): @@ -526,6 +519,9 @@ def step(self): # Compute the probability of transmission beta = cvd.default_float(self['beta'][strain]) # Compute immunity factors + init_immunity = cvd.default_float(self['immunity'][strain]['init_immunity']) + half_life = cvd.default_float(self['immunity'][strain]['half_life']) + decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. immunity_factors = cvu.compute_immunity(people.immunity_factors[:,strain], t, date_rec, init_immunity, decay_rate) for lkey, layer in contacts.items(): diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 8c4102d23..abd0a065d 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -11,8 +11,16 @@ def test_multistrains(do_plot=False, do_show=True, do_save=False, fig_path=None) sc.heading('Setting up...') - pars = {'n_strains': 2, - 'beta': [0.016, 0.035]} + immunity = [ + {'init_immunity':1., 'half_life':180}, + {'init_immunity':1., 'half_life':50} + ] + + pars = { + 'n_strains': 2, + 'beta': [0.016, 0.035], + 'immunity': immunity + } sim = cv.Sim(pars=pars) sim.run() @@ -31,8 +39,8 @@ def test_importstrain(do_plot=False, do_show=True, do_save=False, fig_path=None) sc.heading('Setting up...') # Run sim with several strains initially, then introduce a new strain that's more transmissible on day 10 - pars = {'n_strains': 10, 'beta': [0.016] * 10, 'max_strains': 11} # Checking here that increasing max_strains works - imports = cv.import_strain(day=30, n_imports=20, beta=0.05, rel_trans=1, rel_sus=1) + pars = {'n_strains': 3, 'beta': [0.016] * 3} # Checking here that increasing max_strains works + imports = cv.import_strain(days=30, n_imports=50, beta=0.5, init_immunity=1, half_life=50) sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') sim.run() From cb355084bd2b89fcd00c4000d04a0622008fc787 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 8 Feb 2021 20:51:34 -0500 Subject: [PATCH 030/569] swapped row and columns for 2d arrays, a few other improvements --- covasim/base.py | 9 ++++++--- covasim/people.py | 34 +++++++++++++++++++-------------- covasim/sim.py | 23 +++++++++++++--------- tests/devtests/test_variants.py | 19 ++++++++++++------ 4 files changed, 53 insertions(+), 32 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 60b3f6ad3..3f44e3c68 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -143,7 +143,7 @@ class Result(object): print(r1.values) ''' - def __init__(self, name=None, npts=None, scale=True, color=None, max_strains=10): + def __init__(self, name=None, npts=None, scale=True, color=None, max_strains=30): self.name = name # Name of this result self.scale = scale # Whether or not to scale the result by the scale factor if color is None: @@ -152,7 +152,7 @@ def __init__(self, name=None, npts=None, scale=True, color=None, max_strains=10) if npts is None: npts = 0 if 'by_strain' in self.name or 'by strain' in self.name: - self.values = np.full((npts, max_strains), 0, dtype=cvd.result_float) + self.values = np.full((max_strains, npts), 0, dtype=cvd.result_float, order='F') else: self.values = np.array(np.zeros(int(npts)), dtype=cvd.result_float) self.low = None @@ -916,7 +916,7 @@ def count(self, key): def count_by_strain(self, key, strain): ''' Count the number of people for a given key ''' - return (self[key][:,strain]>0).sum() + return (self[key][strain,:]>0).sum() def count_not(self, key): @@ -988,6 +988,9 @@ def validate(self, die=True, verbose=False): expected_len = len(self) for key in self.keys(): actual_len = len(self[key]) + # check if it's 2d + if self[key].ndim > 1: + actual_len = len(self[key][0]) if actual_len != expected_len: if die: errormsg = f'Length of key "{key}" did not match population size ({actual_len} vs. {expected_len})' diff --git a/covasim/people.py b/covasim/people.py index 4e5ce96af..308cedd86 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -54,14 +54,23 @@ def __init__(self, pars, strict=True, **kwargs): self.init_contacts() # Initialize the contacts self.infection_log = [] # Record of infections - keys for ['source','target','date','layer'] + self.strain_specific_pars = [ + 'rel_trans', + 'rel_sus', + 'time_of_last_inf', + 'immunity_factors', + 'exposed_by_strain', + 'infectious_by_strain', + ] + # Set person properties -- all floats except for UID for key in self.meta.person: if key == 'uid': self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif key == 'rel_trans' or key == 'rel_sus' or key == 'time_of_last_inf': - self[key] = np.full((self.pop_size, self.pars['max_strains']), np.nan, dtype=cvd.default_float) + self[key] = np.full((self.pars['max_strains'], self.pop_size), np.nan, dtype=cvd.default_float, order='F') elif key == 'immunity_factors': # everyone starts out with no immunity to either strain. - self[key] = np.full((self.pop_size, self.pars['max_strains']), 0, dtype=cvd.default_float) + self[key] = np.full((self.pars['max_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -72,7 +81,7 @@ def __init__(self, pars, strict=True, **kwargs): elif key == 'exposed_strain' or key == 'infectious_strain': self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) elif key == 'infectious_by_strain' or key == 'exposed_by_strain': - self[key] = np.full((self.pop_size, self.pars['max_strains']), False, dtype=bool) + self[key] = np.full((self.pars['max_strains'], self.pop_size), False, dtype=bool, order='F') else: self[key] = np.full(self.pop_size, False, dtype=bool) @@ -147,8 +156,8 @@ def find_cutoff(age_cutoffs, age): self.death_prob[:] = progs['death_probs'][inds] # Probability of death for strain in range(self.pars['max_strains']): #TODO -- make this strain specific in inputs if needed? - self.rel_sus[:, strain] = progs['sus_ORs'][inds] # Default susceptibilities - self.rel_trans[:, strain] = progs['trans_ORs'][inds]*cvu.sample(**self.pars['beta_dist'], size=len(inds)) # Default transmissibilities, with viral load drawn from a distribution + self.rel_sus[strain, :] = progs['sus_ORs'][inds] # Default susceptibilities + self.rel_trans[strain, :] = progs['trans_ORs'][inds]*cvu.sample(**self.pars['beta_dist'], size=len(inds)) # Default transmissibilities, with viral load drawn from a distribution return @@ -230,13 +239,10 @@ def check_infectious(self): inds = self.check_inds(self.infectious, self.date_infectious, filter_inds=self.is_exp) self.infectious[inds] = True self.infectious_strain[inds] = self.exposed_strain[inds] - for strain in range(self.pars['n_strains']): - inf_strain = self.infectious_strain == strain - inf_strain = inf_strain[inf_strain == True] - # inds_strain = [index for index, value in enumerate(self.infectious_strain) if value == strain] - # self.infectious_by_strain[inds_strain, strain] = True - self.flows['new_infectious_by_strain'][strain] += len(inf_strain) - + strains, counts = np.unique(self.infectious_strain[~np.isnan(self.infectious_strain)], return_counts=True) + if len(strains)>0: + for strain, _ in enumerate(strains): + self.flows['new_infections_by_strain'][strain] += counts[strain] return len(inds) @@ -391,7 +397,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.susceptible[inds] = False self.exposed[inds] = True self.exposed_strain[inds] = strain - self.exposed_by_strain[inds, strain] = True + self.exposed_by_strain[strain, inds] = True self.date_exposed[inds] = self.t self.flows['new_infections'] += len(inds) self.flows['new_infections_by_strain'][strain] += len(inds) @@ -401,7 +407,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.infection_log.append(dict(source=source[i] if source is not None else None, target=target, date=self.t, layer=layer)) # Record time of infection - self.time_of_last_inf[inds, strain] = self.t + self.time_of_last_inf[strain, inds] = self.t # Calculate how long before this person can infect other people self.dur_exp2inf[inds] = cvu.sample(**durpars['exp2inf'], size=n_infections) diff --git a/covasim/sim.py b/covasim/sim.py index e3155eb8b..adf12525e 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -522,7 +522,7 @@ def step(self): init_immunity = cvd.default_float(self['immunity'][strain]['init_immunity']) half_life = cvd.default_float(self['immunity'][strain]['half_life']) decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. - immunity_factors = cvu.compute_immunity(people.immunity_factors[:,strain], t, date_rec, init_immunity, decay_rate) + immunity_factors = cvu.compute_immunity(people.immunity_factors[strain, :], t, date_rec, init_immunity, decay_rate) for lkey, layer in contacts.items(): p1 = layer['p1'] @@ -530,8 +530,8 @@ def step(self): betas = layer['beta'] # Compute relative transmission and susceptibility - rel_trans = people.rel_trans[:,strain] - rel_sus = people.rel_sus[:,strain] + rel_trans = people.rel_trans[strain, :] + rel_sus = people.rel_sus[strain, :] inf = people.infectious inf_by_this_strain = sc.dcp(inf) @@ -559,7 +559,7 @@ def step(self): for key in cvd.result_stocks.keys(): if 'by_strain' in key or 'by strain' in key: for strain in range(self['n_strains']): - self.results[f'n_{key}'][t][strain] = people.count_by_strain(key, strain) + self.results[f'n_{key}'][strain][t] = people.count_by_strain(key, strain) else: self.results[f'n_{key}'][t] = people.count(key) @@ -567,7 +567,7 @@ def step(self): for key,count in people.flows.items(): if 'by_strain' in key or 'by strain' in key: for strain in range(self['n_strains']): - self.results[key][t][strain] += count[strain] + self.results[key][strain][t] += count[strain] else: self.results[key][t] += count @@ -682,9 +682,10 @@ def finalize(self, verbose=None, restore_pars=True): for reskey in self.result_keys(): if 'by_strain' in reskey: # resize results to include only active strains - self.results[reskey].values = self.results[reskey].values[:, :self['n_strains']] + self.results[reskey].values = self.results[reskey].values[:self['n_strains'], :] if self.results[reskey].scale: # Scale the result dynamically if 'by_strain' in reskey: + self.results[reskey].values = np.rot90(self.results[reskey].values) self.results[reskey].values = np.einsum('ij,i->ij',self.results[reskey].values,self.rescale_vec) else: self.results[reskey].values *= self.rescale_vec @@ -742,8 +743,8 @@ def compute_prev_inci(self): self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence - self.results['incidence_by_strain'][:] = np.einsum('ij,i->ij',res['new_infections_by_strain'][:],1/res['n_susceptible'][:]) # Calculate the incidence - self.results['prevalence_by_strain'][:] = np.einsum('ij,i->ij',res['n_exposed_by_strain'][:], 1/res['n_alive'][:]) # Calculate the prevalence + self.results['incidence_by_strain'][:] = np.rot90(np.einsum('ij,i->ij',res['new_infections_by_strain'][:],1/res['n_susceptible'][:])) # Calculate the incidence + self.results['prevalence_by_strain'][:] = np.rot90(np.einsum('ij,i->ij',res['n_exposed_by_strain'][:], 1/res['n_alive'][:])) # Calculate the prevalence return @@ -947,7 +948,11 @@ def compute_summary(self, full=None, t=None, update=True, output=False, require_ summary = sc.objdict() for key in self.result_keys(): - summary[key] = self.results[key][t] + if len(self.results[key]) < t: + self.results[key].values = np.rot90(self.results[key].values) + summary[key] = self.results[key][t] + else: + summary[key] = self.results[key][t] # Update the stored state if update: diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index abd0a065d..81a9f17e8 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -39,15 +39,22 @@ def test_importstrain(do_plot=False, do_show=True, do_save=False, fig_path=None) sc.heading('Setting up...') # Run sim with several strains initially, then introduce a new strain that's more transmissible on day 10 - pars = {'n_strains': 3, 'beta': [0.016] * 3} # Checking here that increasing max_strains works + pars = {'n_strains': 3, 'beta': [0.016] * 3} imports = cv.import_strain(days=30, n_imports=50, beta=0.5, init_immunity=1, half_life=50) sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') sim.run() + labels = [ + 'strain1', + 'strain2', + 'strain3', + 'strain4' + ] + if do_plot: - sim.plot_result('cum_infections_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim2_cum_infections_by_strain') - sim.plot_result('incidence_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim2_incidence_by_strain') - sim.plot_result('prevalence_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim2_prevalence_by_strain') + sim.plot_result('cum_infections_by_strain', label=labels, do_show=do_show, do_save=do_save, fig_path='results/sim2_cum_infections_by_strain') + sim.plot_result('incidence_by_strain', label=labels, do_show=do_show, do_save=do_save, fig_path='results/sim2_incidence_by_strain') + sim.plot_result('prevalence_by_strain', label=labels, do_show=do_show, do_save=do_save, fig_path='results/sim2_prevalence_by_strain') return sim @@ -57,8 +64,8 @@ def test_importstrain(do_plot=False, do_show=True, do_save=False, fig_path=None) if __name__ == '__main__': sc.tic() - # sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) - sim2 = test_importstrain(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + # sim2 = test_importstrain(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) sc.toc() From 6704223098e4bcde5b8660da2c984a87434135d2 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 9 Feb 2021 13:06:18 +0100 Subject: [PATCH 031/569] recovery by strain --- covasim/defaults.py | 2 ++ covasim/people.py | 3 ++- covasim/sim.py | 26 +++++++++++++++----------- covasim/utils.py | 3 ++- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 41f3b792d..de906e76f 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -62,6 +62,8 @@ class PeopleMeta(sc.prettyobj): 'exposed_by_strain', 'infectious_strain', 'infectious_by_strain', + 'recovered_strain', + 'recovered_by_strain', 'symptomatic', 'severe', 'critical', diff --git a/covasim/people.py b/covasim/people.py index 308cedd86..4014508f7 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -78,7 +78,7 @@ def __init__(self, pars, strict=True, **kwargs): for key in self.meta.states: if key == 'susceptible': self[key] = np.full(self.pop_size, True, dtype=bool) - elif key == 'exposed_strain' or key == 'infectious_strain': + elif key in ['exposed_strain','infectious_strain','recovered_strain']: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) elif key == 'infectious_by_strain' or key == 'exposed_by_strain': self[key] = np.full((self.pars['max_strains'], self.pop_size), False, dtype=bool, order='F') @@ -276,6 +276,7 @@ def check_recovery(self): self.severe[inds] = False self.critical[inds] = False self.recovered[inds] = True + self.recovered_strain[inds] = self.infectious_strain[inds] # TODO: check that this works self.infectious_strain[inds] = np.nan return len(inds) diff --git a/covasim/sim.py b/covasim/sim.py index adf12525e..e477bc6d3 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -510,19 +510,32 @@ def step(self): date_dead = people.date_dead viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) - # Extract additional parameters + # Shorten additional useful parameters and indicators that aren't by strain asymp_factor = cvd.default_float(self['asymp_factor']) + sus = people.susceptible + inf = people.infectious + rec = people.recovered # Both susceptible and recovered people can get reinfected + symp = people.symptomatic + diag = people.diagnosed + quar = people.quarantined # Iterate through n_strains to calculate infections for strain in range(self['n_strains']): # Compute the probability of transmission beta = cvd.default_float(self['beta'][strain]) + + # Define indices for this strain + inf_by_this_strain = sc.dcp(inf) + inf_by_this_strain[cvu.false(people.infectious_strain == strain)] = False + date_rec_from_this_strain = sc.dcp(date_rec) + date_rec_from_this_strain[cvu.false(people.recovered_strain == strain)] = np.nan + # Compute immunity factors init_immunity = cvd.default_float(self['immunity'][strain]['init_immunity']) half_life = cvd.default_float(self['immunity'][strain]['half_life']) decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. - immunity_factors = cvu.compute_immunity(people.immunity_factors[strain, :], t, date_rec, init_immunity, decay_rate) + immunity_factors = cvu.compute_immunity(people.immunity_factors[strain, :], t, date_rec_from_this_strain, init_immunity, decay_rate) for lkey, layer in contacts.items(): p1 = layer['p1'] @@ -533,15 +546,6 @@ def step(self): rel_trans = people.rel_trans[strain, :] rel_sus = people.rel_sus[strain, :] - inf = people.infectious - inf_by_this_strain = sc.dcp(inf) - inf_by_this_strain[cvu.false(people.infectious_strain==strain)] = False - - sus = people.susceptible - rec = people.recovered # Both susceptible and recovered people can get reinfected - symp = people.symptomatic - diag = people.diagnosed - quar = people.quarantined iso_factor = cvd.default_float(self['iso_factor'][lkey]) quar_factor = cvd.default_float(self['quar_factor'][lkey]) beta_layer = cvd.default_float(self['beta_layer'][lkey]) diff --git a/covasim/utils.py b/covasim/utils.py index d9ebda4bc..7a4bc0275 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -69,7 +69,7 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -@nb.njit( (nbfloat[:], nbint, nbfloat[:], nbfloat, nbfloat), cache=True, parallel=parallel) +#@nb.njit( (nbfloat[:], nbint, nbfloat[:], nbfloat, nbfloat), cache=True, parallel=parallel) def compute_immunity(immunity_factors, t, date_rec, init_immunity, decay_rate): # pragma: no cover ''' Calculate immunity factors for time t @@ -83,6 +83,7 @@ def compute_immunity(immunity_factors, t, date_rec, init_immunity, decay_r Returns: immunity_factors (float[]): immunity factors ''' + time_since_rec = t - date_rec # Time since recovery inds = (time_since_rec>0).nonzero()[0] # Extract people who have recovered immunity_factors[inds] = init_immunity * np.exp(-decay_rate * time_since_rec[inds]) # Calculate their immunity factors From 91c0b43d7b07c0adc6bc4e40a73899ab9e2392bb Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 9 Feb 2021 13:15:54 +0100 Subject: [PATCH 032/569] needs rethinking --- covasim/sim.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/covasim/sim.py b/covasim/sim.py index e477bc6d3..ecb9b49b7 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -528,6 +528,13 @@ def step(self): # Define indices for this strain inf_by_this_strain = sc.dcp(inf) inf_by_this_strain[cvu.false(people.infectious_strain == strain)] = False + + if t>15: + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() + date_rec_from_this_strain = sc.dcp(date_rec) date_rec_from_this_strain[cvu.false(people.recovered_strain == strain)] = np.nan From 523fa419591502708f81d8826a87ed431e0314f1 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 9 Feb 2021 13:23:37 +0100 Subject: [PATCH 033/569] clean up --- covasim/sim.py | 7 ------- covasim/utils.py | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index ecb9b49b7..e477bc6d3 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -528,13 +528,6 @@ def step(self): # Define indices for this strain inf_by_this_strain = sc.dcp(inf) inf_by_this_strain[cvu.false(people.infectious_strain == strain)] = False - - if t>15: - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() - date_rec_from_this_strain = sc.dcp(date_rec) date_rec_from_this_strain[cvu.false(people.recovered_strain == strain)] = np.nan diff --git a/covasim/utils.py b/covasim/utils.py index 7a4bc0275..9adc2b1ca 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -69,7 +69,7 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -#@nb.njit( (nbfloat[:], nbint, nbfloat[:], nbfloat, nbfloat), cache=True, parallel=parallel) +@nb.njit( (nbfloat[:], nbint, nbfloat[:], nbfloat, nbfloat), cache=True, parallel=parallel) def compute_immunity(immunity_factors, t, date_rec, init_immunity, decay_rate): # pragma: no cover ''' Calculate immunity factors for time t From 8256314e0803aa24efa254dbb5f812055290bed7 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 9 Feb 2021 14:31:17 +0100 Subject: [PATCH 034/569] add basic cross-immunity --- covasim/parameters.py | 2 +- covasim/sim.py | 22 ++++++++++++++-------- covasim/utils.py | 12 +++++------- tests/devtests/test_variants.py | 4 ++-- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 0a0075876..0bd0dc957 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -58,7 +58,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['n_strains'] = 1 # The number of strains currently circulating in the population pars['max_strains'] = 30 # For allocating memory with numpy arrays # pars['immunity'] = dict(init_immunity=1., half_life=180) # Protection from immunity. If half_life is None immunity is constant; if it's a number it decays exponentially. TODO: improve this with data, e.g. https://www.nejm.org/doi/full/10.1056/nejmc2025179 - pars['immunity'] = [dict(init_immunity=1., half_life=180) for _ in range(pars['max_strains'])] + pars['immunity'] = [dict(init_immunity=1., half_life=180, cross_factor=0.5) for _ in range(pars['max_strains'])] # Efficacy of protection measures pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 diff --git a/covasim/sim.py b/covasim/sim.py index e477bc6d3..ffb85fc81 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -525,17 +525,23 @@ def step(self): # Compute the probability of transmission beta = cvd.default_float(self['beta'][strain]) - # Define indices for this strain - inf_by_this_strain = sc.dcp(inf) - inf_by_this_strain[cvu.false(people.infectious_strain == strain)] = False - date_rec_from_this_strain = sc.dcp(date_rec) - date_rec_from_this_strain[cvu.false(people.recovered_strain == strain)] = np.nan - # Compute immunity factors + immune = people.recovered_strain == strain # Whether people have some immunity to this strain from a prior infection with this strain + cross_immune = (~np.isnan(people.recovered_strain)) & (people.recovered_strain != strain) # Whether people with some immunity to this strain from a prior infection with another strain + immune_time = t - date_rec[immune] # Time since recovery from this strain + cross_immune_time = t - date_rec[cross_immune] # Time since recovery from the last strain a person was infected by + immune_inds = cvd.default_int(cvu.true(immune)) # People with some immunity to this strain from a prior infection with this strain + cross_immune_inds = cvd.default_int(cvu.true(cross_immune_inds)) # People with some immunity to this strain from a prior infection with another strain init_immunity = cvd.default_float(self['immunity'][strain]['init_immunity']) - half_life = cvd.default_float(self['immunity'][strain]['half_life']) + half_life = self['immunity'][strain]['half_life'] decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. - immunity_factors = cvu.compute_immunity(people.immunity_factors[strain, :], t, date_rec_from_this_strain, init_immunity, decay_rate) + decay_rate = cvd.default_float(decay_rate) + cross_factor = cvd.default_float(self['immunity'][strain]['cross_factor']) + immunity_factors = cvu.compute_immunity(people.immunity_factors[strain, :], immune_time, cross_immune_time, immune_inds, cross_immune_inds, init_immunity, decay_rate, cross_factor) + + # Define indices for this strain + inf_by_this_strain = sc.dcp(inf) + inf_by_this_strain[cvu.false(people.infectious_strain == strain)] = False for lkey, layer in contacts.items(): p1 = layer['p1'] diff --git a/covasim/utils.py b/covasim/utils.py index 9adc2b1ca..7dee3106c 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -69,8 +69,8 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -@nb.njit( (nbfloat[:], nbint, nbfloat[:], nbfloat, nbfloat), cache=True, parallel=parallel) -def compute_immunity(immunity_factors, t, date_rec, init_immunity, decay_rate): # pragma: no cover +@nb.njit( (nbfloat[:], nbfloat[:], nbfloat[:], nbint[:], nbint[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) +def compute_immunity(immunity_factors, immune_time, cross_immune_time, immune_inds, cross_immune_inds, init_immunity, decay_rate, cross_factor): # pragma: no cover ''' Calculate immunity factors for time t @@ -83,10 +83,8 @@ def compute_immunity(immunity_factors, t, date_rec, init_immunity, decay_r Returns: immunity_factors (float[]): immunity factors ''' - - time_since_rec = t - date_rec # Time since recovery - inds = (time_since_rec>0).nonzero()[0] # Extract people who have recovered - immunity_factors[inds] = init_immunity * np.exp(-decay_rate * time_since_rec[inds]) # Calculate their immunity factors + immunity_factors[immune_inds] = init_immunity * np.exp(-decay_rate * immune_time) # Calculate immunity factors + immunity_factors[cross_immune_inds] = (init_immunity * np.exp(-decay_rate * cross_immune_time)) * cross_factor # Calculate cross-immunity factors return immunity_factors @@ -561,7 +559,7 @@ def ifalse(arr, inds): def idefined(arr, inds): ''' - Returns the indices that are true in the array -- name is short for indices[defined] + Returns the indices that are defined in the array -- name is short for indices[defined] Args: arr (array): any array, used as a filter diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 81a9f17e8..521fb02a9 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -12,8 +12,8 @@ def test_multistrains(do_plot=False, do_show=True, do_save=False, fig_path=None) sc.heading('Setting up...') immunity = [ - {'init_immunity':1., 'half_life':180}, - {'init_immunity':1., 'half_life':50} + {'init_immunity':1., 'half_life':180, 'cross_factor':0.5}, + {'init_immunity':1., 'half_life':50, 'cross_factor':0.5} ] pars = { From ec0c1c3e55ba23b6695164c8ecb2dd717515d106 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 9 Feb 2021 14:35:40 +0100 Subject: [PATCH 035/569] fix indexing --- covasim/sim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/sim.py b/covasim/sim.py index ffb85fc81..1acad3b2e 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -531,7 +531,7 @@ def step(self): immune_time = t - date_rec[immune] # Time since recovery from this strain cross_immune_time = t - date_rec[cross_immune] # Time since recovery from the last strain a person was infected by immune_inds = cvd.default_int(cvu.true(immune)) # People with some immunity to this strain from a prior infection with this strain - cross_immune_inds = cvd.default_int(cvu.true(cross_immune_inds)) # People with some immunity to this strain from a prior infection with another strain + cross_immune_inds = cvd.default_int(cvu.true(cross_immune)) # People with some immunity to this strain from a prior infection with another strain init_immunity = cvd.default_float(self['immunity'][strain]['init_immunity']) half_life = self['immunity'][strain]['half_life'] decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. From 7073351362c59cf1a7fdebd805da916c7677c18b Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 9 Feb 2021 14:46:09 +0100 Subject: [PATCH 036/569] testing --- covasim/sim.py | 4 ++-- tests/devtests/test_variants.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 1acad3b2e..3e786f2ac 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -528,8 +528,8 @@ def step(self): # Compute immunity factors immune = people.recovered_strain == strain # Whether people have some immunity to this strain from a prior infection with this strain cross_immune = (~np.isnan(people.recovered_strain)) & (people.recovered_strain != strain) # Whether people with some immunity to this strain from a prior infection with another strain - immune_time = t - date_rec[immune] # Time since recovery from this strain - cross_immune_time = t - date_rec[cross_immune] # Time since recovery from the last strain a person was infected by + immune_time = t - date_rec[immune] # Time since recovery for people who were last infected by this strain + cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by another strain immune_inds = cvd.default_int(cvu.true(immune)) # People with some immunity to this strain from a prior infection with this strain cross_immune_inds = cvd.default_int(cvu.true(cross_immune)) # People with some immunity to this strain from a prior infection with another strain init_immunity = cvd.default_float(self['immunity'][strain]['init_immunity']) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 521fb02a9..d171a53e5 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -13,7 +13,7 @@ def test_multistrains(do_plot=False, do_show=True, do_save=False, fig_path=None) immunity = [ {'init_immunity':1., 'half_life':180, 'cross_factor':0.5}, - {'init_immunity':1., 'half_life':50, 'cross_factor':0.5} + {'init_immunity':1., 'half_life':50, 'cross_factor':0.9} ] pars = { From 6e9b2bc0298fb521b9ec547889fd064f892fdd51 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 9 Feb 2021 15:39:47 +0100 Subject: [PATCH 037/569] remove recovered by strain --- covasim/defaults.py | 2 +- covasim/sim.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index de906e76f..17e5b93a2 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -63,7 +63,7 @@ class PeopleMeta(sc.prettyobj): 'infectious_strain', 'infectious_by_strain', 'recovered_strain', - 'recovered_by_strain', + #'recovered_by_strain', 'symptomatic', 'severe', 'critical', diff --git a/covasim/sim.py b/covasim/sim.py index 3e786f2ac..64405c026 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -527,7 +527,7 @@ def step(self): # Compute immunity factors immune = people.recovered_strain == strain # Whether people have some immunity to this strain from a prior infection with this strain - cross_immune = (~np.isnan(people.recovered_strain)) & (people.recovered_strain != strain) # Whether people with some immunity to this strain from a prior infection with another strain + cross_immune = (~np.isnan(people.recovered_strain)) & (people.recovered_strain != strain) # Whether people have some immunity to this strain from a prior infection with another strain immune_time = t - date_rec[immune] # Time since recovery for people who were last infected by this strain cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by another strain immune_inds = cvd.default_int(cvu.true(immune)) # People with some immunity to this strain from a prior infection with this strain From 8a4ce62411a628d704b60551e23d632bfb031f7c Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 9 Feb 2021 11:27:11 -0500 Subject: [PATCH 038/569] added plotting to test script --- covasim/interventions.py | 13 +++-- covasim/sim.py | 3 +- tests/devtests/test_variants.py | 91 ++++++++++++++++++++++++++------- 3 files changed, 85 insertions(+), 22 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 84762b0da..7f10f00e1 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1140,7 +1140,7 @@ class import_strain(Intervention): interv = cv.import_strain(day=50, beta=[0.3, 0.5], init_immunity=1, half_life=180) ''' - def __init__(self, days=None, n_imports=None, beta=None, init_immunity=None, half_life=None, **kwargs): + def __init__(self, days=None, n_imports=None, beta=None, init_immunity=None, half_life=None, cross_factor=None, **kwargs): # Do standard initialization super().__init__(**kwargs) # Initialize the Intervention object self._store_args() # Store the input arguments so the intervention can be recreated @@ -1151,6 +1151,7 @@ def __init__(self, days=None, n_imports=None, beta=None, init_immunity=None, hal beta = sc.promotetolist(beta) init_immunity = sc.promotetolist(init_immunity) half_life = sc.promotetolist(half_life) + cross_factor = sc.promotetolist(cross_factor) len_imports = len(n_imports) len_betas = len(beta) if len_imports != len_betas: @@ -1165,6 +1166,7 @@ def __init__(self, days=None, n_imports=None, beta=None, init_immunity=None, hal self.beta = beta self.init_immunity = init_immunity self.half_life = half_life + self.cross_factor = cross_factor return def initialize(self, sim): @@ -1182,8 +1184,13 @@ def apply(self, sim): raise ValueError(errormsg) sim['beta'].append(self.beta[strain]) - sim['immunity'][prev_strains + strain]['init_immunity'] = self.init_immunity[strain] - sim['immunity'][prev_strains + strain]['half_life'] = self.half_life[strain] + sim['immunity'].append( + { + 'init_immunity': self.init_immunity[strain], + 'half_life': self.half_life[strain], + 'cross_factor': self.cross_factor[strain] + } + ) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[ strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains + strain) diff --git a/covasim/sim.py b/covasim/sim.py index 64405c026..07d1888ad 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -387,8 +387,9 @@ def init_people(self, save_pop=False, load_pop=False, popfile=None, reset=False, self.people.initialize() # Fully initialize the people # Create the seed infections + pop_infected_per_strain = cvd.default_int(self['pop_infected']/self['n_strains']) for strain in range(self['n_strains']): - inds = cvu.choose(self['pop_size'], self['pop_infected']) + inds = cvu.choose(self['pop_size'], pop_infected_per_strain) self.people.infect(inds=inds, layer='seed_infection', strain=strain) return diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index d171a53e5..649a3bccd 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -1,5 +1,7 @@ import covasim as cv import sciris as sc +import matplotlib +import matplotlib.pyplot as plt do_plot = 1 do_show = 1 @@ -11,6 +13,11 @@ def test_multistrains(do_plot=False, do_show=True, do_save=False, fig_path=None) sc.heading('Setting up...') + strain_labels = [ + 'Strain 1: beta 0.016', + 'Strain 2: beta 0.035' + ] + immunity = [ {'init_immunity':1., 'half_life':180, 'cross_factor':0.5}, {'init_immunity':1., 'half_life':50, 'cross_factor':0.9} @@ -26,38 +33,85 @@ def test_multistrains(do_plot=False, do_show=True, do_save=False, fig_path=None) sim.run() if do_plot: - sim.plot_result('cum_infections_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim1_cum_infections_by_strain') - sim.plot_result('incidence_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim1_incidence_by_strain') - sim.plot_result('prevalence_by_strain', do_show=do_show, do_save=do_save, fig_path='results/sim1_prevalence_by_strain') - + plot_results(sim, key='incidence_by_strain', title='Multiple strains', labels=strain_labels) return sim -def test_importstrain(do_plot=False, do_show=True, do_save=False, fig_path=None): - sc.heading('Test introducing a new strain partway through a sim') +def test_importstrain_withcrossimmunity(do_plot=False, do_show=True, do_save=False, fig_path=None): + sc.heading('Test introducing a new strain partway through a sim with full cross immunity') sc.heading('Setting up...') + strain_labels = [ + 'Strain 1: beta 0.016', + 'Strain 2: beta 0.05' + ] # Run sim with several strains initially, then introduce a new strain that's more transmissible on day 10 - pars = {'n_strains': 3, 'beta': [0.016] * 3} - imports = cv.import_strain(days=30, n_imports=50, beta=0.5, init_immunity=1, half_life=50) + immunity = [ + {'init_immunity': 1., 'half_life': 180, 'cross_factor': 1}, + ] + pars = { + 'n_strains': 1, + 'beta': [0.016], + 'immunity': immunity + } + imports = cv.import_strain(days=10, n_imports=30, beta=0.05, init_immunity=1, half_life=180, cross_factor=1) sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') sim.run() - labels = [ - 'strain1', - 'strain2', - 'strain3', - 'strain4' + if do_plot: + plot_results(sim, key='incidence_by_strain', title='Imported strain on day 10 (cross immunity)', labels=strain_labels) + return sim + +def test_importstrain_nocrossimmunity(do_plot=False, do_show=True, do_save=False, fig_path=None): + sc.heading('Test introducing a new strain partway through a sim with cross immunity') + + sc.heading('Setting up...') + + strain_labels = [ + 'Strain 1: beta 0.016', + 'Strain 2: beta 0.05' ] + # Run sim with several strains initially, then introduce a new strain that's more transmissible on day 10 + immunity = [ + {'init_immunity': 1., 'half_life': 180, 'cross_factor': 0}, + ] + pars = { + 'n_strains': 1, + 'beta': [0.016], + 'immunity': immunity + } + imports = cv.import_strain(days=10, n_imports=30, beta=0.05, init_immunity=1, half_life=180, cross_factor=0) + sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') + sim.run() if do_plot: - sim.plot_result('cum_infections_by_strain', label=labels, do_show=do_show, do_save=do_save, fig_path='results/sim2_cum_infections_by_strain') - sim.plot_result('incidence_by_strain', label=labels, do_show=do_show, do_save=do_save, fig_path='results/sim2_incidence_by_strain') - sim.plot_result('prevalence_by_strain', label=labels, do_show=do_show, do_save=do_save, fig_path='results/sim2_prevalence_by_strain') - + plot_results(sim, key='incidence_by_strain', title='Imported strain on day 10 (no cross immunity)', labels=strain_labels) return sim +def plot_results(sim, key, title, labels=None): + + results = sim.results + results_to_plot = results[key] + + # extract data for plotting + x = sim.results['t'] + y = results_to_plot.values + + fig, ax = plt.subplots() + ax.plot(x, y) + + ax.set(xlabel='Day of simulation', ylabel=results_to_plot.name, title=title) + + if labels is None: + labels = [0]*len(y[0]) + for strain in range(len(y[0])): + labels[strain] = f'Strain {strain +1}' + ax.legend(labels) + plt.show() + + return + #%% Run as a script @@ -65,7 +119,8 @@ def test_importstrain(do_plot=False, do_show=True, do_save=False, fig_path=None) sc.tic() sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) - # sim2 = test_importstrain(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) sc.toc() From 0bf71eecace1a4c890119e6006796c63d785dcec Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 9 Feb 2021 12:29:11 -0500 Subject: [PATCH 039/569] improving plotting in test script --- covasim/interventions.py | 2 +- tests/devtests/test_variants.py | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 7f10f00e1..3202063c7 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1193,7 +1193,7 @@ def apply(self, sim): ) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[ strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely - sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains + strain) + sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) sim['n_strains'] += 1 return \ No newline at end of file diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 649a3bccd..96d6698b5 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -2,6 +2,8 @@ import sciris as sc import matplotlib import matplotlib.pyplot as plt +import numpy as np + do_plot = 1 do_show = 1 @@ -55,12 +57,12 @@ def test_importstrain_withcrossimmunity(do_plot=False, do_show=True, do_save=Fal 'beta': [0.016], 'immunity': immunity } - imports = cv.import_strain(days=10, n_imports=30, beta=0.05, init_immunity=1, half_life=180, cross_factor=1) + imports = cv.import_strain(days=30, n_imports=30, beta=0.05, init_immunity=1, half_life=180, cross_factor=1) sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') sim.run() if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strain on day 10 (cross immunity)', labels=strain_labels) + plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (cross immunity)', labels=strain_labels) return sim def test_importstrain_nocrossimmunity(do_plot=False, do_show=True, do_save=False, fig_path=None): @@ -68,10 +70,7 @@ def test_importstrain_nocrossimmunity(do_plot=False, do_show=True, do_save=False sc.heading('Setting up...') - strain_labels = [ - 'Strain 1: beta 0.016', - 'Strain 2: beta 0.05' - ] + # Run sim with several strains initially, then introduce a new strain that's more transmissible on day 10 immunity = [ {'init_immunity': 1., 'half_life': 180, 'cross_factor': 0}, @@ -81,12 +80,19 @@ def test_importstrain_nocrossimmunity(do_plot=False, do_show=True, do_save=False 'beta': [0.016], 'immunity': immunity } - imports = cv.import_strain(days=10, n_imports=30, beta=0.05, init_immunity=1, half_life=180, cross_factor=0) + imports = cv.import_strain(days=[10, 20], n_imports=[10, 20], beta=[0.035, 0.05], init_immunity=[1, 1], + half_life=[180, 180], cross_factor=[0, 0]) sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') sim.run() + strain_labels = [ + 'Strain 1: beta 0.016', + 'Strain 2: beta 0.035, 10 imported day 10', + 'Strain 3: beta 0.05, 20 imported day 20' + ] + if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strain on day 10 (no cross immunity)', labels=strain_labels) + plot_results(sim, key='incidence_by_strain', title='Imported strains', labels=strain_labels) return sim def plot_results(sim, key, title, labels=None): @@ -97,6 +103,7 @@ def plot_results(sim, key, title, labels=None): # extract data for plotting x = sim.results['t'] y = results_to_plot.values + y = np.flip(y, 1) fig, ax = plt.subplots() ax.plot(x, y) @@ -118,8 +125,8 @@ def plot_results(sim, key, title, labels=None): if __name__ == '__main__': sc.tic() - sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) - sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + # sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + # sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) sc.toc() From dd290aedb1a5d1a0ce520b749c99f873e4e1ffa0 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 10 Feb 2021 13:36:55 +0100 Subject: [PATCH 040/569] generalise intervention args --- covasim/interventions.py | 100 ++++++++++++++++++-------------- tests/devtests/test_variants.py | 62 ++++++++++++++++---- 2 files changed, 105 insertions(+), 57 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 3202063c7..5859e4c09 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1088,7 +1088,7 @@ def apply(self, sim): ''' Perform vaccination ''' # If this day is found in the list, apply the intervention - for ind in find_day(self.days, sim.t): + for ind in find_day(self.days, sim.t): # TODO -- investigate this, why does it loop over a variable that isn't subsequently used? Also, comments need updating # Construct the testing probabilities piece by piece -- complicated, since need to do it in the right order vacc_probs = np.full(sim.n, self.prob) # Begin by assigning equal testing probability to everyone @@ -1127,7 +1127,7 @@ class import_strain(Intervention): Introduce a new variant(s) to the population through an importation at a given time point. Args: - days (int or list of ints): the day(s) to apply the interventions + days (int or list of ints): days on which new variants are introduced. Note, the interpretation of a list differs from other interventions; see examples below n_imports (list of ints): the number of imports of strain(s) beta (list of floats): per contact transmission of strain(s) init_immunity (list of floats): initial immunity against strain(s) once recovered; 1 = perfect, 0 = no immunity @@ -1136,64 +1136,74 @@ class import_strain(Intervention): **Examples**:: - interv = cv.import_strain(day=50, beta=0.3, init_immunity=1, half_life=180) - interv = cv.import_strain(day=50, beta=[0.3, 0.5], init_immunity=1, half_life=180) + interv = cv.import_strain(days=50, beta=0.03) # On day 50, import one new strain (one case) + interv = cv.import_strain(days=[10, 50], beta=0.03) # On day 50, import one new strain (one case) + interv = cv.import_strain(days=50, beta=[0.03, 0.05]) # On day 50, import two new strains (one case of each) + interv = cv.import_strain(days=[10, 20], n_imports=[5, 10], beta=[0.03, 0.05], init_immunity=[1, 1], half_life=[180, 180], cross_factor=[0, 0]) # On day 10, import 5 cases of one new strain, on day 20, import 10 cases of another ''' - def __init__(self, days=None, n_imports=None, beta=None, init_immunity=None, half_life=None, cross_factor=None, **kwargs): + def __init__(self, days=None, beta=None, n_imports=1, init_immunity=1, half_life=180, cross_factor=0, **kwargs): # Do standard initialization super().__init__(**kwargs) # Initialize the Intervention object self._store_args() # Store the input arguments so the intervention can be recreated # Handle inputs - days = sc.promotetolist(days) - n_imports = sc.promotetolist(n_imports) - beta = sc.promotetolist(beta) - init_immunity = sc.promotetolist(init_immunity) - half_life = sc.promotetolist(half_life) - cross_factor = sc.promotetolist(cross_factor) - len_imports = len(n_imports) - len_betas = len(beta) - if len_imports != len_betas: - raise ValueError( - f'Number of different imports ({len_imports} does not match the number of betas ({len_betas})') - else: - self.new_strains = len_imports # Number of new strains being introduced - - # Set attributes - self.days = days - self.n_imports = n_imports - self.beta = beta - self.init_immunity = init_immunity - self.half_life = half_life - self.cross_factor = cross_factor + self.beta = sc.promotetolist(beta) + self.days = sc.promotetolist(days) + self.n_imports = sc.promotetolist(n_imports) + self.init_immunity = sc.promotetolist(init_immunity) + self.half_life = sc.promotetolist(half_life) + self.cross_factor = sc.promotetolist(cross_factor) + self.new_strains = self.check_args(['beta', 'days', 'n_imports', 'init_immunity', 'half_life', 'cross_factor']) return + + def check_args(self, args): + ''' Check the length of supplied arguments''' + argvals = [getattr(self,arg) for arg in args] + arglengths = np.array([len(argval) for argval in argvals]) # Get lengths of all arguments + multi_d_args = arglengths[cvu.true(arglengths > 1)].tolist() # Get multidimensional arguments + if len(multi_d_args)==0: # Introducing a single new strain, and all arguments have the right length + new_strains = 1 + elif len(multi_d_args)>=1: # Introducing more than one new strain, but with only one property varying + if len(multi_d_args)>1 and (multi_d_args.count(multi_d_args[0])!=len(multi_d_args)): # This raises an error: more than one multi-dim argument and they're not equal length + raise ValueError(f'Mismatch in the lengths of arguments supplied.') + else: + new_strains = multi_d_args[0] + for arg,argval in zip(args,argvals): + if len(argval)==1: + setattr(self,arg,[argval[0]]*new_strains) + return new_strains + + def initialize(self, sim): self.days = process_days(sim, self.days) self.max_strains = sim['max_strains'] self.initialized = True + def apply(self, sim): - for strain in find_day(self.days, sim.t): - # Check number of strains - prev_strains = sim['n_strains'] - if prev_strains + 1 > self.max_strains: - errormsg = f"Number of existing strains ({sim['n_strains']}) plus new strain exceeds the maximal allowable ({sim['max_strains']}. Increase pars['max_strains'])." - raise ValueError(errormsg) - - sim['beta'].append(self.beta[strain]) - sim['immunity'].append( - { - 'init_immunity': self.init_immunity[strain], - 'half_life': self.half_life[strain], - 'cross_factor': self.cross_factor[strain] - } - ) - importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[ - strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely - sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) - sim['n_strains'] += 1 + # Loop over strains + for strain in range(self.new_strains): + + if sim.t == self.days[strain]: # Time to introduce this strain + # Check number of strains + prev_strains = sim['n_strains'] + if prev_strains + 1 > self.max_strains: + errormsg = f"Number of existing strains ({sim['n_strains']}) plus new strain exceeds the maximal allowable ({sim['max_strains']}. Increase pars['max_strains'])." + raise ValueError(errormsg) + + sim['beta'].append(self.beta[strain]) + sim['immunity'].append( + { + 'init_immunity': self.init_immunity[strain], + 'half_life': self.half_life[strain], + 'cross_factor': self.cross_factor[strain] + } + ) + importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely + sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) + sim['n_strains'] += 1 return \ No newline at end of file diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 96d6698b5..cb8f7e604 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -6,11 +6,11 @@ do_plot = 1 -do_show = 1 -do_save = 0 +do_show = 0 +do_save = 1 -def test_multistrains(do_plot=False, do_show=True, do_save=False, fig_path=None): +def test_multistrains(do_plot=False, do_show=True, do_save=False): sc.heading('Run basic sim with multiple strains') sc.heading('Setting up...') @@ -35,11 +35,45 @@ def test_multistrains(do_plot=False, do_show=True, do_save=False, fig_path=None) sim.run() if do_plot: - plot_results(sim, key='incidence_by_strain', title='Multiple strains', labels=strain_labels) + plot_results(sim, key='incidence_by_strain', title='Multiple strains', labels=strain_labels, do_show=do_show, do_save=do_save) return sim -def test_importstrain_withcrossimmunity(do_plot=False, do_show=True, do_save=False, fig_path=None): +def test_importstrain_args(): + sc.heading('Test flexibility of arguments for the import strain "intervention"') + + # Run sim with a single strain initially, then introduce a new strain that's more transmissible on day 10 + immunity = [ + {'init_immunity': 1., 'half_life': 180, 'cross_factor': 1}, + ] + pars = { + 'n_strains': 1, + 'beta': [0.016], + 'immunity': immunity + } + + # All these should run + #imports = cv.import_strain(days=50, beta=0.03) + #imports = cv.import_strain(days=[10, 50], beta=0.03) + #imports = cv.import_strain(days=50, beta=[0.03, 0.05]) + imports = cv.import_strain(days=[10, 20], beta=[0.03, 0.05]) + #imports = cv.import_strain(days=50, beta=[0.03, 0.05, 0.06]) + #imports = cv.import_strain(days=[10, 20], n_imports=[5, 10], beta=[0.03, 0.05], init_immunity=[1, 1], + # half_life=[180, 180], cross_factor=[0, 0]) + #imports = cv.import_strain(days=[10, 50], beta=0.03, cross_factor=[0.4, 0.6]) + #imports = cv.import_strain(days=['2020-04-01', '2020-05-01'], beta=0.03) + + # This should fail + #imports = cv.import_strain(days=[20, 50], beta=[0.03, 0.05, 0.06]) + + sim = cv.Sim(pars=pars, interventions=imports) + sim.run() + + + return sim + + +def test_importstrain_withcrossimmunity(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing a new strain partway through a sim with full cross immunity') sc.heading('Setting up...') @@ -62,10 +96,10 @@ def test_importstrain_withcrossimmunity(do_plot=False, do_show=True, do_save=Fal sim.run() if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (cross immunity)', labels=strain_labels) + plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (cross immunity)', labels=strain_labels, do_show=do_show, do_save=do_save) return sim -def test_importstrain_nocrossimmunity(do_plot=False, do_show=True, do_save=False, fig_path=None): +def test_importstrain_nocrossimmunity(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing a new strain partway through a sim with cross immunity') sc.heading('Setting up...') @@ -92,10 +126,10 @@ def test_importstrain_nocrossimmunity(do_plot=False, do_show=True, do_save=False ] if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strains', labels=strain_labels) + plot_results(sim, key='incidence_by_strain', title='Imported strains', labels=strain_labels, do_show=do_show, do_save=do_save) return sim -def plot_results(sim, key, title, labels=None): +def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): results = sim.results results_to_plot = results[key] @@ -115,7 +149,10 @@ def plot_results(sim, key, title, labels=None): for strain in range(len(y[0])): labels[strain] = f'Strain {strain +1}' ax.legend(labels) - plt.show() + if do_show: + plt.show() + if do_save: + cv.savefig(f'results/{title}.png') return @@ -126,8 +163,9 @@ def plot_results(sim, key, title, labels=None): sc.tic() # sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) - # sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) - sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + # sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + sim4 = test_importstrain_args() sc.toc() From d63d706cb8c38dc3e7f20a94f74eae3adb166c48 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 10 Feb 2021 15:55:18 +0100 Subject: [PATCH 041/569] begin adding cross immunity matrix (messy wip) --- covasim/base.py | 2 + covasim/parameters.py | 82 ++++++++++++++++++++++++++++----- covasim/sim.py | 22 ++++++--- covasim/utils.py | 5 +- tests/devtests/test_variants.py | 21 +++++---- 5 files changed, 104 insertions(+), 28 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 3f44e3c68..6565a6ffc 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -258,6 +258,8 @@ def update_pars(self, pars=None, create=False, **kwargs): cvpar.reset_layer_pars(pars, force=False) if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses + if pars['cross_immunity'] is None: + pars['cross_immunity'] = cvpar.update_cross_immunity(pars) # Set cross immunity super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/parameters.py b/covasim/parameters.py index 0bd0dc957..700adb6b5 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -6,6 +6,7 @@ import sciris as sc from .settings import options as cvo # For setting global options from . import misc as cvm +from . import defaults as cvd __all__ = ['make_pars', 'reset_layer_pars', 'get_prognoses'] @@ -48,17 +49,19 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['rescale_factor'] = 1.2 # Factor by which the population is rescaled on each step # Basic disease transmission - pars['beta'] = [0.016] # Beta per symptomatic contact; absolute value, calibrated - pars['contacts'] = None # The number of contacts per layer; set by reset_layer_pars() below - pars['dynam_layer'] = None # Which layers are dynamic; set by reset_layer_pars() below - pars['beta_layer'] = None # Transmissibility per layer; set by reset_layer_pars() below - pars['n_imports'] = 0 # Average daily number of imported cases (actual number is drawn from Poisson distribution) - pars['beta_dist'] = dict(dist='neg_binomial', par1=1.0, par2=0.45, step=0.01) # Distribution to draw individual level transmissibility; dispersion from https://www.researchsquare.com/article/rs-29548/v1 - pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 - pars['n_strains'] = 1 # The number of strains currently circulating in the population - pars['max_strains'] = 30 # For allocating memory with numpy arrays + pars['beta'] = [0.016] # Beta per symptomatic contact; absolute value, calibrated + pars['contacts'] = None # The number of contacts per layer; set by reset_layer_pars() below + pars['dynam_layer'] = None # Which layers are dynamic; set by reset_layer_pars() below + pars['beta_layer'] = None # Transmissibility per layer; set by reset_layer_pars() below + pars['n_imports'] = 0 # Average daily number of imported cases (actual number is drawn from Poisson distribution) + pars['beta_dist'] = dict(dist='neg_binomial', par1=1.0, par2=0.45, step=0.01) # Distribution to draw individual level transmissibility; dispersion from https://www.researchsquare.com/article/rs-29548/v1 + pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 + pars['n_strains'] = 1 # The number of strains currently circulating in the population + pars['max_strains'] = 30 # For allocating memory with numpy arrays # pars['immunity'] = dict(init_immunity=1., half_life=180) # Protection from immunity. If half_life is None immunity is constant; if it's a number it decays exponentially. TODO: improve this with data, e.g. https://www.nejm.org/doi/full/10.1056/nejmc2025179 - pars['immunity'] = [dict(init_immunity=1., half_life=180, cross_factor=0.5) for _ in range(pars['max_strains'])] + pars['immunity'] = [dict(init_immunity=1., half_life=180) for _ in range(pars['max_strains'])] + pars['default_cross_immunity'] = 0.5 # Default cross-immunity protection factor + pars['cross_immunity'] = None # Matrix of cross-immunity factors, set by set_cross_immunity() below # Efficacy of protection measures pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 @@ -277,4 +280,61 @@ def absolute_prognoses(prognoses): out['severe_probs'] *= out['symp_probs'] # Absolute probability of severe symptoms out['crit_probs'] *= out['severe_probs'] # Absolute probability of critical symptoms out['death_probs'] *= out['crit_probs'] # Absolute probability of dying - return out \ No newline at end of file + return out + + +def update_cross_immunity(pars, update_strain=None, immunity_from=None, immunity_to=None): + ''' + Helper function to set the cross-immunity matrix. + Matrix is of size sim['max_strains']*sim['max_strains'] and takes the form: + A B ... + array([[nan, 1.0, ...], + [0.4, nan, ...], + ..., ..., ...]) + ... meaning that people who've had strand A have perfect protection against strand B, but + people who've had strand B have an initial 40% protection against getting strand A. + The matrix has nan entries outside of any active strains. + + Args: + pars (dict): the parameters dictionary + update_strain: the index of the strain to update + immunity_from (array): used when adding a new strain; specifies the immunity protection against existing strains from having the new strain + immunity_to (array): used when adding a strain; specifies the immunity protection against the new strain from having one of the existing strains + + **Example 1**: + # Adding a strain C to the example above. Strain C gives perfect immunity against strain A + # and 90% immunity against strain B. People who've had strain A have 50% immunity to strain C, + # and people who've had strain B have 70% immunity to strain C + cross_immunity = update_cross_immunity(pars, update_strain=2, immunity_from=[1. 0.9], immunity_to=[0.5, 0.7]) + A B C ... + array([[nan, 1.0, 0.5 ...], + [0.4, nan, 0.7 ...], + [1.0, 0.9, nan ...], + ..., ..., ..., ...]) + + **Example 2**: + # Updating the immunity protection factors for strain B + cross_immunity = update_cross_immunity(pars, update_strain=1, immunity_from=[0.6 0.8], immunity_to=[0.9, 0.7]) + A B C ... + array([[nan, 0.9, 0.5 ...], + [0.6, nan, 0.8 ...], + [1.0, 0.7, nan ...], + ..., ..., ..., ...]) + + Mostly for internal use (? TBC) + ''' + + # Initialise if not already initialised + if pars['cross_immunity'] is None: + cross_immunity = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) + if pars['n_strains'] > 1: + for i in range(pars['n_strains']): + for j in range(pars['n_strains']): + if i != j: cross_immunity[i, j] = pars['default_cross_immunity'] + + # Update immunity for a strain if supplied + if update_strain is not None: + cross_immunity = sc.dcp(pars['cross_immunity']) + cross_immunity[update_strain, ] # TODO + + return cross_immunity diff --git a/covasim/sim.py b/covasim/sim.py index 07d1888ad..077f98f24 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -525,20 +525,30 @@ def step(self): # Compute the probability of transmission beta = cvd.default_float(self['beta'][strain]) + immunity_factors = people.immunity_factors[strain, :] - # Compute immunity factors + # Process immunity parameters and indices immune = people.recovered_strain == strain # Whether people have some immunity to this strain from a prior infection with this strain - cross_immune = (~np.isnan(people.recovered_strain)) & (people.recovered_strain != strain) # Whether people have some immunity to this strain from a prior infection with another strain immune_time = t - date_rec[immune] # Time since recovery for people who were last infected by this strain - cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by another strain immune_inds = cvd.default_int(cvu.true(immune)) # People with some immunity to this strain from a prior infection with this strain - cross_immune_inds = cvd.default_int(cvu.true(cross_immune)) # People with some immunity to this strain from a prior infection with another strain init_immunity = cvd.default_float(self['immunity'][strain]['init_immunity']) half_life = self['immunity'][strain]['half_life'] decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. decay_rate = cvd.default_float(decay_rate) - cross_factor = cvd.default_float(self['immunity'][strain]['cross_factor']) - immunity_factors = cvu.compute_immunity(people.immunity_factors[strain, :], immune_time, cross_immune_time, immune_inds, cross_immune_inds, init_immunity, decay_rate, cross_factor) + immunity_factors[immune_inds] = init_immunity * np.exp(-decay_rate * immune_time) # Calculate immunity factors + + # Process cross-immunity parameters and indices, if relevant + if self['n_strains']>1: + for cross_strain in range(self['n_strains']): + if cross_strain != strain: + cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain + cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by the cross strain + cross_immune_inds = cvd.default_int(cvu.true(cross_immune)) # People with some immunity to this strain from a prior infection with another strain + cross_immunity = cvd.default_float(self['cross_immunity'][cross_strain, strain]) # Immunity protection again this strain from other strains + immunity_factors[cross_immune_inds] = immunity_factors[cross_immune_inds] = cross_immunity * np.exp(-decay_rate * cross_immune_time) # Calculate cross-immunity factors + + # Compute protection factors from both immunity and cross immunity + ##immunity_factors = cvu.compute_immunity(people.immunity_factors[strain, :], immune_time, cross_immune_time, immune_inds, cross_immune_inds, init_immunity, decay_rate, cross_immunity) # Define indices for this strain inf_by_this_strain = sc.dcp(inf) diff --git a/covasim/utils.py b/covasim/utils.py index 7dee3106c..b64645d47 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -69,8 +69,8 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -@nb.njit( (nbfloat[:], nbfloat[:], nbfloat[:], nbint[:], nbint[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) -def compute_immunity(immunity_factors, immune_time, cross_immune_time, immune_inds, cross_immune_inds, init_immunity, decay_rate, cross_factor): # pragma: no cover +#@nb.njit( (nbfloat[:], nbfloat[:], nbfloat[:], nbint[:], nbint[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) +def compute_immunity(immunity_factors, immune_time, cross_immune_time, immune_inds, cross_immune_inds, init_immunity, decay_rate, cross_immunity): # pragma: no cover ''' Calculate immunity factors for time t @@ -83,6 +83,7 @@ def compute_immunity(immunity_factors, immune_time, cross_immune_time, immune_in Returns: immunity_factors (float[]): immunity factors ''' + immunity_factors[immune_inds] = init_immunity * np.exp(-decay_rate * immune_time) # Calculate immunity factors immunity_factors[cross_immune_inds] = (init_immunity * np.exp(-decay_rate * cross_immune_time)) * cross_factor # Calculate cross-immunity factors return immunity_factors diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index cb8f7e604..80dc7c3d7 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -21,15 +21,18 @@ def test_multistrains(do_plot=False, do_show=True, do_save=False): ] immunity = [ - {'init_immunity':1., 'half_life':180, 'cross_factor':0.5}, - {'init_immunity':1., 'half_life':50, 'cross_factor':0.9} + {'init_immunity':1., 'half_life':180}, + {'init_immunity':1., 'half_life':50} ] + cross_immunity = np.array([[np.nan, 0.5],[0.5, np.nan]]) + pars = { 'n_strains': 2, - 'beta': [0.016, 0.035], - 'immunity': immunity - } + 'beta': [0.016, 0.035], + 'immunity': immunity, + 'cross_immunity':cross_immunity + } sim = cv.Sim(pars=pars) sim.run() @@ -56,7 +59,7 @@ def test_importstrain_args(): #imports = cv.import_strain(days=50, beta=0.03) #imports = cv.import_strain(days=[10, 50], beta=0.03) #imports = cv.import_strain(days=50, beta=[0.03, 0.05]) - imports = cv.import_strain(days=[10, 20], beta=[0.03, 0.05]) + #imports = cv.import_strain(days=[10, 20], beta=[0.03, 0.05]) #imports = cv.import_strain(days=50, beta=[0.03, 0.05, 0.06]) #imports = cv.import_strain(days=[10, 20], n_imports=[5, 10], beta=[0.03, 0.05], init_immunity=[1, 1], # half_life=[180, 180], cross_factor=[0, 0]) @@ -162,10 +165,10 @@ def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): if __name__ == '__main__': sc.tic() - # sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) + sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=None) - sim4 = test_importstrain_args() + # sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim4 = test_importstrain_args() sc.toc() From 158dac0bc1c407f658f7f7f3a3bd4f0082e16a74 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 10 Feb 2021 14:39:43 -0500 Subject: [PATCH 042/569] big changes to immunity: it is now a matrix where the diagonal is immune-factor and all other cells are immunity to/from each strain --- covasim/base.py | 6 ++- covasim/interventions.py | 23 ++++---- covasim/parameters.py | 93 +++++++++++++++++++++++++++++++-- covasim/sim.py | 6 +-- covasim/utils.py | 2 +- tests/devtests/test_variants.py | 36 ++++++------- 6 files changed, 125 insertions(+), 41 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 6565a6ffc..44f787de0 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -258,8 +258,10 @@ def update_pars(self, pars=None, create=False, **kwargs): cvpar.reset_layer_pars(pars, force=False) if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses - if pars['cross_immunity'] is None: - pars['cross_immunity'] = cvpar.update_cross_immunity(pars) # Set cross immunity + # if pars['cross_immunity'] is None: + # pars['cross_immunity'] = cvpar.update_cross_immunity(pars) # Set cross immunity + if create: + pars['immunity'] = cvpar.update_immunity(pars) # Set immunity super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/interventions.py b/covasim/interventions.py index 5859e4c09..f58cedcb2 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1132,6 +1132,8 @@ class import_strain(Intervention): beta (list of floats): per contact transmission of strain(s) init_immunity (list of floats): initial immunity against strain(s) once recovered; 1 = perfect, 0 = no immunity half_life (list of floats): determines decay rate of immunity against strain(s); If half_life is None immunity is constant + immunity_to (list of list of floats): cross immunity to existing strains in model + immunity_from (list of list of floats): cross immunity from existing strains in model kwargs (dict): passed to Intervention() **Examples**:: @@ -1139,10 +1141,12 @@ class import_strain(Intervention): interv = cv.import_strain(days=50, beta=0.03) # On day 50, import one new strain (one case) interv = cv.import_strain(days=[10, 50], beta=0.03) # On day 50, import one new strain (one case) interv = cv.import_strain(days=50, beta=[0.03, 0.05]) # On day 50, import two new strains (one case of each) - interv = cv.import_strain(days=[10, 20], n_imports=[5, 10], beta=[0.03, 0.05], init_immunity=[1, 1], half_life=[180, 180], cross_factor=[0, 0]) # On day 10, import 5 cases of one new strain, on day 20, import 10 cases of another + interv = cv.import_strain(days=[10, 20], n_imports=[5, 10], beta=[0.03, 0.05], init_immunity=[1, 1], + half_life=[180, 180], immunity_to=[[0, 0], [0,0]], immunity_from=[[0, 0], [0,0]]) # On day 10, import 5 cases of one new strain, on day 20, import 10 cases of another ''' - def __init__(self, days=None, beta=None, n_imports=1, init_immunity=1, half_life=180, cross_factor=0, **kwargs): + def __init__(self, days=None, beta=None, n_imports=1, init_immunity=1, half_life=180, immunity_to=0, + immunity_from=0, **kwargs): # Do standard initialization super().__init__(**kwargs) # Initialize the Intervention object self._store_args() # Store the input arguments so the intervention can be recreated @@ -1153,8 +1157,9 @@ def __init__(self, days=None, beta=None, n_imports=1, init_immunity=1, half_life self.n_imports = sc.promotetolist(n_imports) self.init_immunity = sc.promotetolist(init_immunity) self.half_life = sc.promotetolist(half_life) - self.cross_factor = sc.promotetolist(cross_factor) - self.new_strains = self.check_args(['beta', 'days', 'n_imports', 'init_immunity', 'half_life', 'cross_factor']) + self.immunity_to = sc.promotetolist(immunity_to) + self.immunity_from = sc.promotetolist(immunity_from) + self.new_strains = self.check_args(['beta', 'days', 'n_imports', 'init_immunity', 'half_life']) return @@ -1195,13 +1200,9 @@ def apply(self, sim): raise ValueError(errormsg) sim['beta'].append(self.beta[strain]) - sim['immunity'].append( - { - 'init_immunity': self.init_immunity[strain], - 'half_life': self.half_life[strain], - 'cross_factor': self.cross_factor[strain] - } - ) + cvpar.update_immunity(sim.pars, update_strain=prev_strains, immunity_from=self.immunity_from[strain], + immunity_to=self.immunity_to[strain], init_immunity=self.init_immunity[strain], + half_life=self.half_life[strain]) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) sim['n_strains'] += 1 diff --git a/covasim/parameters.py b/covasim/parameters.py index 700adb6b5..ebe8f74d0 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -58,10 +58,11 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 pars['n_strains'] = 1 # The number of strains currently circulating in the population pars['max_strains'] = 30 # For allocating memory with numpy arrays - # pars['immunity'] = dict(init_immunity=1., half_life=180) # Protection from immunity. If half_life is None immunity is constant; if it's a number it decays exponentially. TODO: improve this with data, e.g. https://www.nejm.org/doi/full/10.1056/nejmc2025179 - pars['immunity'] = [dict(init_immunity=1., half_life=180) for _ in range(pars['max_strains'])] pars['default_cross_immunity'] = 0.5 # Default cross-immunity protection factor - pars['cross_immunity'] = None # Matrix of cross-immunity factors, set by set_cross_immunity() below + pars['default_immunity'] = 1. # Default initial immunity + pars['default_half_life']= 180 # Default half life + pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by set_immunity() below + pars['half_life'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) # Efficacy of protection measures pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 @@ -338,3 +339,89 @@ def update_cross_immunity(pars, update_strain=None, immunity_from=None, immunity cross_immunity[update_strain, ] # TODO return cross_immunity + + +def update_immunity(pars, update_strain=None, immunity_from=None, immunity_to=None, init_immunity=None, half_life=None): + ''' + Helper function to set the immunity and half_life matrices. + Matrix is of size sim['max_strains']*sim['max_strains'] and takes the form: + A B ... + array([[1.0, 1.0, ...], + [0.4, 1.0, ...], + ..., ..., ...]) + ... meaning that people who've had strand A have perfect protection against strand B, but + people who've had strand B have an initial 40% protection against getting strand A. + The matrix has nan entries outside of any active strains. The diagonals represent immunity + against the same strain, meaning people who've had strand A have perfect protection against + strand A, the same for B. + + Args: + pars (dict): the parameters dictionary + update_strain: the index of the strain to update + immunity_from (array): used when adding a new strain; specifies the immunity protection against existing strains from having the new strain + immunity_to (array): used when adding a strain; specifies the immunity protection against the new strain from having one of the existing strains + + **Example 1**: + # Adding a strain C to the example above. Strain C gives perfect immunity against strain A + # and 90% immunity against strain B. People who've had strain A have 50% immunity to strain C, + # and people who've had strain B have 70% immunity to strain C + cross_immunity = update_cross_immunity(pars, update_strain=2, immunity_from=[1. 0.9], immunity_to=[0.5, 0.7]) + A B C ... + array([[nan, 1.0, 0.5 ...], + [0.4, nan, 0.7 ...], + [1.0, 0.9, nan ...], + ..., ..., ..., ...]) + + ''' + # If initial immunity/cross immunity factors are provided, use those, otherwise use defaults + + # Initialise if not already initialised + if pars['immunity'] is None: + immunity = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) + for i in range(pars['n_strains']): + pars['half_life'][i] = pars['default_half_life'] + for j in range(pars['n_strains']): + if i != j: + immunity[i, j] = pars['default_cross_immunity'] + else: + immunity[i, j] = pars['default_immunity'] + + else: + immunity = pars['immunity'] + + # Update immunity for a strain if supplied + if update_strain is not None: + + # check that immunity_from, immunity_to and init_immunity are provided and the right length. + # Else use default values + if immunity_from is None: + print('Immunity from pars not provided, using default value') + immunity_from = [pars['default_cross_immunity']]*pars['n_strains'] + if immunity_to is None: + print('Immunity to pars not provided, using default value') + immunity_to = [pars['default_cross_immunity']]*pars['n_strains'] + if init_immunity is None: + print('Initial immunity pars not provided, using default value') + init_immunity = pars['default_immunity'] + if half_life is None: + print('Half life is not provided, using default value') + half_life = pars['default_half_life'] + + immunity_from = sc.promotetolist(immunity_from) + immunity_to = sc.promotetolist(immunity_to) + + # create the immunity[update_strain,] and immunity[,update_strain] arrays + new_immunity_row = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) + new_immunity_column = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) + for i in range(pars['n_strains']+1): + if i != update_strain: + new_immunity_row[i] = immunity_from[i] + new_immunity_column[i] = immunity_to[i] + else: + new_immunity_row[i] = new_immunity_column[i] = init_immunity + pars['half_life'][i] = half_life + + immunity[update_strain, :] = new_immunity_row + immunity[:, update_strain] = new_immunity_column + + return immunity diff --git a/covasim/sim.py b/covasim/sim.py index 077f98f24..ece1f3f37 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -531,8 +531,8 @@ def step(self): immune = people.recovered_strain == strain # Whether people have some immunity to this strain from a prior infection with this strain immune_time = t - date_rec[immune] # Time since recovery for people who were last infected by this strain immune_inds = cvd.default_int(cvu.true(immune)) # People with some immunity to this strain from a prior infection with this strain - init_immunity = cvd.default_float(self['immunity'][strain]['init_immunity']) - half_life = self['immunity'][strain]['half_life'] + init_immunity = cvd.default_float(self['immunity'][strain, strain]) + half_life = self['half_life'][strain] decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. decay_rate = cvd.default_float(decay_rate) immunity_factors[immune_inds] = init_immunity * np.exp(-decay_rate * immune_time) # Calculate immunity factors @@ -544,7 +544,7 @@ def step(self): cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by the cross strain cross_immune_inds = cvd.default_int(cvu.true(cross_immune)) # People with some immunity to this strain from a prior infection with another strain - cross_immunity = cvd.default_float(self['cross_immunity'][cross_strain, strain]) # Immunity protection again this strain from other strains + cross_immunity = cvd.default_float(self['immunity'][cross_strain, strain]) # Immunity protection again this strain from other strains immunity_factors[cross_immune_inds] = immunity_factors[cross_immune_inds] = cross_immunity * np.exp(-decay_rate * cross_immune_time) # Calculate cross-immunity factors # Compute protection factors from both immunity and cross immunity diff --git a/covasim/utils.py b/covasim/utils.py index b64645d47..9d0de9aca 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -85,7 +85,7 @@ def compute_immunity(immunity_factors, immune_time, cross_immune_time, immune_in ''' immunity_factors[immune_inds] = init_immunity * np.exp(-decay_rate * immune_time) # Calculate immunity factors - immunity_factors[cross_immune_inds] = (init_immunity * np.exp(-decay_rate * cross_immune_time)) * cross_factor # Calculate cross-immunity factors + immunity_factors[cross_immune_inds] = (init_immunity * np.exp(-decay_rate * cross_immune_time)) * cross_immunity # Calculate cross-immunity factors return immunity_factors diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 80dc7c3d7..c5e1f2b2a 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -1,40 +1,34 @@ import covasim as cv import sciris as sc -import matplotlib import matplotlib.pyplot as plt import numpy as np do_plot = 1 -do_show = 0 -do_save = 1 +do_show = 1 +do_save = 0 def test_multistrains(do_plot=False, do_show=True, do_save=False): - sc.heading('Run basic sim with multiple strains') + sc.heading('Run basic sim with an imported strain') sc.heading('Setting up...') + immunity_to = 0 + immunity_from = 0 + init_immunity = 0 + n_imports = 20 + days = 30 + + imports = cv.import_strain(days=days, beta=0.035, n_imports=n_imports, immunity_to=immunity_to, immunity_from=immunity_from, + init_immunity=init_immunity) + strain_labels = [ 'Strain 1: beta 0.016', - 'Strain 2: beta 0.035' - ] - - immunity = [ - {'init_immunity':1., 'half_life':180}, - {'init_immunity':1., 'half_life':50} + f'Strain 2: beta 0.035 on day {days}, {immunity_to}% to A, {immunity_from}% from A' ] - cross_immunity = np.array([[np.nan, 0.5],[0.5, np.nan]]) - - pars = { - 'n_strains': 2, - 'beta': [0.016, 0.035], - 'immunity': immunity, - 'cross_immunity':cross_immunity - } - - sim = cv.Sim(pars=pars) + sim = cv.Sim(interventions=imports) sim.run() if do_plot: @@ -56,7 +50,7 @@ def test_importstrain_args(): } # All these should run - #imports = cv.import_strain(days=50, beta=0.03) + imports = cv.import_strain(days=50, beta=0.03) #imports = cv.import_strain(days=[10, 50], beta=0.03) #imports = cv.import_strain(days=50, beta=[0.03, 0.05]) #imports = cv.import_strain(days=[10, 20], beta=[0.03, 0.05]) From 0db2a571a0597355c157812408493462b5ec57c4 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 10 Feb 2021 17:03:52 -0500 Subject: [PATCH 043/569] updating immunity if arguments are passed to sim for first strain --- covasim/base.py | 6 +++--- covasim/interventions.py | 2 +- covasim/parameters.py | 26 ++++++++++++++++++-------- covasim/sim.py | 4 ++-- tests/devtests/test_variants.py | 27 +++++++++++++++++++-------- 5 files changed, 43 insertions(+), 22 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 44f787de0..7c5968b85 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -258,10 +258,10 @@ def update_pars(self, pars=None, create=False, **kwargs): cvpar.reset_layer_pars(pars, force=False) if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses - # if pars['cross_immunity'] is None: - # pars['cross_immunity'] = cvpar.update_cross_immunity(pars) # Set cross immunity if create: - pars['immunity'] = cvpar.update_immunity(pars) # Set immunity + pars['immunity'] = cvpar.update_immunity(pars=pars, create=create) # Set immunity + else: + self.pars['immunity'] = cvpar.update_immunity(pars=self.pars, create=create, new_pars=pars) # update with any provided params super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/interventions.py b/covasim/interventions.py index f58cedcb2..38bfaa016 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1200,7 +1200,7 @@ def apply(self, sim): raise ValueError(errormsg) sim['beta'].append(self.beta[strain]) - cvpar.update_immunity(sim.pars, update_strain=prev_strains, immunity_from=self.immunity_from[strain], + cvpar.update_immunity(pars=sim.pars, create=False, update_strain=prev_strains, immunity_from=self.immunity_from[strain], immunity_to=self.immunity_to[strain], init_immunity=self.init_immunity[strain], half_life=self.half_life[strain]) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely diff --git a/covasim/parameters.py b/covasim/parameters.py index ebe8f74d0..e1a997603 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -61,8 +61,10 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['default_cross_immunity'] = 0.5 # Default cross-immunity protection factor pars['default_immunity'] = 1. # Default initial immunity pars['default_half_life']= 180 # Default half life + pars['half_life'] = None + pars['init_immunity'] = None pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by set_immunity() below - pars['half_life'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) + pars['init_half_life'] = None # Efficacy of protection measures pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 @@ -341,7 +343,8 @@ def update_cross_immunity(pars, update_strain=None, immunity_from=None, immunity return cross_immunity -def update_immunity(pars, update_strain=None, immunity_from=None, immunity_to=None, init_immunity=None, half_life=None): +def update_immunity(pars, create, new_pars=None, update_strain=None, immunity_from=None, immunity_to=None, + init_immunity=None, half_life=None): ''' Helper function to set the immunity and half_life matrices. Matrix is of size sim['max_strains']*sim['max_strains'] and takes the form: @@ -374,9 +377,8 @@ def update_immunity(pars, update_strain=None, immunity_from=None, immunity_to=No ''' # If initial immunity/cross immunity factors are provided, use those, otherwise use defaults - - # Initialise if not already initialised - if pars['immunity'] is None: + if create: + pars['half_life'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) immunity = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) for i in range(pars['n_strains']): pars['half_life'][i] = pars['default_half_life'] @@ -385,13 +387,21 @@ def update_immunity(pars, update_strain=None, immunity_from=None, immunity_to=No immunity[i, j] = pars['default_cross_immunity'] else: immunity[i, j] = pars['default_immunity'] - else: - immunity = pars['immunity'] + if new_pars is not None: + immunity = pars['immunity'] + if 'init_immunity' in new_pars.keys(): + for i in range(pars['n_strains']): + for j in range(pars['n_strains']): + if i == j: + immunity[i, j] = new_pars['init_immunity'] + + if 'init_half_life' in new_pars.keys(): + pars['half_life'][0] = new_pars['init_half_life'] # Update immunity for a strain if supplied if update_strain is not None: - + immunity = pars['immunity'] # check that immunity_from, immunity_to and init_immunity are provided and the right length. # Else use default values if immunity_from is None: diff --git a/covasim/sim.py b/covasim/sim.py index ece1f3f37..042f68d32 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -545,7 +545,7 @@ def step(self): cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by the cross strain cross_immune_inds = cvd.default_int(cvu.true(cross_immune)) # People with some immunity to this strain from a prior infection with another strain cross_immunity = cvd.default_float(self['immunity'][cross_strain, strain]) # Immunity protection again this strain from other strains - immunity_factors[cross_immune_inds] = immunity_factors[cross_immune_inds] = cross_immunity * np.exp(-decay_rate * cross_immune_time) # Calculate cross-immunity factors + immunity_factors[cross_immune_inds] = cross_immunity * np.exp(-decay_rate * cross_immune_time) # Calculate cross-immunity factors # Compute protection factors from both immunity and cross immunity ##immunity_factors = cvu.compute_immunity(people.immunity_factors[strain, :], immune_time, cross_immune_time, immune_inds, cross_immune_inds, init_immunity, decay_rate, cross_immunity) @@ -568,7 +568,7 @@ def step(self): beta_layer = cvd.default_float(self['beta_layer'][lkey]) rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, rec, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors) - rel_sus = np.float32(rel_sus) # TODO: why doesn't this get returned in this format already? + # rel_sus = np.float32(rel_sus) # TODO: why doesn't this get returned in this format already? # Calculate actual transmission for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index c5e1f2b2a..1176d299c 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -16,23 +16,34 @@ def test_multistrains(do_plot=False, do_show=True, do_save=False): immunity_to = 0 immunity_from = 0 - init_immunity = 0 - n_imports = 20 - days = 30 + init_immunity = 1 + half_life = 180 + n_imports = 100 + day = 30 - imports = cv.import_strain(days=days, beta=0.035, n_imports=n_imports, immunity_to=immunity_to, immunity_from=immunity_from, - init_immunity=init_immunity) + imports = cv.import_strain(days=day, beta=0.05, n_imports=n_imports, immunity_to=immunity_to, + immunity_from=immunity_from, init_immunity=init_immunity, half_life=half_life) strain_labels = [ 'Strain 1: beta 0.016', - f'Strain 2: beta 0.035 on day {days}, {immunity_to}% to A, {immunity_from}% from A' + f'Strain 2: beta 0.05 on day {day}, {immunity_to}% to A, {immunity_from}% from A' ] - sim = cv.Sim(interventions=imports) + pars = { + 'n_days': 150, + 'beta': [0.016], + 'init_immunity': 1.0, + 'init_half_life': 180 + } + + sim = cv.Sim( + pars=pars, + interventions=imports + ) sim.run() if do_plot: - plot_results(sim, key='incidence_by_strain', title='Multiple strains', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_results(sim, key='incidence_by_strain', title='2 strains, 0 init', labels=strain_labels, do_show=do_show, do_save=do_save) return sim From 55efe7b41a017ea68d8f04d6536f7f929074d4fc Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 10 Feb 2021 20:46:32 -0500 Subject: [PATCH 044/569] HELP! Something is totally messed up on line 768 of sim.py --- covasim/people.py | 21 ++++----------------- covasim/sim.py | 12 ++++++------ covasim/utils.py | 6 +++--- tests/devtests/test_variants.py | 29 ++++++++++++++++------------- 4 files changed, 29 insertions(+), 39 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index 4014508f7..4da4dbf2a 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -54,15 +54,6 @@ def __init__(self, pars, strict=True, **kwargs): self.init_contacts() # Initialize the contacts self.infection_log = [] # Record of infections - keys for ['source','target','date','layer'] - self.strain_specific_pars = [ - 'rel_trans', - 'rel_sus', - 'time_of_last_inf', - 'immunity_factors', - 'exposed_by_strain', - 'infectious_by_strain', - ] - # Set person properties -- all floats except for UID for key in self.meta.person: if key == 'uid': @@ -78,7 +69,7 @@ def __init__(self, pars, strict=True, **kwargs): for key in self.meta.states: if key == 'susceptible': self[key] = np.full(self.pop_size, True, dtype=bool) - elif key in ['exposed_strain','infectious_strain','recovered_strain']: + elif key in ['exposed_strain','infectious_strain', 'recovered_strain']: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) elif key == 'infectious_by_strain' or key == 'exposed_by_strain': self[key] = np.full((self.pars['max_strains'], self.pop_size), False, dtype=bool, order='F') @@ -239,10 +230,6 @@ def check_infectious(self): inds = self.check_inds(self.infectious, self.date_infectious, filter_inds=self.is_exp) self.infectious[inds] = True self.infectious_strain[inds] = self.exposed_strain[inds] - strains, counts = np.unique(self.infectious_strain[~np.isnan(self.infectious_strain)], return_counts=True) - if len(strains)>0: - for strain, _ in enumerate(strains): - self.flows['new_infections_by_strain'][strain] += counts[strain] return len(inds) @@ -269,13 +256,13 @@ def check_critical(self): def check_recovery(self): ''' Check for recovery ''' - inds = self.check_inds(self.recovered, self.date_recovered, filter_inds=self.is_exp) + inds = self.check_inds(self.susceptible, self.date_recovered, filter_inds=self.is_exp) self.exposed[inds] = False self.infectious[inds] = False self.symptomatic[inds] = False self.severe[inds] = False self.critical[inds] = False - self.recovered[inds] = True + self.susceptible[inds] = True self.recovered_strain[inds] = self.infectious_strain[inds] # TODO: check that this works self.infectious_strain[inds] = np.nan return len(inds) @@ -289,7 +276,7 @@ def check_death(self): self.symptomatic[inds] = False self.severe[inds] = False self.critical[inds] = False - self.recovered[inds] = False + self.susceptible[inds] = False self.dead[inds] = True return len(inds) diff --git a/covasim/sim.py b/covasim/sim.py index 042f68d32..39e80b36e 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -515,7 +515,6 @@ def step(self): asymp_factor = cvd.default_float(self['asymp_factor']) sus = people.susceptible inf = people.infectious - rec = people.recovered # Both susceptible and recovered people can get reinfected symp = people.symptomatic diag = people.diagnosed quar = people.quarantined @@ -566,9 +565,9 @@ def step(self): iso_factor = cvd.default_float(self['iso_factor'][lkey]) quar_factor = cvd.default_float(self['quar_factor'][lkey]) beta_layer = cvd.default_float(self['beta_layer'][lkey]) - rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, rec, beta_layer, viral_load, symp, + rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors) - # rel_sus = np.float32(rel_sus) # TODO: why doesn't this get returned in this format already? + rel_sus = np.float32(rel_sus) # TODO: why doesn't this get returned in this format already? # Calculate actual transmission for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 @@ -708,6 +707,8 @@ def finalize(self, verbose=None, restore_pars=True): if 'by_strain' in reskey: self.results[reskey].values = np.rot90(self.results[reskey].values) self.results[reskey].values = np.einsum('ij,i->ij',self.results[reskey].values,self.rescale_vec) + self.results[reskey].values = np.flipud(self.results[reskey].values) + self.results[reskey].values = np.rot90(self.results[reskey].values) else: self.results[reskey].values *= self.rescale_vec @@ -764,9 +765,8 @@ def compute_prev_inci(self): self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence - self.results['incidence_by_strain'][:] = np.rot90(np.einsum('ij,i->ij',res['new_infections_by_strain'][:],1/res['n_susceptible'][:])) # Calculate the incidence - self.results['prevalence_by_strain'][:] = np.rot90(np.einsum('ij,i->ij',res['n_exposed_by_strain'][:], 1/res['n_alive'][:])) # Calculate the prevalence - + self.results['incidence_by_strain'][:] = np.einsum('ij,i->ij',res['new_infections_by_strain'][:], 1/res['n_susceptible'][:]) # Calculate the incidence + self.results['prevalence_by_strain'][:] = np.einsum('ij,i->ij',res['new_infections_by_strain'][:], 1/res['n_alive'][:]) # Calculate the prevalence return diff --git a/covasim/utils.py b/covasim/utils.py index 9d0de9aca..ade8663eb 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -89,14 +89,14 @@ def compute_immunity(immunity_factors, immune_time, cross_immune_time, immune_in return immunity_factors -@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=parallel) -def compute_trans_sus(rel_trans, rel_sus, inf, sus, rec, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors): # pragma: no cover +# @nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=parallel) +def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] f_iso = ~diag + diag * iso_factor # Isolation factor, changes e.g. [0,1] with a factor of 0.2 to [1,0.2] f_quar = ~quar + quar * quar_factor # Quarantine, changes e.g. [0,1] with a factor of 0.5 to [1,0.5] rel_trans = beta_layer * rel_trans * inf * f_quar * f_asymp * f_iso * viral_load # Recalculate transmissibility - rel_sus = rel_sus * sus * f_quar + rec * (1.-immunity_factors) # Recalculate susceptibility + rel_sus = rel_sus * sus * f_quar *(1.-immunity_factors) # Recalculate susceptibility return rel_trans, rel_sus diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 1176d299c..ce49bd77a 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -14,36 +14,39 @@ def test_multistrains(do_plot=False, do_show=True, do_save=False): sc.heading('Setting up...') - immunity_to = 0 - immunity_from = 0 - init_immunity = 1 - half_life = 180 - n_imports = 100 - day = 30 - - imports = cv.import_strain(days=day, beta=0.05, n_imports=n_imports, immunity_to=immunity_to, + immunity_to = [0,[0,0]] + immunity_from = [0,[0,0]] + init_immunity = [1,1] + half_life = [180, 180] + n_imports = [30, 50] + betas = [0.035, 0.05] + day = [30, 40] + + imports = cv.import_strain(days=day, beta=betas, n_imports=n_imports, immunity_to=immunity_to, immunity_from=immunity_from, init_immunity=init_immunity, half_life=half_life) strain_labels = [ 'Strain 1: beta 0.016', - f'Strain 2: beta 0.05 on day {day}, {immunity_to}% to A, {immunity_from}% from A' + f'Strain 2: beta 0.035 on day {day[0]}, {immunity_to[0]}% to A, {immunity_from[0]}% from A', + f'Strain 3: beta 0.05 on day {day[1]}, {immunity_to[1]}% to A, {immunity_from[1]}% from A' + ] pars = { 'n_days': 150, 'beta': [0.016], - 'init_immunity': 1.0, - 'init_half_life': 180 + 'init_immunity': 1, + 'init_half_life': 50 } sim = cv.Sim( pars=pars, - interventions=imports + # interventions=imports ) sim.run() if do_plot: - plot_results(sim, key='incidence_by_strain', title='2 strains, 0 init', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_results(sim, key='incidence_by_strain', title='1 strain, no immunity', labels=strain_labels, do_show=do_show, do_save=do_save) return sim From b9f7ed3aeb308775bddbc60772a79a0433910e3f Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 10 Feb 2021 21:06:58 -0500 Subject: [PATCH 045/569] changed calculaton of n_susceptible --- covasim/sim.py | 7 ++++--- tests/devtests/test_variants.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 39e80b36e..203425f4b 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -762,11 +762,12 @@ def compute_prev_inci(self): ''' res = self.results self.results['n_alive'][:] = self.scaled_pop_size - res['cum_deaths'][:] # Number of people still alive - self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents + # self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents + self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] # Recalculate the number of susceptible people, not agents self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence - self.results['incidence_by_strain'][:] = np.einsum('ij,i->ij',res['new_infections_by_strain'][:], 1/res['n_susceptible'][:]) # Calculate the incidence - self.results['prevalence_by_strain'][:] = np.einsum('ij,i->ij',res['new_infections_by_strain'][:], 1/res['n_alive'][:]) # Calculate the prevalence + self.results['incidence_by_strain'][:] = np.einsum('ji,i->ji',res['new_infections_by_strain'][:], 1/res['n_susceptible'][:]) # Calculate the incidence + self.results['prevalence_by_strain'][:] = np.einsum('ji,i->ji',res['new_infections_by_strain'][:], 1/res['n_alive'][:]) # Calculate the prevalence return diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index ce49bd77a..83d5afe1f 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -33,7 +33,7 @@ def test_multistrains(do_plot=False, do_show=True, do_save=False): ] pars = { - 'n_days': 150, + 'n_days': 80, 'beta': [0.016], 'init_immunity': 1, 'init_half_life': 50 @@ -148,7 +148,7 @@ def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): # extract data for plotting x = sim.results['t'] y = results_to_plot.values - y = np.flip(y, 1) + y = np.flipud(y) fig, ax = plt.subplots() ax.plot(x, y) From be6b03c7a4484e33362b9d148b901485c8bc55a6 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Thu, 11 Feb 2021 11:52:48 +0100 Subject: [PATCH 046/569] add reinefections to results NEEDS CHECKING --- covasim/defaults.py | 96 +++++++++++++++++---------------- covasim/parameters.py | 8 +-- covasim/people.py | 23 +++++--- covasim/sim.py | 1 + tests/devtests/test_variants.py | 33 ++++++++++-- 5 files changed, 102 insertions(+), 59 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 17e5b93a2..fbf85e6a4 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -58,27 +58,31 @@ class PeopleMeta(sc.prettyobj): 'susceptible', 'exposed', 'infectious', - 'exposed_strain', - 'exposed_by_strain', - 'infectious_strain', - 'infectious_by_strain', - 'recovered_strain', - #'recovered_by_strain', 'symptomatic', 'severe', 'critical', 'tested', 'diagnosed', - 'recovered', + #'recovered', 'dead', 'known_contact', 'quarantined', ] + strain_states = [ + 'exposed_strain', + 'exposed_by_strain', + 'infectious_strain', + 'infectious_by_strain', + 'recovered_strain', + # 'recovered_by_strain', + ] + # Set the dates various events took place: these are floats per person -- used in people.py dates = [f'date_{state}' for state in states] # Convert each state into a date dates.append('date_pos_test') # Store the date when a person tested which will come back positive dates.append('date_end_quarantine') # Store the date when a person comes out of quarantine + dates.append('date_recovered') # Store the date when a person recovers # Duration of different states: these are floats per person -- used in people.py durs = [ @@ -89,38 +93,39 @@ class PeopleMeta(sc.prettyobj): 'dur_disease', ] - all_states = person + states + dates + durs + all_states = person + states + strain_states + dates + durs #%% Define other defaults # A subset of the above states are used for results result_stocks = { - 'susceptible': 'Number susceptible', - 'exposed': 'Number exposed', - 'exposed_by_strain': 'Number exposed by strain', - 'infectious': 'Number infectious', + 'susceptible': 'Number susceptible', + 'exposed': 'Number exposed', + 'exposed_by_strain': 'Number exposed by strain', + 'infectious': 'Number infectious', 'infectious_by_strain': 'Number infectious by strain', - 'symptomatic': 'Number symptomatic', - 'severe': 'Number of severe cases', - 'critical': 'Number of critical cases', - 'diagnosed': 'Number of confirmed cases', - 'quarantined': 'Number in quarantine', + 'symptomatic': 'Number symptomatic', + 'severe': 'Number of severe cases', + 'critical': 'Number of critical cases', + 'diagnosed': 'Number of confirmed cases', + 'quarantined': 'Number in quarantine', } # The types of result that are counted as flows -- used in sim.py; value is the label suffix -result_flows = {'infections': 'infections', +result_flows = {'infections': 'infections', + 'reinfections': 'reinfections', 'infections_by_strain': 'infections_by_strain', - 'infectious': 'infectious', + 'infectious': 'infectious', 'infectious_by_strain': 'infectious_by_strain', - 'tests': 'tests', - 'diagnoses': 'diagnoses', - 'recoveries': 'recoveries', - 'symptomatic': 'symptomatic cases', - 'severe': 'severe cases', - 'critical': 'critical cases', - 'deaths': 'deaths', - 'quarantined': 'quarantined people', + 'tests': 'tests', + 'diagnoses': 'diagnoses', + 'recoveries': 'recoveries', + 'symptomatic': 'symptomatic cases', + 'severe': 'severe cases', + 'critical': 'critical cases', + 'deaths': 'deaths', + 'quarantined': 'quarantined people', } # Define these here as well @@ -158,24 +163,25 @@ def get_colors(): NB, includes duplicates since stocks and flows are named differently. ''' colors = sc.objdict( - susceptible = '#5e7544', - infectious = '#c78f65', - infectious_by_strain ='#c78f65', - infections = '#c75649', - infections_by_strain='#c78f65', - exposed = '#c75649', # Duplicate - exposed_by_strain ='#c75649', # Duplicate - tests = '#aaa8ff', - diagnoses = '#8886cc', - diagnosed = '#8886cc', # Duplicate - recoveries = '#799956', - recovered = '#799956', # Duplicate - symptomatic = '#c1ad71', - severe = '#c1981d', - quarantined = '#5f1914', - critical = '#b86113', - deaths = '#000000', - dead = '#000000', # Duplicate + susceptible = '#5e7544', + infectious = '#c78f65', + infectious_by_strain = '#c78f65', + infections = '#c75649', + reinfections = '#732e26', + infections_by_strain = '#c78f65', + exposed = '#c75649', # Duplicate + exposed_by_strain = '#c75649', # Duplicate + tests = '#aaa8ff', + diagnoses = '#8886cc', + diagnosed = '#8886cc', # Duplicate + recoveries = '#799956', +# recovered = '#799956', # Duplicate + symptomatic = '#c1ad71', + severe = '#c1981d', + quarantined = '#5f1914', + critical = '#b86113', + deaths = '#000000', + dead = '#000000', # Duplicate ) return colors diff --git a/covasim/parameters.py b/covasim/parameters.py index e1a997603..ac50b497a 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -364,15 +364,15 @@ def update_immunity(pars, create, new_pars=None, update_strain=None, immunity_fr immunity_from (array): used when adding a new strain; specifies the immunity protection against existing strains from having the new strain immunity_to (array): used when adding a strain; specifies the immunity protection against the new strain from having one of the existing strains - **Example 1**: + **Example 1**: #TODO NEEDS UPDATING # Adding a strain C to the example above. Strain C gives perfect immunity against strain A # and 90% immunity against strain B. People who've had strain A have 50% immunity to strain C, # and people who've had strain B have 70% immunity to strain C cross_immunity = update_cross_immunity(pars, update_strain=2, immunity_from=[1. 0.9], immunity_to=[0.5, 0.7]) A B C ... - array([[nan, 1.0, 0.5 ...], - [0.4, nan, 0.7 ...], - [1.0, 0.9, nan ...], + array([[1.0, 1.0, 0.5 ...], + [0.4, 1.0, 0.7 ...], + [1.0, 0.9, 1.0 ...], ..., ..., ..., ...]) ''' diff --git a/covasim/people.py b/covasim/people.py index 4da4dbf2a..1131d9f96 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -69,13 +69,16 @@ def __init__(self, pars, strict=True, **kwargs): for key in self.meta.states: if key == 'susceptible': self[key] = np.full(self.pop_size, True, dtype=bool) - elif key in ['exposed_strain','infectious_strain', 'recovered_strain']: - self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) - elif key == 'infectious_by_strain' or key == 'exposed_by_strain': - self[key] = np.full((self.pars['max_strains'], self.pop_size), False, dtype=bool, order='F') else: self[key] = np.full(self.pop_size, False, dtype=bool) + # Set strain states, which store info about which strain a person is exposed to + for key in self.meta.strain_states: + if 'by' in key: + self[key] = np.full((self.pars['max_strains'], self.pop_size), False, dtype=bool, order='F') + else: + self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) + # Set dates and durations -- both floats for key in self.meta.dates + self.meta.durs: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -170,7 +173,7 @@ def update_states_pre(self, t): self.flows['new_severe'] += self.check_severe() self.flows['new_critical'] += self.check_critical() self.flows['new_deaths'] += self.check_death() - self.flows['new_recoveries'] += self.check_recovery() + self.flows['new_recoveries'] += self.check_recovery() # TODO: check logic here return @@ -341,6 +344,12 @@ def make_susceptible(self, inds): else: self[key][inds] = False + for key in self.meta.strain_states: + if 'by' in key: + self[key][:, inds] = False + else: + self[key][inds] = np.nan + for key in self.meta.dates + self.meta.durs: self[key][inds] = np.nan @@ -381,7 +390,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str n_infections = len(inds) durpars = self.pars['dur'] - # Set states + # Update states, strain info, and flows self.susceptible[inds] = False self.exposed[inds] = True self.exposed_strain[inds] = strain @@ -389,6 +398,8 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.date_exposed[inds] = self.t self.flows['new_infections'] += len(inds) self.flows['new_infections_by_strain'][strain] += len(inds) + self.flows['new_reinfections'] += len(cvu.defined(self.date_recovered[inds])) # Record reinfections + self.date_recovered[inds] = np.nan # Reset date they recovered - we only store the last recovery # Record transmissions for i, target in enumerate(inds): diff --git a/covasim/sim.py b/covasim/sim.py index 203425f4b..2bf152637 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -544,6 +544,7 @@ def step(self): cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by the cross strain cross_immune_inds = cvd.default_int(cvu.true(cross_immune)) # People with some immunity to this strain from a prior infection with another strain cross_immunity = cvd.default_float(self['immunity'][cross_strain, strain]) # Immunity protection again this strain from other strains + # TODO cross immunity not working? immunity_factors[cross_immune_inds] = cross_immunity * np.exp(-decay_rate * cross_immune_time) # Calculate cross-immunity factors # Compute protection factors from both immunity and cross immunity diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 83d5afe1f..d68830a56 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -4,9 +4,32 @@ import numpy as np -do_plot = 1 -do_show = 1 -do_save = 0 +do_plot = 0 +do_show = 0 +do_save = 1 + + +def test_2strains(do_plot=False, do_show=True, do_save=False): + sc.heading('Run basic sim with 2 strains') + + sc.heading('Setting up...') + + pars = { + 'n_days': 80, + 'beta': [0.016, 0.035], + 'n_strains': 2, + #'init_immunity': [1, 1], + #'init_half_life': [50, 50], + } + + sim = cv.Sim(pars=pars) + sim.run() + + if do_plot: + plot_results(sim, key='incidence_by_strain', title='1 strain, no immunity', labels=strain_labels, do_show=do_show, do_save=do_save) + return sim + + def test_multistrains(do_plot=False, do_show=True, do_save=False): @@ -160,6 +183,7 @@ def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): for strain in range(len(y[0])): labels[strain] = f'Strain {strain +1}' ax.legend(labels) + if do_show: plt.show() if do_save: @@ -173,7 +197,8 @@ def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): if __name__ == '__main__': sc.tic() - sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim0 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_importstrain_args() From 338b354692d03271c6f95735a9d1adef29257248 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Thu, 11 Feb 2021 15:31:25 +0100 Subject: [PATCH 047/569] change initialisation of immunity, possibly to no avail --- covasim/base.py | 12 ++-- covasim/parameters.py | 104 ++++++++------------------------ covasim/sim.py | 6 +- tests/devtests/test_variants.py | 26 +++++--- 4 files changed, 54 insertions(+), 94 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 7c5968b85..4036224d4 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -250,7 +250,7 @@ def _brief(self): return string - def update_pars(self, pars=None, create=False, **kwargs): + def update_pars(self, pars=None, create=False, immunity_pars=None, **kwargs): ''' Ensure that metaparameters get used properly before being updated ''' pars = sc.mergedicts(pars, kwargs) if pars: @@ -258,10 +258,12 @@ def update_pars(self, pars=None, create=False, **kwargs): cvpar.reset_layer_pars(pars, force=False) if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses - if create: - pars['immunity'] = cvpar.update_immunity(pars=pars, create=create) # Set immunity - else: - self.pars['immunity'] = cvpar.update_immunity(pars=self.pars, create=create, new_pars=pars) # update with any provided params + if pars.get('n_strains') and pars['n_strains']>1: + pars = sc.mergedicts(pars, immunity_pars) + pars = cvpar.update_immunity(pars) # Update immunity + # else: + # self.pars['immunity'] = cvpar.update_immunity(pars=self.pars, create=set_immunity, new_pars=pars) # update with any provided params + super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/parameters.py b/covasim/parameters.py index ac50b497a..a807743e0 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -111,7 +111,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): reset_layer_pars(pars) if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses - + pars = initialise_immunity(pars) # Initialise immunity # If version is specified, load old parameters if version is not None: @@ -286,67 +286,24 @@ def absolute_prognoses(prognoses): return out -def update_cross_immunity(pars, update_strain=None, immunity_from=None, immunity_to=None): +def initialise_immunity(pars): ''' - Helper function to set the cross-immunity matrix. - Matrix is of size sim['max_strains']*sim['max_strains'] and takes the form: - A B ... - array([[nan, 1.0, ...], - [0.4, nan, ...], - ..., ..., ...]) - ... meaning that people who've had strand A have perfect protection against strand B, but - people who've had strand B have an initial 40% protection against getting strand A. - The matrix has nan entries outside of any active strains. - - Args: - pars (dict): the parameters dictionary - update_strain: the index of the strain to update - immunity_from (array): used when adding a new strain; specifies the immunity protection against existing strains from having the new strain - immunity_to (array): used when adding a strain; specifies the immunity protection against the new strain from having one of the existing strains - - **Example 1**: - # Adding a strain C to the example above. Strain C gives perfect immunity against strain A - # and 90% immunity against strain B. People who've had strain A have 50% immunity to strain C, - # and people who've had strain B have 70% immunity to strain C - cross_immunity = update_cross_immunity(pars, update_strain=2, immunity_from=[1. 0.9], immunity_to=[0.5, 0.7]) - A B C ... - array([[nan, 1.0, 0.5 ...], - [0.4, nan, 0.7 ...], - [1.0, 0.9, nan ...], - ..., ..., ..., ...]) - - **Example 2**: - # Updating the immunity protection factors for strain B - cross_immunity = update_cross_immunity(pars, update_strain=1, immunity_from=[0.6 0.8], immunity_to=[0.9, 0.7]) - A B C ... - array([[nan, 0.9, 0.5 ...], - [0.6, nan, 0.8 ...], - [1.0, 0.7, nan ...], - ..., ..., ..., ...]) - - Mostly for internal use (? TBC) + Helper function to initialise the immunity and half_life matrices. + Matrix is of size sim['max_strains']*sim['max_strains'] and is initialised with default values ''' - - # Initialise if not already initialised - if pars['cross_immunity'] is None: - cross_immunity = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) - if pars['n_strains'] > 1: - for i in range(pars['n_strains']): - for j in range(pars['n_strains']): - if i != j: cross_immunity[i, j] = pars['default_cross_immunity'] - - # Update immunity for a strain if supplied - if update_strain is not None: - cross_immunity = sc.dcp(pars['cross_immunity']) - cross_immunity[update_strain, ] # TODO - - return cross_immunity + # If initial immunity/cross immunity factors are provided, use those, otherwise use defaults + pars['half_life'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) + pars['immunity'] = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) + for i in range(pars['n_strains']): + pars['half_life'][i] = pars['default_half_life'] + pars['immunity'][i, i] = pars['default_immunity'] + return pars -def update_immunity(pars, create, new_pars=None, update_strain=None, immunity_from=None, immunity_to=None, +def update_immunity(pars, create=True, new_pars=None, update_strain=None, immunity_from=None, immunity_to=None, init_immunity=None, half_life=None): ''' - Helper function to set the immunity and half_life matrices. + Helper function to update the immunity and half_life matrices. Matrix is of size sim['max_strains']*sim['max_strains'] and takes the form: A B ... array([[1.0, 1.0, ...], @@ -376,28 +333,19 @@ def update_immunity(pars, create, new_pars=None, update_strain=None, immunity_fr ..., ..., ..., ...]) ''' - # If initial immunity/cross immunity factors are provided, use those, otherwise use defaults if create: - pars['half_life'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - immunity = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) - for i in range(pars['n_strains']): - pars['half_life'][i] = pars['default_half_life'] - for j in range(pars['n_strains']): - if i != j: - immunity[i, j] = pars['default_cross_immunity'] - else: - immunity[i, j] = pars['default_immunity'] - else: - if new_pars is not None: - immunity = pars['immunity'] - if 'init_immunity' in new_pars.keys(): - for i in range(pars['n_strains']): - for j in range(pars['n_strains']): - if i == j: - immunity[i, j] = new_pars['init_immunity'] - - if 'init_half_life' in new_pars.keys(): - pars['half_life'][0] = new_pars['init_half_life'] + # Cross immunity values are set if there is more than one strain circulating + pars = initialise_immunity(pars) + ns = pars['n_strains'] # Shorten + + # Update own-immunity and half lives, if values have been supplied + if pars.get('init_half_life'): # Values have been supplied for the half lives + pars['half_life'][:ns] = pars['init_half_life'] + pars['immunity'][:ns, :ns] = pars['default_cross_immunity'] + if pars.get('init_immunity'): # Values have been supplied for own-immunity + np.fill_diagonal(pars['immunity'][:ns,:ns], pars['init_immunity']) + else: + np.fill_diagonal(pars['immunity'][:ns,:ns], pars['default_immunity']) # Update immunity for a strain if supplied if update_strain is not None: @@ -434,4 +382,4 @@ def update_immunity(pars, create, new_pars=None, update_strain=None, immunity_fr immunity[update_strain, :] = new_immunity_row immunity[:, update_strain] = new_immunity_column - return immunity + return pars diff --git a/covasim/sim.py b/covasim/sim.py index 2bf152637..bfc465f77 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -74,7 +74,11 @@ def __init__(self, pars=None, datafile=None, datacols=None, label=None, simfile= # Now update everything self.set_metadata(simfile) # Set the simulation date and filename - self.update_pars(pars, **kwargs) # Update the parameters, if provided + if pars.get('n_strains') and pars['n_strains']>1: + immunity_pars = dict(max_strains=default_pars['max_strains'], default_half_life=default_pars['default_half_life'], default_immunity=default_pars['default_immunity'], default_cross_immunity=default_pars['default_cross_immunity']) + else: + immunity_pars = None + self.update_pars(pars, immunity_pars=immunity_pars, **kwargs) # Update the parameters, if provided self.load_data(datafile, datacols) # Load the data, if provided if self.load_pop: self.load_population(popfile) # Load the population, if provided diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index d68830a56..ec90c8db8 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -4,7 +4,7 @@ import numpy as np -do_plot = 0 +do_plot = 1 do_show = 0 do_save = 1 @@ -16,23 +16,29 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): pars = { 'n_days': 80, - 'beta': [0.016, 0.035], + 'beta': [0.015, 0.03], 'n_strains': 2, - #'init_immunity': [1, 1], - #'init_half_life': [50, 50], + 'init_immunity': [0.9, 0.9], + 'init_half_life': [20, 180], # Rapidly waning immunity from the less infections strain A } sim = cv.Sim(pars=pars) + sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B sim.run() + strain_labels = [ + f'Strain A: beta {pars["beta"][0]}, half_life {pars["init_half_life"][0]}', + f'Strain B: beta {pars["beta"][1]}, half_life {pars["init_half_life"][1]}', + ] + if do_plot: - plot_results(sim, key='incidence_by_strain', title='1 strain, no immunity', labels=strain_labels, do_show=do_show, do_save=do_save) + sim.plot_result('cum_reinfections', do_show=do_show, do_save=do_save) + # TODO: using the following line seems to flip the results??? + plot_results(sim, key='cum_reinfections', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}', labels=strain_labels, do_show=do_show, do_save=do_save) return sim - - -def test_multistrains(do_plot=False, do_show=True, do_save=False): +def test_2strains_import(do_plot=False, do_show=True, do_save=False): sc.heading('Run basic sim with an imported strain') sc.heading('Setting up...') @@ -64,7 +70,7 @@ def test_multistrains(do_plot=False, do_show=True, do_save=False): sim = cv.Sim( pars=pars, - # interventions=imports + interventions=imports ) sim.run() @@ -198,7 +204,7 @@ def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): sc.tic() sim0 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim1 = test_multistrains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim1 = test_2strains_import(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_importstrain_args() From a677f2aa8335472995cc43075c4a44208140490c Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 11 Feb 2021 11:24:30 -0500 Subject: [PATCH 048/569] want to be able to update immunity matrix even if running single strain --- covasim/base.py | 20 ++++++++------------ covasim/parameters.py | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 4036224d4..7077883df 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -116,12 +116,12 @@ def update_pars(self, pars=None, create=False): raise sc.KeyNotFoundError(errormsg) self.pars.update(pars) - if 'n_strains' in pars.keys(): - # check that length of beta is same as length of strains (there is a beta for each strain) - if 'beta' not in pars.keys(): - raise ValueError(f'You supplied strains without betas for each strain') - else: - self.pars['beta'] = pars['beta'] + # if 'n_strains' in pars.keys(): + # # check that length of beta is same as length of strains (there is a beta for each strain) + # if 'beta' not in pars.keys(): + # raise ValueError(f'You supplied strains without betas for each strain') + # else: + # self.pars['beta'] = pars['beta'] return @@ -258,12 +258,8 @@ def update_pars(self, pars=None, create=False, immunity_pars=None, **kwargs): cvpar.reset_layer_pars(pars, force=False) if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses - if pars.get('n_strains') and pars['n_strains']>1: - pars = sc.mergedicts(pars, immunity_pars) - pars = cvpar.update_immunity(pars) # Update immunity - # else: - # self.pars['immunity'] = cvpar.update_immunity(pars=self.pars, create=set_immunity, new_pars=pars) # update with any provided params - + pars = sc.mergedicts(pars, immunity_pars) + pars = cvpar.update_immunity(pars) # Update immunity with values provided super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/parameters.py b/covasim/parameters.py index a807743e0..cc43eef34 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -300,7 +300,7 @@ def initialise_immunity(pars): return pars -def update_immunity(pars, create=True, new_pars=None, update_strain=None, immunity_from=None, immunity_to=None, +def update_immunity(pars, create=True, update_strain=None, immunity_from=None, immunity_to=None, init_immunity=None, half_life=None): ''' Helper function to update the immunity and half_life matrices. From 12e7f52a7adcd68ccca8913b6a375a08c2431c4f Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 11 Feb 2021 14:46:56 -0500 Subject: [PATCH 049/569] intervention working --- covasim/parameters.py | 14 +++++---- covasim/sim.py | 15 +++++----- tests/devtests/test_variants.py | 50 +++++++++++++++++---------------- 3 files changed, 42 insertions(+), 37 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index cc43eef34..7fcdb44cb 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -111,7 +111,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): reset_layer_pars(pars) if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses - pars = initialise_immunity(pars) # Initialise immunity + pars = initialize_immunity(pars) # Initialise immunity # If version is specified, load old parameters if version is not None: @@ -286,10 +286,10 @@ def absolute_prognoses(prognoses): return out -def initialise_immunity(pars): +def initialize_immunity(pars): ''' - Helper function to initialise the immunity and half_life matrices. - Matrix is of size sim['max_strains']*sim['max_strains'] and is initialised with default values + Helper function to initialize the immunity and half_life matrices. + Matrix is of size sim['max_strains']*sim['max_strains'] and is initialized with default values ''' # If initial immunity/cross immunity factors are provided, use those, otherwise use defaults pars['half_life'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) @@ -335,7 +335,10 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i ''' if create: # Cross immunity values are set if there is more than one strain circulating - pars = initialise_immunity(pars) + # if 'n_strains' isn't provided, assume it's 1 + if not pars.get('n_strains'): + pars['n_strains'] = 1 + pars = initialize_immunity(pars) ns = pars['n_strains'] # Shorten # Update own-immunity and half lives, if values have been supplied @@ -381,5 +384,6 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i immunity[update_strain, :] = new_immunity_row immunity[:, update_strain] = new_immunity_column + pars['immunity'] = immunity return pars diff --git a/covasim/sim.py b/covasim/sim.py index bfc465f77..0fbe8079a 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -74,10 +74,9 @@ def __init__(self, pars=None, datafile=None, datacols=None, label=None, simfile= # Now update everything self.set_metadata(simfile) # Set the simulation date and filename - if pars.get('n_strains') and pars['n_strains']>1: - immunity_pars = dict(max_strains=default_pars['max_strains'], default_half_life=default_pars['default_half_life'], default_immunity=default_pars['default_immunity'], default_cross_immunity=default_pars['default_cross_immunity']) - else: - immunity_pars = None + immunity_pars = dict(max_strains=default_pars['max_strains'], default_half_life=default_pars['default_half_life'], + default_immunity=default_pars['default_immunity'], + default_cross_immunity=default_pars['default_cross_immunity']) self.update_pars(pars, immunity_pars=immunity_pars, **kwargs) # Update the parameters, if provided self.load_data(datafile, datacols) # Load the data, if provided if self.load_pop: @@ -710,10 +709,10 @@ def finalize(self, verbose=None, restore_pars=True): self.results[reskey].values = self.results[reskey].values[:self['n_strains'], :] if self.results[reskey].scale: # Scale the result dynamically if 'by_strain' in reskey: - self.results[reskey].values = np.rot90(self.results[reskey].values) - self.results[reskey].values = np.einsum('ij,i->ij',self.results[reskey].values,self.rescale_vec) - self.results[reskey].values = np.flipud(self.results[reskey].values) - self.results[reskey].values = np.rot90(self.results[reskey].values) + # self.results[reskey].values = np.rot90(self.results[reskey].values) + self.results[reskey].values = np.einsum('ij,j->ij',self.results[reskey].values,self.rescale_vec) + # self.results[reskey].values = np.flipud(self.results[reskey].values) + # self.results[reskey].values = np.rot90(self.results[reskey].values) else: self.results[reskey].values *= self.rescale_vec diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index ec90c8db8..9e64e0cf2 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -5,8 +5,8 @@ do_plot = 1 -do_show = 0 -do_save = 1 +do_show = 1 +do_save = 0 def test_2strains(do_plot=False, do_show=True, do_save=False): @@ -16,14 +16,15 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): pars = { 'n_days': 80, - 'beta': [0.015, 0.03], + 'beta': [0.015, 0.025], 'n_strains': 2, - 'init_immunity': [0.9, 0.9], - 'init_half_life': [20, 180], # Rapidly waning immunity from the less infections strain A + 'init_immunity': [1, 1], + 'init_half_life': [30, 30], # Rapidly waning immunity from the less infections strain A } sim = cv.Sim(pars=pars) sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B + sim['immunity'][1,0] = 1.0 # Say that strain B gives perfect immunity to strain A sim.run() strain_labels = [ @@ -32,9 +33,12 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): ] if do_plot: - sim.plot_result('cum_reinfections', do_show=do_show, do_save=do_save) + sim.plot_result('new_reinfections', do_show=do_show, do_save=do_save) # TODO: using the following line seems to flip the results??? - plot_results(sim, key='cum_reinfections', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}', labels=strain_labels, do_show=do_show, do_save=do_save) + # plot_results(sim, key='cum_reinfections', + # title=f'2 strain test, A->B immunity {sim["immunity"][0, 1]}, B->A immunity {sim["immunity"][1, 0]}', + # labels=strain_labels, do_show=do_show, do_save=do_save) + plot_results(sim, key='incidence_by_strain', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}, B->A immunity {sim["immunity"][1,0]}', labels=strain_labels, do_show=do_show, do_save=do_save) return sim @@ -43,24 +47,17 @@ def test_2strains_import(do_plot=False, do_show=True, do_save=False): sc.heading('Setting up...') - immunity_to = [0,[0,0]] - immunity_from = [0,[0,0]] - init_immunity = [1,1] - half_life = [180, 180] - n_imports = [30, 50] - betas = [0.035, 0.05] - day = [30, 40] + immunity_to = [0] # Say that strain A gives no immunity to strain B + immunity_from = [.5] # Say that strain B gives perfect immunity to strain A + init_immunity = [1] + half_life = [20] + n_imports = [30] + betas = [0.025] + day = [10] imports = cv.import_strain(days=day, beta=betas, n_imports=n_imports, immunity_to=immunity_to, immunity_from=immunity_from, init_immunity=init_immunity, half_life=half_life) - strain_labels = [ - 'Strain 1: beta 0.016', - f'Strain 2: beta 0.035 on day {day[0]}, {immunity_to[0]}% to A, {immunity_from[0]}% from A', - f'Strain 3: beta 0.05 on day {day[1]}, {immunity_to[1]}% to A, {immunity_from[1]}% from A' - - ] - pars = { 'n_days': 80, 'beta': [0.016], @@ -68,6 +65,11 @@ def test_2strains_import(do_plot=False, do_show=True, do_save=False): 'init_half_life': 50 } + strain_labels = [ + f'Strain A: beta {pars["beta"][0]}', + f'Strain B: beta {betas[0]}, {n_imports[0]} imports on day {day[0]}', + ] + sim = cv.Sim( pars=pars, interventions=imports @@ -75,7 +77,7 @@ def test_2strains_import(do_plot=False, do_show=True, do_save=False): sim.run() if do_plot: - plot_results(sim, key='incidence_by_strain', title='1 strain, no immunity', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_results(sim, key='incidence_by_strain', title=f'imported 2 strain test, A->B immunity {immunity_to[0]}, B->A immunity {immunity_from[0]}', labels=strain_labels, do_show=do_show, do_save=do_save) return sim @@ -203,8 +205,8 @@ def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): if __name__ == '__main__': sc.tic() - sim0 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim1 = test_2strains_import(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim0 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_2strains_import(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_importstrain_args() From e71176c53c3ec19f9c32e2ee006d1265849efce0 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 15 Feb 2021 12:00:13 +0100 Subject: [PATCH 050/569] adding plotting --- tests/devtests/test_variants.py | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 9e64e0cf2..2660abfa0 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -81,6 +81,36 @@ def test_2strains_import(do_plot=False, do_show=True, do_save=False): return sim +def test_importB117(do_plot=False, do_show=True, do_save=False): + sc.heading('Run basic sim with an imported strain similar to B117') + + sc.heading('Setting up...') + + pars = { + 'n_days': 120, + 'beta': [0.016], + } + + imports = cv.import_strain(days=40, beta=0.025) + + strain_labels = [ + f'Strain A: beta {pars["beta"][0]}', + f'Strain B: beta {betas[0]}, {n_imports[0]} imports on day {day[0]}', + ] + + sim = cv.Sim( + pars=pars, + interventions=imports + ) + sim.run() + + if do_plot: + sim.plot_result('new_infections', do_show=do_show, do_save=do_save) + plot_results(sim, key='incidence_by_strain', title=f'imported 2 strain test, A->B immunity {immunity_to[0]}, B->A immunity {immunity_from[0]}', labels=strain_labels, do_show=do_show, do_save=do_save) + return sim + + + def test_importstrain_args(): sc.heading('Test flexibility of arguments for the import strain "intervention"') @@ -200,6 +230,34 @@ def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): return +def plot_shares(sim, key, title, do_show=True, do_save=False, labels=None): + + results = sim.results + results_to_plot = results[key] + + # extract data for plotting + x = sim.results['t'] + y = results_to_plot.values + y = np.flipud(y) + + fig, ax = plt.subplots() + ax.plot(x, y) + + ax.set(xlabel='Day of simulation', ylabel=results_to_plot.name, title=title) + + if labels is None: + labels = [0]*len(y[0]) + for strain in range(len(y[0])): + labels[strain] = f'Strain {strain +1}' + ax.legend(labels) + + if do_show: + plt.show() + if do_save: + cv.savefig(f'results/{title}.png') + + return + #%% Run as a script if __name__ == '__main__': From 36fee5b39694678bcd2ac7396e008bdc1b965c04 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 15 Feb 2021 12:19:01 +0100 Subject: [PATCH 051/569] something wrong with imports --- covasim/sim.py | 5 ++ tests/devtests/test_variants.py | 88 +++++++++++++++------------------ 2 files changed, 46 insertions(+), 47 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 0fbe8079a..643234f60 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -588,6 +588,11 @@ def step(self): self.results[f'n_{key}'][t] = people.count(key) # Update counts for this time step: flows + if t> + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() for key,count in people.flows.items(): if 'by_strain' in key or 'by strain' in key: for strain in range(self['n_strains']): diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 2660abfa0..58e4e2366 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -5,8 +5,8 @@ do_plot = 1 -do_show = 1 -do_save = 0 +do_show = 0 +do_save = 1 def test_2strains(do_plot=False, do_show=True, do_save=False): @@ -81,36 +81,6 @@ def test_2strains_import(do_plot=False, do_show=True, do_save=False): return sim -def test_importB117(do_plot=False, do_show=True, do_save=False): - sc.heading('Run basic sim with an imported strain similar to B117') - - sc.heading('Setting up...') - - pars = { - 'n_days': 120, - 'beta': [0.016], - } - - imports = cv.import_strain(days=40, beta=0.025) - - strain_labels = [ - f'Strain A: beta {pars["beta"][0]}', - f'Strain B: beta {betas[0]}, {n_imports[0]} imports on day {day[0]}', - ] - - sim = cv.Sim( - pars=pars, - interventions=imports - ) - sim.run() - - if do_plot: - sim.plot_result('new_infections', do_show=do_show, do_save=do_save) - plot_results(sim, key='incidence_by_strain', title=f'imported 2 strain test, A->B immunity {immunity_to[0]}, B->A immunity {immunity_from[0]}', labels=strain_labels, do_show=do_show, do_save=do_save) - return sim - - - def test_importstrain_args(): sc.heading('Test flexibility of arguments for the import strain "intervention"') @@ -201,6 +171,31 @@ def test_importstrain_nocrossimmunity(do_plot=False, do_show=True, do_save=False plot_results(sim, key='incidence_by_strain', title='Imported strains', labels=strain_labels, do_show=do_show, do_save=do_save) return sim + +def test_importB117(do_plot=False, do_show=True, do_save=False): + sc.heading('Run basic sim with an imported strain similar to B117') + + sc.heading('Setting up...') + + pars = { + 'n_days': 120, + 'beta': [0.016], + } + + imports = cv.import_strain(days=40, beta=0.025) + + sim = cv.Sim( + pars=pars, + interventions=imports + ) + sim.run() + + if do_plot: + sim.plot_result('new_infections', do_show=do_show, do_save=do_save) + plot_shares(sim, key='new_infections', title='Shares of new infections by strain', do_show=do_show, do_save=do_save) + return sim + + def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): results = sim.results @@ -233,23 +228,21 @@ def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): def plot_shares(sim, key, title, do_show=True, do_save=False, labels=None): results = sim.results - results_to_plot = results[key] + n_strains = sim.results['new_infections_by_strain'].values.shape[1] # TODO: this should be stored in the sim somewhere more intuitive! - # extract data for plotting - x = sim.results['t'] - y = results_to_plot.values - y = np.flipud(y) + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() + prop_new = {f'Strain{s}': results[key+'_by_strain'].values[1:,s]/results[key].values[1:] for s in range(n_strains)} + # extract data for plotting + x = sim.results['t'][1:] fig, ax = plt.subplots() - ax.plot(x, y) - - ax.set(xlabel='Day of simulation', ylabel=results_to_plot.name, title=title) - - if labels is None: - labels = [0]*len(y[0]) - for strain in range(len(y[0])): - labels[strain] = f'Strain {strain +1}' - ax.legend(labels) + ax.stackplot(x, prop_new.values(), + labels=prop_new.keys()) + ax.legend(loc='upper left') + ax.set_title(title) if do_show: plt.show() @@ -264,10 +257,11 @@ def plot_shares(sim, key, title, do_show=True, do_save=False, labels=None): sc.tic() # sim0 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_2strains_import(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim1 = test_2strains_import(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_importstrain_args() + sim5 = test_importB117(do_plot=do_plot, do_save=do_save, do_show=do_show) sc.toc() From 1dac39037e33ec0aa2f2145e945b81d03a32199c Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 15 Feb 2021 14:13:24 +0100 Subject: [PATCH 052/569] stuck on dimensions --- covasim/interventions.py | 1 + covasim/sim.py | 11 ++++++----- tests/devtests/test_variants.py | 7 +------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 38bfaa016..43670af18 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1193,6 +1193,7 @@ def apply(self, sim): for strain in range(self.new_strains): if sim.t == self.days[strain]: # Time to introduce this strain + # Check number of strains prev_strains = sim['n_strains'] if prev_strains + 1 > self.max_strains: diff --git a/covasim/sim.py b/covasim/sim.py index 643234f60..68931d3f0 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -588,11 +588,6 @@ def step(self): self.results[f'n_{key}'][t] = people.count(key) # Update counts for this time step: flows - if t> - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() for key,count in people.flows.items(): if 'by_strain' in key or 'by strain' in key: for strain in range(self['n_strains']): @@ -727,6 +722,12 @@ def finalize(self, verbose=None, restore_pars=True): for key in ['cum_infections','cum_infections_by_strain']: self.results[key].values += self['pop_infected']*self.rescale_vec[0] # Include initially infected people + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() + + # Final settings self.results_ready = True # Set this first so self.summary() knows to print the results self.t -= 1 # During the run, this keeps track of the next step; restore this be the final day of the sim diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 58e4e2366..f63f10927 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -182,7 +182,7 @@ def test_importB117(do_plot=False, do_show=True, do_save=False): 'beta': [0.016], } - imports = cv.import_strain(days=40, beta=0.025) + imports = cv.import_strain(days=40, beta=0.025, n_imports=10) sim = cv.Sim( pars=pars, @@ -229,11 +229,6 @@ def plot_shares(sim, key, title, do_show=True, do_save=False, labels=None): results = sim.results n_strains = sim.results['new_infections_by_strain'].values.shape[1] # TODO: this should be stored in the sim somewhere more intuitive! - - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() prop_new = {f'Strain{s}': results[key+'_by_strain'].values[1:,s]/results[key].values[1:] for s in range(n_strains)} # extract data for plotting From ec6025bc70ab5332ab570f63eb0708ed207eba36 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 15 Feb 2021 14:45:53 +0100 Subject: [PATCH 053/569] exploratory share plotting --- covasim/sim.py | 14 +++++--------- tests/devtests/test_variants.py | 22 +++++++++++++++------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 68931d3f0..17f528f11 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -722,12 +722,6 @@ def finalize(self, verbose=None, restore_pars=True): for key in ['cum_infections','cum_infections_by_strain']: self.results[key].values += self['pop_infected']*self.rescale_vec[0] # Include initially infected people - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() - - # Final settings self.results_ready = True # Set this first so self.summary() knows to print the results self.t -= 1 # During the run, this keeps track of the next step; restore this be the final day of the sim @@ -980,9 +974,11 @@ def compute_summary(self, full=None, t=None, update=True, output=False, require_ summary = sc.objdict() for key in self.result_keys(): - if len(self.results[key]) < t: - self.results[key].values = np.rot90(self.results[key].values) - summary[key] = self.results[key][t] + if 'by_strain' in key: + summary[key] = self.results[key][:,t] + # TODO: the following line rotates the results - do we need this? + #if len(self.results[key]) < t: + # self.results[key].values = np.rot90(self.results[key].values) else: summary[key] = self.results[key][t] diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index f63f10927..e5f0cff0f 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -173,8 +173,11 @@ def test_importstrain_nocrossimmunity(do_plot=False, do_show=True, do_save=False def test_importB117(do_plot=False, do_show=True, do_save=False): + ''' + The purpose of this test is to try out plotting the relative shares of infections by strain, + and see whether we get something similar to what other investigations of B117 dynamics have found + ''' sc.heading('Run basic sim with an imported strain similar to B117') - sc.heading('Setting up...') pars = { @@ -228,16 +231,21 @@ def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): def plot_shares(sim, key, title, do_show=True, do_save=False, labels=None): results = sim.results - n_strains = sim.results['new_infections_by_strain'].values.shape[1] # TODO: this should be stored in the sim somewhere more intuitive! - prop_new = {f'Strain{s}': results[key+'_by_strain'].values[1:,s]/results[key].values[1:] for s in range(n_strains)} + n_strains = sim.results['new_infections_by_strain'].values.shape[0] # TODO: this should be stored in the sim somewhere more intuitive! + prop_new = {f'Strain {s}': results[key+'_by_strain'].values[s,1:]/results[key].values[1:] for s in range(n_strains)} + num_new = {f'Strain {s}': results[key+'_by_strain'].values[s,:] for s in range(n_strains)} # extract data for plotting x = sim.results['t'][1:] - fig, ax = plt.subplots() - ax.stackplot(x, prop_new.values(), + fig, ax = plt.subplots(2,1,sharex=True) + ax[0].stackplot(x, prop_new.values(), labels=prop_new.keys()) - ax.legend(loc='upper left') - ax.set_title(title) + ax[0].legend(loc='upper left') + ax[0].set_title(title) + ax[1].stackplot(sim.results['t'], num_new.values(), + labels=num_new.keys()) + ax[1].legend(loc='upper left') + ax[1].set_title(title) if do_show: plt.show() From 3ac826b6d81a25cd8935a1502cd119dd719a97e7 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 15 Feb 2021 15:06:02 +0100 Subject: [PATCH 054/569] add parameter class - is this anything?? --- covasim/base.py | 36 ++++++++++++++++++++++++++++++++ covasim/parameters.py | 48 +++++++++++++++++++++++-------------------- covasim/sim.py | 1 + 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 7077883df..672a7e206 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -186,6 +186,42 @@ def npts(self): return len(self.values) +class Par(object): + ''' + Stores a single parameter -- by default, acts like an array. + Args: + name (str): name of this parameter, e.g. beta + by_strain (bool): whether or not the parameter varies by strain + ''' + + def __init__(self, name=None, val=None, by_strain=False): + self.name = name # Name of this parameter + self.val = val # Value of this parameter + self.by_strain = by_strain # Whether or not the parameter varies by strain + return + + def __repr__(self, *args, **kwargs): + ''' Use pretty repr, like sc.prettyobj, but displaying full values ''' + output = sc.prepr(self, use_repr=False) + return output + + def __getitem__(self, *args, **kwargs): + ''' To allow e.g. par[2] instead of par.val[5] ''' + return self.val.__getitem__(*args, **kwargs) + + def __setitem__(self, *args, **kwargs): + ''' To allow e.g. par[:] = 1 instead of par.val[:] = 1 ''' + return self.val.__setitem__(*args, **kwargs) + + def __len__(self): + ''' To allow len(par) instead of len(par.val) ''' + return len(self.val) + + @property + def n_strains(self): + return len(self.val) + + def set_metadata(obj): ''' Set standard metadata for an object ''' obj.created = sc.now() diff --git a/covasim/parameters.py b/covasim/parameters.py index 7fcdb44cb..5145ebc44 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -48,45 +48,43 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['rescale_threshold'] = 0.05 # Fraction susceptible population that will trigger rescaling if rescaling pars['rescale_factor'] = 1.2 # Factor by which the population is rescaled on each step - # Basic disease transmission - pars['beta'] = [0.016] # Beta per symptomatic contact; absolute value, calibrated + # Network parameters, generally initialized after the population has been constructed pars['contacts'] = None # The number of contacts per layer; set by reset_layer_pars() below pars['dynam_layer'] = None # Which layers are dynamic; set by reset_layer_pars() below pars['beta_layer'] = None # Transmissibility per layer; set by reset_layer_pars() below pars['n_imports'] = 0 # Average daily number of imported cases (actual number is drawn from Poisson distribution) + + # Basic disease transmission parameters pars['beta_dist'] = dict(dist='neg_binomial', par1=1.0, par2=0.45, step=0.01) # Distribution to draw individual level transmissibility; dispersion from https://www.researchsquare.com/article/rs-29548/v1 pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 - pars['n_strains'] = 1 # The number of strains currently circulating in the population - pars['max_strains'] = 30 # For allocating memory with numpy arrays - pars['default_cross_immunity'] = 0.5 # Default cross-immunity protection factor - pars['default_immunity'] = 1. # Default initial immunity - pars['default_half_life']= 180 # Default half life - pars['half_life'] = None - pars['init_immunity'] = None - pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by set_immunity() below - pars['init_half_life'] = None - - # Efficacy of protection measures - pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 - pars['iso_factor'] = None # Multiply beta by this factor for diagnosed cases to represent isolation; set by reset_layer_pars() below - pars['quar_factor'] = None # Quarantine multiplier on transmissibility and susceptibility; set by reset_layer_pars() below - pars['quar_period'] = 14 # Number of days to quarantine for; assumption based on standard policies - # Duration parameters: time for disease progression + # Parameters that control settings and defaults for multi-strain runs + pars['n_strains'] = 1 # The number of strains currently circulating in the population + pars['max_strains'] = 30 # For allocating memory with numpy arrays + pars['default_cross_immunity'] = 0.5 # Default cross-immunity protection factor + pars['default_immunity'] = 1. # Default initial immunity + pars['default_half_life'] = 180 # Default half life + pars['half_life'] = None + pars['init_immunity'] = None + pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by set_immunity() below + pars['init_half_life'] = None + + # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains + pars['beta'] = [0.016] # Beta per symptomatic contact; absolute value, calibrated + pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 pars['dur'] = {} + # Duration parameters: time for disease progression pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.6, par2=4.8) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, subtracting inf2sim duration pars['dur']['inf2sym'] = dict(dist='lognormal_int', par1=1.0, par2=0.9) # Duration from infectious to symptomatic; see Linton et al., https://doi.org/10.3390/jcm9020538 pars['dur']['sym2sev'] = dict(dist='lognormal_int', par1=6.6, par2=4.9) # Duration from symptomatic to severe symptoms; see Linton et al., https://doi.org/10.3390/jcm9020538 pars['dur']['sev2crit'] = dict(dist='lognormal_int', par1=3.0, par2=7.4) # Duration from severe symptoms to requiring ICU; see Wang et al., https://jamanetwork.com/journals/jama/fullarticle/2761044 - - # Duration parameters: time for disease recovery + # Duration parameters: time for disease recovery pars['dur']['asym2rec'] = dict(dist='lognormal_int', par1=8.0, par2=2.0) # Duration for asymptomatic people to recover; see Wölfel et al., https://www.nature.com/articles/s41586-020-2196-x pars['dur']['mild2rec'] = dict(dist='lognormal_int', par1=8.0, par2=2.0) # Duration for people with mild symptoms to recover; see Wölfel et al., https://www.nature.com/articles/s41586-020-2196-x pars['dur']['sev2rec'] = dict(dist='lognormal_int', par1=14.0, par2=2.4) # Duration for people with severe symptoms to recover, 22.6 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf pars['dur']['crit2rec'] = dict(dist='lognormal_int', par1=14.0, par2=2.4) # Duration for people with critical symptoms to recover, 22.6 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf pars['dur']['crit2die'] = dict(dist='lognormal_int', par1=6.2, par2=1.7) # Duration from critical symptoms to death, 17.8 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf - - # Severity parameters: probabilities of symptom progression + # Severity parameters: probabilities of symptom progression pars['rel_symp_prob'] = 1.0 # Scale factor for proportion of symptomatic cases pars['rel_severe_prob'] = 1.0 # Scale factor for proportion of symptomatic cases that become severe pars['rel_crit_prob'] = 1.0 # Scale factor for proportion of severe cases that become critical @@ -94,6 +92,12 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['prog_by_age'] = prog_by_age # Whether to set disease progression based on the person's age pars['prognoses'] = None # The actual arrays of prognoses by age; this is populated later + + # Efficacy of protection measures + pars['iso_factor'] = None # Multiply beta by this factor for diagnosed cases to represent isolation; set by reset_layer_pars() below + pars['quar_factor'] = None # Quarantine multiplier on transmissibility and susceptibility; set by reset_layer_pars() below + pars['quar_period'] = 14 # Number of days to quarantine for; assumption based on standard policies + # Events and interventions pars['interventions'] = [] # The interventions present in this simulation; populated by the user pars['analyzers'] = [] # Custom analysis functions; populated by the user diff --git a/covasim/sim.py b/covasim/sim.py index 17f528f11..b25e0ce1a 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -977,6 +977,7 @@ def compute_summary(self, full=None, t=None, update=True, output=False, require_ if 'by_strain' in key: summary[key] = self.results[key][:,t] # TODO: the following line rotates the results - do we need this? + # TODO: the following line rotates the results - do we need this? #if len(self.results[key]) < t: # self.results[key].values = np.rot90(self.results[key].values) else: From 9023f0992cb972cbce29854efe70e0e633c0c7c5 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 15 Feb 2021 15:12:45 +0100 Subject: [PATCH 055/569] will need a lot of work --- covasim/parameters.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 5145ebc44..0577682df 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -29,6 +29,12 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): ''' pars = {} + # Helper function to initialize parameters that are stored as Par objects + def init_par(*args, **kwargs): + ''' Initialize a single result object ''' + output = cvb.Par(*args, **kwargs) + return output + # Population parameters pars['pop_size'] = 20e3 # Number of agents, i.e., people susceptible to SARS-CoV-2 pars['pop_infected'] = 20 # Number of initial infections @@ -92,7 +98,6 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['prog_by_age'] = prog_by_age # Whether to set disease progression based on the person's age pars['prognoses'] = None # The actual arrays of prognoses by age; this is populated later - # Efficacy of protection measures pars['iso_factor'] = None # Multiply beta by this factor for diagnosed cases to represent isolation; set by reset_layer_pars() below pars['quar_factor'] = None # Quarantine multiplier on transmissibility and susceptibility; set by reset_layer_pars() below From fdf81f1798914c668b96a2ef4f7f24c3f5f46ea1 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 15 Feb 2021 16:41:57 +0100 Subject: [PATCH 056/569] still considering par class --- covasim/base.py | 12 +++++++++--- tests/devtests/test_variants.py | 25 ++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 672a7e206..870004b06 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -16,7 +16,7 @@ from .settings import options as cvo # Specify all externally visible classes this file defines -__all__ = ['ParsObj', 'Result', 'BaseSim', 'BasePeople', 'Person', 'FlexDict', 'Contacts', 'Layer'] +__all__ = ['ParsObj', 'Result', 'Par', 'BaseSim', 'BasePeople', 'Person', 'FlexDict', 'Contacts', 'Layer'] #%% Define simulation classes @@ -191,12 +191,15 @@ class Par(object): Stores a single parameter -- by default, acts like an array. Args: name (str): name of this parameter, e.g. beta + val: value(s) of the parameter - can take various forms + by_age: whether the parameter is differentiated by age + is_dist: whether the parameter is a distribution by_strain (bool): whether or not the parameter varies by strain ''' - def __init__(self, name=None, val=None, by_strain=False): + def __init__(self, name=None, val=None, by_age=False, is_dist=False, by_strain=False): self.name = name # Name of this parameter - self.val = val # Value of this parameter + self.val = sc.promotetoarray(val) # Value of this parameter self.by_strain = by_strain # Whether or not the parameter varies by strain return @@ -221,6 +224,9 @@ def __len__(self): def n_strains(self): return len(self.val) + def add_strain(self, new_val=None): + self.val = np.append(self[:], new_val) # TODO: so far this only works for 1D parameters + def set_metadata(obj): ''' Set standard metadata for an object ''' diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index e5f0cff0f..84e848559 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -199,6 +199,27 @@ def test_importB117(do_plot=False, do_show=True, do_save=False): return sim +def test_par_refactor(): + ''' + The purpose of this test is to experiment with different representations of the parameter structures + Still WIP! + ''' + + # Simplest case: add a strain to beta + p1 = cv.Par(name='beta', val=0.016, by_strain=True) + print(p1.val) # Prints all the stored values of beta + print(p1[0]) # Can index beta like an array to pull out strain-specific values + p1.add_strain(new_val = 0.025) + + # Complex case: add a strain that's differentiated by severity for kids 0-20 + p2 = cv.Par(name='sus_ORs', val=np.array([0.34, 0.67, 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47]), by_strain=True) + print(p2.val) # Prints all the stored values for the original strain + print(p2[0]) # Can index beta like an array to pull out strain-specific values + p2.add_strain(new_val=np.array([1., 1., 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47])) + + return p1, p2 + + def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): results = sim.results @@ -264,7 +285,9 @@ def plot_shares(sim, key, title, do_show=True, do_save=False, labels=None): # sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_importstrain_args() - sim5 = test_importB117(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim5 = test_importB117(do_plot=do_plot, do_save=do_save, do_show=do_show) + + p1, p2 = test_par_refactor() sc.toc() From b6acf06010a760bf019bec93d30e80aac114944d Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 15 Feb 2021 16:46:51 +0100 Subject: [PATCH 057/569] stopping for call --- covasim/base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/covasim/base.py b/covasim/base.py index 870004b06..a09ff1b4f 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -201,6 +201,8 @@ def __init__(self, name=None, val=None, by_age=False, is_dist=False, by_strain=F self.name = name # Name of this parameter self.val = sc.promotetoarray(val) # Value of this parameter self.by_strain = by_strain # Whether or not the parameter varies by strain + self.by_age = by_age # Whether or not the parameter varies by age + self.is_dist = is_dist # Whether or not the parameter is stored as a distribution return def __repr__(self, *args, **kwargs): @@ -225,7 +227,13 @@ def n_strains(self): return len(self.val) def add_strain(self, new_val=None): - self.val = np.append(self[:], new_val) # TODO: so far this only works for 1D parameters + if self.by_age is False and self.is_dist is False: # TODO: refactor + self.val = np.append(self[:], new_val) # TODO: so far this only works for 1D parameters + elif self.by_age: # TODO: not working yet + self.val = np.append(self[:], new_val) + elif self.is_dist: # TODO: not working yet + self.val = np.array(self[:], new_val) + def set_metadata(obj): From 80c5d4654a696a0b00ba91077a134727f4b0f8c3 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 15 Feb 2021 19:27:05 -0500 Subject: [PATCH 058/569] added in immune waning by severity. Currently just modifies the decay rate. --- covasim/defaults.py | 1 + covasim/interventions.py | 6 +++--- covasim/parameters.py | 24 ++++++++++++++++++------ covasim/people.py | 6 +++++- covasim/sim.py | 23 ++++++++++++----------- covasim/utils.py | 18 ++++++++++-------- tests/devtests/test_variants.py | 10 +++++----- 7 files changed, 54 insertions(+), 34 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index fbf85e6a4..3ec64e96f 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -50,6 +50,7 @@ class PeopleMeta(sc.prettyobj): 'rel_sus', # Float 'time_of_last_inf', # Int 'immunity_factors', # Float + 'half_life', # Float # 'immune_factor_by_strain', # Float ] diff --git a/covasim/interventions.py b/covasim/interventions.py index 38bfaa016..a6878b130 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1131,7 +1131,7 @@ class import_strain(Intervention): n_imports (list of ints): the number of imports of strain(s) beta (list of floats): per contact transmission of strain(s) init_immunity (list of floats): initial immunity against strain(s) once recovered; 1 = perfect, 0 = no immunity - half_life (list of floats): determines decay rate of immunity against strain(s); If half_life is None immunity is constant + half_life (list of dicts): determines decay rate of immunity against strain(s) broken down by severity; If half_life is None immunity is constant immunity_to (list of list of floats): cross immunity to existing strains in model immunity_from (list of list of floats): cross immunity from existing strains in model kwargs (dict): passed to Intervention() @@ -1159,7 +1159,7 @@ def __init__(self, days=None, beta=None, n_imports=1, init_immunity=1, half_life self.half_life = sc.promotetolist(half_life) self.immunity_to = sc.promotetolist(immunity_to) self.immunity_from = sc.promotetolist(immunity_from) - self.new_strains = self.check_args(['beta', 'days', 'n_imports', 'init_immunity', 'half_life']) + self.new_strains = self.check_args(['beta', 'days', 'n_imports', 'init_immunity']) return @@ -1204,7 +1204,7 @@ def apply(self, sim): immunity_to=self.immunity_to[strain], init_immunity=self.init_immunity[strain], half_life=self.half_life[strain]) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely - sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) + sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains, half_life=sim['half_life']) sim['n_strains'] += 1 return \ No newline at end of file diff --git a/covasim/parameters.py b/covasim/parameters.py index 7fcdb44cb..755cb8bca 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -61,7 +61,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['default_cross_immunity'] = 0.5 # Default cross-immunity protection factor pars['default_immunity'] = 1. # Default initial immunity pars['default_half_life']= 180 # Default half life - pars['half_life'] = None + pars['half_life'] = dict() pars['init_immunity'] = None pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by set_immunity() below pars['init_half_life'] = None @@ -292,10 +292,17 @@ def initialize_immunity(pars): Matrix is of size sim['max_strains']*sim['max_strains'] and is initialized with default values ''' # If initial immunity/cross immunity factors are provided, use those, otherwise use defaults - pars['half_life'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - pars['immunity'] = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) + pars['half_life'] = dict() + pars['half_life']['asymptomatic'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) + pars['half_life']['mild'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) + pars['half_life']['severe'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) + pars['half_life']['critical'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) + pars['immunity'] = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) for i in range(pars['n_strains']): - pars['half_life'][i] = pars['default_half_life'] + pars['half_life']['asymptomatic'][i] = pars['default_half_life'] + pars['half_life']['mild'][i] = pars['default_half_life'] + pars['half_life']['severe'][i] = pars['default_half_life'] + pars['half_life']['critical'][i] = pars['default_half_life'] pars['immunity'][i, i] = pars['default_immunity'] return pars @@ -320,6 +327,7 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i update_strain: the index of the strain to update immunity_from (array): used when adding a new strain; specifies the immunity protection against existing strains from having the new strain immunity_to (array): used when adding a strain; specifies the immunity protection against the new strain from having one of the existing strains + half_life (dicts): dictionary of floats for half life of new strain **Example 1**: #TODO NEEDS UPDATING # Adding a strain C to the example above. Strain C gives perfect immunity against strain A @@ -343,7 +351,9 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i # Update own-immunity and half lives, if values have been supplied if pars.get('init_half_life'): # Values have been supplied for the half lives - pars['half_life'][:ns] = pars['init_half_life'] + pars['half_life']['asymptomatic'][:ns] = pars['init_half_life']['asymptomatic'] + pars['half_life']['mild'][:ns] = pars['init_half_life']['mild'] + pars['half_life']['severe'][:ns] = pars['init_half_life']['severe'] pars['immunity'][:ns, :ns] = pars['default_cross_immunity'] if pars.get('init_immunity'): # Values have been supplied for own-immunity np.fill_diagonal(pars['immunity'][:ns,:ns], pars['init_immunity']) @@ -380,7 +390,9 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i new_immunity_column[i] = immunity_to[i] else: new_immunity_row[i] = new_immunity_column[i] = init_immunity - pars['half_life'][i] = half_life + pars['half_life']['asymptomatic'][i] = half_life['asymptomatic'] + pars['half_life']['mild'][i] = half_life['mild'] + pars['half_life']['severe'][i] = half_life['severe'] immunity[update_strain, :] = new_immunity_row immunity[:, update_strain] = new_immunity_column diff --git a/covasim/people.py b/covasim/people.py index 1131d9f96..c7b3548a4 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -268,6 +268,7 @@ def check_recovery(self): self.susceptible[inds] = True self.recovered_strain[inds] = self.infectious_strain[inds] # TODO: check that this works self.infectious_strain[inds] = np.nan + self.half_life[inds] = np.nan return len(inds) @@ -356,7 +357,7 @@ def make_susceptible(self, inds): return - def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, strain=0): + def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, strain=0, half_life=None): ''' Infect people and determine their eventual outcomes. * Every infected person can infect other people, regardless of whether they develop symptoms @@ -422,6 +423,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str dur_asym2rec = cvu.sample(**durpars['asym2rec'], size=len(asymp_inds)) self.date_recovered[asymp_inds] = self.date_infectious[asymp_inds] + dur_asym2rec # Date they recover self.dur_disease[asymp_inds] = self.dur_exp2inf[asymp_inds] + dur_asym2rec # Store how long this person had COVID-19 + self.half_life[asymp_inds] = half_life['asymptomatic'][strain] # CASE 2: Symptomatic: can either be mild, severe, or critical n_symp_inds = len(symp_inds) @@ -436,10 +438,12 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str dur_mild2rec = cvu.sample(**durpars['mild2rec'], size=len(mild_inds)) self.date_recovered[mild_inds] = self.date_symptomatic[mild_inds] + dur_mild2rec # Date they recover self.dur_disease[mild_inds] = self.dur_exp2inf[mild_inds] + self.dur_inf2sym[mild_inds] + dur_mild2rec # Store how long this person had COVID-19 + self.half_life[mild_inds] = half_life['mild'][strain] # CASE 2.2: Severe cases: hospitalization required, may become critical self.dur_sym2sev[sev_inds] = cvu.sample(**durpars['sym2sev'], size=len(sev_inds)) # Store how long this person took to develop severe symptoms self.date_severe[sev_inds] = self.date_symptomatic[sev_inds] + self.dur_sym2sev[sev_inds] # Date symptoms become severe + self.half_life[sev_inds] = half_life['severe'][strain] crit_probs = self.pars['rel_crit_prob'] * self.crit_prob[sev_inds] * (self.pars['no_hosp_factor'] if hosp_max else 1.)# Probability of these people becoming critical - higher if no beds available is_crit = cvu.binomial_arr(crit_probs) # See if they're a critical case crit_inds = sev_inds[is_crit] diff --git a/covasim/sim.py b/covasim/sim.py index 0fbe8079a..46a4658f8 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -393,7 +393,7 @@ def init_people(self, save_pop=False, load_pop=False, popfile=None, reset=False, pop_infected_per_strain = cvd.default_int(self['pop_infected']/self['n_strains']) for strain in range(self['n_strains']): inds = cvu.choose(self['pop_size'], pop_infected_per_strain) - self.people.infect(inds=inds, layer='seed_infection', strain=strain) + self.people.infect(inds=inds, layer='seed_infection', strain=strain, half_life=self['half_life']) return @@ -489,7 +489,8 @@ def step(self): for strain, n_imports in enumerate(imports): if n_imports>0: importation_inds = cvu.choose(max_n=len(people), n=n_imports) - people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation', strain=strain) + people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation', + strain=strain, half_life=self['half_life']) # TODO -- Randomly introduce new strain @@ -530,14 +531,18 @@ def step(self): immunity_factors = people.immunity_factors[strain, :] # Process immunity parameters and indices + # TODO-- make this severity-specific immune = people.recovered_strain == strain # Whether people have some immunity to this strain from a prior infection with this strain immune_time = t - date_rec[immune] # Time since recovery for people who were last infected by this strain immune_inds = cvd.default_int(cvu.true(immune)) # People with some immunity to this strain from a prior infection with this strain init_immunity = cvd.default_float(self['immunity'][strain, strain]) - half_life = self['half_life'][strain] - decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. + #TODO -- make half life by severity + half_life = people.half_life + # decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. + decay_rate = np.log(2) / half_life + decay_rate[np.isnan(decay_rate)] = 0 decay_rate = cvd.default_float(decay_rate) - immunity_factors[immune_inds] = init_immunity * np.exp(-decay_rate * immune_time) # Calculate immunity factors + immunity_factors = cvu.compute_immunity(immunity_factors, immune_time, immune_inds, init_immunity, decay_rate) # Calculate immunity factors # Process cross-immunity parameters and indices, if relevant if self['n_strains']>1: @@ -547,11 +552,7 @@ def step(self): cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by the cross strain cross_immune_inds = cvd.default_int(cvu.true(cross_immune)) # People with some immunity to this strain from a prior infection with another strain cross_immunity = cvd.default_float(self['immunity'][cross_strain, strain]) # Immunity protection again this strain from other strains - # TODO cross immunity not working? - immunity_factors[cross_immune_inds] = cross_immunity * np.exp(-decay_rate * cross_immune_time) # Calculate cross-immunity factors - - # Compute protection factors from both immunity and cross immunity - ##immunity_factors = cvu.compute_immunity(people.immunity_factors[strain, :], immune_time, cross_immune_time, immune_inds, cross_immune_inds, init_immunity, decay_rate, cross_immunity) + immunity_factors = cvu.compute_immunity(immunity_factors, cross_immune_time, cross_immune_inds, cross_immunity, decay_rate) # Calculate cross_immunity factors # Define indices for this strain inf_by_this_strain = sc.dcp(inf) @@ -577,7 +578,7 @@ def step(self): for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, - layer=lkey, strain=strain) # Actually infect people + layer=lkey, strain=strain, half_life=self['half_life']) # Actually infect people # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): diff --git a/covasim/utils.py b/covasim/utils.py index ade8663eb..4a132235f 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -69,27 +69,29 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -#@nb.njit( (nbfloat[:], nbfloat[:], nbfloat[:], nbint[:], nbint[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) -def compute_immunity(immunity_factors, immune_time, cross_immune_time, immune_inds, cross_immune_inds, init_immunity, decay_rate, cross_immunity): # pragma: no cover +# @nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat, nbfloat[:]), cache=True, parallel=parallel) +def compute_immunity(immunity_factors, immune_time, immune_inds, init_immunity, decay_rate): # pragma: no cover ''' Calculate immunity factors for time t Args: - t: (int) timestep - date_rec: (float[]) recovery dates - init_immunity: (float) initial immunity protection (1=perfect protection, 0=no protection) - decay_rate: (float) decay rate of immunity. If 0, immunity stays constant + immunity_factors (array of floats): + immune_time: + immune_inds: + init_immunity: + decay_rate: Returns: immunity_factors (float[]): immunity factors ''' + decay_rate = decay_rate[immune_inds] + immunity_factors[immune_inds] = init_immunity * np.exp(-decay_rate * immune_time) # Calculate immunity factors - immunity_factors[cross_immune_inds] = (init_immunity * np.exp(-decay_rate * cross_immune_time)) * cross_immunity # Calculate cross-immunity factors return immunity_factors -# @nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=parallel) +@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=parallel) def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 9e64e0cf2..4aa5cda5d 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -18,13 +18,13 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): 'n_days': 80, 'beta': [0.015, 0.025], 'n_strains': 2, - 'init_immunity': [1, 1], - 'init_half_life': [30, 30], # Rapidly waning immunity from the less infections strain A + 'init_immunity': [0.5, 0.5], + 'init_half_life': dict(asymptomatic=[10, 10], mild=[50, 50], severe=[150, 150]), } sim = cv.Sim(pars=pars) sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B - sim['immunity'][1,0] = 1.0 # Say that strain B gives perfect immunity to strain A + sim['immunity'][1,0] = 0.0 # Say that strain B gives perfect immunity to strain A sim.run() strain_labels = [ @@ -50,7 +50,7 @@ def test_2strains_import(do_plot=False, do_show=True, do_save=False): immunity_to = [0] # Say that strain A gives no immunity to strain B immunity_from = [.5] # Say that strain B gives perfect immunity to strain A init_immunity = [1] - half_life = [20] + half_life = [dict(asymptomatic=10, mild=50, severe=150)] n_imports = [30] betas = [0.025] day = [10] @@ -62,7 +62,7 @@ def test_2strains_import(do_plot=False, do_show=True, do_save=False): 'n_days': 80, 'beta': [0.016], 'init_immunity': 1, - 'init_half_life': 50 + 'init_half_life': dict(asymptomatic=[10], mild=[50], severe=[150]) } strain_labels = [ From c65c2a0e256dff923856cb1277d827b0a9cf0c6b Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 15 Feb 2021 20:28:14 -0500 Subject: [PATCH 059/569] fix to half life --- covasim/parameters.py | 2 -- covasim/people.py | 1 - tests/devtests/test_variants.py | 35 ++++++++++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 755cb8bca..0597cbc74 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -296,13 +296,11 @@ def initialize_immunity(pars): pars['half_life']['asymptomatic'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) pars['half_life']['mild'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) pars['half_life']['severe'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - pars['half_life']['critical'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) pars['immunity'] = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) for i in range(pars['n_strains']): pars['half_life']['asymptomatic'][i] = pars['default_half_life'] pars['half_life']['mild'][i] = pars['default_half_life'] pars['half_life']['severe'][i] = pars['default_half_life'] - pars['half_life']['critical'][i] = pars['default_half_life'] pars['immunity'][i, i] = pars['default_immunity'] return pars diff --git a/covasim/people.py b/covasim/people.py index c7b3548a4..4028cfb54 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -268,7 +268,6 @@ def check_recovery(self): self.susceptible[inds] = True self.recovered_strain[inds] = self.infectious_strain[inds] # TODO: check that this works self.infectious_strain[inds] = np.nan - self.half_life[inds] = np.nan return len(inds) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 4aa5cda5d..ae80df455 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -171,6 +171,38 @@ def test_importstrain_nocrossimmunity(do_plot=False, do_show=True, do_save=False plot_results(sim, key='incidence_by_strain', title='Imported strains', labels=strain_labels, do_show=do_show, do_save=do_save) return sim +def test_halflife_by_severity(do_plot=False, do_show=True, do_save=False): + sc.heading('Run basic sim with 2 strains and half life by severity') + + sc.heading('Setting up...') + + pars = { + 'n_days': 80, + 'beta': [0.015, 0.015], + 'n_strains': 2, + 'init_immunity': [1, 1], + 'init_half_life': dict(asymptomatic=[1, 10], mild=[1, 100], severe=[1, 100]), + } + + sim = cv.Sim(pars=pars) + sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B + sim['immunity'][1,0] = 0.0 # Say that strain B gives no immunity to strain A + sim.run() + + strain_labels = [ + f'Strain A: beta {pars["beta"][0]}, half_life by severity', + f'Strain B: beta {pars["beta"][1]}, half_life not by severity', + ] + + if do_plot: + sim.plot_result('new_reinfections', do_show=do_show, do_save=do_save) + # TODO: using the following line seems to flip the results??? + # plot_results(sim, key='cum_reinfections', + # title=f'2 strain test, A->B immunity {sim["immunity"][0, 1]}, B->A immunity {sim["immunity"][1, 0]}', + # labels=strain_labels, do_show=do_show, do_save=do_save) + plot_results(sim, key='incidence_by_strain', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}, B->A immunity {sim["immunity"][1,0]}', labels=strain_labels, do_show=do_show, do_save=do_save) + return sim + def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): results = sim.results @@ -206,10 +238,11 @@ def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): sc.tic() # sim0 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_2strains_import(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim1 = test_2strains_import(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_importstrain_args() + sim5 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) sc.toc() From 4908612c643d0e7a330f3347947b42fbbdfdedc9 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 16 Feb 2021 09:34:23 +0100 Subject: [PATCH 060/569] add default strain pars --- covasim/base.py | 16 +++++++++------- covasim/defaults.py | 10 ++++++++++ covasim/parameters.py | 8 ++++---- tests/devtests/test_variants.py | 15 +++++++++++---- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index a09ff1b4f..9ce98dd7c 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -199,7 +199,7 @@ class Par(object): def __init__(self, name=None, val=None, by_age=False, is_dist=False, by_strain=False): self.name = name # Name of this parameter - self.val = sc.promotetoarray(val) # Value of this parameter + self.val = sc.promotetolist(val) # Value of this parameter self.by_strain = by_strain # Whether or not the parameter varies by strain self.by_age = by_age # Whether or not the parameter varies by age self.is_dist = is_dist # Whether or not the parameter is stored as a distribution @@ -227,13 +227,15 @@ def n_strains(self): return len(self.val) def add_strain(self, new_val=None): - if self.by_age is False and self.is_dist is False: # TODO: refactor - self.val = np.append(self[:], new_val) # TODO: so far this only works for 1D parameters - elif self.by_age: # TODO: not working yet - self.val = np.append(self[:], new_val) - elif self.is_dist: # TODO: not working yet - self.val = np.array(self[:], new_val) + self.val.append(new_val) + def get(self, strain=0, n=None): + if self.by_strain: + if not self.is_dist: output = self.val[strain] + else: output = cvu.sample(**self.val[strain], size=n) + else: + output = self.val + return output def set_metadata(obj): diff --git a/covasim/defaults.py b/covasim/defaults.py index 3ec64e96f..5c598e10c 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -133,6 +133,16 @@ class PeopleMeta(sc.prettyobj): new_result_flows = [f'new_{key}' for key in result_flows.keys()] cum_result_flows = [f'cum_{key}' for key in result_flows.keys()] +# Parameters that can vary by strain +strain_pars = ['beta', + 'asymp_factor', + 'dur', + 'rel_symp_prob', + 'rel_severe_prob', + 'rel_crit_prob', + 'rel_death_prob', + 'prognoses'] + # Default age data, based on Seattle 2018 census data -- used in population.py default_age_data = np.array([ [ 0, 4, 0.0605], diff --git a/covasim/parameters.py b/covasim/parameters.py index fd91c6d2b..622124480 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -67,16 +67,16 @@ def init_par(*args, **kwargs): # Parameters that control settings and defaults for multi-strain runs pars['n_strains'] = 1 # The number of strains currently circulating in the population pars['max_strains'] = 30 # For allocating memory with numpy arrays - pars['default_cross_immunity'] = 0.5 # Default cross-immunity protection factor - pars['default_immunity'] = 1. # Default initial immunity - pars['default_half_life'] = 180 # Default half life + pars['default_cross_immunity'] = 0.5 # Default cross-immunity protection factor + pars['default_immunity'] = 1. # Default initial immunity + pars['default_half_life'] = 180 # Default half life pars['half_life'] = dict() pars['init_immunity'] = None pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by set_immunity() below pars['init_half_life'] = None # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains - pars['beta'] = [0.016] # Beta per symptomatic contact; absolute value, calibrated + pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 pars['dur'] = {} # Duration parameters: time for disease progression diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 6d53e72a8..edbb68577 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -212,12 +212,19 @@ def test_par_refactor(): p1.add_strain(new_val = 0.025) # Complex case: add a strain that's differentiated by severity for kids 0-20 - p2 = cv.Par(name='sus_ORs', val=np.array([0.34, 0.67, 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47]), by_strain=True) + p2 = cv.Par(name='sus_ORs', val=np.array([0.34, 0.67, 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47]), by_strain=True, by_age=True) print(p2.val) # Prints all the stored values for the original strain print(p2[0]) # Can index beta like an array to pull out strain-specific values p2.add_strain(new_val=np.array([1., 1., 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47])) - return p1, p2 + # Complex case: add a strain that's differentiated by duration of disease + p3 = cv.Par(name='dur_asym2rec', val=dict(dist='lognormal_int', par1=8.0, par2=2.0), by_strain=True, is_dist=True) + print(p3.val) # Prints all the stored values for the original strain + print(p3[0]) # Can index beta like an array to pull out strain-specific values + p3.add_strain(new_val=dict(dist='lognormal_int', par1=12.0, par2=2.0)) + p3.get(strain=1, n=6) + + return p1, p2, p3 def test_halflife_by_severity(do_plot=False, do_show=True, do_save=False): @@ -319,8 +326,8 @@ def plot_shares(sim, key, title, do_show=True, do_save=False, labels=None): # sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_importstrain_args() # sim5 = test_importB117(do_plot=do_plot, do_save=do_save, do_show=do_show) - p1, p2 = test_par_refactor() - sim6 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) + p1, p2, p3 = test_par_refactor() + # sim6 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) sc.toc() From 73d97b0d058940356a59c4533a4a905beb4a556e Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 16 Feb 2021 15:16:46 +0100 Subject: [PATCH 061/569] separate strain arguments --- covasim/base.py | 8 +- covasim/defaults.py | 5 +- covasim/interventions.py | 42 +++-- covasim/parameters.py | 62 +++----- covasim/people.py | 27 ++-- covasim/sim.py | 27 ++-- tests/devtests/test_variants.py | 271 ++++++++++++-------------------- 7 files changed, 201 insertions(+), 241 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 9ce98dd7c..b57f85760 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -302,7 +302,7 @@ def _brief(self): return string - def update_pars(self, pars=None, create=False, immunity_pars=None, **kwargs): + def update_pars(self, pars=None, create=False, immunity_pars=None, strain_pars=None, **kwargs): ''' Ensure that metaparameters get used properly before being updated ''' pars = sc.mergedicts(pars, kwargs) if pars: @@ -310,8 +310,10 @@ def update_pars(self, pars=None, create=False, immunity_pars=None, **kwargs): cvpar.reset_layer_pars(pars, force=False) if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses - pars = sc.mergedicts(pars, immunity_pars) - pars = cvpar.update_immunity(pars) # Update immunity with values provided + if pars.get('strains'): + pars['n_strains'] = 1 + len(sc.promotetolist(next(iter(pars['strains'].values())))) + pars = sc.mergedicts(immunity_pars, strain_pars, pars) + pars = cvpar.update_immunity(pars) # Update immunity with values provided super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/defaults.py b/covasim/defaults.py index 5c598e10c..59dd640ec 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -136,12 +136,13 @@ class PeopleMeta(sc.prettyobj): # Parameters that can vary by strain strain_pars = ['beta', 'asymp_factor', + 'half_life', + 'init_immunity', 'dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', - 'rel_death_prob', - 'prognoses'] + 'rel_death_prob'] # Default age data, based on Seattle 2018 census data -- used in population.py default_age_data = np.array([ diff --git a/covasim/interventions.py b/covasim/interventions.py index 7fdd086dd..892278899 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1145,21 +1145,21 @@ class import_strain(Intervention): half_life=[180, 180], immunity_to=[[0, 0], [0,0]], immunity_from=[[0, 0], [0,0]]) # On day 10, import 5 cases of one new strain, on day 20, import 10 cases of another ''' - def __init__(self, days=None, beta=None, n_imports=1, init_immunity=1, half_life=180, immunity_to=0, - immunity_from=0, **kwargs): + def __init__(self, strain=None, days=None, n_imports=1, immunity_to=None, immunity_from=None, **kwargs): # Do standard initialization super().__init__(**kwargs) # Initialize the Intervention object self._store_args() # Store the input arguments so the intervention can be recreated # Handle inputs - self.beta = sc.promotetolist(beta) self.days = sc.promotetolist(days) self.n_imports = sc.promotetolist(n_imports) - self.init_immunity = sc.promotetolist(init_immunity) - self.half_life = sc.promotetolist(half_life) - self.immunity_to = sc.promotetolist(immunity_to) - self.immunity_from = sc.promotetolist(immunity_from) - self.new_strains = self.check_args(['beta', 'days', 'n_imports', 'init_immunity']) + self.immunity_to = sc.promotetolist(immunity_to) if immunity_to is not None else [None] + self.immunity_from = sc.promotetolist(immunity_from) if immunity_from is not None else [None] + self.strain = {par: sc.promotetolist(val) for par, val in strain.items()} + for par, val in self.strain.items(): setattr(self, par, val) + if not getattr(self,'init_immunity'): + self.init_immunity = None + self.new_strains = self.check_args(['days', 'n_imports']+list(self.strain.keys())) return @@ -1200,12 +1200,30 @@ def apply(self, sim): errormsg = f"Number of existing strains ({sim['n_strains']}) plus new strain exceeds the maximal allowable ({sim['max_strains']}. Increase pars['max_strains'])." raise ValueError(errormsg) - sim['beta'].append(self.beta[strain]) + # Update strain info + if sim['strains'] is None: + sim['strains'] = self.strain + for par, val in sim['strains'].items(): + sim[par] = sc.promotetolist(sim[par]) + sc.promotetolist(val) + else: # TODO, this could be improved a LOT - surely there's a variant of mergedicts or update that works here + all_strain_keys = list(set([*sim['strains']]+[*self.strain])) # Merged list of all parameters that need to be by strain + for sk in all_strain_keys: + if sk in sim['strains'].keys(): # This was already by strain + if sk in self.strain.keys(): # We add a new value + sim['strains'][sk] = sc.promotetolist(sim['strains'][sk]) + sc.promotetolist(self.strain[sk]) + sim[sk] = sc.promotetolist(sim[sk]) + sc.promotetolist(self.strain[sk]) + else: + sim['strains'][sk] = sc.promotetolist(sim['strains'][sk]) + sim['strains'][sk].append(sim[sk][0]) + sim[sk].append(sim[sk][0]) + else: # This wasn't previously stored by strain, so now it needs to be added by strain + sim['strains'][sk] = sc.promotetolist(sim[sk]) + sc.promotetolist(self.strain[sk]) + sim[sk] = sc.promotetolist(sim[sk]) + sc.promotetolist(sim['strains'][sk]) + cvpar.update_immunity(pars=sim.pars, create=False, update_strain=prev_strains, immunity_from=self.immunity_from[strain], - immunity_to=self.immunity_to[strain], init_immunity=self.init_immunity[strain], - half_life=self.half_life[strain]) + immunity_to=self.immunity_to[strain], init_immunity=self.init_immunity[strain]) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely - sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains, half_life=sim['half_life']) + sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) sim['n_strains'] += 1 return \ No newline at end of file diff --git a/covasim/parameters.py b/covasim/parameters.py index 622124480..980af927a 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -65,19 +65,17 @@ def init_par(*args, **kwargs): pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 # Parameters that control settings and defaults for multi-strain runs - pars['n_strains'] = 1 # The number of strains currently circulating in the population - pars['max_strains'] = 30 # For allocating memory with numpy arrays - pars['default_cross_immunity'] = 0.5 # Default cross-immunity protection factor - pars['default_immunity'] = 1. # Default initial immunity - pars['default_half_life'] = 180 # Default half life - pars['half_life'] = dict() - pars['init_immunity'] = None - pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by set_immunity() below - pars['init_half_life'] = None + pars['strains'] = None # Structure for storing strain info + pars['n_strains'] = 1 # The number of strains currently circulating in the population + pars['max_strains'] = 30 # For allocating memory with numpy arrays + pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor + pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by set_immunity() below # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated - pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 + pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 + pars['init_immunity'] = 1.0 # Default initial immunity + pars['half_life'] = dict(asymptomatic=180, mild=180, severe=180) pars['dur'] = {} # Duration parameters: time for disease progression pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.6, par2=4.8) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, subtracting inf2sim duration @@ -297,20 +295,13 @@ def absolute_prognoses(prognoses): def initialize_immunity(pars): ''' - Helper function to initialize the immunity and half_life matrices. + Helper function to initialize the immunity matrices. Matrix is of size sim['max_strains']*sim['max_strains'] and is initialized with default values ''' # If initial immunity/cross immunity factors are provided, use those, otherwise use defaults - pars['half_life'] = dict() - pars['half_life']['asymptomatic'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - pars['half_life']['mild'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - pars['half_life']['severe'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) pars['immunity'] = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) for i in range(pars['n_strains']): - pars['half_life']['asymptomatic'][i] = pars['default_half_life'] - pars['half_life']['mild'][i] = pars['default_half_life'] - pars['half_life']['severe'][i] = pars['default_half_life'] - pars['immunity'][i, i] = pars['default_immunity'] + pars['immunity'][i, i] = pars['init_immunity'] return pars @@ -356,40 +347,37 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i pars = initialize_immunity(pars) ns = pars['n_strains'] # Shorten - # Update own-immunity and half lives, if values have been supplied - if pars.get('init_half_life'): # Values have been supplied for the half lives - pars['half_life']['asymptomatic'][:ns] = pars['init_half_life']['asymptomatic'] - pars['half_life']['mild'][:ns] = pars['init_half_life']['mild'] - pars['half_life']['severe'][:ns] = pars['init_half_life']['severe'] - pars['immunity'][:ns, :ns] = pars['default_cross_immunity'] + # Update all strain-specific values + for par,val in pars['strains'].items(): + if pars.get(par): + pars[par] = sc.promotetolist(pars[par]) + sc.promotetolist(val) + + # Update own-immunity, if values have been supplied + pars['immunity'][:ns, :ns] = pars['cross_immunity'] if pars.get('init_immunity'): # Values have been supplied for own-immunity np.fill_diagonal(pars['immunity'][:ns,:ns], pars['init_immunity']) - else: - np.fill_diagonal(pars['immunity'][:ns,:ns], pars['default_immunity']) # Update immunity for a strain if supplied if update_strain is not None: + immunity = pars['immunity'] # check that immunity_from, immunity_to and init_immunity are provided and the right length. # Else use default values if immunity_from is None: print('Immunity from pars not provided, using default value') - immunity_from = [pars['default_cross_immunity']]*pars['n_strains'] + immunity_from = [pars['cross_immunity']]*pars['n_strains'] if immunity_to is None: print('Immunity to pars not provided, using default value') - immunity_to = [pars['default_cross_immunity']]*pars['n_strains'] + immunity_to = [pars['cross_immunity']]*pars['n_strains'] if init_immunity is None: - print('Initial immunity pars not provided, using default value') - init_immunity = pars['default_immunity'] - if half_life is None: - print('Half life is not provided, using default value') - half_life = pars['default_half_life'] + print('Initial immunity not provided, using default value') + init_immunity = pars['init_immunity'] immunity_from = sc.promotetolist(immunity_from) immunity_to = sc.promotetolist(immunity_to) # create the immunity[update_strain,] and immunity[,update_strain] arrays - new_immunity_row = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) + new_immunity_row = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) new_immunity_column = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) for i in range(pars['n_strains']+1): if i != update_strain: @@ -397,12 +385,10 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i new_immunity_column[i] = immunity_to[i] else: new_immunity_row[i] = new_immunity_column[i] = init_immunity - pars['half_life']['asymptomatic'][i] = half_life['asymptomatic'] - pars['half_life']['mild'][i] = half_life['mild'] - pars['half_life']['severe'][i] = half_life['severe'] immunity[update_strain, :] = new_immunity_row immunity[:, update_strain] = new_immunity_column + pars['immunity'] = immunity return pars diff --git a/covasim/people.py b/covasim/people.py index 4028cfb54..ed9c94d17 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -356,7 +356,7 @@ def make_susceptible(self, inds): return - def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, strain=0, half_life=None): + def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, strain=0): ''' Infect people and determine their eventual outcomes. * Every infected person can infect other people, regardless of whether they develop symptoms @@ -387,8 +387,17 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str if source is not None: source = source[keep] + # Deal with strain parameters + infect_parkeys = ['dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob', 'half_life'] + infect_pars = dict() + for key in infect_parkeys: + if self.pars['strains'] is not None and key in self.pars['strains'].keys(): # This parameter varies by strain: extract strain-specific value + infect_pars[key] = self.pars[key][strain] + else: + infect_pars[key] = self.pars[key] + n_infections = len(inds) - durpars = self.pars['dur'] + durpars = infect_pars['dur'] # Update states, strain info, and flows self.susceptible[inds] = False @@ -413,7 +422,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.date_infectious[inds] = self.dur_exp2inf[inds] + self.t # Use prognosis probabilities to determine what happens to them - symp_probs = self.pars['rel_symp_prob']*self.symp_prob[inds] # Calculate their actual probability of being symptomatic + symp_probs = infect_pars['rel_symp_prob']*self.symp_prob[inds] # Calculate their actual probability of being symptomatic is_symp = cvu.binomial_arr(symp_probs) # Determine if they develop symptoms symp_inds = inds[is_symp] asymp_inds = inds[~is_symp] # Asymptomatic @@ -422,13 +431,13 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str dur_asym2rec = cvu.sample(**durpars['asym2rec'], size=len(asymp_inds)) self.date_recovered[asymp_inds] = self.date_infectious[asymp_inds] + dur_asym2rec # Date they recover self.dur_disease[asymp_inds] = self.dur_exp2inf[asymp_inds] + dur_asym2rec # Store how long this person had COVID-19 - self.half_life[asymp_inds] = half_life['asymptomatic'][strain] + self.half_life[asymp_inds] = infect_pars['half_life']['asymptomatic'] # CASE 2: Symptomatic: can either be mild, severe, or critical n_symp_inds = len(symp_inds) self.dur_inf2sym[symp_inds] = cvu.sample(**durpars['inf2sym'], size=n_symp_inds) # Store how long this person took to develop symptoms self.date_symptomatic[symp_inds] = self.date_infectious[symp_inds] + self.dur_inf2sym[symp_inds] # Date they become symptomatic - sev_probs = self.pars['rel_severe_prob'] * self.severe_prob[symp_inds] # Probability of these people being severe + sev_probs = infect_pars['rel_severe_prob'] * self.severe_prob[symp_inds] # Probability of these people being severe is_sev = cvu.binomial_arr(sev_probs) # See if they're a severe or mild case sev_inds = symp_inds[is_sev] mild_inds = symp_inds[~is_sev] # Not severe @@ -437,13 +446,13 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str dur_mild2rec = cvu.sample(**durpars['mild2rec'], size=len(mild_inds)) self.date_recovered[mild_inds] = self.date_symptomatic[mild_inds] + dur_mild2rec # Date they recover self.dur_disease[mild_inds] = self.dur_exp2inf[mild_inds] + self.dur_inf2sym[mild_inds] + dur_mild2rec # Store how long this person had COVID-19 - self.half_life[mild_inds] = half_life['mild'][strain] + self.half_life[mild_inds] = infect_pars['half_life']['mild'] # CASE 2.2: Severe cases: hospitalization required, may become critical self.dur_sym2sev[sev_inds] = cvu.sample(**durpars['sym2sev'], size=len(sev_inds)) # Store how long this person took to develop severe symptoms self.date_severe[sev_inds] = self.date_symptomatic[sev_inds] + self.dur_sym2sev[sev_inds] # Date symptoms become severe - self.half_life[sev_inds] = half_life['severe'][strain] - crit_probs = self.pars['rel_crit_prob'] * self.crit_prob[sev_inds] * (self.pars['no_hosp_factor'] if hosp_max else 1.)# Probability of these people becoming critical - higher if no beds available + self.half_life[sev_inds] = infect_pars['half_life']['severe'] + crit_probs = infect_pars['rel_crit_prob'] * self.crit_prob[sev_inds] * (self.pars['no_hosp_factor'] if hosp_max else 1.)# Probability of these people becoming critical - higher if no beds available is_crit = cvu.binomial_arr(crit_probs) # See if they're a critical case crit_inds = sev_inds[is_crit] non_crit_inds = sev_inds[~is_crit] @@ -456,7 +465,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str # CASE 2.2.2: Critical cases: ICU required, may die self.dur_sev2crit[crit_inds] = cvu.sample(**durpars['sev2crit'], size=len(crit_inds)) self.date_critical[crit_inds] = self.date_severe[crit_inds] + self.dur_sev2crit[crit_inds] # Date they become critical - death_probs = self.pars['rel_death_prob'] * self.death_prob[crit_inds] * (self.pars['no_icu_factor'] if icu_max else 1.) # Probability they'll die + death_probs = infect_pars['rel_death_prob'] * self.death_prob[crit_inds] * (self.pars['no_icu_factor'] if icu_max else 1.) # Probability they'll die is_dead = cvu.binomial_arr(death_probs) # Death outcome dead_inds = crit_inds[is_dead] alive_inds = crit_inds[~is_dead] diff --git a/covasim/sim.py b/covasim/sim.py index 9c3ab66e4..c2a27d489 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -74,10 +74,10 @@ def __init__(self, pars=None, datafile=None, datacols=None, label=None, simfile= # Now update everything self.set_metadata(simfile) # Set the simulation date and filename - immunity_pars = dict(max_strains=default_pars['max_strains'], default_half_life=default_pars['default_half_life'], - default_immunity=default_pars['default_immunity'], - default_cross_immunity=default_pars['default_cross_immunity']) - self.update_pars(pars, immunity_pars=immunity_pars, **kwargs) # Update the parameters, if provided + immunity_pars = dict(max_strains=default_pars['max_strains'], + cross_immunity=default_pars['cross_immunity']) + strain_pars = {par: default_pars[par] for par in cvd.strain_pars} + self.update_pars(pars, immunity_pars=immunity_pars, strain_pars=strain_pars, **kwargs) # Update the parameters, if provided self.load_data(datafile, datacols) # Load the data, if provided if self.load_pop: self.load_population(popfile) # Load the population, if provided @@ -393,7 +393,7 @@ def init_people(self, save_pop=False, load_pop=False, popfile=None, reset=False, pop_infected_per_strain = cvd.default_int(self['pop_infected']/self['n_strains']) for strain in range(self['n_strains']): inds = cvu.choose(self['pop_size'], pop_infected_per_strain) - self.people.infect(inds=inds, layer='seed_infection', strain=strain, half_life=self['half_life']) + self.people.infect(inds=inds, layer='seed_infection', strain=strain) return @@ -490,7 +490,7 @@ def step(self): if n_imports>0: importation_inds = cvu.choose(max_n=len(people), n=n_imports) people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation', - strain=strain, half_life=self['half_life']) + strain=strain) # TODO -- Randomly introduce new strain @@ -516,7 +516,6 @@ def step(self): viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) # Shorten additional useful parameters and indicators that aren't by strain - asymp_factor = cvd.default_float(self['asymp_factor']) sus = people.susceptible inf = people.infectious symp = people.symptomatic @@ -526,8 +525,18 @@ def step(self): # Iterate through n_strains to calculate infections for strain in range(self['n_strains']): + # Deal with strain parameters + strain_parkeys = ['beta', 'asymp_factor'] + strain_pars = dict() + for key in strain_parkeys: + if self['strains'] is not None and key in self['strains'].keys(): # This parameter varies by strain: extract strain-specific value + strain_pars[key] = cvd.default_float(self[key][strain]) + else: + strain_pars[key] = cvd.default_float(self[key]) + # Compute the probability of transmission - beta = cvd.default_float(self['beta'][strain]) + beta = strain_pars['beta'] + asymp_factor = strain_pars['asymp_factor'] immunity_factors = people.immunity_factors[strain, :] # Process immunity parameters and indices @@ -578,7 +587,7 @@ def step(self): for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, - layer=lkey, strain=strain, half_life=self['half_life']) # Actually infect people + layer=lkey, strain=strain) # Actually infect people # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index edbb68577..f551654a9 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -11,191 +11,95 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): sc.heading('Run basic sim with 2 strains') - sc.heading('Setting up...') + strains = {'beta': 0.025, + 'rel_severe_prob': 1.3, # 30% more severe across all ages + 'half_life': dict(asymptomatic=20, mild=80, severe=200), + 'init_immunity': 0.9 + } + pars = { + 'beta': 0.016, 'n_days': 80, - 'beta': [0.015, 0.025], - 'n_strains': 2, - 'init_immunity': [0.5, 0.5], - 'init_half_life': dict(asymptomatic=[10, 10], mild=[50, 50], severe=[150, 150]), + 'strains': strains, } sim = cv.Sim(pars=pars) sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B - sim['immunity'][1,0] = 0.0 # Say that strain B gives perfect immunity to strain A + sim['immunity'][1,0] = 0.9 # Say that strain B gives high immunity to strain A sim.run() strain_labels = [ - f'Strain A: beta {pars["beta"][0]}, half_life {pars["init_half_life"][0]}', - f'Strain B: beta {pars["beta"][1]}, half_life {pars["init_half_life"][1]}', + f'Strain A: beta {sim["beta"][0]}, half_life {sim["half_life"][0]}', + f'Strain B: beta {sim["beta"][1]}, half_life {sim["half_life"][1]}', ] if do_plot: - sim.plot_result('new_reinfections', do_show=do_show, do_save=do_save) - # TODO: using the following line seems to flip the results??? - # plot_results(sim, key='cum_reinfections', - # title=f'2 strain test, A->B immunity {sim["immunity"][0, 1]}, B->A immunity {sim["immunity"][1, 0]}', - # labels=strain_labels, do_show=do_show, do_save=do_save) - plot_results(sim, key='incidence_by_strain', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}, B->A immunity {sim["immunity"][1,0]}', labels=strain_labels, do_show=do_show, do_save=do_save) + sim.plot_result('new_reinfections', do_show=do_show, do_save=do_save, fig_path=f'results/test_2strains.png') + plot_results(sim, key='incidence_by_strain', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}, B->A immunity {sim["immunity"][1,0]}', filename='test_2strains2', labels=strain_labels, do_show=do_show, do_save=do_save) return sim -def test_2strains_import(do_plot=False, do_show=True, do_save=False): - sc.heading('Run basic sim with an imported strain') - +def test_importstrain1(do_plot=False, do_show=True, do_save=False): + sc.heading('Test introducing a new strain partway through a sim') sc.heading('Setting up...') - immunity_to = [0] # Say that strain A gives no immunity to strain B - immunity_from = [.5] # Say that strain B gives perfect immunity to strain A - init_immunity = [1] - half_life = [dict(asymptomatic=10, mild=50, severe=150)] - n_imports = [30] - betas = [0.025] - day = [10] - - imports = cv.import_strain(days=day, beta=betas, n_imports=n_imports, immunity_to=immunity_to, - immunity_from=immunity_from, init_immunity=init_immunity, half_life=half_life) - - pars = { - 'n_days': 80, - 'beta': [0.016], - 'init_immunity': 1, - 'init_half_life': dict(asymptomatic=[10], mild=[50], severe=[150]) - } - strain_labels = [ - f'Strain A: beta {pars["beta"][0]}', - f'Strain B: beta {betas[0]}, {n_imports[0]} imports on day {day[0]}', + 'Strain 1: beta 0.016', + 'Strain 2: beta 0.025' ] - sim = cv.Sim( - pars=pars, - interventions=imports - ) - sim.run() - - if do_plot: - plot_results(sim, key='incidence_by_strain', title=f'imported 2 strain test, A->B immunity {immunity_to[0]}, B->A immunity {immunity_from[0]}', labels=strain_labels, do_show=do_show, do_save=do_save) - return sim - - -def test_importstrain_args(): - sc.heading('Test flexibility of arguments for the import strain "intervention"') - - # Run sim with a single strain initially, then introduce a new strain that's more transmissible on day 10 - immunity = [ - {'init_immunity': 1., 'half_life': 180, 'cross_factor': 1}, - ] - pars = { - 'n_strains': 1, - 'beta': [0.016], - 'immunity': immunity + imported_strain = { + 'beta': 0.025, + 'half_life': dict(asymptomatic=10, mild=50, severe=150), + 'init_immunity': 0.9 } - # All these should run - imports = cv.import_strain(days=50, beta=0.03) - #imports = cv.import_strain(days=[10, 50], beta=0.03) - #imports = cv.import_strain(days=50, beta=[0.03, 0.05]) - #imports = cv.import_strain(days=[10, 20], beta=[0.03, 0.05]) - #imports = cv.import_strain(days=50, beta=[0.03, 0.05, 0.06]) - #imports = cv.import_strain(days=[10, 20], n_imports=[5, 10], beta=[0.03, 0.05], init_immunity=[1, 1], - # half_life=[180, 180], cross_factor=[0, 0]) - #imports = cv.import_strain(days=[10, 50], beta=0.03, cross_factor=[0.4, 0.6]) - #imports = cv.import_strain(days=['2020-04-01', '2020-05-01'], beta=0.03) - - # This should fail - #imports = cv.import_strain(days=[20, 50], beta=[0.03, 0.05, 0.06]) - - sim = cv.Sim(pars=pars, interventions=imports) - sim.run() - - - return sim - - -def test_importstrain_withcrossimmunity(do_plot=False, do_show=True, do_save=False): - sc.heading('Test introducing a new strain partway through a sim with full cross immunity') - - sc.heading('Setting up...') - - strain_labels = [ - 'Strain 1: beta 0.016', - 'Strain 2: beta 0.05' - ] - # Run sim with several strains initially, then introduce a new strain that's more transmissible on day 10 - immunity = [ - {'init_immunity': 1., 'half_life': 180, 'cross_factor': 1}, - ] - pars = { - 'n_strains': 1, - 'beta': [0.016], - 'immunity': immunity - } - imports = cv.import_strain(days=30, n_imports=30, beta=0.05, init_immunity=1, half_life=180, cross_factor=1) - sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') + imports = cv.import_strain(strain=imported_strain, days=30, n_imports=30) + sim = cv.Sim(interventions=imports, label='With imported infections') sim.run() if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (cross immunity)', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (cross immunity)', filename='test_importstrain1', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_shares(sim, key='new_infections', title='Shares of new infections by strain', filename='test_importstrain1_shares', do_show=do_show, do_save=do_save) return sim -def test_importstrain_nocrossimmunity(do_plot=False, do_show=True, do_save=False): - sc.heading('Test introducing a new strain partway through a sim with cross immunity') +def test_importstrain2(do_plot=False, do_show=True, do_save=False): + sc.heading('Test introducing a new strain partway through a sim with 2 strains') sc.heading('Setting up...') - - # Run sim with several strains initially, then introduce a new strain that's more transmissible on day 10 - immunity = [ - {'init_immunity': 1., 'half_life': 180, 'cross_factor': 0}, - ] + strain2 = {'beta': 0.025, + 'rel_severe_prob': 1.3, + 'half_life': dict(asymptomatic=20, mild=80, severe=200), + 'init_immunity': 0.9 + } pars = { - 'n_strains': 1, - 'beta': [0.016], - 'immunity': immunity + 'n_days': 80, + 'strains': strain2, } - imports = cv.import_strain(days=[10, 20], n_imports=[10, 20], beta=[0.035, 0.05], init_immunity=[1, 1], - half_life=[180, 180], cross_factor=[0, 0]) + strain3 = { + 'beta': 0.05, + 'rel_symp_prob': 1.6, + 'half_life': dict(asymptomatic=10, mild=50, severe=150), + 'init_immunity': 0.4 + } + + imports = cv.import_strain(strain=strain3, days=10, n_imports=20) sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') sim.run() strain_labels = [ 'Strain 1: beta 0.016', - 'Strain 2: beta 0.035, 10 imported day 10', - 'Strain 3: beta 0.05, 20 imported day 20' + 'Strain 2: beta 0.025', + 'Strain 3: beta 0.05, 20 imported day 10' ] if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strains', labels=strain_labels, do_show=do_show, do_save=do_save) - return sim - - -def test_importB117(do_plot=False, do_show=True, do_save=False): - ''' - The purpose of this test is to try out plotting the relative shares of infections by strain, - and see whether we get something similar to what other investigations of B117 dynamics have found - ''' - sc.heading('Run basic sim with an imported strain similar to B117') - sc.heading('Setting up...') - - pars = { - 'n_days': 120, - 'beta': [0.016], - } - - imports = cv.import_strain(days=40, beta=0.025, n_imports=10) - - sim = cv.Sim( - pars=pars, - interventions=imports - ) - sim.run() - - if do_plot: - sim.plot_result('new_infections', do_show=do_show, do_save=do_save) - plot_shares(sim, key='new_infections', title='Shares of new infections by strain', do_show=do_show, do_save=do_save) + plot_results(sim, key='incidence_by_strain', title='Imported strains', filename='test_importstrain2', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_shares(sim, key='new_infections', title='Shares of new infections by strain', + filename='test_importstrain2_shares', do_show=do_show, do_save=do_save) return sim @@ -229,15 +133,14 @@ def test_par_refactor(): def test_halflife_by_severity(do_plot=False, do_show=True, do_save=False): sc.heading('Run basic sim with 2 strains and half life by severity') - sc.heading('Setting up...') + strains = {'half_life': dict(asymptomatic=100, mild=150, severe=200)} + pars = { 'n_days': 80, - 'beta': [0.015, 0.015], - 'n_strains': 2, - 'init_immunity': [1, 1], - 'init_half_life': dict(asymptomatic=[1, 10], mild=[1, 100], severe=[1, 100]), + 'beta': 0.015, + 'strains': strains, } sim = cv.Sim(pars=pars) @@ -246,21 +149,17 @@ def test_halflife_by_severity(do_plot=False, do_show=True, do_save=False): sim.run() strain_labels = [ - f'Strain A: beta {pars["beta"][0]}, half_life by severity', - f'Strain B: beta {pars["beta"][1]}, half_life not by severity', + f'Strain A: beta {pars["beta"]}, half_life not by severity', + f'Strain B: beta {pars["beta"]}, half_life by severity', ] if do_plot: - sim.plot_result('new_reinfections', do_show=do_show, do_save=do_save) - # TODO: using the following line seems to flip the results??? - # plot_results(sim, key='cum_reinfections', - # title=f'2 strain test, A->B immunity {sim["immunity"][0, 1]}, B->A immunity {sim["immunity"][1, 0]}', - # labels=strain_labels, do_show=do_show, do_save=do_save) - plot_results(sim, key='incidence_by_strain', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}, B->A immunity {sim["immunity"][1,0]}', labels=strain_labels, do_show=do_show, do_save=do_save) + sim.plot_result('new_reinfections', fig_path='results/test_halflife_by_severity.png', do_show=do_show, do_save=do_save) + plot_results(sim, key='incidence_by_strain', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}, B->A immunity {sim["immunity"][1,0]}', filename='test_halflife_by_severity', labels=strain_labels, do_show=do_show, do_save=do_save) return sim -def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): +def plot_results(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): results = sim.results results_to_plot = results[key] @@ -268,7 +167,7 @@ def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): # extract data for plotting x = sim.results['t'] y = results_to_plot.values - y = np.flipud(y) + y = np.transpose(y) fig, ax = plt.subplots() ax.plot(x, y) @@ -284,12 +183,12 @@ def plot_results(sim, key, title, do_show=True, do_save=False, labels=None): if do_show: plt.show() if do_save: - cv.savefig(f'results/{title}.png') + cv.savefig(f'results/{filename}.png') return -def plot_shares(sim, key, title, do_show=True, do_save=False, labels=None): +def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): results = sim.results n_strains = sim.results['new_infections_by_strain'].values.shape[0] # TODO: this should be stored in the sim somewhere more intuitive! @@ -311,7 +210,7 @@ def plot_shares(sim, key, title, do_show=True, do_save=False, labels=None): if do_show: plt.show() if do_save: - cv.savefig(f'results/{title}.png') + cv.savefig(f'results/{filename}.png') return @@ -320,16 +219,52 @@ def plot_shares(sim, key, title, do_show=True, do_save=False, labels=None): if __name__ == '__main__': sc.tic() - # sim0 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim1 = test_2strains_import(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim2 = test_importstrain_withcrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim3 = test_importstrain_nocrossimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim4 = test_importstrain_args() - # sim5 = test_importB117(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) p1, p2, p3 = test_par_refactor() - # sim6 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) + + # simX = test_importstrain_args() sc.toc() print('Done.') + + + +# DEPRECATED +# def test_importstrain_args(): +# sc.heading('Test flexibility of arguments for the import strain "intervention"') +# +# # Run sim with a single strain initially, then introduce a new strain that's more transmissible on day 10 +# immunity = [ +# {'init_immunity': 1., 'half_life': 180, 'cross_factor': 1}, +# ] +# pars = { +# 'n_strains': 1, +# 'beta': [0.016], +# 'immunity': immunity +# } +# +# # All these should run +# imports = cv.import_strain(days=50, beta=0.03) +# #imports = cv.import_strain(days=[10, 50], beta=0.03) +# #imports = cv.import_strain(days=50, beta=[0.03, 0.05]) +# #imports = cv.import_strain(days=[10, 20], beta=[0.03, 0.05]) +# #imports = cv.import_strain(days=50, beta=[0.03, 0.05, 0.06]) +# #imports = cv.import_strain(days=[10, 20], n_imports=[5, 10], beta=[0.03, 0.05], init_immunity=[1, 1], +# # half_life=[180, 180], cross_factor=[0, 0]) +# #imports = cv.import_strain(days=[10, 50], beta=0.03, cross_factor=[0.4, 0.6]) +# #imports = cv.import_strain(days=['2020-04-01', '2020-05-01'], beta=0.03) +# +# # This should fail +# #imports = cv.import_strain(days=[20, 50], beta=[0.03, 0.05, 0.06]) +# +# sim = cv.Sim(pars=pars, interventions=imports) +# sim.run() +# +# +# return sim +# From 2f48d6ea7e02a8976044ff24af5965f2d7fe0e23 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 16 Feb 2021 15:45:30 +0100 Subject: [PATCH 062/569] testing reinfection --- tests/devtests/test_variants.py | 69 ++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index f551654a9..2f5339c39 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -9,6 +9,64 @@ do_save = 1 + +def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): + sc.heading('Run a basic sim with 1 strain, allowing for reinfection') + sc.heading('Setting up...') + + # Define baseline parameters + base_pars = { + 'beta': 0.1, # Make beta higher than usual so people get infected quickly + 'n_days': 120, + } + + n_runs = 3 + base_sim = cv.Sim(base_pars) + + # Define the scenarios + scenarios = { + 'baseline': { + 'name':'No reinfection', + 'pars': { + 'half_life': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection + } + }, + 'med_halflife': { + 'name':'Reinfection: slow-waning immunity', + 'pars': { + 'half_life': dict(asymptomatic=60, mild=60, severe=60), # immunity from reinfection + } + }, + 'short_halflife': { + 'name': 'Reinfection: fast-waning immunity', + 'pars': { + 'half_life': dict(asymptomatic=10, mild=10, severe=10), # Rapidly-decaying immunity from reinfection + } + }, + 'severity_halflife': { + 'name': 'Reinfection: immunity by severity', + 'pars': { + 'half_life': dict(asymptomatic=10, mild=30, severe=60), # Rapidly-decaying immunity from reinfection + } + }, + } + + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'Cumulative infections': ['cum_infections'], + 'New reinfections': ['new_reinfections'], + 'Cumulative reinfections': ['cum_reinfections'], + }) + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_basic_reinfection.png', to_plot=to_plot) + + return scens + + def test_2strains(do_plot=False, do_show=True, do_save=False): sc.heading('Run basic sim with 2 strains') sc.heading('Setting up...') @@ -219,11 +277,12 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab if __name__ == '__main__': sc.tic() - sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) - p1, p2, p3 = test_par_refactor() - sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) + #p1, p2, p3 = test_par_refactor() + #sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) # simX = test_importstrain_args() From 8678c61442fa9ee68f6826a0aae592010742f277 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 16 Feb 2021 15:59:59 +0100 Subject: [PATCH 063/569] final progress --- covasim/base.py | 1 + covasim/parameters.py | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index b57f85760..417330624 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -188,6 +188,7 @@ def npts(self): class Par(object): ''' + NB NOT USED ANYWHERE YET- PLACEHOLDER CLASS IN CASE USEFUL Stores a single parameter -- by default, acts like an array. Args: name (str): name of this parameter, e.g. beta diff --git a/covasim/parameters.py b/covasim/parameters.py index 980af927a..5c71fdcfb 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -29,12 +29,6 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): ''' pars = {} - # Helper function to initialize parameters that are stored as Par objects - def init_par(*args, **kwargs): - ''' Initialize a single result object ''' - output = cvb.Par(*args, **kwargs) - return output - # Population parameters pars['pop_size'] = 20e3 # Number of agents, i.e., people susceptible to SARS-CoV-2 pars['pop_infected'] = 20 # Number of initial infections From 5f10029a243f2e07958f944b442a0fca030533e4 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 16 Feb 2021 16:15:14 +0100 Subject: [PATCH 064/569] fix test --- tests/devtests/test_variants.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 2f5339c39..4466dd9c9 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -5,8 +5,8 @@ do_plot = 1 -do_show = 0 -do_save = 1 +do_show = 1 +do_save = 0 @@ -278,11 +278,11 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() scens = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) - #p1, p2, p3 = test_par_refactor() - #sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) + p1, p2, p3 = test_par_refactor() + sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) # simX = test_importstrain_args() From 3e6c056b7d99602fb0e35566d53974c2e6b8bc78 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 17 Feb 2021 15:37:16 +0100 Subject: [PATCH 065/569] plot shares --- tests/devtests/test_variants.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 4466dd9c9..73db53481 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -5,8 +5,8 @@ do_plot = 1 -do_show = 1 -do_save = 0 +do_show = 0 +do_save = 1 @@ -108,14 +108,17 @@ def test_importstrain1(do_plot=False, do_show=True, do_save=False): 'Strain 2: beta 0.025' ] + pars = {'n_days': 80, 'half_life': dict(asymptomatic=None, mild=None, severe=None), + 'cross_immunity':1.} + imported_strain = { 'beta': 0.025, - 'half_life': dict(asymptomatic=10, mild=50, severe=150), - 'init_immunity': 0.9 + 'half_life': dict(asymptomatic=None, mild=None, severe=None), + 'init_immunity': 1.0 } - imports = cv.import_strain(strain=imported_strain, days=30, n_imports=30) - sim = cv.Sim(interventions=imports, label='With imported infections') + imports = cv.import_strain(strain=imported_strain, days=10, n_imports=30) + sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') sim.run() if do_plot: @@ -250,11 +253,11 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab results = sim.results n_strains = sim.results['new_infections_by_strain'].values.shape[0] # TODO: this should be stored in the sim somewhere more intuitive! - prop_new = {f'Strain {s}': results[key+'_by_strain'].values[s,1:]/results[key].values[1:] for s in range(n_strains)} + prop_new = {f'Strain {s}': sc.safedivide(results[key+'_by_strain'].values[s,:], results[key].values, 0) for s in range(n_strains)} num_new = {f'Strain {s}': results[key+'_by_strain'].values[s,:] for s in range(n_strains)} # extract data for plotting - x = sim.results['t'][1:] + x = sim.results['t'] fig, ax = plt.subplots(2,1,sharex=True) ax[0].stackplot(x, prop_new.values(), labels=prop_new.keys()) @@ -277,12 +280,12 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab if __name__ == '__main__': sc.tic() - scens = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + #scens = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) - p1, p2, p3 = test_par_refactor() - sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) + #p1, p2, p3 = test_par_refactor() + #sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) # simX = test_importstrain_args() From 5a84e64ab6bf2c614d62466dc6cb13cc09766e45 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 17 Feb 2021 16:09:45 +0100 Subject: [PATCH 066/569] add new test --- covasim/sim.py | 3 ++- tests/devtests/test_variants.py | 36 +++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index c2a27d489..92b9f3f4c 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -864,8 +864,9 @@ def compute_r_eff(self, method='daily', smoothing=2, window=7): # Calculate R_eff as the mean infectious duration times the number of new infectious divided by the number of infectious people on a given day raw_values = mean_inf*self.results['new_infections'].values/(self.results['n_infectious'].values+1e-6) len_raw = len(raw_values) # Calculate the number of raw values + dur_pars = self['dur'][0] # TODO: fix this, need to somehow take all strains into account if len_raw >= 3: # Can't smooth arrays shorter than this since the default smoothing kernel has length 3 - initial_period = self['dur']['exp2inf']['par1'] + self['dur']['asym2rec']['par1'] # Approximate the duration of the seed infections for averaging + initial_period = dur_pars['exp2inf']['par1'] + dur_pars['asym2rec']['par1'] # Approximate the duration of the seed infections for averaging initial_period = int(min(len_raw, initial_period)) # Ensure we don't have too many points for ind in range(initial_period): # Loop over each of the initial inds raw_values[ind] = raw_values[ind:initial_period].mean() # Replace these values with their average diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 73db53481..574300170 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -99,6 +99,38 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): return sim +def test_sneakystrain(do_plot=False, do_show=True, do_save=False): + sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') + sc.heading('Setting up...') + + sim = cv.Sim() + dur = sc.dcp(sim['dur']) + dur['inf2sym'] = {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9} # Let's say this strain takes 10 days before you get symptoms + strains = {'dur': dur} + + pars = { + 'beta': 0.016, + 'n_days': 80, + 'strains': strains, + } + + cv.test_prob(symp_prob=0.2) + sim = cv.Sim(pars=pars) + sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B + sim['immunity'][1,0] = 0.9 # Say that strain B gives high immunity to strain A + sim.run() + + strain_labels = [ + f'Strain A: beta {sim["beta"][0]}, half_life {sim["half_life"][0]}', + f'Strain B: beta {sim["beta"][1]}, half_life {sim["half_life"][1]}', + ] + + if do_plot: + sim.plot_result('new_reinfections', do_show=do_show, do_save=do_save, fig_path=f'results/test_2strains.png') + plot_results(sim, key='incidence_by_strain', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}, B->A immunity {sim["immunity"][1,0]}', filename='test_2strains2', labels=strain_labels, do_show=do_show, do_save=do_save) + return sim + + def test_importstrain1(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing a new strain partway through a sim') sc.heading('Setting up...') @@ -281,8 +313,8 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() #scens = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) #sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) #p1, p2, p3 = test_par_refactor() #sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) From b46787c878fe9af15403472cb050bd630e9ef16b Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Thu, 18 Feb 2021 10:12:47 +0100 Subject: [PATCH 067/569] scenarios need refactoring --- covasim/interventions.py | 6 ++--- covasim/run.py | 12 +++++++-- covasim/sim.py | 3 ++- tests/devtests/test_variants.py | 47 ++++++++++++++++++++++++++++----- 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 892278899..664f841b1 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1145,7 +1145,7 @@ class import_strain(Intervention): half_life=[180, 180], immunity_to=[[0, 0], [0,0]], immunity_from=[[0, 0], [0,0]]) # On day 10, import 5 cases of one new strain, on day 20, import 10 cases of another ''' - def __init__(self, strain=None, days=None, n_imports=1, immunity_to=None, immunity_from=None, **kwargs): + def __init__(self, strain=None, days=None, n_imports=1, immunity_to=None, immunity_from=None, init_immunity=None, **kwargs): # Do standard initialization super().__init__(**kwargs) # Initialize the Intervention object self._store_args() # Store the input arguments so the intervention can be recreated @@ -1157,8 +1157,8 @@ def __init__(self, strain=None, days=None, n_imports=1, immunity_to=None, immuni self.immunity_from = sc.promotetolist(immunity_from) if immunity_from is not None else [None] self.strain = {par: sc.promotetolist(val) for par, val in strain.items()} for par, val in self.strain.items(): setattr(self, par, val) - if not getattr(self,'init_immunity'): - self.init_immunity = None + if not hasattr(self,'init_immunity'): + self.init_immunity = [None] self.new_strains = self.check_args(['days', 'n_imports']+list(self.strain.keys())) return diff --git a/covasim/run.py b/covasim/run.py index cd8ecc0b4..2a75e9c65 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -944,9 +944,17 @@ def print_heading(string): scenraw = {} for reskey in reskeys: - scenraw[reskey] = np.zeros((self.npts, len(scen_sims))) + if 'by_strain' in reskey: + scenraw[reskey] = np.zeros((self.npts, len(scen_sims))) + else: + scenraw[reskey] = np.zeros((self.npts, len(scen_sims))) for s,sim in enumerate(scen_sims): - scenraw[reskey][:,s] = sim.results[reskey].values + try: scenraw[reskey][:,s] = sim.results[reskey].values + except: + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() scenres = sc.objdict() scenres.best = {} diff --git a/covasim/sim.py b/covasim/sim.py index 92b9f3f4c..3bc86ade4 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -864,7 +864,8 @@ def compute_r_eff(self, method='daily', smoothing=2, window=7): # Calculate R_eff as the mean infectious duration times the number of new infectious divided by the number of infectious people on a given day raw_values = mean_inf*self.results['new_infections'].values/(self.results['n_infectious'].values+1e-6) len_raw = len(raw_values) # Calculate the number of raw values - dur_pars = self['dur'][0] # TODO: fix this, need to somehow take all strains into account + if sc.checktype(self['dur'], list): dur_pars = self['dur'][0] # TODO: fix this, need to somehow take all strains into account + else: dur_pars = self['dur'] if len_raw >= 3: # Can't smooth arrays shorter than this since the default smoothing kernel has length 3 initial_period = dur_pars['exp2inf']['par1'] + dur_pars['asym2rec']['par1'] # Approximate the duration of the seed infections for averaging initial_period = int(min(len_raw, initial_period)) # Ensure we don't have too many points diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 574300170..f00f8d19d 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -106,15 +106,47 @@ def test_sneakystrain(do_plot=False, do_show=True, do_save=False): sim = cv.Sim() dur = sc.dcp(sim['dur']) dur['inf2sym'] = {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9} # Let's say this strain takes 10 days before you get symptoms - strains = {'dur': dur} + imported_strain = {'dur': dur} - pars = { - 'beta': 0.016, - 'n_days': 80, - 'strains': strains, + imports = cv.import_strain(strain=imported_strain, days=10, n_imports=30) + tp = cv.test_prob(symp_prob=0.2) # Add an efficient testing program + + base_pars = { + 'beta': 0.015, # Make beta higher than usual so people get infected quickly + 'n_days': 120, + } + + n_runs = 3 + base_sim = cv.Sim(base_pars) + + # Define the scenarios + scenarios = { + 'baseline': { + 'name':'1 day to symptoms', + 'pars': {'interventions': [tp]} + }, + 'slowsymp': { + 'name':'10 days to symptoms', + 'pars': {'interventions': [imports, tp]} + } } - cv.test_prob(symp_prob=0.2) + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'Cumulative infections': ['cum_infections'], + 'New diagnoses': ['new_diagnoses'], + 'Cumulative diagnoses': ['cum_diagnoses'], + }) + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_sneakystrain.png', to_plot=to_plot) + + return scens + + sim = cv.Sim(pars=pars) sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B sim['immunity'][1,0] = 0.9 # Say that strain B gives high immunity to strain A @@ -313,7 +345,8 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() #scens = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens = test_sneakystrain(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) #sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) #sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) #p1, p2, p3 = test_par_refactor() From c62581aca8a7a148b79734d24a6ba9b0d2167a61 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Thu, 18 Feb 2021 12:01:31 +0100 Subject: [PATCH 068/569] fixes for scenarios --- covasim/run.py | 16 ++++++++-------- tests/devtests/test_variants.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index 2a75e9c65..b5ccb7fa8 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -939,22 +939,22 @@ def print_heading(string): else: scen_sims = multi_run(scen_sim, **run_args, **kwargs) # This is where the sims actually get run + # Get number of strains + ns = scen_sims[0].results['cum_infections_by_strain'].values.shape[0] + # Process the simulations print_heading(f'Processing {scenkey}') - scenraw = {} for reskey in reskeys: if 'by_strain' in reskey: - scenraw[reskey] = np.zeros((self.npts, len(scen_sims))) + scenraw[reskey] = np.zeros((ns, self.npts, len(scen_sims))) else: scenraw[reskey] = np.zeros((self.npts, len(scen_sims))) for s,sim in enumerate(scen_sims): - try: scenraw[reskey][:,s] = sim.results[reskey].values - except: - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() + if 'by_strain' in reskey: + scenraw[reskey][:,:,s] = sim.results[reskey].values + else: + scenraw[reskey][:,s] = sim.results[reskey].values scenres = sc.objdict() scenres.best = {} diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index f00f8d19d..35d5e56f3 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -116,7 +116,7 @@ def test_sneakystrain(do_plot=False, do_show=True, do_save=False): 'n_days': 120, } - n_runs = 3 + n_runs = 1 base_sim = cv.Sim(base_pars) # Define the scenarios From 5957c7f4cdb6052a93e6e5ed978ca65607b6699f Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Thu, 18 Feb 2021 13:25:26 +0100 Subject: [PATCH 069/569] small fixes to scenarios --- covasim/run.py | 13 +++++++++---- tests/devtests/test_variants.py | 18 +++++++++--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index b5ccb7fa8..189c91d54 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -961,9 +961,10 @@ def print_heading(string): scenres.low = {} scenres.high = {} for reskey in reskeys: - scenres.best[reskey] = np.quantile(scenraw[reskey], q=0.5, axis=1) # Changed from median to mean for smoother plots - scenres.low[reskey] = np.quantile(scenraw[reskey], q=self['quantiles']['low'], axis=1) - scenres.high[reskey] = np.quantile(scenraw[reskey], q=self['quantiles']['high'], axis=1) + axis = 2 if 'by_strain' in reskey else 1 + scenres.best[reskey] = np.quantile(scenraw[reskey], q=0.5, axis=axis) # Changed from median to mean for smoother plots + scenres.low[reskey] = np.quantile(scenraw[reskey], q=self['quantiles']['low'], axis=axis) + scenres.high[reskey] = np.quantile(scenraw[reskey], q=self['quantiles']['high'], axis=axis) for reskey in reskeys: self.results[reskey][scenkey]['name'] = scenname @@ -1010,7 +1011,11 @@ def compare(self, t=None, output=False): x = defaultdict(dict) for scenkey in self.scenarios.keys(): for reskey in self.result_keys(): - val = self.results[reskey][scenkey].best[day] + if 'by_strain' in reskey: + val = self.results[reskey][scenkey].best[0, day] # Only prints results for infections by first strain + reskey = reskey+'0' # Add strain number to the summary output + else: + val = self.results[reskey][scenkey].best[day] if reskey not in ['r_eff', 'doubling_time']: val = int(val) x[scenkey][reskey] = val diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 35d5e56f3..e97d0e667 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -99,7 +99,7 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): return sim -def test_sneakystrain(do_plot=False, do_show=True, do_save=False): +def test_strainduration(do_plot=False, do_show=True, do_save=False): sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') sc.heading('Setting up...') @@ -142,7 +142,7 @@ def test_sneakystrain(do_plot=False, do_show=True, do_save=False): 'Cumulative diagnoses': ['cum_diagnoses'], }) if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_sneakystrain.png', to_plot=to_plot) + scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_strainduration.png', to_plot=to_plot) return scens @@ -344,13 +344,13 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab if __name__ == '__main__': sc.tic() - #scens = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - scens = test_sneakystrain(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) - #p1, p2, p3 = test_par_refactor() - #sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) + p1, p2, p3 = test_par_refactor() + sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) # simX = test_importstrain_args() From 88b84463948a9cb74271e612feccd984f715af23 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Thu, 18 Feb 2021 13:31:57 +0100 Subject: [PATCH 070/569] remove old code --- tests/devtests/test_variants.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index e97d0e667..afc1f1cc6 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -147,21 +147,6 @@ def test_strainduration(do_plot=False, do_show=True, do_save=False): return scens - sim = cv.Sim(pars=pars) - sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B - sim['immunity'][1,0] = 0.9 # Say that strain B gives high immunity to strain A - sim.run() - - strain_labels = [ - f'Strain A: beta {sim["beta"][0]}, half_life {sim["half_life"][0]}', - f'Strain B: beta {sim["beta"][1]}, half_life {sim["half_life"][1]}', - ] - - if do_plot: - sim.plot_result('new_reinfections', do_show=do_show, do_save=do_save, fig_path=f'results/test_2strains.png') - plot_results(sim, key='incidence_by_strain', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}, B->A immunity {sim["immunity"][1,0]}', filename='test_2strains2', labels=strain_labels, do_show=do_show, do_save=do_save) - return sim - def test_importstrain1(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing a new strain partway through a sim') From 7c548eceae7134d660e01dee88b9b4a17fb51032 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 18 Feb 2021 08:55:41 -0500 Subject: [PATCH 071/569] changes to test_variants to play with duration changes --- covasim/sim.py | 4 +- tests/devtests/test_variants.py | 68 ++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index c2a27d489..6ca4f4f1b 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -830,12 +830,12 @@ def compute_doubling(self, window=3, max_doubling_time=30): return self.results['doubling_time'].values - def compute_r_eff(self, method='daily', smoothing=2, window=7): + def compute_r_eff(self, method='infectious', smoothing=2, window=7): ''' Effective reproduction number based on number of people each person infected. Args: - method (str): 'instant' uses daily infections, 'infectious' counts from the date infectious, 'outcome' counts from the date recovered/dead + method (str): 'daily' uses daily infections, 'infectious' counts from the date infectious, 'outcome' counts from the date recovered/dead smoothing (int): the number of steps to smooth over for the 'daily' method window (int): the size of the window used for 'infectious' and 'outcome' calculations (larger values are more accurate but less precise) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 4466dd9c9..70c426e91 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -73,8 +73,8 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): strains = {'beta': 0.025, 'rel_severe_prob': 1.3, # 30% more severe across all ages - 'half_life': dict(asymptomatic=20, mild=80, severe=200), - 'init_immunity': 0.9 + 'half_life': dict(asymptomatic=20, mild=80, severe=100), + 'init_immunity': 0.5 } pars = { @@ -85,7 +85,7 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): sim = cv.Sim(pars=pars) sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B - sim['immunity'][1,0] = 0.9 # Say that strain B gives high immunity to strain A + sim['immunity'][1,0] = 0.0 # Say that strain B gives high immunity to strain A sim.run() strain_labels = [ @@ -94,7 +94,7 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): ] if do_plot: - sim.plot_result('new_reinfections', do_show=do_show, do_save=do_save, fig_path=f'results/test_2strains.png') + # sim.plot_result('new_reinfections', do_show=do_show, do_save=do_save, fig_path=f'results/test_2strains.png') plot_results(sim, key='incidence_by_strain', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}, B->A immunity {sim["immunity"][1,0]}', filename='test_2strains2', labels=strain_labels, do_show=do_show, do_save=do_save) return sim @@ -110,8 +110,8 @@ def test_importstrain1(do_plot=False, do_show=True, do_save=False): imported_strain = { 'beta': 0.025, - 'half_life': dict(asymptomatic=10, mild=50, severe=150), - 'init_immunity': 0.9 + 'half_life': dict(asymptomatic=20, mild=80, severe=100), + 'init_immunity': 0.5 } imports = cv.import_strain(strain=imported_strain, days=30, n_imports=30) @@ -202,8 +202,8 @@ def test_halflife_by_severity(do_plot=False, do_show=True, do_save=False): } sim = cv.Sim(pars=pars) - sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B - sim['immunity'][1,0] = 0.0 # Say that strain B gives no immunity to strain A + sim['immunity'][0,1] = 1.0 # Say that strain A gives no immunity to strain B + sim['immunity'][1,0] = 1.0 # Say that strain B gives no immunity to strain A sim.run() strain_labels = [ @@ -217,6 +217,45 @@ def test_halflife_by_severity(do_plot=False, do_show=True, do_save=False): return sim +def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): + sc.heading('Test introducing a new strain with longer duration partway through a sim') + sc.heading('Setting up...') + + strain_labels = [ + 'Strain 1: beta 0.016', + 'Strain 2: beta 0.025' + ] + + pars = { + 'n_days': 120, + } + + imported_strain = { + 'beta': 0.025, + 'half_life': dict(asymptomatic=20, mild=80, severe=100), + 'init_immunity': 0.5, + 'dur': dict(exp2inf=dict(dist='lognormal_int', par1=6.0, par2=2.0), + inf2sym=dict(dist='lognormal_int', par1=4.0, par2=2.0), + sym2sev=dict(dist='lognormal_int', par1=8.0, par2=2.0), + sev2crit=dict(dist='lognormal_int', par1=8.0, par2=2.0), + asym2rec=dict(dist='lognormal_int', par1=5.0, par2=2.0), + mild2rec=dict(dist='lognormal_int', par1=12.0, par2=2.0), + sev2rec=dict(dist='lognormal_int', par1=12.0, par2=2.0), + crit2rec=dict(dist='lognormal_int', par1=12.0, par2=2.0), + crit2die=dict(dist='lognormal_int', par1=12.0, par2=2.0)), + + } + + imports = cv.import_strain(strain=imported_strain, days=10, n_imports=30) + sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') + sim.run() + + if do_plot: + plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (longer duration)', filename='test_importstrain1', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_shares(sim, key='new_infections', title='Shares of new infections by strain', filename='test_importstrain_longerdur_shares', do_show=do_show, do_save=do_save) + return sim + + def plot_results(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): results = sim.results @@ -277,12 +316,13 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab if __name__ == '__main__': sc.tic() - scens = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) - p1, p2, p3 = test_par_refactor() - sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) + # scens = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) + # p1, p2, p3 = test_par_refactor() + # sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim5 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # simX = test_importstrain_args() From 80ecbac8429677e6fb03604d2fa39e0b435331ac Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Thu, 18 Feb 2021 15:00:43 +0100 Subject: [PATCH 072/569] testing tests --- tests/devtests/test_variants.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index afc1f1cc6..5a865aa31 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -329,13 +329,13 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab if __name__ == '__main__': sc.tic() - scens = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - scens = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) + # scens = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + # scens = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) - p1, p2, p3 = test_par_refactor() - sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) + # p1, p2, p3 = test_par_refactor() + # sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) # simX = test_importstrain_args() From 0ded0132c26aa22b5d0db1250dfce7ed87861082 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 18 Feb 2021 15:56:41 -0500 Subject: [PATCH 073/569] added in pathogenic and transmissibility immunity --- covasim/defaults.py | 8 +++-- covasim/parameters.py | 42 ++++++++++++++++------ covasim/people.py | 41 ++++++++++++--------- covasim/sim.py | 40 +++++++++++++++------ covasim/utils.py | 8 +++-- tests/devtests/test_variants.py | 63 ++++++++++++++++++++++++++------- 6 files changed, 146 insertions(+), 56 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 59dd640ec..e5cc3a1ac 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -49,8 +49,12 @@ class PeopleMeta(sc.prettyobj): 'rel_trans', # Float 'rel_sus', # Float 'time_of_last_inf', # Int - 'immunity_factors', # Float - 'half_life', # Float + 'sus_immunity_factors', # Float + 'trans_immunity_factors', # Float + 'prog_immunity_factors', # Float + 'sus_half_life', # Float + 'trans_half_life', # Float + 'prog_half_life', # Float # 'immune_factor_by_strain', # Float ] diff --git a/covasim/parameters.py b/covasim/parameters.py index 5c71fdcfb..ab835f136 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -69,7 +69,11 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 pars['init_immunity'] = 1.0 # Default initial immunity - pars['half_life'] = dict(asymptomatic=180, mild=180, severe=180) + pars['half_life'] = {} + pars['half_life']['sus'] = dict(asymptomatic=180, mild=180, severe=180) + pars['half_life']['trans'] = dict(asymptomatic=180, mild=180, severe=180) + pars['half_life']['prog'] = dict(asymptomatic=180, mild=180, severe=180) + pars['dur'] = {} # Duration parameters: time for disease progression pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.6, par2=4.8) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, subtracting inf2sim duration @@ -290,19 +294,26 @@ def absolute_prognoses(prognoses): def initialize_immunity(pars): ''' Helper function to initialize the immunity matrices. - Matrix is of size sim['max_strains']*sim['max_strains'] and is initialized with default values + Susceptibility matrix is of size sim['max_strains']*sim['max_strains'] and is initialized with default values + Progression is a matrix of scalars of size sim['max_strains'] initialized with default values + Transmission is a matrix of scalars of size sim['max_strains'] initialized with default values ''' # If initial immunity/cross immunity factors are provided, use those, otherwise use defaults - pars['immunity'] = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) + pars['immunity'] = {} + pars['immunity']['sus'] = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) + pars['immunity']['prog'] = np.full( pars['max_strains'], np.nan, dtype=cvd.default_float) + pars['immunity']['trans'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) for i in range(pars['n_strains']): - pars['immunity'][i, i] = pars['init_immunity'] + pars['immunity']['sus'][i, i] = pars['init_immunity'] + pars['immunity']['prog'][i] = pars['init_immunity'] + pars['immunity']['trans'][i] = pars['init_immunity'] return pars def update_immunity(pars, create=True, update_strain=None, immunity_from=None, immunity_to=None, - init_immunity=None, half_life=None): + init_immunity=None, prog_immunity=None, trans_immunity=None): ''' - Helper function to update the immunity and half_life matrices. + Helper function to update the immunity matrices. Matrix is of size sim['max_strains']*sim['max_strains'] and takes the form: A B ... array([[1.0, 1.0, ...], @@ -347,13 +358,14 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i pars[par] = sc.promotetolist(pars[par]) + sc.promotetolist(val) # Update own-immunity, if values have been supplied - pars['immunity'][:ns, :ns] = pars['cross_immunity'] + pars['immunity']['sus'][:ns, :ns] = pars['cross_immunity'] if pars.get('init_immunity'): # Values have been supplied for own-immunity - np.fill_diagonal(pars['immunity'][:ns,:ns], pars['init_immunity']) + np.fill_diagonal(pars['immunity']['sus'][:ns,:ns], pars['init_immunity']) + pars['immunity']['prog'][:ns] = pars['init_immunity'] + pars['immunity']['trans'][:ns] = pars['init_immunity'] # Update immunity for a strain if supplied if update_strain is not None: - immunity = pars['immunity'] # check that immunity_from, immunity_to and init_immunity are provided and the right length. # Else use default values @@ -366,6 +378,12 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i if init_immunity is None: print('Initial immunity not provided, using default value') init_immunity = pars['init_immunity'] + if prog_immunity is None: + print('Initial immunity from progression not provided, using default value') + prog_immunity = pars['init_immunity'] + if trans_immunity is None: + print('Initial immunity from transmission not provided, using default value') + trans_immunity = pars['init_immunity'] immunity_from = sc.promotetolist(immunity_from) immunity_to = sc.promotetolist(immunity_to) @@ -380,8 +398,10 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i else: new_immunity_row[i] = new_immunity_column[i] = init_immunity - immunity[update_strain, :] = new_immunity_row - immunity[:, update_strain] = new_immunity_column + immunity['sus'][update_strain, :] = new_immunity_row + immunity['sus'][:, update_strain] = new_immunity_column + immunity['prog'][update_strain] = prog_immunity + immunity['trans'][update_strain] = trans_immunity pars['immunity'] = immunity diff --git a/covasim/people.py b/covasim/people.py index ed9c94d17..c6e195093 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -60,7 +60,7 @@ def __init__(self, pars, strict=True, **kwargs): self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif key == 'rel_trans' or key == 'rel_sus' or key == 'time_of_last_inf': self[key] = np.full((self.pars['max_strains'], self.pop_size), np.nan, dtype=cvd.default_float, order='F') - elif key == 'immunity_factors': # everyone starts out with no immunity to either strain. + elif key == 'sus_immunity_factors' or key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. self[key] = np.full((self.pars['max_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -398,6 +398,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str n_infections = len(inds) durpars = infect_pars['dur'] + halflifepars = infect_pars['half_life'] # Update states, strain info, and flows self.susceptible[inds] = False @@ -418,65 +419,71 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.time_of_last_inf[strain, inds] = self.t # Calculate how long before this person can infect other people - self.dur_exp2inf[inds] = cvu.sample(**durpars['exp2inf'], size=n_infections) + self.dur_exp2inf[inds] = cvu.sample(**durpars['exp2inf'], size=n_infections)*(1-self.trans_immunity_factors[strain, inds]) self.date_infectious[inds] = self.dur_exp2inf[inds] + self.t # Use prognosis probabilities to determine what happens to them - symp_probs = infect_pars['rel_symp_prob']*self.symp_prob[inds] # Calculate their actual probability of being symptomatic + symp_probs = infect_pars['rel_symp_prob']*self.symp_prob[inds]*(1-self.prog_immunity_factors[strain, inds]) # Calculate their actual probability of being symptomatic is_symp = cvu.binomial_arr(symp_probs) # Determine if they develop symptoms symp_inds = inds[is_symp] asymp_inds = inds[~is_symp] # Asymptomatic # CASE 1: Asymptomatic: may infect others, but have no symptoms and do not die - dur_asym2rec = cvu.sample(**durpars['asym2rec'], size=len(asymp_inds)) + dur_asym2rec = cvu.sample(**durpars['asym2rec'], size=len(asymp_inds))*(1-self.trans_immunity_factors[strain, asymp_inds]) self.date_recovered[asymp_inds] = self.date_infectious[asymp_inds] + dur_asym2rec # Date they recover self.dur_disease[asymp_inds] = self.dur_exp2inf[asymp_inds] + dur_asym2rec # Store how long this person had COVID-19 - self.half_life[asymp_inds] = infect_pars['half_life']['asymptomatic'] + self.sus_half_life[asymp_inds] = halflifepars['sus']['asymptomatic'] + self.trans_half_life[asymp_inds] = halflifepars['trans']['asymptomatic'] + self.prog_half_life[asymp_inds] = halflifepars['prog']['asymptomatic'] # CASE 2: Symptomatic: can either be mild, severe, or critical n_symp_inds = len(symp_inds) - self.dur_inf2sym[symp_inds] = cvu.sample(**durpars['inf2sym'], size=n_symp_inds) # Store how long this person took to develop symptoms + self.dur_inf2sym[symp_inds] = cvu.sample(**durpars['inf2sym'], size=n_symp_inds)*(1-self.trans_immunity_factors[strain, symp_inds]) # Store how long this person took to develop symptoms self.date_symptomatic[symp_inds] = self.date_infectious[symp_inds] + self.dur_inf2sym[symp_inds] # Date they become symptomatic - sev_probs = infect_pars['rel_severe_prob'] * self.severe_prob[symp_inds] # Probability of these people being severe + sev_probs = infect_pars['rel_severe_prob'] * self.severe_prob[symp_inds]*(1-self.prog_immunity_factors[strain, symp_inds]) # Probability of these people being severe is_sev = cvu.binomial_arr(sev_probs) # See if they're a severe or mild case sev_inds = symp_inds[is_sev] mild_inds = symp_inds[~is_sev] # Not severe # CASE 2.1: Mild symptoms, no hospitalization required and no probability of death - dur_mild2rec = cvu.sample(**durpars['mild2rec'], size=len(mild_inds)) + dur_mild2rec = cvu.sample(**durpars['mild2rec'], size=len(mild_inds))*(1-self.trans_immunity_factors[strain, mild_inds]) self.date_recovered[mild_inds] = self.date_symptomatic[mild_inds] + dur_mild2rec # Date they recover self.dur_disease[mild_inds] = self.dur_exp2inf[mild_inds] + self.dur_inf2sym[mild_inds] + dur_mild2rec # Store how long this person had COVID-19 - self.half_life[mild_inds] = infect_pars['half_life']['mild'] + self.sus_half_life[mild_inds] = halflifepars['sus']['mild'] + self.trans_half_life[mild_inds] = halflifepars['trans']['mild'] + self.prog_half_life[mild_inds] = halflifepars['prog']['mild'] # CASE 2.2: Severe cases: hospitalization required, may become critical - self.dur_sym2sev[sev_inds] = cvu.sample(**durpars['sym2sev'], size=len(sev_inds)) # Store how long this person took to develop severe symptoms + self.dur_sym2sev[sev_inds] = cvu.sample(**durpars['sym2sev'], size=len(sev_inds))*(1-self.trans_immunity_factors[strain, sev_inds]) # Store how long this person took to develop severe symptoms self.date_severe[sev_inds] = self.date_symptomatic[sev_inds] + self.dur_sym2sev[sev_inds] # Date symptoms become severe - self.half_life[sev_inds] = infect_pars['half_life']['severe'] - crit_probs = infect_pars['rel_crit_prob'] * self.crit_prob[sev_inds] * (self.pars['no_hosp_factor'] if hosp_max else 1.)# Probability of these people becoming critical - higher if no beds available + self.sus_half_life[sev_inds] = halflifepars['sus']['severe'] + self.trans_half_life[sev_inds] = halflifepars['trans']['severe'] + self.prog_half_life[sev_inds] = halflifepars['prog']['severe'] + crit_probs = infect_pars['rel_crit_prob'] * self.crit_prob[sev_inds] * (self.pars['no_hosp_factor'] if hosp_max else 1.) *(1-self.prog_immunity_factors[strain, sev_inds]) # Probability of these people becoming critical - higher if no beds available is_crit = cvu.binomial_arr(crit_probs) # See if they're a critical case crit_inds = sev_inds[is_crit] non_crit_inds = sev_inds[~is_crit] # CASE 2.2.1 Not critical - they will recover - dur_sev2rec = cvu.sample(**durpars['sev2rec'], size=len(non_crit_inds)) + dur_sev2rec = cvu.sample(**durpars['sev2rec'], size=len(non_crit_inds))*(1-self.trans_immunity_factors[strain, non_crit_inds]) self.date_recovered[non_crit_inds] = self.date_severe[non_crit_inds] + dur_sev2rec # Date they recover self.dur_disease[non_crit_inds] = self.dur_exp2inf[non_crit_inds] + self.dur_inf2sym[non_crit_inds] + self.dur_sym2sev[non_crit_inds] + dur_sev2rec # Store how long this person had COVID-19 # CASE 2.2.2: Critical cases: ICU required, may die - self.dur_sev2crit[crit_inds] = cvu.sample(**durpars['sev2crit'], size=len(crit_inds)) + self.dur_sev2crit[crit_inds] = cvu.sample(**durpars['sev2crit'], size=len(crit_inds))*(1-self.trans_immunity_factors[strain, crit_inds]) self.date_critical[crit_inds] = self.date_severe[crit_inds] + self.dur_sev2crit[crit_inds] # Date they become critical - death_probs = infect_pars['rel_death_prob'] * self.death_prob[crit_inds] * (self.pars['no_icu_factor'] if icu_max else 1.) # Probability they'll die + death_probs = infect_pars['rel_death_prob'] * self.death_prob[crit_inds] * (self.pars['no_icu_factor'] if icu_max else 1.)*(1-self.prog_immunity_factors[strain, crit_inds]) # Probability they'll die is_dead = cvu.binomial_arr(death_probs) # Death outcome dead_inds = crit_inds[is_dead] alive_inds = crit_inds[~is_dead] # CASE 2.2.2.1: Did not die - dur_crit2rec = cvu.sample(**durpars['crit2rec'], size=len(alive_inds)) + dur_crit2rec = cvu.sample(**durpars['crit2rec'], size=len(alive_inds))*(1-self.trans_immunity_factors[strain, alive_inds]) self.date_recovered[alive_inds] = self.date_critical[alive_inds] + dur_crit2rec # Date they recover self.dur_disease[alive_inds] = self.dur_exp2inf[alive_inds] + self.dur_inf2sym[alive_inds] + self.dur_sym2sev[alive_inds] + self.dur_sev2crit[alive_inds] + dur_crit2rec # Store how long this person had COVID-19 # CASE 2.2.2.2: Did die - dur_crit2die = cvu.sample(**durpars['crit2die'], size=len(dead_inds)) + dur_crit2die = cvu.sample(**durpars['crit2die'], size=len(dead_inds))*(1-self.trans_immunity_factors[strain, dead_inds]) self.date_dead[dead_inds] = self.date_critical[dead_inds] + dur_crit2die # Date of death self.dur_disease[dead_inds] = self.dur_exp2inf[dead_inds] + self.dur_inf2sym[dead_inds] + self.dur_sym2sev[dead_inds] + self.dur_sev2crit[dead_inds] + dur_crit2die # Store how long this person had COVID-19 diff --git a/covasim/sim.py b/covasim/sim.py index 4e2abf187..75c176051 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -537,20 +537,35 @@ def step(self): # Compute the probability of transmission beta = strain_pars['beta'] asymp_factor = strain_pars['asymp_factor'] - immunity_factors = people.immunity_factors[strain, :] - + immunity_factors = { + 'sus': people.sus_immunity_factors[strain, :], + 'trans': people.trans_immunity_factors[strain, :], + 'prog': people.prog_immunity_factors[strain, :] + } + init_immunity = { + 'sus': cvd.default_float(self['immunity']['sus'][strain, strain]), + 'trans': cvd.default_float(self['immunity']['trans'][strain]), + 'prog': cvd.default_float(self['immunity']['prog'][strain]), + } # Process immunity parameters and indices - # TODO-- make this severity-specific immune = people.recovered_strain == strain # Whether people have some immunity to this strain from a prior infection with this strain immune_time = t - date_rec[immune] # Time since recovery for people who were last infected by this strain immune_inds = cvd.default_int(cvu.true(immune)) # People with some immunity to this strain from a prior infection with this strain - init_immunity = cvd.default_float(self['immunity'][strain, strain]) - #TODO -- make half life by severity - half_life = people.half_life - # decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. - decay_rate = np.log(2) / half_life - decay_rate[np.isnan(decay_rate)] = 0 - decay_rate = cvd.default_float(decay_rate) + + half_life = { + 'sus': people.sus_half_life, + 'trans': people.trans_half_life, + 'prog': people.prog_half_life + } + + decay_rate = {} + + for key, val in half_life.items(): + rate = np.log(2) / val + rate[np.isnan(rate)] = 0 + rate = cvd.default_float(rate) + decay_rate[key] = rate + immunity_factors = cvu.compute_immunity(immunity_factors, immune_time, immune_inds, init_immunity, decay_rate) # Calculate immunity factors # Process cross-immunity parameters and indices, if relevant @@ -563,6 +578,9 @@ def step(self): cross_immunity = cvd.default_float(self['immunity'][cross_strain, strain]) # Immunity protection again this strain from other strains immunity_factors = cvu.compute_immunity(immunity_factors, cross_immune_time, cross_immune_inds, cross_immunity, decay_rate) # Calculate cross_immunity factors + people.trans_immunity_factors[strain, :] = immunity_factors['trans'] + people.prog_immunity_factors[strain, :] = immunity_factors['prog'] + # Define indices for this strain inf_by_this_strain = sc.dcp(inf) inf_by_this_strain[cvu.false(people.infectious_strain == strain)] = False @@ -580,7 +598,7 @@ def step(self): quar_factor = cvd.default_float(self['quar_factor'][lkey]) beta_layer = cvd.default_float(self['beta_layer'][lkey]) rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, - diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors) + diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors['sus']) rel_sus = np.float32(rel_sus) # TODO: why doesn't this get returned in this format already? # Calculate actual transmission diff --git a/covasim/utils.py b/covasim/utils.py index 4a132235f..f60e1e100 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -85,9 +85,13 @@ def compute_immunity(immunity_factors, immune_time, immune_inds, init_immunity, immunity_factors (float[]): immunity factors ''' - decay_rate = decay_rate[immune_inds] + immune_type_keys = immunity_factors.keys() + + for key in immune_type_keys: + decay_rate[key] = decay_rate[key][immune_inds] + immunity_factors[key][immune_inds] = init_immunity[key] * np.exp(-decay_rate[key] * immune_time) + - immunity_factors[immune_inds] = init_immunity * np.exp(-decay_rate * immune_time) # Calculate immunity factors return immunity_factors diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 9ea0d83ec..2f1adeed7 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -24,29 +24,66 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): base_sim = cv.Sim(base_pars) # Define the scenarios + scenarios = { 'baseline': { 'name':'No reinfection', 'pars': { - 'half_life': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection + 'half_life': { + 'sus': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection } - }, + } + }, 'med_halflife': { - 'name':'Reinfection: slow-waning immunity', + 'name':'Slow-waning susceptible, transmission and progression immunity', 'pars': { - 'half_life': dict(asymptomatic=60, mild=60, severe=60), # immunity from reinfection + 'half_life': { + 'sus': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection } - }, + } + }, 'short_halflife': { - 'name': 'Reinfection: fast-waning immunity', + 'name': 'Fast-waning susceptible, transmission and progression immunity', + 'pars': { + 'half_life': { + 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection + } + } + }, + 'short_susceptible_halflife_long_prog': { + 'name': 'Fast-waning suscptible, slow-waning progression and transmission immunity', + 'pars': { + 'half_life': { + 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection + } + } + }, + 'short_susceptible_trans_long_prog': { + 'name': 'Fast-waning susceptible and transmission, slow-waning progression immunity', 'pars': { - 'half_life': dict(asymptomatic=10, mild=10, severe=10), # Rapidly-decaying immunity from reinfection + 'half_life': { + 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection + } } }, - 'severity_halflife': { - 'name': 'Reinfection: immunity by severity', + 'short_susceptible_prog_long_trans': { + 'name': 'Fast-waning susceptible and progression, slow-waning transmission immunity', 'pars': { - 'half_life': dict(asymptomatic=10, mild=30, severe=60), # Rapidly-decaying immunity from reinfection + 'half_life': { + 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection + } } }, } @@ -59,7 +96,7 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): 'New infections': ['new_infections'], 'Cumulative infections': ['cum_infections'], 'New reinfections': ['new_reinfections'], - 'Cumulative reinfections': ['cum_reinfections'], + # 'Cumulative reinfections': ['cum_reinfections'], }) if do_plot: scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_basic_reinfection.png', to_plot=to_plot) @@ -368,14 +405,14 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab if __name__ == '__main__': sc.tic() - # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) # scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) # p1, p2, p3 = test_par_refactor() # sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim5 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim5 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # simX = test_importstrain_args() From f84b16308e2ad1f8442966429723e59023b2cffb Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 19 Feb 2021 08:48:12 -0500 Subject: [PATCH 074/569] some more changes! --- covasim/parameters.py | 23 +++++----- covasim/utils.py | 1 - tests/devtests/test_variants.py | 78 ++++++++++++++++----------------- 3 files changed, 52 insertions(+), 50 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index ab835f136..7d2ec6714 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -68,7 +68,10 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 - pars['init_immunity'] = 1.0 # Default initial immunity + pars['init_immunity'] = {} + pars['init_immunity']['sus'] = 1.0 # Default initial immunity + pars['init_immunity']['trans'] = 0.5 # Default initial immunity + pars['init_immunity']['prog'] = 0.5 # Default initial immunity pars['half_life'] = {} pars['half_life']['sus'] = dict(asymptomatic=180, mild=180, severe=180) pars['half_life']['trans'] = dict(asymptomatic=180, mild=180, severe=180) @@ -304,9 +307,9 @@ def initialize_immunity(pars): pars['immunity']['prog'] = np.full( pars['max_strains'], np.nan, dtype=cvd.default_float) pars['immunity']['trans'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) for i in range(pars['n_strains']): - pars['immunity']['sus'][i, i] = pars['init_immunity'] - pars['immunity']['prog'][i] = pars['init_immunity'] - pars['immunity']['trans'][i] = pars['init_immunity'] + pars['immunity']['sus'][i, i] = pars['init_immunity']['sus'] + pars['immunity']['prog'][i] = pars['init_immunity']['prog'] + pars['immunity']['trans'][i] = pars['init_immunity']['trans'] return pars @@ -360,9 +363,9 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i # Update own-immunity, if values have been supplied pars['immunity']['sus'][:ns, :ns] = pars['cross_immunity'] if pars.get('init_immunity'): # Values have been supplied for own-immunity - np.fill_diagonal(pars['immunity']['sus'][:ns,:ns], pars['init_immunity']) - pars['immunity']['prog'][:ns] = pars['init_immunity'] - pars['immunity']['trans'][:ns] = pars['init_immunity'] + np.fill_diagonal(pars['immunity']['sus'][:ns,:ns], pars['init_immunity']['sus']) + pars['immunity']['prog'][:ns] = pars['init_immunity']['prog'] + pars['immunity']['trans'][:ns] = pars['init_immunity']['trans'] # Update immunity for a strain if supplied if update_strain is not None: @@ -377,13 +380,13 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i immunity_to = [pars['cross_immunity']]*pars['n_strains'] if init_immunity is None: print('Initial immunity not provided, using default value') - init_immunity = pars['init_immunity'] + init_immunity = pars['init_immunity']['sus'] if prog_immunity is None: print('Initial immunity from progression not provided, using default value') - prog_immunity = pars['init_immunity'] + prog_immunity = pars['init_immunity']['prog'] if trans_immunity is None: print('Initial immunity from transmission not provided, using default value') - trans_immunity = pars['init_immunity'] + trans_immunity = pars['init_immunity']['trans'] immunity_from = sc.promotetolist(immunity_from) immunity_to = sc.promotetolist(immunity_to) diff --git a/covasim/utils.py b/covasim/utils.py index f60e1e100..bfefb7c46 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -91,7 +91,6 @@ def compute_immunity(immunity_factors, immune_time, immune_inds, init_immunity, decay_rate[key] = decay_rate[key][immune_inds] immunity_factors[key][immune_inds] = init_immunity[key] * np.exp(-decay_rate[key] * immune_time) - return immunity_factors diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 2f1adeed7..999f5afc6 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -36,56 +36,56 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): } } }, - 'med_halflife': { - 'name':'Slow-waning susceptible, transmission and progression immunity', - 'pars': { - 'half_life': { - 'sus': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection - } - } - }, - 'short_halflife': { - 'name': 'Fast-waning susceptible, transmission and progression immunity', - 'pars': { - 'half_life': { - 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection - } - } - }, + # 'med_halflife': { + # 'name':'Slow-waning susceptible, transmission and progression immunity', + # 'pars': { + # 'half_life': { + # 'sus': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, + # 'trans': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, + # 'prog': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection + # } + # } + # }, + # 'short_halflife': { + # 'name': 'Fast-waning susceptible, transmission and progression immunity', + # 'pars': { + # 'half_life': { + # 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + # 'trans': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + # 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection + # } + # } + # }, 'short_susceptible_halflife_long_prog': { - 'name': 'Fast-waning suscptible, slow-waning progression and transmission immunity', + 'name': 'Fast-waning susceptible, no waning progression and transmission immunity', 'pars': { 'half_life': { - 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection + 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection } } }, - 'short_susceptible_trans_long_prog': { - 'name': 'Fast-waning susceptible and transmission, slow-waning progression immunity', - 'pars': { - 'half_life': { - 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection - } - } - }, - 'short_susceptible_prog_long_trans': { - 'name': 'Fast-waning susceptible and progression, slow-waning transmission immunity', + 'long_half_life': { + 'name': 'Fast-waning susceptible, no waning progression and transmission immunity', 'pars': { 'half_life': { - 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection + 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection } } }, + # 'short_susceptible_prog_long_trans': { + # 'name': 'Fast-waning susceptible and progression, slow-waning transmission immunity', + # 'pars': { + # 'half_life': { + # 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + # 'trans': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, + # 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection + # } + # } + # }, } metapars = {'n_runs': n_runs} From 0fa4a34066ea3f5d076b687df6f2556048cc01d3 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 19 Feb 2021 15:00:54 +0100 Subject: [PATCH 075/569] add sus index --- covasim/parameters.py | 1 + covasim/sim.py | 2 +- tests/devtests/test_variants.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 7d2ec6714..fd1c30e13 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -362,6 +362,7 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i # Update own-immunity, if values have been supplied pars['immunity']['sus'][:ns, :ns] = pars['cross_immunity'] + if pars.get('init_immunity'): # Values have been supplied for own-immunity np.fill_diagonal(pars['immunity']['sus'][:ns,:ns], pars['init_immunity']['sus']) pars['immunity']['prog'][:ns] = pars['init_immunity']['prog'] diff --git a/covasim/sim.py b/covasim/sim.py index 75c176051..4284ad6aa 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -575,7 +575,7 @@ def step(self): cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by the cross strain cross_immune_inds = cvd.default_int(cvu.true(cross_immune)) # People with some immunity to this strain from a prior infection with another strain - cross_immunity = cvd.default_float(self['immunity'][cross_strain, strain]) # Immunity protection again this strain from other strains + cross_immunity = cvd.default_float(self['immunity']['sus'][cross_strain, strain]) # Immunity protection again this strain from other strains immunity_factors = cvu.compute_immunity(immunity_factors, cross_immune_time, cross_immune_inds, cross_immunity, decay_rate) # Calculate cross_immunity factors people.trans_immunity_factors[strain, :] = immunity_factors['trans'] diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 999f5afc6..1da69ca20 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -405,9 +405,9 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab if __name__ == '__main__': sc.tic() - scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) # scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) # p1, p2, p3 = test_par_refactor() From 455c199db393cf524defad8a08d6b81aa278d6c3 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 19 Feb 2021 15:31:45 +0100 Subject: [PATCH 076/569] decay rate fix --- covasim/parameters.py | 1 - covasim/sim.py | 8 +++++++- covasim/utils.py | 4 ++-- tests/devtests/test_variants.py | 13 ++++++++----- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index fd1c30e13..7d2ec6714 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -362,7 +362,6 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i # Update own-immunity, if values have been supplied pars['immunity']['sus'][:ns, :ns] = pars['cross_immunity'] - if pars.get('init_immunity'): # Values have been supplied for own-immunity np.fill_diagonal(pars['immunity']['sus'][:ns,:ns], pars['init_immunity']['sus']) pars['immunity']['prog'][:ns] = pars['init_immunity']['prog'] diff --git a/covasim/sim.py b/covasim/sim.py index 4284ad6aa..59833acbc 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -571,11 +571,17 @@ def step(self): # Process cross-immunity parameters and indices, if relevant if self['n_strains']>1: for cross_strain in range(self['n_strains']): + if cross_strain != strain: cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by the cross strain cross_immune_inds = cvd.default_int(cvu.true(cross_immune)) # People with some immunity to this strain from a prior infection with another strain - cross_immunity = cvd.default_float(self['immunity']['sus'][cross_strain, strain]) # Immunity protection again this strain from other strains + cross_immunity = { + 'sus': cvd.default_float(self['immunity']['sus'][cross_strain, strain]), + 'trans': cvd.default_float(self['immunity']['trans'][strain]), + 'prog': cvd.default_float(self['immunity']['prog'][strain]), + } + immunity_factors = cvu.compute_immunity(immunity_factors, cross_immune_time, cross_immune_inds, cross_immunity, decay_rate) # Calculate cross_immunity factors people.trans_immunity_factors[strain, :] = immunity_factors['trans'] diff --git a/covasim/utils.py b/covasim/utils.py index bfefb7c46..a8b7846ad 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -88,8 +88,8 @@ def compute_immunity(immunity_factors, immune_time, immune_inds, init_immunity, immune_type_keys = immunity_factors.keys() for key in immune_type_keys: - decay_rate[key] = decay_rate[key][immune_inds] - immunity_factors[key][immune_inds] = init_immunity[key] * np.exp(-decay_rate[key] * immune_time) + this_decay_rate = decay_rate[key][immune_inds] + immunity_factors[key][immune_inds] = init_immunity[key] * np.exp(-this_decay_rate * immune_time) return immunity_factors diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 1da69ca20..0b5ba01fb 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -110,8 +110,11 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): strains = {'beta': 0.025, 'rel_severe_prob': 1.3, # 30% more severe across all ages - 'half_life': dict(asymptomatic=20, mild=80, severe=100), - 'init_immunity': 0.5 + 'half_life': { + 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection + }, } pars = { @@ -121,8 +124,8 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): } sim = cv.Sim(pars=pars) - sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B - sim['immunity'][1,0] = 0.0 # Say that strain B gives high immunity to strain A + #sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B + #sim['immunity'][1,0] = 0.0 # Say that strain B gives high immunity to strain A sim.run() strain_labels = [ @@ -132,7 +135,7 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): if do_plot: # sim.plot_result('new_reinfections', do_show=do_show, do_save=do_save, fig_path=f'results/test_2strains.png') - plot_results(sim, key='incidence_by_strain', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}, B->A immunity {sim["immunity"][1,0]}', filename='test_2strains2', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_results(sim, key='incidence_by_strain', title=f'2 strain test', filename='test_2strains2', labels=strain_labels, do_show=do_show, do_save=do_save) return sim From 3feb06e9b25565b6fb721965c07eb5b26141eb49 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 19 Feb 2021 15:40:04 +0100 Subject: [PATCH 077/569] running all tests --- tests/devtests/test_variants.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 0b5ba01fb..59358493b 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -197,13 +197,16 @@ def test_importstrain1(do_plot=False, do_show=True, do_save=False): 'Strain 2: beta 0.025' ] - pars = {'n_days': 80, 'half_life': dict(asymptomatic=None, mild=None, severe=None), + pars = {'n_days': 80, + 'half_life': {'sus': dict(asymptomatic=None, mild=None, severe=None), + 'trans': dict(asymptomatic=None, mild=None, severe=None), + 'prog': dict(asymptomatic=None, mild=None, severe=None),}, 'cross_immunity':1.} imported_strain = { 'beta': 0.025, 'half_life': dict(asymptomatic=20, mild=80, severe=100), - 'init_immunity': 0.5 + 'init_immunity': {'sus': 0.5, 'trans': 0.5, 'prog': 0.5} } imports = cv.import_strain(strain=imported_strain, days=10, n_imports=30) @@ -285,7 +288,9 @@ def test_halflife_by_severity(do_plot=False, do_show=True, do_save=False): sc.heading('Run basic sim with 2 strains and half life by severity') sc.heading('Setting up...') - strains = {'half_life': dict(asymptomatic=100, mild=150, severe=200)} + strains = {'half_life': {'sus': dict(asymptomatic=100, mild=150, severe=200), + 'trans': dict(asymptomatic=100, mild=150, severe=200), + 'prog': dict(asymptomatic=100, mild=150, severe=200)}} pars = { 'n_days': 80, @@ -324,8 +329,8 @@ def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): imported_strain = { 'beta': 0.025, - 'half_life': dict(asymptomatic=20, mild=80, severe=100), - 'init_immunity': 0.5, +# 'half_life': dict(asymptomatic=20, mild=80, severe=100), +# 'init_immunity': 0.5, 'dur': dict(exp2inf=dict(dist='lognormal_int', par1=6.0, par2=2.0), inf2sym=dict(dist='lognormal_int', par1=4.0, par2=2.0), sym2sev=dict(dist='lognormal_int', par1=8.0, par2=2.0), @@ -408,15 +413,18 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab if __name__ == '__main__': sc.tic() - # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - # scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + p1, p2, p3 = test_par_refactor() + sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim5 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + + # Importing strains is not currently working, so the following tests break # sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) - # p1, p2, p3 = test_par_refactor() - # sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim5 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + # This next test is deprecated, can be removed # simX = test_importstrain_args() sc.toc() From 8d4fbff978e731073af6b6c8abd2b94f76da251a Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 19 Feb 2021 10:13:20 -0500 Subject: [PATCH 078/569] fixed import_strain intervention --- covasim/interventions.py | 9 +++++---- covasim/parameters.py | 2 +- tests/devtests/test_variants.py | 26 ++++++++++++++------------ 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 664f841b1..ab6165274 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1145,7 +1145,7 @@ class import_strain(Intervention): half_life=[180, 180], immunity_to=[[0, 0], [0,0]], immunity_from=[[0, 0], [0,0]]) # On day 10, import 5 cases of one new strain, on day 20, import 10 cases of another ''' - def __init__(self, strain=None, days=None, n_imports=1, immunity_to=None, immunity_from=None, init_immunity=None, **kwargs): + def __init__(self, strain=None, days=None, n_imports=1, immunity_to=None, immunity_from=None, **kwargs): # Do standard initialization super().__init__(**kwargs) # Initialize the Intervention object self._store_args() # Store the input arguments so the intervention can be recreated @@ -1219,11 +1219,12 @@ def apply(self, sim): else: # This wasn't previously stored by strain, so now it needs to be added by strain sim['strains'][sk] = sc.promotetolist(sim[sk]) + sc.promotetolist(self.strain[sk]) sim[sk] = sc.promotetolist(sim[sk]) + sc.promotetolist(sim['strains'][sk]) - + sim['n_strains'] += 1 cvpar.update_immunity(pars=sim.pars, create=False, update_strain=prev_strains, immunity_from=self.immunity_from[strain], - immunity_to=self.immunity_to[strain], init_immunity=self.init_immunity[strain]) + immunity_to=self.immunity_to[strain], init_immunity=self.init_immunity[strain]['sus'], + prog_immunity=self.init_immunity[strain]['prog'], trans_immunity=self.init_immunity[strain]['trans']) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) - sim['n_strains'] += 1 + return \ No newline at end of file diff --git a/covasim/parameters.py b/covasim/parameters.py index 7d2ec6714..126de5c72 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -394,7 +394,7 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i # create the immunity[update_strain,] and immunity[,update_strain] arrays new_immunity_row = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) new_immunity_column = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - for i in range(pars['n_strains']+1): + for i in range(pars['n_strains']): if i != update_strain: new_immunity_row[i] = immunity_from[i] new_immunity_column[i] = immunity_to[i] diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 59358493b..d62f88c1f 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -9,7 +9,6 @@ do_save = 1 - def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): sc.heading('Run a basic sim with 1 strain, allowing for reinfection') sc.heading('Setting up...') @@ -187,7 +186,6 @@ def test_strainduration(do_plot=False, do_show=True, do_save=False): return scens - def test_importstrain1(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing a new strain partway through a sim') sc.heading('Setting up...') @@ -201,12 +199,16 @@ def test_importstrain1(do_plot=False, do_show=True, do_save=False): 'half_life': {'sus': dict(asymptomatic=None, mild=None, severe=None), 'trans': dict(asymptomatic=None, mild=None, severe=None), 'prog': dict(asymptomatic=None, mild=None, severe=None),}, - 'cross_immunity':1.} + # 'cross_immunity':1. + } imported_strain = { 'beta': 0.025, - 'half_life': dict(asymptomatic=20, mild=80, severe=100), - 'init_immunity': {'sus': 0.5, 'trans': 0.5, 'prog': 0.5} + 'half_life': {'sus': dict(asymptomatic=None, mild=None, severe=None), + 'trans': dict(asymptomatic=None, mild=None, severe=None), + 'prog': dict(asymptomatic=None, mild=None, severe=None), + }, + 'init_immunity': {'sus': 0.5, 'trans': 0.5, 'prog': 0.5}, } imports = cv.import_strain(strain=imported_strain, days=10, n_imports=30) @@ -413,15 +415,15 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab if __name__ == '__main__': sc.tic() - scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - p1, p2, p3 = test_par_refactor() - sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim5 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + # scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # p1, p2, p3 = test_par_refactor() + # sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim5 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # Importing strains is not currently working, so the following tests break - # sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) # This next test is deprecated, can be removed From 410603b0e9bdada13702af5c838b8f9fcfd22af9 Mon Sep 17 00:00:00 2001 From: rlatkowski Date: Fri, 19 Feb 2021 16:52:06 +0100 Subject: [PATCH 079/569] Minor comment improvements (misspeling inf2sym and infect) --- covasim/README.rst | 2 +- covasim/parameters.py | 2 +- covasim/people.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/covasim/README.rst b/covasim/README.rst index 47118c562..b214604bd 100644 --- a/covasim/README.rst +++ b/covasim/README.rst @@ -45,7 +45,7 @@ Efficacy of protection measures Time for disease progression ---------------------------- -* ``exp2inf`` = Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, subtracting inf2sim duration +* ``exp2inf`` = Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, subtracting inf2sym duration * ``inf2sym`` = Duration from infectious to symptomatic; see Linton et al., https://doi.org/10.3390/jcm9020538 * ``sym2sev`` = Duration from symptomatic to severe symptoms; see Linton et al., https://doi.org/10.3390/jcm9020538Duration from severe symptoms to requiring ICU; see Wang et al., https://jamanetwork.com/journals/jama/fullarticle/2761044Duration from severe symptoms to requiring ICU diff --git a/covasim/parameters.py b/covasim/parameters.py index 5b71306bf..f806150bf 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -64,7 +64,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Duration parameters: time for disease progression pars['dur'] = {} - pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.6, par2=4.8) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, subtracting inf2sim duration + pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.6, par2=4.8) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, subtracting inf2sym duration pars['dur']['inf2sym'] = dict(dist='lognormal_int', par1=1.0, par2=0.9) # Duration from infectious to symptomatic; see Linton et al., https://doi.org/10.3390/jcm9020538 pars['dur']['sym2sev'] = dict(dist='lognormal_int', par1=6.6, par2=4.9) # Duration from symptomatic to severe symptoms; see Linton et al., https://doi.org/10.3390/jcm9020538 pars['dur']['sev2crit'] = dict(dist='lognormal_int', par1=3.0, par2=7.4) # Duration from severe symptoms to requiring ICU; see Wang et al., https://jamanetwork.com/journals/jama/fullarticle/2761044 diff --git a/covasim/people.py b/covasim/people.py index 87a3af34a..0000cc37e 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -329,6 +329,8 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None): * Infected people that develop symptoms are disaggregated into mild vs. severe (=requires hospitalization) vs. critical (=requires ICU) * Every asymptomatic, mildly symptomatic, and severely symptomatic person recovers * Critical cases either recover or die + Method also deduplicates input arrays in case one agent is infected many times + and stores who infected whom in infection_log list. Args: inds (array): array of people to infect From 26dd3b59b64f013b562f35a4b644b90d3a0b5aa6 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 22 Feb 2021 17:03:15 -0500 Subject: [PATCH 080/569] ugly fix for update_immunity so that it updates init_immunity --- covasim/base.py | 5 +- covasim/parameters.py | 26 ++++++---- tests/devtests/test_variants.py | 90 +++++++++++++++++++-------------- 3 files changed, 72 insertions(+), 49 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 417330624..d55966474 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -312,7 +312,10 @@ def update_pars(self, pars=None, create=False, immunity_pars=None, strain_pars=N if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses if pars.get('strains'): - pars['n_strains'] = 1 + len(sc.promotetolist(next(iter(pars['strains'].values())))) + n_strains = len(sc.promotetolist(next(iter(pars['strains'].values())))) + print(f'provided information for {n_strains} circulating strains') + pars['n_strains'] = n_strains + pars['max_strains'] = self.pars['max_strains'] pars = sc.mergedicts(immunity_pars, strain_pars, pars) pars = cvpar.update_immunity(pars) # Update immunity with values provided super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj diff --git a/covasim/parameters.py b/covasim/parameters.py index 126de5c72..c848ff555 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -306,10 +306,13 @@ def initialize_immunity(pars): pars['immunity']['sus'] = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) pars['immunity']['prog'] = np.full( pars['max_strains'], np.nan, dtype=cvd.default_float) pars['immunity']['trans'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) + for par, val in pars['init_immunity'].items(): + if not isinstance(val, list): + pars['init_immunity'][par] = sc.promotetolist(val) for i in range(pars['n_strains']): - pars['immunity']['sus'][i, i] = pars['init_immunity']['sus'] - pars['immunity']['prog'][i] = pars['init_immunity']['prog'] - pars['immunity']['trans'][i] = pars['init_immunity']['trans'] + pars['immunity']['sus'][i, i] = pars['init_immunity']['sus'][i] + pars['immunity']['prog'][i] = pars['init_immunity']['prog'][i] + pars['immunity']['trans'][i] = pars['init_immunity']['trans'][i] return pars @@ -348,6 +351,15 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i ''' if create: + # Update all strain-specific values + for par, val in pars['strains'].items(): + if par == 'init_immunity': + pars[par] = {} + for subpar, subval in val.items(): + pars[par][subpar] = sc.promotetolist(subval) + else: + pars[par] = sc.promotetolist(val) + # Cross immunity values are set if there is more than one strain circulating # if 'n_strains' isn't provided, assume it's 1 if not pars.get('n_strains'): @@ -355,13 +367,9 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i pars = initialize_immunity(pars) ns = pars['n_strains'] # Shorten - # Update all strain-specific values - for par,val in pars['strains'].items(): - if pars.get(par): - pars[par] = sc.promotetolist(pars[par]) + sc.promotetolist(val) - # Update own-immunity, if values have been supplied - pars['immunity']['sus'][:ns, :ns] = pars['cross_immunity'] + if pars.get('cross_immunity'): # Values have been supplied for cross-immunity + pars['immunity']['sus'][:ns, :ns] = pars['cross_immunity'] if pars.get('init_immunity'): # Values have been supplied for own-immunity np.fill_diagonal(pars['immunity']['sus'][:ns,:ns], pars['init_immunity']['sus']) pars['immunity']['prog'][:ns] = pars['init_immunity']['prog'] diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index d62f88c1f..190347bbb 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -28,53 +28,65 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): 'baseline': { 'name':'No reinfection', 'pars': { - 'half_life': { - 'sus': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection + 'strains': { + 'half_life': { + 'sus': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection + }, + 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, } } }, - # 'med_halflife': { - # 'name':'Slow-waning susceptible, transmission and progression immunity', - # 'pars': { + 'med_halflife': { + 'name':'Fast-waning susceptible, transmission and progression immunity, 50% init', + 'pars': { + 'strains': { + 'half_life': { + 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection + }, + 'init_immunity': {'sus': 0.5, 'trans': 0.5, 'prog': 0.5}, + } + } + }, + 'short_halflife': { + 'name': 'Fast-waning susceptible, transmission and progression immunity', + 'pars': { + 'strains': { + 'half_life': { + 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection + }, + 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, + } + + } + }, + # 'short_half_life': { + # 'name': 'Fast-waning susceptible, progression and transmission immunity', + # 'pars': { # 'half_life': { - # 'sus': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, - # 'trans': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, - # 'prog': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection - # } + # 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, + # 'trans': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection, + # 'prog': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection + # }, + # 'init_immunity': {'sus': 1, 'trans': 0.5, 'prog': 0.5}, # } # }, - # 'short_halflife': { - # 'name': 'Fast-waning susceptible, transmission and progression immunity', + # 'med_halflife_higherinit': { + # 'name': 'Slow-waning susceptible, progression and transmission immunity, 100% init', # 'pars': { - # 'half_life': { - # 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, + # 'half_life': { + # 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, # 'trans': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, # 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection - # } - # } + # }, + # 'init_immunity': {'sus': 0, 'trans': 0, 'prog': 0}, + # } # }, - 'short_susceptible_halflife_long_prog': { - 'name': 'Fast-waning susceptible, no waning progression and transmission immunity', - 'pars': { - 'half_life': { - 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection - } - } - }, - 'long_half_life': { - 'name': 'Fast-waning susceptible, no waning progression and transmission immunity', - 'pars': { - 'half_life': { - 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection - } - } - }, # 'short_susceptible_prog_long_trans': { # 'name': 'Fast-waning susceptible and progression, slow-waning transmission immunity', # 'pars': { @@ -415,7 +427,7 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab if __name__ == '__main__': sc.tic() - # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) # scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # p1, p2, p3 = test_par_refactor() @@ -423,7 +435,7 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab # sim5 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # Importing strains is not currently working, so the following tests break - sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) # This next test is deprecated, can be removed From 6d8595d623ebb33d80b0d0e89d577776543926f9 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 22 Feb 2021 17:10:09 -0500 Subject: [PATCH 081/569] updates to test file --- tests/devtests/test_variants.py | 50 ++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 190347bbb..7fb1c143c 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -52,7 +52,7 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): } }, 'short_halflife': { - 'name': 'Fast-waning susceptible, transmission and progression immunity', + 'name': 'Fast-waning susceptible, transmission and progression immunity, 100% init', 'pars': { 'strains': { 'half_life': { @@ -65,28 +65,32 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): } }, - # 'short_half_life': { - # 'name': 'Fast-waning susceptible, progression and transmission immunity', - # 'pars': { - # 'half_life': { - # 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, - # 'trans': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection, - # 'prog': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection - # }, - # 'init_immunity': {'sus': 1, 'trans': 0.5, 'prog': 0.5}, - # } - # }, - # 'med_halflife_higherinit': { - # 'name': 'Slow-waning susceptible, progression and transmission immunity, 100% init', - # 'pars': { - # 'half_life': { - # 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - # 'trans': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - # 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection - # }, - # 'init_immunity': {'sus': 0, 'trans': 0, 'prog': 0}, - # } - # }, + 'short_half_life': { + 'name': 'Fast-waning susceptible, slow progression and transmission immunity, 100% init', + 'pars': { + 'strains': { + 'half_life': { + 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection + }, + 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, + } + } + }, + 'short_half_life_50init': { + 'name': 'Fast-waning susceptible, slow progression and transmission immunity, 50% init', + 'pars': { + 'strains': { + 'half_life': { + 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection + }, + 'init_immunity': {'sus': .5, 'trans': .5, 'prog': .5}, + } + } + }, # 'short_susceptible_prog_long_trans': { # 'name': 'Fast-waning susceptible and progression, slow-waning transmission immunity', # 'pars': { From 16c56c4bd025ee85cb183d2d6cacdf67cd133a91 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 22 Feb 2021 18:54:08 -0500 Subject: [PATCH 082/569] updates to test file --- tests/devtests/test_variants.py | 68 +++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 7fb1c143c..2257143ef 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -16,9 +16,20 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): # Define baseline parameters base_pars = { 'beta': 0.1, # Make beta higher than usual so people get infected quickly - 'n_days': 120, + 'n_days': 240, } + imported_strain = { + 'beta': 0.2, + 'half_life': { + 'sus': dict(asymptomatic=55, mild=60, severe=150), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection + }, + 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, + } + + imports = cv.import_strain(strain=imported_strain, immunity_to=0.5, immunity_from=0.5, days=50, n_imports=30) n_runs = 3 base_sim = cv.Sim(base_pars) @@ -39,26 +50,26 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): } }, 'med_halflife': { - 'name':'Fast-waning susceptible, transmission and progression immunity, 50% init', + 'name':'3 month waning susceptible, transmission and progression immunity', 'pars': { 'strains': { 'half_life': { - 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection + 'sus': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection }, - 'init_immunity': {'sus': 0.5, 'trans': 0.5, 'prog': 0.5}, + 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, } } }, 'short_halflife': { - 'name': 'Fast-waning susceptible, transmission and progression immunity, 100% init', + 'name': '3 month waning susceptible, slow waning transmission and progression immunity', 'pars': { 'strains': { 'half_life': { - 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection + 'sus': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection }, 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, } @@ -66,31 +77,32 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): } }, 'short_half_life': { - 'name': 'Fast-waning susceptible, slow progression and transmission immunity, 100% init', + 'name': '3 month waning susceptible, slow progression and transmission immunity, new strain on day 50', 'pars': { 'strains': { 'half_life': { - 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection + 'sus': dict(asymptomatic=55, mild=60, severe=150), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection }, 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, - } - } - }, - 'short_half_life_50init': { - 'name': 'Fast-waning susceptible, slow progression and transmission immunity, 50% init', - 'pars': { - 'strains': { - 'half_life': { - 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection - }, - 'init_immunity': {'sus': .5, 'trans': .5, 'prog': .5}, - } + }, + 'interventions': imports } }, + # 'short_half_life_50init': { + # 'name': 'Fast-waning susceptible, slow progression and transmission immunity, 50% init', + # 'pars': { + # 'strains': { + # 'half_life': { + # 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, + # 'trans': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection, + # 'prog': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection + # }, + # 'init_immunity': {'sus': .5, 'trans': .5, 'prog': .5}, + # } + # } + # }, # 'short_susceptible_prog_long_trans': { # 'name': 'Fast-waning susceptible and progression, slow-waning transmission immunity', # 'pars': { From 7bd152548010c87525613ba91f066c82dd7649fb Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 23 Feb 2021 17:15:44 -0500 Subject: [PATCH 083/569] some of Robyn's edits, preserving a lot --- covasim/base.py | 7 ++--- covasim/defaults.py | 4 +-- covasim/interventions.py | 2 +- covasim/parameters.py | 56 ++++++++++++++++++--------------- covasim/people.py | 2 +- covasim/run.py | 7 ++++- covasim/sim.py | 17 +++++----- tests/devtests/test_variants.py | 14 +++++---- 8 files changed, 59 insertions(+), 50 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index d55966474..27413b030 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -312,10 +312,9 @@ def update_pars(self, pars=None, create=False, immunity_pars=None, strain_pars=N if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses if pars.get('strains'): - n_strains = len(sc.promotetolist(next(iter(pars['strains'].values())))) - print(f'provided information for {n_strains} circulating strains') - pars['n_strains'] = n_strains - pars['max_strains'] = self.pars['max_strains'] + pars = sc.mergedicts(immunity_pars, strain_pars, pars) + pars = cvpar.update_strain_pars(pars) + print(f'provided information for {pars["n_strains"]} circulating strains') pars = sc.mergedicts(immunity_pars, strain_pars, pars) pars = cvpar.update_immunity(pars) # Update immunity with values provided super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj diff --git a/covasim/defaults.py b/covasim/defaults.py index e5cc3a1ac..c201d7faf 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -68,7 +68,6 @@ class PeopleMeta(sc.prettyobj): 'critical', 'tested', 'diagnosed', - #'recovered', 'dead', 'known_contact', 'quarantined', @@ -80,7 +79,6 @@ class PeopleMeta(sc.prettyobj): 'infectious_strain', 'infectious_by_strain', 'recovered_strain', - # 'recovered_by_strain', ] # Set the dates various events took place: these are floats per person -- used in people.py @@ -148,6 +146,8 @@ class PeopleMeta(sc.prettyobj): 'rel_crit_prob', 'rel_death_prob'] +immunity_axes = ['sus', 'trans', 'prog'] + # Default age data, based on Seattle 2018 census data -- used in population.py default_age_data = np.array([ [ 0, 4, 0.0605], diff --git a/covasim/interventions.py b/covasim/interventions.py index ab6165274..7f6854f05 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1158,7 +1158,7 @@ def __init__(self, strain=None, days=None, n_imports=1, immunity_to=None, immuni self.strain = {par: sc.promotetolist(val) for par, val in strain.items()} for par, val in self.strain.items(): setattr(self, par, val) if not hasattr(self,'init_immunity'): - self.init_immunity = [None] + self.init_immunity = [{key:None for key in cvd.immunity_axes}] self.new_strains = self.check_args(['days', 'n_imports']+list(self.strain.keys())) return diff --git a/covasim/parameters.py b/covasim/parameters.py index c848ff555..22db2bb5c 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -119,7 +119,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): reset_layer_pars(pars) if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses - pars = initialize_immunity(pars) # Initialise immunity + pars = initialize_immunity(pars) # Initialize immunity # If version is specified, load old parameters if version is not None: @@ -294,25 +294,31 @@ def absolute_prognoses(prognoses): return out +def update_strain_pars(pars): + ''' Helper function to update parameters that have been set to vary by strain ''' + # Update all strain-specific values + for sp in cvd.strain_pars: + if sp in pars['strains'].keys(): + pars[sp] = sc.promotetolist(pars['strains'][sp]) + pars['n_strains'] = len(pars[sp]) # This gets overwritten for each parameter, but that's ok + return pars + def initialize_immunity(pars): ''' - Helper function to initialize the immunity matrices. + Initialize the immunity matrices. Susceptibility matrix is of size sim['max_strains']*sim['max_strains'] and is initialized with default values Progression is a matrix of scalars of size sim['max_strains'] initialized with default values Transmission is a matrix of scalars of size sim['max_strains'] initialized with default values ''' - # If initial immunity/cross immunity factors are provided, use those, otherwise use defaults + pars['immunity'] = {} pars['immunity']['sus'] = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) pars['immunity']['prog'] = np.full( pars['max_strains'], np.nan, dtype=cvd.default_float) pars['immunity']['trans'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) for par, val in pars['init_immunity'].items(): - if not isinstance(val, list): - pars['init_immunity'][par] = sc.promotetolist(val) - for i in range(pars['n_strains']): - pars['immunity']['sus'][i, i] = pars['init_immunity']['sus'][i] - pars['immunity']['prog'][i] = pars['init_immunity']['prog'][i] - pars['immunity']['trans'][i] = pars['init_immunity']['trans'][i] + if par == 'sus': pars['immunity'][par][0,0] = val + else: pars['immunity'][par][0] = val + return pars @@ -351,29 +357,27 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i ''' if create: - # Update all strain-specific values - for par, val in pars['strains'].items(): - if par == 'init_immunity': - pars[par] = {} - for subpar, subval in val.items(): - pars[par][subpar] = sc.promotetolist(subval) - else: - pars[par] = sc.promotetolist(val) - # Cross immunity values are set if there is more than one strain circulating # if 'n_strains' isn't provided, assume it's 1 if not pars.get('n_strains'): pars['n_strains'] = 1 - pars = initialize_immunity(pars) ns = pars['n_strains'] # Shorten - # Update own-immunity, if values have been supplied - if pars.get('cross_immunity'): # Values have been supplied for cross-immunity - pars['immunity']['sus'][:ns, :ns] = pars['cross_immunity'] - if pars.get('init_immunity'): # Values have been supplied for own-immunity - np.fill_diagonal(pars['immunity']['sus'][:ns,:ns], pars['init_immunity']['sus']) - pars['immunity']['prog'][:ns] = pars['init_immunity']['prog'] - pars['immunity']['trans'][:ns] = pars['init_immunity']['trans'] + # Update immunity matrix + pars['immunity']['sus'][:ns, :ns] = pars['cross_immunity'] + if pars['strains'].get('init_immunity'): + for ps in cvd.immunity_axes: + for strain in range(ns): + if ps == 'sus': + pars['immunity'][ps][strain, strain] = pars['init_immunity'][strain][ps] + else: + pars['immunity'][ps][strain] = pars['init_immunity'][strain][ps] + else: + for ps in cvd.immunity_axes: + if ps == 'sus': + np.fill_diagonal(pars['immunity'][ps][:ns, :ns], pars['init_immunity'][ps]) + else: + pars['immunity'][ps][:ns] = pars['init_immunity'][ps] # Update immunity for a strain if supplied if update_strain is not None: diff --git a/covasim/people.py b/covasim/people.py index c6e195093..497f98e74 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -60,7 +60,7 @@ def __init__(self, pars, strict=True, **kwargs): self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif key == 'rel_trans' or key == 'rel_sus' or key == 'time_of_last_inf': self[key] = np.full((self.pars['max_strains'], self.pop_size), np.nan, dtype=cvd.default_float, order='F') - elif key == 'sus_immunity_factors' or key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. + elif key == 'sus_immunity_factors' or key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. self[key] = np.full((self.pars['max_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) diff --git a/covasim/run.py b/covasim/run.py index 189c91d54..79eb03a85 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -930,7 +930,12 @@ def print_heading(string): print_heading(f'Multirun for {scenkey}') scen_sim = sc.dcp(self.base_sim) scen_sim.label = scenkey - scen_sim.update_pars(scenpars) + immunity_pars = dict(max_strains=scen_sim['max_strains'], + cross_immunity=scen_sim['cross_immunity'], + immunity=scen_sim['immunity']) + strain_pars = {par: scen_sim[par] for par in cvd.strain_pars} + scen_sim.update_pars(scenpars, immunity_pars=immunity_pars, strain_pars=strain_pars, + **kwargs) # Update the parameters, if provided run_args = dict(n_runs=self['n_runs'], noise=self['noise'], noisepar=self['noisepar'], keep_people=keep_people, verbose=verbose) if debug: print('Running in debug mode (not parallelized)') diff --git a/covasim/sim.py b/covasim/sim.py index 59833acbc..92c148ba6 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -68,13 +68,14 @@ def __init__(self, pars=None, datafile=None, datacols=None, label=None, simfile= self._default_ver = version # Default version of parameters used self._orig_pars = None # Store original parameters to optionally restore at the end of the simulation - # Update the parameters + # Make default parameters (using values from parameters.py) default_pars = cvpar.make_pars(version=version) # Start with default pars super().__init__(default_pars) # Initialize and set the parameters as attributes # Now update everything self.set_metadata(simfile) # Set the simulation date and filename - immunity_pars = dict(max_strains=default_pars['max_strains'], + immunity_pars = dict(immunity=default_pars['immunity'], + max_strains=default_pars['max_strains'], cross_immunity=default_pars['cross_immunity']) strain_pars = {par: default_pars[par] for par in cvd.strain_pars} self.update_pars(pars, immunity_pars=immunity_pars, strain_pars=strain_pars, **kwargs) # Update the parameters, if provided @@ -482,18 +483,16 @@ def step(self): contacts = people.update_contacts() # Compute new contacts hosp_max = people.count('severe') > self['n_beds_hosp'] if self['n_beds_hosp'] else False # Check for acute bed constraint icu_max = people.count('critical') > self['n_beds_icu'] if self['n_beds_icu'] else False # Check for ICU bed constraint + ns = self['n_strains'] # Shorten number of strains # Randomly infect some people (imported infections) - imports = cvu.n_poisson(self['n_imports'], self['n_strains']) # Imported cases - # TODO -- calculate imports per strain. + imports = cvu.n_poisson(self['n_imports'], ns) # Imported cases for strain, n_imports in enumerate(imports): if n_imports>0: importation_inds = cvu.choose(max_n=len(people), n=n_imports) people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation', strain=strain) - # TODO -- Randomly introduce new strain - # Apply interventions for intervention in self['interventions']: if isinstance(intervention, cvi.Intervention): @@ -523,7 +522,7 @@ def step(self): quar = people.quarantined # Iterate through n_strains to calculate infections - for strain in range(self['n_strains']): + for strain in range(ns): # Deal with strain parameters strain_parkeys = ['beta', 'asymp_factor'] @@ -535,8 +534,8 @@ def step(self): strain_pars[key] = cvd.default_float(self[key]) # Compute the probability of transmission - beta = strain_pars['beta'] - asymp_factor = strain_pars['asymp_factor'] + beta = cvd.default_float(strain_pars['beta']) + asymp_factor = cvd.default_float(strain_pars['asymp_factor']) immunity_factors = { 'sus': people.sus_immunity_factors[strain, :], 'trans': people.trans_immunity_factors[strain, :], diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 2257143ef..9fb0aa7f3 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -68,8 +68,8 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): 'strains': { 'half_life': { 'sus': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection + 'trans': dict(asymptomatic=100, mild=100, severe=100), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=100, mild=100, severe=100), # Constant immunity from reinfection }, 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, } @@ -224,10 +224,12 @@ def test_importstrain1(do_plot=False, do_show=True, do_save=False): ] pars = {'n_days': 80, - 'half_life': {'sus': dict(asymptomatic=None, mild=None, severe=None), - 'trans': dict(asymptomatic=None, mild=None, severe=None), - 'prog': dict(asymptomatic=None, mild=None, severe=None),}, - # 'cross_immunity':1. + 'strains': { + 'half_life': {'sus': dict(asymptomatic=None, mild=None, severe=None), + 'trans': dict(asymptomatic=None, mild=None, severe=None), + 'prog': dict(asymptomatic=None, mild=None, severe=None), }, + } + } imported_strain = { From 583707d4769183349eb127bd0ff79324a3f53801 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 23 Feb 2021 17:59:14 -0500 Subject: [PATCH 084/569] some of Robyn's edits, preserving a lot --- covasim/defaults.py | 2 -- covasim/people.py | 7 ++----- covasim/sim.py | 22 +++++++++------------- covasim/utils.py | 2 +- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index c201d7faf..a7c1c7b83 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -48,8 +48,6 @@ class PeopleMeta(sc.prettyobj): 'death_prob', # Float 'rel_trans', # Float 'rel_sus', # Float - 'time_of_last_inf', # Int - 'sus_immunity_factors', # Float 'trans_immunity_factors', # Float 'prog_immunity_factors', # Float 'sus_half_life', # Float diff --git a/covasim/people.py b/covasim/people.py index 497f98e74..26b9e0e43 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -58,9 +58,9 @@ def __init__(self, pars, strict=True, **kwargs): for key in self.meta.person: if key == 'uid': self[key] = np.arange(self.pop_size, dtype=cvd.default_int) - elif key == 'rel_trans' or key == 'rel_sus' or key == 'time_of_last_inf': + elif key == 'rel_trans' or key == 'rel_sus': self[key] = np.full((self.pars['max_strains'], self.pop_size), np.nan, dtype=cvd.default_float, order='F') - elif key == 'sus_immunity_factors' or key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. + elif key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. self[key] = np.full((self.pars['max_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -415,9 +415,6 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str for i, target in enumerate(inds): self.infection_log.append(dict(source=source[i] if source is not None else None, target=target, date=self.t, layer=layer)) - # Record time of infection - self.time_of_last_inf[strain, inds] = self.t - # Calculate how long before this person can infect other people self.dur_exp2inf[inds] = cvu.sample(**durpars['exp2inf'], size=n_infections)*(1-self.trans_immunity_factors[strain, inds]) self.date_infectious[inds] = self.dur_exp2inf[inds] + self.t diff --git a/covasim/sim.py b/covasim/sim.py index 92c148ba6..4ebaff606 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -532,12 +532,12 @@ def step(self): strain_pars[key] = cvd.default_float(self[key][strain]) else: strain_pars[key] = cvd.default_float(self[key]) - - # Compute the probability of transmission beta = cvd.default_float(strain_pars['beta']) asymp_factor = cvd.default_float(strain_pars['asymp_factor']) + # Create susceptible immunity factors matrix + sus_immunity_factors = np.full(self['pop_size'], 0, dtype=cvd.default_float, order='F') immunity_factors = { - 'sus': people.sus_immunity_factors[strain, :], + 'sus': sus_immunity_factors, 'trans': people.trans_immunity_factors[strain, :], 'prog': people.prog_immunity_factors[strain, :] } @@ -546,31 +546,27 @@ def step(self): 'trans': cvd.default_float(self['immunity']['trans'][strain]), 'prog': cvd.default_float(self['immunity']['prog'][strain]), } - # Process immunity parameters and indices - immune = people.recovered_strain == strain # Whether people have some immunity to this strain from a prior infection with this strain - immune_time = t - date_rec[immune] # Time since recovery for people who were last infected by this strain - immune_inds = cvd.default_int(cvu.true(immune)) # People with some immunity to this strain from a prior infection with this strain - half_life = { 'sus': people.sus_half_life, 'trans': people.trans_half_life, 'prog': people.prog_half_life } - decay_rate = {} - for key, val in half_life.items(): rate = np.log(2) / val rate[np.isnan(rate)] = 0 rate = cvd.default_float(rate) decay_rate[key] = rate + # Process immunity parameters and indices + immune = people.recovered_strain == strain # Whether people have some immunity to this strain from a prior infection with this strain + immune_time = t - date_rec[immune] # Time since recovery for people who were last infected by this strain + immune_inds = cvd.default_int(cvu.true(immune)) # People with some immunity to this strain from a prior infection with this strain immunity_factors = cvu.compute_immunity(immunity_factors, immune_time, immune_inds, init_immunity, decay_rate) # Calculate immunity factors # Process cross-immunity parameters and indices, if relevant - if self['n_strains']>1: - for cross_strain in range(self['n_strains']): - + if ns>1: + for cross_strain in range(ns): if cross_strain != strain: cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by the cross strain diff --git a/covasim/utils.py b/covasim/utils.py index a8b7846ad..75016f58c 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -101,7 +101,7 @@ def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, f_iso = ~diag + diag * iso_factor # Isolation factor, changes e.g. [0,1] with a factor of 0.2 to [1,0.2] f_quar = ~quar + quar * quar_factor # Quarantine, changes e.g. [0,1] with a factor of 0.5 to [1,0.5] rel_trans = beta_layer * rel_trans * inf * f_quar * f_asymp * f_iso * viral_load # Recalculate transmissibility - rel_sus = rel_sus * sus * f_quar *(1.-immunity_factors) # Recalculate susceptibility + rel_sus = rel_sus * sus * f_quar * (1.-immunity_factors) # Recalculate susceptibility return rel_trans, rel_sus From 6673df48440e866de67795b2b4f48e1462986aac Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 24 Feb 2021 11:51:15 -0500 Subject: [PATCH 085/569] duration adjustment -- immunity doesnt effect time to death --- covasim/parameters.py | 16 ++++++++-------- covasim/people.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 22db2bb5c..321fe088d 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -69,13 +69,13 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 pars['init_immunity'] = {} - pars['init_immunity']['sus'] = 1.0 # Default initial immunity - pars['init_immunity']['trans'] = 0.5 # Default initial immunity - pars['init_immunity']['prog'] = 0.5 # Default initial immunity - pars['half_life'] = {} - pars['half_life']['sus'] = dict(asymptomatic=180, mild=180, severe=180) - pars['half_life']['trans'] = dict(asymptomatic=180, mild=180, severe=180) - pars['half_life']['prog'] = dict(asymptomatic=180, mild=180, severe=180) + pars['half_life'] = {} + for axis in cvd.immunity_axes: + if axis == 'sus': + pars['init_immunity'][axis] = 1.0 # Default initial immunity + else: + pars['init_immunity'][axis] = 0.5 # Default initial immunity + pars['half_life'][axis] = dict(asymptomatic=180, mild=180, severe=180) pars['dur'] = {} # Duration parameters: time for disease progression @@ -313,7 +313,7 @@ def initialize_immunity(pars): pars['immunity'] = {} pars['immunity']['sus'] = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) - pars['immunity']['prog'] = np.full( pars['max_strains'], np.nan, dtype=cvd.default_float) + pars['immunity']['prog'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) pars['immunity']['trans'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) for par, val in pars['init_immunity'].items(): if par == 'sus': pars['immunity'][par][0,0] = val diff --git a/covasim/people.py b/covasim/people.py index 26b9e0e43..520810939 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -416,7 +416,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.infection_log.append(dict(source=source[i] if source is not None else None, target=target, date=self.t, layer=layer)) # Calculate how long before this person can infect other people - self.dur_exp2inf[inds] = cvu.sample(**durpars['exp2inf'], size=n_infections)*(1-self.trans_immunity_factors[strain, inds]) + self.dur_exp2inf[inds] = cvu.sample(**durpars['exp2inf'], size=n_infections) self.date_infectious[inds] = self.dur_exp2inf[inds] + self.t # Use prognosis probabilities to determine what happens to them @@ -480,7 +480,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.dur_disease[alive_inds] = self.dur_exp2inf[alive_inds] + self.dur_inf2sym[alive_inds] + self.dur_sym2sev[alive_inds] + self.dur_sev2crit[alive_inds] + dur_crit2rec # Store how long this person had COVID-19 # CASE 2.2.2.2: Did die - dur_crit2die = cvu.sample(**durpars['crit2die'], size=len(dead_inds))*(1-self.trans_immunity_factors[strain, dead_inds]) + dur_crit2die = cvu.sample(**durpars['crit2die'], size=len(dead_inds)) self.date_dead[dead_inds] = self.date_critical[dead_inds] + dur_crit2die # Date of death self.dur_disease[dead_inds] = self.dur_exp2inf[dead_inds] + self.dur_inf2sym[dead_inds] + self.dur_sym2sev[dead_inds] + self.dur_sev2crit[dead_inds] + dur_crit2die # Store how long this person had COVID-19 From 6bdc2dc6b086f3f4e9d7acdfe36296d16a6fb7e8 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 24 Feb 2021 14:59:19 -0500 Subject: [PATCH 086/569] if a value within dictionary is not provided, default is used (ie 'sus' provided but not 'trans' and 'prog') --- covasim/parameters.py | 19 +++++-- tests/devtests/test_variants.py | 90 +++++++++++++++++---------------- 2 files changed, 61 insertions(+), 48 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 321fe088d..a2db23fff 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -72,10 +72,11 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['half_life'] = {} for axis in cvd.immunity_axes: if axis == 'sus': - pars['init_immunity'][axis] = 1.0 # Default initial immunity + pars['init_immunity'][axis] = 0.75 # Default initial immunity + pars['half_life'][axis] = dict(asymptomatic=180, mild=180, severe=180) else: - pars['init_immunity'][axis] = 0.5 # Default initial immunity - pars['half_life'][axis] = dict(asymptomatic=180, mild=180, severe=180) + pars['init_immunity'][axis] = 0.5 # Default -- 50% shorter duration and probability of symptoms + pars['half_life'][axis] = dict(asymptomatic=None, mild=None, severe=None) pars['dur'] = {} # Duration parameters: time for disease progression @@ -299,8 +300,16 @@ def update_strain_pars(pars): # Update all strain-specific values for sp in cvd.strain_pars: if sp in pars['strains'].keys(): - pars[sp] = sc.promotetolist(pars['strains'][sp]) - pars['n_strains'] = len(pars[sp]) # This gets overwritten for each parameter, but that's ok + if isinstance(pars['strains'][sp], dict): + for key in cvd.immunity_axes: + if key in pars['strains'][sp].keys(): + pars[sp][key] = sc.promotetolist(pars['strains'][sp][key]) + else: + pars[sp][key] = sc.promotetolist(pars[sp][key]) + pars['n_strains'] = len(pars[sp][key]) + else: + pars[sp] = sc.promotetolist(pars['strains'][sp]) + pars['n_strains'] = len(pars[sp]) # This gets overwritten for each parameter, but that's ok return pars def initialize_immunity(pars): diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 9fb0aa7f3..8b5c37320 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -42,54 +42,58 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): 'strains': { 'half_life': { 'sus': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection + # 'trans': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, + # 'prog': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection }, - 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, - } - } - }, - 'med_halflife': { - 'name':'3 month waning susceptible, transmission and progression immunity', - 'pars': { - 'strains': { - 'half_life': { - 'sus': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection + 'init_immunity': { + 'sus': 1, + # 'trans': 1, + # 'prog': 1 }, - 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, } } }, - 'short_halflife': { - 'name': '3 month waning susceptible, slow waning transmission and progression immunity', - 'pars': { - 'strains': { - 'half_life': { - 'sus': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=100, mild=100, severe=100), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=100, mild=100, severe=100), # Constant immunity from reinfection - }, - 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, - } - - } - }, - 'short_half_life': { - 'name': '3 month waning susceptible, slow progression and transmission immunity, new strain on day 50', - 'pars': { - 'strains': { - 'half_life': { - 'sus': dict(asymptomatic=55, mild=60, severe=150), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection - }, - 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, - }, - 'interventions': imports - } - }, + # 'med_halflife': { + # 'name':'3 month waning susceptible, transmission and progression immunity', + # 'pars': { + # 'strains': { + # 'half_life': { + # 'sus': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, + # 'trans': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, + # 'prog': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection + # }, + # 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, + # } + # } + # }, + # 'short_halflife': { + # 'name': '3 month waning susceptible, slow waning transmission and progression immunity', + # 'pars': { + # 'strains': { + # 'half_life': { + # 'sus': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, + # 'trans': dict(asymptomatic=100, mild=100, severe=100), # Constant immunity from reinfection, + # 'prog': dict(asymptomatic=100, mild=100, severe=100), # Constant immunity from reinfection + # }, + # 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, + # } + # + # } + # }, + # 'short_half_life': { + # 'name': '3 month waning susceptible, slow progression and transmission immunity, new strain on day 50', + # 'pars': { + # 'strains': { + # 'half_life': { + # 'sus': dict(asymptomatic=55, mild=60, severe=150), # Constant immunity from reinfection, + # 'trans': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection, + # 'prog': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection + # }, + # 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, + # }, + # 'interventions': imports + # } + # }, # 'short_half_life_50init': { # 'name': 'Fast-waning susceptible, slow progression and transmission immunity, 50% init', # 'pars': { From aa488499d8e4df2ad30c9e75743a7005ebdb5489 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 24 Feb 2021 17:13:22 -0500 Subject: [PATCH 087/569] added in some validation in case not all values are provided. BUT now need to use import_strain to add any additional strains --- covasim/base.py | 1 - covasim/interventions.py | 15 ++++- covasim/parameters.py | 33 ++--------- covasim/run.py | 3 +- covasim/sim.py | 3 +- tests/devtests/test_variants.py | 99 +++++++++++++++++---------------- 6 files changed, 75 insertions(+), 79 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 27413b030..f9e71b568 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -314,7 +314,6 @@ def update_pars(self, pars=None, create=False, immunity_pars=None, strain_pars=N if pars.get('strains'): pars = sc.mergedicts(immunity_pars, strain_pars, pars) pars = cvpar.update_strain_pars(pars) - print(f'provided information for {pars["n_strains"]} circulating strains') pars = sc.mergedicts(immunity_pars, strain_pars, pars) pars = cvpar.update_immunity(pars) # Update immunity with values provided super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj diff --git a/covasim/interventions.py b/covasim/interventions.py index 7f6854f05..11ae94107 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1200,6 +1200,18 @@ def apply(self, sim): errormsg = f"Number of existing strains ({sim['n_strains']}) plus new strain exceeds the maximal allowable ({sim['max_strains']}. Increase pars['max_strains'])." raise ValueError(errormsg) + # Validate half_life and init_immunity (make sure there are values for all cvd.immunity_axes + for key in cvd.immunity_axes: + if key not in self.init_immunity[strain]: + print(f'initial immunity for imported strain for {key} not provided, using default value') + self.init_immunity[strain][key] = sim['init_immunity'][0][key] + if key not in self.half_life[strain]: + print(f'half life for imported strain for {key} not provided, using default value') + self.half_life[strain][key] = sim['half_life'][0][key] + + self.strain['half_life'][strain] = self.half_life[strain] + self.strain['init_immunity'][strain] = self.init_immunity[strain] + # Update strain info if sim['strains'] is None: sim['strains'] = self.strain @@ -1221,8 +1233,7 @@ def apply(self, sim): sim[sk] = sc.promotetolist(sim[sk]) + sc.promotetolist(sim['strains'][sk]) sim['n_strains'] += 1 cvpar.update_immunity(pars=sim.pars, create=False, update_strain=prev_strains, immunity_from=self.immunity_from[strain], - immunity_to=self.immunity_to[strain], init_immunity=self.init_immunity[strain]['sus'], - prog_immunity=self.init_immunity[strain]['prog'], trans_immunity=self.init_immunity[strain]['trans']) + immunity_to=self.immunity_to[strain], init_immunity=self.init_immunity[strain]) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) diff --git a/covasim/parameters.py b/covasim/parameters.py index a2db23fff..4b2950903 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -301,15 +301,12 @@ def update_strain_pars(pars): for sp in cvd.strain_pars: if sp in pars['strains'].keys(): if isinstance(pars['strains'][sp], dict): + pars[sp] = sc.promotetolist(pars[sp]) for key in cvd.immunity_axes: if key in pars['strains'][sp].keys(): - pars[sp][key] = sc.promotetolist(pars['strains'][sp][key]) - else: - pars[sp][key] = sc.promotetolist(pars[sp][key]) - pars['n_strains'] = len(pars[sp][key]) + pars[sp][0][key] = pars['strains'][sp][key] else: pars[sp] = sc.promotetolist(pars['strains'][sp]) - pars['n_strains'] = len(pars[sp]) # This gets overwritten for each parameter, but that's ok return pars def initialize_immunity(pars): @@ -332,7 +329,7 @@ def initialize_immunity(pars): def update_immunity(pars, create=True, update_strain=None, immunity_from=None, immunity_to=None, - init_immunity=None, prog_immunity=None, trans_immunity=None): + init_immunity=None): ''' Helper function to update the immunity matrices. Matrix is of size sim['max_strains']*sim['max_strains'] and takes the form: @@ -391,24 +388,6 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i # Update immunity for a strain if supplied if update_strain is not None: immunity = pars['immunity'] - # check that immunity_from, immunity_to and init_immunity are provided and the right length. - # Else use default values - if immunity_from is None: - print('Immunity from pars not provided, using default value') - immunity_from = [pars['cross_immunity']]*pars['n_strains'] - if immunity_to is None: - print('Immunity to pars not provided, using default value') - immunity_to = [pars['cross_immunity']]*pars['n_strains'] - if init_immunity is None: - print('Initial immunity not provided, using default value') - init_immunity = pars['init_immunity']['sus'] - if prog_immunity is None: - print('Initial immunity from progression not provided, using default value') - prog_immunity = pars['init_immunity']['prog'] - if trans_immunity is None: - print('Initial immunity from transmission not provided, using default value') - trans_immunity = pars['init_immunity']['trans'] - immunity_from = sc.promotetolist(immunity_from) immunity_to = sc.promotetolist(immunity_to) @@ -420,12 +399,12 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i new_immunity_row[i] = immunity_from[i] new_immunity_column[i] = immunity_to[i] else: - new_immunity_row[i] = new_immunity_column[i] = init_immunity + new_immunity_row[i] = new_immunity_column[i] = init_immunity['sus'] immunity['sus'][update_strain, :] = new_immunity_row immunity['sus'][:, update_strain] = new_immunity_column - immunity['prog'][update_strain] = prog_immunity - immunity['trans'][update_strain] = trans_immunity + immunity['prog'][update_strain] = init_immunity['prog'] + immunity['trans'][update_strain] = init_immunity['trans'] pars['immunity'] = immunity diff --git a/covasim/run.py b/covasim/run.py index 79eb03a85..0d9c680b3 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -930,7 +930,8 @@ def print_heading(string): print_heading(f'Multirun for {scenkey}') scen_sim = sc.dcp(self.base_sim) scen_sim.label = scenkey - immunity_pars = dict(max_strains=scen_sim['max_strains'], + immunity_pars = dict(n_strains=scen_sim['n_strains'], + max_strains=scen_sim['max_strains'], cross_immunity=scen_sim['cross_immunity'], immunity=scen_sim['immunity']) strain_pars = {par: scen_sim[par] for par in cvd.strain_pars} diff --git a/covasim/sim.py b/covasim/sim.py index 4ebaff606..9a3fa7b57 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -74,7 +74,8 @@ def __init__(self, pars=None, datafile=None, datacols=None, label=None, simfile= # Now update everything self.set_metadata(simfile) # Set the simulation date and filename - immunity_pars = dict(immunity=default_pars['immunity'], + immunity_pars = dict(n_strains=default_pars['n_strains'], + immunity=default_pars['immunity'], max_strains=default_pars['max_strains'], cross_immunity=default_pars['cross_immunity']) strain_pars = {par: default_pars[par] for par in cvd.strain_pars} diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 8b5c37320..d8aceb40a 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -22,11 +22,15 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): imported_strain = { 'beta': 0.2, 'half_life': { - 'sus': dict(asymptomatic=55, mild=60, severe=150), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection + 'sus': dict(asymptomatic=25, mild=55, severe=155), # Constant immunity from reinfection, + # 'trans': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection, + # 'prog': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection + }, + 'init_immunity': { + 'sus': 1, + # 'trans': 1, + # 'prog': 1 }, - 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, } imports = cv.import_strain(strain=imported_strain, immunity_to=0.5, immunity_from=0.5, days=50, n_imports=30) @@ -42,8 +46,19 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): 'strains': { 'half_life': { 'sus': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, - # 'trans': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, - # 'prog': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection + }, + 'init_immunity': { + 'sus': 1, + }, + } + } + }, + 'med_halflife': { + 'name':'3 month waning susceptibility', + 'pars': { + 'strains': { + 'half_life': { + 'sus': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, }, 'init_immunity': { 'sus': 1, @@ -53,47 +68,37 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): } } }, - # 'med_halflife': { - # 'name':'3 month waning susceptible, transmission and progression immunity', - # 'pars': { - # 'strains': { - # 'half_life': { - # 'sus': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, - # 'trans': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, - # 'prog': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection - # }, - # 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, - # } - # } - # }, - # 'short_halflife': { - # 'name': '3 month waning susceptible, slow waning transmission and progression immunity', - # 'pars': { - # 'strains': { - # 'half_life': { - # 'sus': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, - # 'trans': dict(asymptomatic=100, mild=100, severe=100), # Constant immunity from reinfection, - # 'prog': dict(asymptomatic=100, mild=100, severe=100), # Constant immunity from reinfection - # }, - # 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, - # } - # - # } - # }, - # 'short_half_life': { - # 'name': '3 month waning susceptible, slow progression and transmission immunity, new strain on day 50', - # 'pars': { - # 'strains': { - # 'half_life': { - # 'sus': dict(asymptomatic=55, mild=60, severe=150), # Constant immunity from reinfection, - # 'trans': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection, - # 'prog': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection - # }, - # 'init_immunity': {'sus': 1, 'trans': 1, 'prog': 1}, - # }, - # 'interventions': imports - # } - # }, + 'med_halflife_bysev': { + 'name':'1, 3, 6 month waning susceptibility, by severity', + 'pars': { + 'strains': { + 'half_life': { + 'sus': dict(asymptomatic=25, mild=55, severe=155), # Constant immunity from reinfection, + }, + 'init_immunity': { + 'sus': 1, + # 'trans': 1, + # 'prog': 1 + }, + } + } + }, + 'short_half_life': { + 'name': '1, 3, 6 month waning susceptibility, by severity, more transmissible strain on day 50', + 'pars': { + 'strains': { + 'half_life': { + 'sus': dict(asymptomatic=25, mild=55, severe=155), # Constant immunity from reinfection, + }, + 'init_immunity': { + 'sus': 1, + # 'trans': 1, + # 'prog': 1 + }, + }, + 'interventions': imports + } + }, # 'short_half_life_50init': { # 'name': 'Fast-waning susceptible, slow progression and transmission immunity, 50% init', # 'pars': { From cc403df823b59181aeac3ef3ea769ccd5be7539f Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 25 Feb 2021 16:40:23 -0500 Subject: [PATCH 088/569] big changes!! trying to simplify strain information and updating. all sims are initialized with one strain. in order to add a strain, must use import_strain intervention, which can only add one strain at a time. --- covasim/base.py | 17 ++-- covasim/defaults.py | 11 ++- covasim/interventions.py | 143 ++++++++++++-------------------- covasim/parameters.py | 93 +++++++-------------- covasim/people.py | 5 +- covasim/run.py | 27 +++--- covasim/sim.py | 12 +-- tests/devtests/test_variants.py | 120 +++++++++++---------------- 8 files changed, 164 insertions(+), 264 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index f9e71b568..4efdc4aae 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -115,13 +115,6 @@ def update_pars(self, pars=None, create=False): errormsg = f'Key(s) {mismatches} not found; available keys are {available_keys}' raise sc.KeyNotFoundError(errormsg) self.pars.update(pars) - - # if 'n_strains' in pars.keys(): - # # check that length of beta is same as length of strains (there is a beta for each strain) - # if 'beta' not in pars.keys(): - # raise ValueError(f'You supplied strains without betas for each strain') - # else: - # self.pars['beta'] = pars['beta'] return @@ -303,7 +296,7 @@ def _brief(self): return string - def update_pars(self, pars=None, create=False, immunity_pars=None, strain_pars=None, **kwargs): + def update_pars(self, pars=None, create=False, strain_pars=None, immunity_pars=None, **kwargs): ''' Ensure that metaparameters get used properly before being updated ''' pars = sc.mergedicts(pars, kwargs) if pars: @@ -311,10 +304,10 @@ def update_pars(self, pars=None, create=False, immunity_pars=None, strain_pars=N cvpar.reset_layer_pars(pars, force=False) if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses - if pars.get('strains'): - pars = sc.mergedicts(immunity_pars, strain_pars, pars) - pars = cvpar.update_strain_pars(pars) - pars = sc.mergedicts(immunity_pars, strain_pars, pars) + # check if any of the strain pars is present in pars, and if so update it + if strain_pars is not None: + pars = cvpar.validate_strain_pars(pars, strain_pars) + pars = sc.mergedicts(pars, immunity_pars) pars = cvpar.update_immunity(pars) # Update immunity with values provided super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/defaults.py b/covasim/defaults.py index a7c1c7b83..a8a36533a 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -53,7 +53,6 @@ class PeopleMeta(sc.prettyobj): 'sus_half_life', # Float 'trans_half_life', # Float 'prog_half_life', # Float - # 'immune_factor_by_strain', # Float ] # Set the states that a person can be in: these are all booleans per person -- used in people.py @@ -133,7 +132,7 @@ class PeopleMeta(sc.prettyobj): new_result_flows = [f'new_{key}' for key in result_flows.keys()] cum_result_flows = [f'cum_{key}' for key in result_flows.keys()] -# Parameters that can vary by strain +# Parameters that can vary by strain (should be in list format) strain_pars = ['beta', 'asymp_factor', 'half_life', @@ -142,7 +141,13 @@ class PeopleMeta(sc.prettyobj): 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', - 'rel_death_prob'] + 'rel_death_prob', +] + +immunity_pars = ['n_strains', + 'max_strains', + 'immunity', +] immunity_axes = ['sus', 'trans', 'prog'] diff --git a/covasim/interventions.py b/covasim/interventions.py index 11ae94107..da8436baf 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1124,25 +1124,23 @@ def apply(self, sim): class import_strain(Intervention): ''' - Introduce a new variant(s) to the population through an importation at a given time point. + Introduce a new variant to the population through an importation at a given time point. Args: - days (int or list of ints): days on which new variants are introduced. Note, the interpretation of a list differs from other interventions; see examples below - n_imports (list of ints): the number of imports of strain(s) - beta (list of floats): per contact transmission of strain(s) - init_immunity (list of floats): initial immunity against strain(s) once recovered; 1 = perfect, 0 = no immunity - half_life (list of dicts): determines decay rate of immunity against strain(s) broken down by severity; If half_life is None immunity is constant - immunity_to (list of list of floats): cross immunity to existing strains in model - immunity_from (list of list of floats): cross immunity from existing strains in model + days (int): day on which new variant is introduced. + n_imports (int): the number of imports of strain + beta (float): per contact transmission of strain + init_immunity (floats): initial immunity against strain once recovered; 1 = perfect, 0 = no immunity + half_life (dicts): determines decay rate of immunity against strain broken down by severity; If half_life is None immunity is constant + immunity_to (list of floats): cross immunity to existing strains in model + immunity_from (list of floats): cross immunity from existing strains in model kwargs (dict): passed to Intervention() **Examples**:: - interv = cv.import_strain(days=50, beta=0.03) # On day 50, import one new strain (one case) - interv = cv.import_strain(days=[10, 50], beta=0.03) # On day 50, import one new strain (one case) - interv = cv.import_strain(days=50, beta=[0.03, 0.05]) # On day 50, import two new strains (one case of each) - interv = cv.import_strain(days=[10, 20], n_imports=[5, 10], beta=[0.03, 0.05], init_immunity=[1, 1], - half_life=[180, 180], immunity_to=[[0, 0], [0,0]], immunity_from=[[0, 0], [0,0]]) # On day 10, import 5 cases of one new strain, on day 20, import 10 cases of another + interv = cv.import_strain(days=50, beta=0.03) # On day 50, import new strain (one case) + interv = cv.import_strain(days=[10, 20], n_imports=[5, 10], beta=0.03, init_immunity=1, + half_life=180, immunity_to=[0, 0], immunity_from=[0, 0]) # On day 10, import 5 cases of new strain, on day 20, import 10 cases ''' def __init__(self, strain=None, days=None, n_imports=1, immunity_to=None, immunity_from=None, **kwargs): @@ -1151,91 +1149,58 @@ def __init__(self, strain=None, days=None, n_imports=1, immunity_to=None, immuni self._store_args() # Store the input arguments so the intervention can be recreated # Handle inputs - self.days = sc.promotetolist(days) - self.n_imports = sc.promotetolist(n_imports) - self.immunity_to = sc.promotetolist(immunity_to) if immunity_to is not None else [None] - self.immunity_from = sc.promotetolist(immunity_from) if immunity_from is not None else [None] - self.strain = {par: sc.promotetolist(val) for par, val in strain.items()} - for par, val in self.strain.items(): setattr(self, par, val) - if not hasattr(self,'init_immunity'): - self.init_immunity = [{key:None for key in cvd.immunity_axes}] - self.new_strains = self.check_args(['days', 'n_imports']+list(self.strain.keys())) + self.days = days + self.n_imports = n_imports + self.immunity_to = immunity_to + self.immunity_from = immunity_from + self.strain = {par: val for par, val in strain.items()} + for par, val in self.strain.items(): + setattr(self, par, val) return - - def check_args(self, args): - ''' Check the length of supplied arguments''' - argvals = [getattr(self,arg) for arg in args] - arglengths = np.array([len(argval) for argval in argvals]) # Get lengths of all arguments - multi_d_args = arglengths[cvu.true(arglengths > 1)].tolist() # Get multidimensional arguments - if len(multi_d_args)==0: # Introducing a single new strain, and all arguments have the right length - new_strains = 1 - elif len(multi_d_args)>=1: # Introducing more than one new strain, but with only one property varying - if len(multi_d_args)>1 and (multi_d_args.count(multi_d_args[0])!=len(multi_d_args)): # This raises an error: more than one multi-dim argument and they're not equal length - raise ValueError(f'Mismatch in the lengths of arguments supplied.') - else: - new_strains = multi_d_args[0] - for arg,argval in zip(args,argvals): - if len(argval)==1: - setattr(self,arg,[argval[0]]*new_strains) - return new_strains - - def initialize(self, sim): self.days = process_days(sim, self.days) self.max_strains = sim['max_strains'] + if not hasattr(self,'init_immunity'): + self.init_immunity = None + if not hasattr(self, 'half_life'): + self.half_life = sim['half_life'][0] self.initialized = True - def apply(self, sim): - # Loop over strains - for strain in range(self.new_strains): - - if sim.t == self.days[strain]: # Time to introduce this strain + if sim.t == self.days: # Time to introduce strain - # Check number of strains - prev_strains = sim['n_strains'] - if prev_strains + 1 > self.max_strains: - errormsg = f"Number of existing strains ({sim['n_strains']}) plus new strain exceeds the maximal allowable ({sim['max_strains']}. Increase pars['max_strains'])." - raise ValueError(errormsg) + # Check number of strains + prev_strains = sim['n_strains'] + if prev_strains + 1 > self.max_strains: + errormsg = f"Number of existing strains ({sim['n_strains']}) plus new strain exceeds the maximal allowable ({sim['max_strains']}. Increase pars['max_strains'])." + raise ValueError(errormsg) - # Validate half_life and init_immunity (make sure there are values for all cvd.immunity_axes - for key in cvd.immunity_axes: - if key not in self.init_immunity[strain]: + # Validate half_life and init_immunity (make sure there are values for all cvd.immunity_axes + for key in cvd.immunity_axes: + if self.init_immunity is not None: + if key not in self.init_immunity: print(f'initial immunity for imported strain for {key} not provided, using default value') - self.init_immunity[strain][key] = sim['init_immunity'][0][key] - if key not in self.half_life[strain]: - print(f'half life for imported strain for {key} not provided, using default value') - self.half_life[strain][key] = sim['half_life'][0][key] - - self.strain['half_life'][strain] = self.half_life[strain] - self.strain['init_immunity'][strain] = self.init_immunity[strain] - - # Update strain info - if sim['strains'] is None: - sim['strains'] = self.strain - for par, val in sim['strains'].items(): - sim[par] = sc.promotetolist(sim[par]) + sc.promotetolist(val) - else: # TODO, this could be improved a LOT - surely there's a variant of mergedicts or update that works here - all_strain_keys = list(set([*sim['strains']]+[*self.strain])) # Merged list of all parameters that need to be by strain - for sk in all_strain_keys: - if sk in sim['strains'].keys(): # This was already by strain - if sk in self.strain.keys(): # We add a new value - sim['strains'][sk] = sc.promotetolist(sim['strains'][sk]) + sc.promotetolist(self.strain[sk]) - sim[sk] = sc.promotetolist(sim[sk]) + sc.promotetolist(self.strain[sk]) - else: - sim['strains'][sk] = sc.promotetolist(sim['strains'][sk]) - sim['strains'][sk].append(sim[sk][0]) - sim[sk].append(sim[sk][0]) - else: # This wasn't previously stored by strain, so now it needs to be added by strain - sim['strains'][sk] = sc.promotetolist(sim[sk]) + sc.promotetolist(self.strain[sk]) - sim[sk] = sc.promotetolist(sim[sk]) + sc.promotetolist(sim['strains'][sk]) - sim['n_strains'] += 1 - cvpar.update_immunity(pars=sim.pars, create=False, update_strain=prev_strains, immunity_from=self.immunity_from[strain], - immunity_to=self.immunity_to[strain], init_immunity=self.init_immunity[strain]) - importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports[strain]) # TODO: do we need to check these people aren't infected? Or just consider it unlikely - sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) - - - return \ No newline at end of file + self.init_immunity[key] = sim['init_immunity'][0][key] + if key not in self.half_life: + print(f'half life for imported strain for {key} not provided, using default value') + self.half_life[key] = sim['half_life'][0][key] + + # Update strain info + for strain_key in cvd.strain_pars: + if hasattr(self, strain_key) is False: + # use default + print(f'{strain_key} not provided for this strain, using default value') + sim[strain_key].append(sim[strain_key][0]) + else: + sim[strain_key].append(sc.promotetolist(getattr(self, strain_key))) + + sim['n_strains'] += 1 + cvpar.update_immunity(pars=sim.pars, create=False, update_strain=prev_strains, + immunity_from=self.immunity_from, immunity_to=self.immunity_to, + init_immunity=self.init_immunity) + importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely + sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) + + return diff --git a/covasim/parameters.py b/covasim/parameters.py index 4b2950903..c86d04c3e 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -59,7 +59,6 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 # Parameters that control settings and defaults for multi-strain runs - pars['strains'] = None # Structure for storing strain info pars['n_strains'] = 1 # The number of strains currently circulating in the population pars['max_strains'] = 30 # For allocating memory with numpy arrays pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor @@ -295,20 +294,24 @@ def absolute_prognoses(prognoses): return out -def update_strain_pars(pars): - ''' Helper function to update parameters that have been set to vary by strain ''' +def validate_strain_pars(pars, default_strain_pars): + ''' Helper function to validate parameters that have been set to vary by strain ''' # Update all strain-specific values for sp in cvd.strain_pars: - if sp in pars['strains'].keys(): - if isinstance(pars['strains'][sp], dict): + if sp in pars.keys(): + if isinstance(pars[sp], dict): pars[sp] = sc.promotetolist(pars[sp]) for key in cvd.immunity_axes: - if key in pars['strains'][sp].keys(): - pars[sp][0][key] = pars['strains'][sp][key] + # check to see if that dictionary item has been provided, use default value + if key not in pars[sp][0].keys(): + pars[sp][0][key] = default_strain_pars[sp][0][key] else: - pars[sp] = sc.promotetolist(pars['strains'][sp]) + pars[sp] = sc.promotetolist(pars[sp]) + else: + pars[sp] = sc.promotetolist(default_strain_pars[sp]) return pars + def initialize_immunity(pars): ''' Initialize the immunity matrices. @@ -328,66 +331,35 @@ def initialize_immunity(pars): return pars -def update_immunity(pars, create=True, update_strain=None, immunity_from=None, immunity_to=None, - init_immunity=None): +def update_immunity(pars, create=True, update_strain=None, immunity_from=None, immunity_to=None, init_immunity=None): ''' - Helper function to update the immunity matrices. - Matrix is of size sim['max_strains']*sim['max_strains'] and takes the form: - A B ... - array([[1.0, 1.0, ...], - [0.4, 1.0, ...], - ..., ..., ...]) - ... meaning that people who've had strand A have perfect protection against strand B, but - people who've had strand B have an initial 40% protection against getting strand A. - The matrix has nan entries outside of any active strains. The diagonals represent immunity - against the same strain, meaning people who've had strand A have perfect protection against - strand A, the same for B. - - Args: - pars (dict): the parameters dictionary - update_strain: the index of the strain to update - immunity_from (array): used when adding a new strain; specifies the immunity protection against existing strains from having the new strain - immunity_to (array): used when adding a strain; specifies the immunity protection against the new strain from having one of the existing strains - half_life (dicts): dictionary of floats for half life of new strain - - **Example 1**: #TODO NEEDS UPDATING - # Adding a strain C to the example above. Strain C gives perfect immunity against strain A - # and 90% immunity against strain B. People who've had strain A have 50% immunity to strain C, - # and people who've had strain B have 70% immunity to strain C - cross_immunity = update_cross_immunity(pars, update_strain=2, immunity_from=[1. 0.9], immunity_to=[0.5, 0.7]) - A B C ... - array([[1.0, 1.0, 0.5 ...], - [0.4, 1.0, 0.7 ...], - [1.0, 0.9, 1.0 ...], - ..., ..., ..., ...]) - + Helper function to update the immunity matrices with user-provided values. If create=True, model is initialized + with a single strain. + If update_strain is not None, it's used to add a new strain with strain-specific values + (called by import_strain intervention) ''' if create: - # Cross immunity values are set if there is more than one strain circulating - # if 'n_strains' isn't provided, assume it's 1 - if not pars.get('n_strains'): - pars['n_strains'] = 1 - ns = pars['n_strains'] # Shorten - # Update immunity matrix - pars['immunity']['sus'][:ns, :ns] = pars['cross_immunity'] - if pars['strains'].get('init_immunity'): - for ps in cvd.immunity_axes: - for strain in range(ns): - if ps == 'sus': - pars['immunity'][ps][strain, strain] = pars['init_immunity'][strain][ps] - else: - pars['immunity'][ps][strain] = pars['init_immunity'][strain][ps] - else: - for ps in cvd.immunity_axes: - if ps == 'sus': - np.fill_diagonal(pars['immunity'][ps][:ns, :ns], pars['init_immunity'][ps]) - else: - pars['immunity'][ps][:ns] = pars['init_immunity'][ps] + for par, val in pars['init_immunity'][0].items(): + if par == 'sus': + pars['immunity'][par][0, 0] = val + else: + pars['immunity'][par][0] = val # Update immunity for a strain if supplied if update_strain is not None: immunity = pars['immunity'] + if immunity_from is None: + print('Immunity from pars not provided, using default value') + immunity_from = [pars['cross_immunity']] * pars['n_strains'] + if immunity_to is None: + print('Immunity to pars not provided, using default value') + immunity_to = [pars['cross_immunity']] * pars['n_strains'] + if init_immunity is None: + print('Initial immunity not provided, using default value') + pars['init_immunity'][update_strain] = pars['init_immunity'][0] + init_immunity = pars['init_immunity'][update_strain] + immunity_from = sc.promotetolist(immunity_from) immunity_to = sc.promotetolist(immunity_to) @@ -405,7 +377,6 @@ def update_immunity(pars, create=True, update_strain=None, immunity_from=None, i immunity['sus'][:, update_strain] = new_immunity_column immunity['prog'][update_strain] = init_immunity['prog'] immunity['trans'][update_strain] = init_immunity['trans'] - pars['immunity'] = immunity return pars diff --git a/covasim/people.py b/covasim/people.py index 520810939..f37a97d12 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -391,10 +391,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str infect_parkeys = ['dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob', 'half_life'] infect_pars = dict() for key in infect_parkeys: - if self.pars['strains'] is not None and key in self.pars['strains'].keys(): # This parameter varies by strain: extract strain-specific value - infect_pars[key] = self.pars[key][strain] - else: - infect_pars[key] = self.pars[key] + infect_pars[key] = self.pars[key][strain] n_infections = len(inds) durpars = infect_pars['dur'] diff --git a/covasim/run.py b/covasim/run.py index 0d9c680b3..c05a75fd1 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -930,12 +930,9 @@ def print_heading(string): print_heading(f'Multirun for {scenkey}') scen_sim = sc.dcp(self.base_sim) scen_sim.label = scenkey - immunity_pars = dict(n_strains=scen_sim['n_strains'], - max_strains=scen_sim['max_strains'], - cross_immunity=scen_sim['cross_immunity'], - immunity=scen_sim['immunity']) strain_pars = {par: scen_sim[par] for par in cvd.strain_pars} - scen_sim.update_pars(scenpars, immunity_pars=immunity_pars, strain_pars=strain_pars, + immunity_pars = {par: scen_sim[par] for par in cvd.immunity_pars} + scen_sim.update_pars(scenpars, strain_pars=strain_pars, immunity_pars=immunity_pars, **kwargs) # Update the parameters, if provided run_args = dict(n_runs=self['n_runs'], noise=self['noise'], noisepar=self['noisepar'], keep_people=keep_people, verbose=verbose) if debug: @@ -1336,18 +1333,18 @@ def single_run(sim, ind=0, reseed=True, noise=0.0, noisepar=None, keep_people=Fa sim.set_seed() # If the noise parameter is not found, guess what it should be - if noisepar is None: - noisepar = 'beta' - if noisepar not in sim.pars.keys(): - raise sc.KeyNotFoundError(f'Noise parameter {noisepar} was not found in sim parameters') + # if noisepar is None: + # noisepar = 'beta' + # if noisepar not in sim.pars.keys(): + # raise sc.KeyNotFoundError(f'Noise parameter {noisepar} was not found in sim parameters') # Handle noise -- normally distributed fractional error - noiseval = noise*np.random.normal() - if noiseval > 0: - noisefactor = 1 + noiseval - else: - noisefactor = 1/(1-noiseval) - sim[noisepar] *= noisefactor + # noiseval = noise*np.random.normal() + # if noiseval > 0: + # noisefactor = 1 + noiseval + # else: + # noisefactor = 1/(1-noiseval) + # sim[noisepar] *= noisefactor if verbose>=1: verb = 'Running' if do_run else 'Creating' diff --git a/covasim/sim.py b/covasim/sim.py index 9a3fa7b57..dfe8bfff8 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -74,12 +74,9 @@ def __init__(self, pars=None, datafile=None, datacols=None, label=None, simfile= # Now update everything self.set_metadata(simfile) # Set the simulation date and filename - immunity_pars = dict(n_strains=default_pars['n_strains'], - immunity=default_pars['immunity'], - max_strains=default_pars['max_strains'], - cross_immunity=default_pars['cross_immunity']) strain_pars = {par: default_pars[par] for par in cvd.strain_pars} - self.update_pars(pars, immunity_pars=immunity_pars, strain_pars=strain_pars, **kwargs) # Update the parameters, if provided + immunity_pars = {par: default_pars[par] for par in cvd.immunity_pars} + self.update_pars(pars, strain_pars=strain_pars, immunity_pars=immunity_pars, **kwargs) # Update the parameters, if provided self.load_data(datafile, datacols) # Load the data, if provided if self.load_pop: self.load_population(popfile) # Load the population, if provided @@ -529,10 +526,7 @@ def step(self): strain_parkeys = ['beta', 'asymp_factor'] strain_pars = dict() for key in strain_parkeys: - if self['strains'] is not None and key in self['strains'].keys(): # This parameter varies by strain: extract strain-specific value - strain_pars[key] = cvd.default_float(self[key][strain]) - else: - strain_pars[key] = cvd.default_float(self[key]) + strain_pars[key] = cvd.default_float(self[key][strain]) beta = cvd.default_float(strain_pars['beta']) asymp_factor = cvd.default_float(strain_pars['asymp_factor']) # Create susceptible immunity factors matrix diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index d8aceb40a..eadc6a871 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -10,7 +10,7 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): - sc.heading('Run a basic sim with 1 strain, allowing for reinfection') + sc.heading('Run a basic sim with 1 strain, varying reinfection risk') sc.heading('Setting up...') # Define baseline parameters @@ -19,21 +19,6 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): 'n_days': 240, } - imported_strain = { - 'beta': 0.2, - 'half_life': { - 'sus': dict(asymptomatic=25, mild=55, severe=155), # Constant immunity from reinfection, - # 'trans': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection, - # 'prog': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection - }, - 'init_immunity': { - 'sus': 1, - # 'trans': 1, - # 'prog': 1 - }, - } - - imports = cv.import_strain(strain=imported_strain, immunity_to=0.5, immunity_from=0.5, days=50, n_imports=30) n_runs = 3 base_sim = cv.Sim(base_pars) @@ -43,21 +28,18 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): 'baseline': { 'name':'No reinfection', 'pars': { - 'strains': { - 'half_life': { - 'sus': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, - }, - 'init_immunity': { - 'sus': 1, - }, - } + 'half_life': { + 'sus': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, + }, + 'init_immunity': { + 'sus': 1, + }, } }, 'med_halflife': { 'name':'3 month waning susceptibility', 'pars': { - 'strains': { - 'half_life': { + 'half_life': { 'sus': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, }, 'init_immunity': { @@ -65,40 +47,21 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): # 'trans': 1, # 'prog': 1 }, - } } }, 'med_halflife_bysev': { 'name':'1, 3, 6 month waning susceptibility, by severity', 'pars': { - 'strains': { - 'half_life': { - 'sus': dict(asymptomatic=25, mild=55, severe=155), # Constant immunity from reinfection, - }, - 'init_immunity': { - 'sus': 1, - # 'trans': 1, - # 'prog': 1 - }, - } + 'half_life': { + 'sus': dict(asymptomatic=25, mild=55, severe=155), # Constant immunity from reinfection, + }, + 'init_immunity': { + 'sus': 1, + # 'trans': 1, + # 'prog': 1 + }, } }, - 'short_half_life': { - 'name': '1, 3, 6 month waning susceptibility, by severity, more transmissible strain on day 50', - 'pars': { - 'strains': { - 'half_life': { - 'sus': dict(asymptomatic=25, mild=55, severe=155), # Constant immunity from reinfection, - }, - 'init_immunity': { - 'sus': 1, - # 'trans': 1, - # 'prog': 1 - }, - }, - 'interventions': imports - } - }, # 'short_half_life_50init': { # 'name': 'Fast-waning susceptible, slow progression and transmission immunity, 50% init', # 'pars': { @@ -148,20 +111,35 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): 'rel_severe_prob': 1.3, # 30% more severe across all ages 'half_life': { 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection }, } + imported_strain = { + 'beta': 0.2, + 'half_life': { + 'sus': dict(asymptomatic=25, mild=55, severe=155), # Constant immunity from reinfection, + 'trans': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection, + 'prog': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection + }, + } + + imports = cv.import_strain(strain=imported_strain, immunity_to=0.5, immunity_from=0.5, days=1, n_imports=30) + pars = { 'beta': 0.016, 'n_days': 80, - 'strains': strains, + 'beta': 0.025, + 'rel_severe_prob': 1.3, # 30% more severe across all ages + 'half_life': { + 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, + }, + 'init_immunity': { + 'sus': 1 + } + # 'strains': strains, } - sim = cv.Sim(pars=pars) - #sim['immunity'][0,1] = 0.0 # Say that strain A gives no immunity to strain B - #sim['immunity'][1,0] = 0.0 # Say that strain B gives high immunity to strain A + sim = cv.Sim(pars=pars, interventions=imports) sim.run() strain_labels = [ @@ -232,20 +210,18 @@ def test_importstrain1(do_plot=False, do_show=True, do_save=False): 'Strain 2: beta 0.025' ] - pars = {'n_days': 80, - 'strains': { - 'half_life': {'sus': dict(asymptomatic=None, mild=None, severe=None), - 'trans': dict(asymptomatic=None, mild=None, severe=None), - 'prog': dict(asymptomatic=None, mild=None, severe=None), }, - } - - } + pars = { + 'n_days': 80, + 'strains': { + 'half_life': { + 'sus': dict(asymptomatic=None, mild=None, severe=None) + } + } + } imported_strain = { 'beta': 0.025, 'half_life': {'sus': dict(asymptomatic=None, mild=None, severe=None), - 'trans': dict(asymptomatic=None, mild=None, severe=None), - 'prog': dict(asymptomatic=None, mild=None, severe=None), }, 'init_immunity': {'sus': 0.5, 'trans': 0.5, 'prog': 0.5}, } @@ -266,7 +242,8 @@ def test_importstrain2(do_plot=False, do_show=True, do_save=False): strain2 = {'beta': 0.025, 'rel_severe_prob': 1.3, - 'half_life': dict(asymptomatic=20, mild=80, severe=200), + 'half_life': {'sus': dict(asymptomatic=20, mild=80, severe=200) + }, 'init_immunity': 0.9 } pars = { @@ -276,7 +253,8 @@ def test_importstrain2(do_plot=False, do_show=True, do_save=False): strain3 = { 'beta': 0.05, 'rel_symp_prob': 1.6, - 'half_life': dict(asymptomatic=10, mild=50, severe=150), + 'half_life': {'sus': dict(asymptomatic=10, mild=50, severe=150), + }, 'init_immunity': 0.4 } From 2c8370808f1700c8237ee358203aee35a0237014 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 25 Feb 2021 17:11:04 -0500 Subject: [PATCH 089/569] all tests work! --- covasim/interventions.py | 6 +- covasim/parameters.py | 1 + tests/devtests/test_variants.py | 170 ++++++++++---------------------- 3 files changed, 54 insertions(+), 123 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index da8436baf..b95120f5b 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1189,12 +1189,12 @@ def apply(self, sim): # Update strain info for strain_key in cvd.strain_pars: - if hasattr(self, strain_key) is False: + if hasattr(self, strain_key): + sim[strain_key].append(getattr(self, strain_key)) + else: # use default print(f'{strain_key} not provided for this strain, using default value') sim[strain_key].append(sim[strain_key][0]) - else: - sim[strain_key].append(sc.promotetolist(getattr(self, strain_key))) sim['n_strains'] += 1 cvpar.update_immunity(pars=sim.pars, create=False, update_strain=prev_strains, diff --git a/covasim/parameters.py b/covasim/parameters.py index c86d04c3e..301b0a0cc 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -304,6 +304,7 @@ def validate_strain_pars(pars, default_strain_pars): for key in cvd.immunity_axes: # check to see if that dictionary item has been provided, use default value if key not in pars[sp][0].keys(): + default_strain_pars[sp] = sc.promotetolist(default_strain_pars[sp]) pars[sp][0][key] = default_strain_pars[sp][0][key] else: pars[sp] = sc.promotetolist(pars[sp]) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index eadc6a871..2afccf8d2 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -40,51 +40,24 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): 'name':'3 month waning susceptibility', 'pars': { 'half_life': { - 'sus': dict(asymptomatic=55, mild=55, severe=55), # Constant immunity from reinfection, - }, - 'init_immunity': { - 'sus': 1, - # 'trans': 1, - # 'prog': 1 - }, + 'sus': dict(asymptomatic=55, mild=55, severe=55), + }, + 'init_immunity': { + 'sus': 1, + }, } }, 'med_halflife_bysev': { 'name':'1, 3, 6 month waning susceptibility, by severity', 'pars': { 'half_life': { - 'sus': dict(asymptomatic=25, mild=55, severe=155), # Constant immunity from reinfection, + 'sus': dict(asymptomatic=25, mild=55, severe=155), }, 'init_immunity': { 'sus': 1, - # 'trans': 1, - # 'prog': 1 }, } }, - # 'short_half_life_50init': { - # 'name': 'Fast-waning susceptible, slow progression and transmission immunity, 50% init', - # 'pars': { - # 'strains': { - # 'half_life': { - # 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, - # 'trans': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection, - # 'prog': dict(asymptomatic=160, mild=160, severe=160), # Constant immunity from reinfection - # }, - # 'init_immunity': {'sus': .5, 'trans': .5, 'prog': .5}, - # } - # } - # }, - # 'short_susceptible_prog_long_trans': { - # 'name': 'Fast-waning susceptible and progression, slow-waning transmission immunity', - # 'pars': { - # 'half_life': { - # 'sus': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection, - # 'trans': dict(asymptomatic=60, mild=60, severe=60), # Constant immunity from reinfection, - # 'prog': dict(asymptomatic=10, mild=10, severe=10), # Constant immunity from reinfection - # } - # } - # }, } metapars = {'n_runs': n_runs} @@ -107,19 +80,12 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): sc.heading('Run basic sim with 2 strains') sc.heading('Setting up...') - strains = {'beta': 0.025, - 'rel_severe_prob': 1.3, # 30% more severe across all ages - 'half_life': { - 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, - }, - } - imported_strain = { 'beta': 0.2, 'half_life': { - 'sus': dict(asymptomatic=25, mild=55, severe=155), # Constant immunity from reinfection, - 'trans': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection, - 'prog': dict(asymptomatic=55, mild=100, severe=160), # Constant immunity from reinfection + 'sus': dict(asymptomatic=25, mild=55, severe=155), + 'trans': dict(asymptomatic=55, mild=100, severe=160), + 'prog': dict(asymptomatic=55, mild=100, severe=160), }, } @@ -128,23 +94,21 @@ def test_2strains(do_plot=False, do_show=True, do_save=False): pars = { 'beta': 0.016, 'n_days': 80, - 'beta': 0.025, 'rel_severe_prob': 1.3, # 30% more severe across all ages 'half_life': { - 'sus': dict(asymptomatic=10, mild=30, severe=50), # Constant immunity from reinfection, + 'sus': dict(asymptomatic=10, mild=30, severe=50), }, 'init_immunity': { 'sus': 1 } - # 'strains': strains, } sim = cv.Sim(pars=pars, interventions=imports) sim.run() strain_labels = [ - f'Strain A: beta {sim["beta"][0]}, half_life {sim["half_life"][0]}', - f'Strain B: beta {sim["beta"][1]}, half_life {sim["half_life"][1]}', + f'Strain A: beta {pars["beta"]}', + f'Strain B: beta {imported_strain["beta"]}', ] if do_plot: @@ -212,17 +176,15 @@ def test_importstrain1(do_plot=False, do_show=True, do_save=False): pars = { 'n_days': 80, - 'strains': { - 'half_life': { - 'sus': dict(asymptomatic=None, mild=None, severe=None) - } + 'half_life': { + 'sus': dict(asymptomatic=None, mild=None, severe=None) } } imported_strain = { 'beta': 0.025, 'half_life': {'sus': dict(asymptomatic=None, mild=None, severe=None), - }, + }, 'init_immunity': {'sus': 0.5, 'trans': 0.5, 'prog': 0.5}, } @@ -237,7 +199,7 @@ def test_importstrain1(do_plot=False, do_show=True, do_save=False): def test_importstrain2(do_plot=False, do_show=True, do_save=False): - sc.heading('Test introducing a new strain partway through a sim with 2 strains') + sc.heading('Test introducing 2 new strains partway through a sim') sc.heading('Setting up...') strain2 = {'beta': 0.025, @@ -248,7 +210,6 @@ def test_importstrain2(do_plot=False, do_show=True, do_save=False): } pars = { 'n_days': 80, - 'strains': strain2, } strain3 = { 'beta': 0.05, @@ -258,14 +219,16 @@ def test_importstrain2(do_plot=False, do_show=True, do_save=False): 'init_immunity': 0.4 } - imports = cv.import_strain(strain=strain3, days=10, n_imports=20) + imports = [cv.import_strain(strain=strain2, days=10, n_imports=20), + cv.import_strain(strain=strain3, days=30, n_imports=20), + ] sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') sim.run() strain_labels = [ 'Strain 1: beta 0.016', - 'Strain 2: beta 0.025', - 'Strain 3: beta 0.05, 20 imported day 10' + 'Strain 2: beta 0.025, 20 imported day 10', + 'Strain 3: beta 0.05, 20 imported day 30' ] if do_plot: @@ -275,62 +238,32 @@ def test_importstrain2(do_plot=False, do_show=True, do_save=False): return sim -def test_par_refactor(): - ''' - The purpose of this test is to experiment with different representations of the parameter structures - Still WIP! - ''' - - # Simplest case: add a strain to beta - p1 = cv.Par(name='beta', val=0.016, by_strain=True) - print(p1.val) # Prints all the stored values of beta - print(p1[0]) # Can index beta like an array to pull out strain-specific values - p1.add_strain(new_val = 0.025) - - # Complex case: add a strain that's differentiated by severity for kids 0-20 - p2 = cv.Par(name='sus_ORs', val=np.array([0.34, 0.67, 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47]), by_strain=True, by_age=True) - print(p2.val) # Prints all the stored values for the original strain - print(p2[0]) # Can index beta like an array to pull out strain-specific values - p2.add_strain(new_val=np.array([1., 1., 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47])) - - # Complex case: add a strain that's differentiated by duration of disease - p3 = cv.Par(name='dur_asym2rec', val=dict(dist='lognormal_int', par1=8.0, par2=2.0), by_strain=True, is_dist=True) - print(p3.val) # Prints all the stored values for the original strain - print(p3[0]) # Can index beta like an array to pull out strain-specific values - p3.add_strain(new_val=dict(dist='lognormal_int', par1=12.0, par2=2.0)) - p3.get(strain=1, n=6) - - return p1, p2, p3 - - -def test_halflife_by_severity(do_plot=False, do_show=True, do_save=False): - sc.heading('Run basic sim with 2 strains and half life by severity') - sc.heading('Setting up...') - - strains = {'half_life': {'sus': dict(asymptomatic=100, mild=150, severe=200), - 'trans': dict(asymptomatic=100, mild=150, severe=200), - 'prog': dict(asymptomatic=100, mild=150, severe=200)}} - - pars = { - 'n_days': 80, - 'beta': 0.015, - 'strains': strains, - } - - sim = cv.Sim(pars=pars) - sim['immunity'][0,1] = 1.0 # Say that strain A gives no immunity to strain B - sim['immunity'][1,0] = 1.0 # Say that strain B gives no immunity to strain A - sim.run() - - strain_labels = [ - f'Strain A: beta {pars["beta"]}, half_life not by severity', - f'Strain B: beta {pars["beta"]}, half_life by severity', - ] - - if do_plot: - sim.plot_result('new_reinfections', fig_path='results/test_halflife_by_severity.png', do_show=do_show, do_save=do_save) - plot_results(sim, key='incidence_by_strain', title=f'2 strain test, A->B immunity {sim["immunity"][0,1]}, B->A immunity {sim["immunity"][1,0]}', filename='test_halflife_by_severity', labels=strain_labels, do_show=do_show, do_save=do_save) - return sim +# def test_par_refactor(): +# ''' +# The purpose of this test is to experiment with different representations of the parameter structures +# Still WIP! +# ''' +# +# # Simplest case: add a strain to beta +# p1 = cv.Par(name='beta', val=0.016, by_strain=True) +# print(p1.val) # Prints all the stored values of beta +# print(p1[0]) # Can index beta like an array to pull out strain-specific values +# p1.add_strain(new_val = 0.025) +# +# # Complex case: add a strain that's differentiated by severity for kids 0-20 +# p2 = cv.Par(name='sus_ORs', val=np.array([0.34, 0.67, 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47]), by_strain=True, by_age=True) +# print(p2.val) # Prints all the stored values for the original strain +# print(p2[0]) # Can index beta like an array to pull out strain-specific values +# p2.add_strain(new_val=np.array([1., 1., 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47])) +# +# # Complex case: add a strain that's differentiated by duration of disease +# p3 = cv.Par(name='dur_asym2rec', val=dict(dist='lognormal_int', par1=8.0, par2=2.0), by_strain=True, is_dist=True) +# print(p3.val) # Prints all the stored values for the original strain +# print(p3[0]) # Can index beta like an array to pull out strain-specific values +# p3.add_strain(new_val=dict(dist='lognormal_int', par1=12.0, par2=2.0)) +# p3.get(strain=1, n=6) +# +# return p1, p2, p3 def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): @@ -348,8 +281,6 @@ def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): imported_strain = { 'beta': 0.025, -# 'half_life': dict(asymptomatic=20, mild=80, severe=100), -# 'init_immunity': 0.5, 'dur': dict(exp2inf=dict(dist='lognormal_int', par1=6.0, par2=2.0), inf2sym=dict(dist='lognormal_int', par1=4.0, par2=2.0), sym2sev=dict(dist='lognormal_int', par1=8.0, par2=2.0), @@ -433,11 +364,10 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - # scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # p1, p2, p3 = test_par_refactor() - # sim4 = test_halflife_by_severity(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim5 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim5 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # Importing strains is not currently working, so the following tests break # sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) From 79236a440b291dae4f89494dcb8b76d501240203 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 26 Feb 2021 13:23:43 +0100 Subject: [PATCH 090/569] little refactors --- covasim/base.py | 15 ++-- covasim/interventions.py | 2 +- covasim/parameters.py | 131 +++++++++++++++--------------- covasim/run.py | 7 +- covasim/sim.py | 68 ++++++---------- covasim/utils.py | 27 ++----- tests/devtests/test_variants.py | 138 +++++++++++--------------------- 7 files changed, 158 insertions(+), 230 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 4efdc4aae..64d352218 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -296,7 +296,7 @@ def _brief(self): return string - def update_pars(self, pars=None, create=False, strain_pars=None, immunity_pars=None, **kwargs): + def update_pars(self, pars=None, create=False, defaults=None, **kwargs): ''' Ensure that metaparameters get used properly before being updated ''' pars = sc.mergedicts(pars, kwargs) if pars: @@ -304,12 +304,15 @@ def update_pars(self, pars=None, create=False, strain_pars=None, immunity_pars=N cvpar.reset_layer_pars(pars, force=False) if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses - # check if any of the strain pars is present in pars, and if so update it - if strain_pars is not None: - pars = cvpar.validate_strain_pars(pars, strain_pars) - pars = sc.mergedicts(pars, immunity_pars) - pars = cvpar.update_immunity(pars) # Update immunity with values provided + + if defaults is not None: # Defaults have been provided: we are now doing updates + pars = cvpar.listify_strain_pars(pars, defaults) # Strain pars need to be lists + pars = cvpar.update_sub_key_pars(pars, defaults) # Update dict parameters by sub-key + if pars.get('init_immunity'): + pars['immunity'] = cvpar.update_init_immunity(defaults['immunity'], pars['init_immunity'][0]) # Update immunity + super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj + return diff --git a/covasim/interventions.py b/covasim/interventions.py index b95120f5b..53f21b44b 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1197,7 +1197,7 @@ def apply(self, sim): sim[strain_key].append(sim[strain_key][0]) sim['n_strains'] += 1 - cvpar.update_immunity(pars=sim.pars, create=False, update_strain=prev_strains, + cvpar.update_immunity(pars=sim.pars, update_strain=prev_strains, immunity_from=self.immunity_from, immunity_to=self.immunity_to, init_immunity=self.init_immunity) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely diff --git a/covasim/parameters.py b/covasim/parameters.py index 301b0a0cc..fb17caabd 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -68,10 +68,10 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 pars['init_immunity'] = {} - pars['half_life'] = {} + pars['half_life'] = {} for axis in cvd.immunity_axes: if axis == 'sus': - pars['init_immunity'][axis] = 0.75 # Default initial immunity + pars['init_immunity'][axis] = 1. # Default initial immunity pars['half_life'][axis] = dict(asymptomatic=180, mild=180, severe=180) else: pars['init_immunity'][axis] = 0.5 # Default -- 50% shorter duration and probability of symptoms @@ -294,90 +294,95 @@ def absolute_prognoses(prognoses): return out -def validate_strain_pars(pars, default_strain_pars): +def update_sub_key_pars(pars, default_pars): + ''' Helper function to update sub-keys of dict parameters ''' + for par,val in pars.items(): + if par in cvd.strain_pars: # It will be stored as a list + newval = val[0] + oldval = sc.promotetolist(default_pars[par])[0] # Might be a list or not! + if isinstance(newval, dict): # Update the dictionary, don't just overwrite it + pars[par] = sc.promotetolist(sc.mergenested(oldval, newval)) + else: + if isinstance(val, dict): # Update the dictionary, don't just overwrite it + pars[par] = sc.mergenested(default_pars[par], val) + return pars + + +def listify_strain_pars(pars, default_pars): ''' Helper function to validate parameters that have been set to vary by strain ''' - # Update all strain-specific values for sp in cvd.strain_pars: if sp in pars.keys(): - if isinstance(pars[sp], dict): - pars[sp] = sc.promotetolist(pars[sp]) - for key in cvd.immunity_axes: - # check to see if that dictionary item has been provided, use default value - if key not in pars[sp][0].keys(): - default_strain_pars[sp] = sc.promotetolist(default_strain_pars[sp]) - pars[sp][0][key] = default_strain_pars[sp][0][key] - else: - pars[sp] = sc.promotetolist(pars[sp]) + pars[sp] = sc.promotetolist(pars[sp]) else: - pars[sp] = sc.promotetolist(default_strain_pars[sp]) + pars[sp] = sc.promotetolist(default_pars[sp]) + return pars + + +def delistify_strain_pars(pars): + ''' Helper function to validate parameters that have been set to vary by strain ''' + for sp in cvd.strain_pars: + pars[sp] = pars[sp][0] return pars def initialize_immunity(pars): ''' - Initialize the immunity matrices. + Initialize the immunity matrices with default values Susceptibility matrix is of size sim['max_strains']*sim['max_strains'] and is initialized with default values Progression is a matrix of scalars of size sim['max_strains'] initialized with default values Transmission is a matrix of scalars of size sim['max_strains'] initialized with default values ''' - pars['immunity'] = {} pars['immunity']['sus'] = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) pars['immunity']['prog'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) pars['immunity']['trans'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - for par, val in pars['init_immunity'].items(): - if par == 'sus': pars['immunity'][par][0,0] = val - else: pars['immunity'][par][0] = val - + pars['immunity'] = update_init_immunity(pars['immunity'], pars['init_immunity']) return pars -def update_immunity(pars, create=True, update_strain=None, immunity_from=None, immunity_to=None, init_immunity=None): +def update_init_immunity(immunity, init_immunity): + '''Update immunity matrices with initial immunity values''' + for par, val in init_immunity.items(): + if par == 'sus': immunity[par][0,0] = val + else: immunity[par][0] = val + return immunity + + +def update_immunity(pars, update_strain=None, immunity_from=None, immunity_to=None, init_immunity=None): ''' - Helper function to update the immunity matrices with user-provided values. If create=True, model is initialized - with a single strain. + Helper function to update the immunity matrices when a strain strain is added. If update_strain is not None, it's used to add a new strain with strain-specific values (called by import_strain intervention) ''' - if create: - # Update immunity matrix - for par, val in pars['init_immunity'][0].items(): - if par == 'sus': - pars['immunity'][par][0, 0] = val - else: - pars['immunity'][par][0] = val - - # Update immunity for a strain if supplied - if update_strain is not None: - immunity = pars['immunity'] - if immunity_from is None: - print('Immunity from pars not provided, using default value') - immunity_from = [pars['cross_immunity']] * pars['n_strains'] - if immunity_to is None: - print('Immunity to pars not provided, using default value') - immunity_to = [pars['cross_immunity']] * pars['n_strains'] - if init_immunity is None: - print('Initial immunity not provided, using default value') - pars['init_immunity'][update_strain] = pars['init_immunity'][0] - init_immunity = pars['init_immunity'][update_strain] - - immunity_from = sc.promotetolist(immunity_from) - immunity_to = sc.promotetolist(immunity_to) - - # create the immunity[update_strain,] and immunity[,update_strain] arrays - new_immunity_row = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - new_immunity_column = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - for i in range(pars['n_strains']): - if i != update_strain: - new_immunity_row[i] = immunity_from[i] - new_immunity_column[i] = immunity_to[i] - else: - new_immunity_row[i] = new_immunity_column[i] = init_immunity['sus'] - - immunity['sus'][update_strain, :] = new_immunity_row - immunity['sus'][:, update_strain] = new_immunity_column - immunity['prog'][update_strain] = init_immunity['prog'] - immunity['trans'][update_strain] = init_immunity['trans'] - pars['immunity'] = immunity + immunity = pars['immunity'] + if immunity_from is None: + print('Immunity from pars not provided, using default value') + immunity_from = [pars['cross_immunity']] * pars['n_strains'] + if immunity_to is None: + print('Immunity to pars not provided, using default value') + immunity_to = [pars['cross_immunity']] * pars['n_strains'] + if init_immunity is None: + print('Initial immunity not provided, using default value') + pars['init_immunity'][update_strain] = pars['init_immunity'][0] + init_immunity = pars['init_immunity'][update_strain] + + immunity_from = sc.promotetolist(immunity_from) + immunity_to = sc.promotetolist(immunity_to) + + # create the immunity[update_strain,] and immunity[,update_strain] arrays + new_immunity_row = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) + new_immunity_column = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) + for i in range(pars['n_strains']): + if i != update_strain: + new_immunity_row[i] = immunity_from[i] + new_immunity_column[i] = immunity_to[i] + else: + new_immunity_row[i] = new_immunity_column[i] = init_immunity['sus'] + + immunity['sus'][update_strain, :] = new_immunity_row + immunity['sus'][:, update_strain] = new_immunity_column + immunity['prog'][update_strain] = init_immunity['prog'] + immunity['trans'][update_strain] = init_immunity['trans'] + pars['immunity'] = immunity return pars diff --git a/covasim/run.py b/covasim/run.py index c05a75fd1..d66fd4b52 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -930,10 +930,9 @@ def print_heading(string): print_heading(f'Multirun for {scenkey}') scen_sim = sc.dcp(self.base_sim) scen_sim.label = scenkey - strain_pars = {par: scen_sim[par] for par in cvd.strain_pars} - immunity_pars = {par: scen_sim[par] for par in cvd.immunity_pars} - scen_sim.update_pars(scenpars, strain_pars=strain_pars, immunity_pars=immunity_pars, - **kwargs) # Update the parameters, if provided + defaults = {par: scen_sim[par] for par in cvd.strain_pars} + defaults['immunity'] = scen_sim['immunity'] + scen_sim.update_pars(scenpars, defaults=defaults, **kwargs) # Update the parameters, if provided run_args = dict(n_runs=self['n_runs'], noise=self['noise'], noisepar=self['noisepar'], keep_people=keep_people, verbose=verbose) if debug: print('Running in debug mode (not parallelized)') diff --git a/covasim/sim.py b/covasim/sim.py index dfe8bfff8..13755dbd1 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -74,9 +74,7 @@ def __init__(self, pars=None, datafile=None, datacols=None, label=None, simfile= # Now update everything self.set_metadata(simfile) # Set the simulation date and filename - strain_pars = {par: default_pars[par] for par in cvd.strain_pars} - immunity_pars = {par: default_pars[par] for par in cvd.immunity_pars} - self.update_pars(pars, strain_pars=strain_pars, immunity_pars=immunity_pars, **kwargs) # Update the parameters, if provided + self.update_pars(pars, defaults=default_pars, **kwargs) # Update the parameters, if provided self.load_data(datafile, datacols) # Load the data, if provided if self.load_pop: self.load_population(popfile) # Load the population, if provided @@ -481,10 +479,9 @@ def step(self): contacts = people.update_contacts() # Compute new contacts hosp_max = people.count('severe') > self['n_beds_hosp'] if self['n_beds_hosp'] else False # Check for acute bed constraint icu_max = people.count('critical') > self['n_beds_icu'] if self['n_beds_icu'] else False # Check for ICU bed constraint - ns = self['n_strains'] # Shorten number of strains # Randomly infect some people (imported infections) - imports = cvu.n_poisson(self['n_imports'], ns) # Imported cases + imports = cvu.n_poisson(self['n_imports'], self['n_strains']) # Imported cases for strain, n_imports in enumerate(imports): if n_imports>0: importation_inds = cvu.choose(max_n=len(people), n=n_imports) @@ -502,6 +499,7 @@ def step(self): raise ValueError(errormsg) people.update_states_post() # Check for state changes after interventions + ns = self['n_strains'] # Shorten number of strains # Compute viral loads frac_time = cvd.default_float(self['viral_dist']['frac_time']) @@ -519,45 +517,24 @@ def step(self): diag = people.diagnosed quar = people.quarantined + # Initialize temp storage for strain parameters + strain_parkeys = ['beta', 'asymp_factor', 'half_life'] + strain_pars = dict() + # Iterate through n_strains to calculate infections for strain in range(ns): # Deal with strain parameters - strain_parkeys = ['beta', 'asymp_factor'] - strain_pars = dict() for key in strain_parkeys: - strain_pars[key] = cvd.default_float(self[key][strain]) + strain_pars[key] = self[key][strain] beta = cvd.default_float(strain_pars['beta']) asymp_factor = cvd.default_float(strain_pars['asymp_factor']) - # Create susceptible immunity factors matrix - sus_immunity_factors = np.full(self['pop_size'], 0, dtype=cvd.default_float, order='F') - immunity_factors = { - 'sus': sus_immunity_factors, - 'trans': people.trans_immunity_factors[strain, :], - 'prog': people.prog_immunity_factors[strain, :] - } - init_immunity = { - 'sus': cvd.default_float(self['immunity']['sus'][strain, strain]), - 'trans': cvd.default_float(self['immunity']['trans'][strain]), - 'prog': cvd.default_float(self['immunity']['prog'][strain]), - } - half_life = { - 'sus': people.sus_half_life, - 'trans': people.trans_half_life, - 'prog': people.prog_half_life - } - decay_rate = {} - for key, val in half_life.items(): - rate = np.log(2) / val - rate[np.isnan(rate)] = 0 - rate = cvd.default_float(rate) - decay_rate[key] = rate - - # Process immunity parameters and indices + + # Determine people with immunity from a past infection from this strain immune = people.recovered_strain == strain # Whether people have some immunity to this strain from a prior infection with this strain immune_time = t - date_rec[immune] # Time since recovery for people who were last infected by this strain immune_inds = cvd.default_int(cvu.true(immune)) # People with some immunity to this strain from a prior infection with this strain - immunity_factors = cvu.compute_immunity(immunity_factors, immune_time, immune_inds, init_immunity, decay_rate) # Calculate immunity factors + immunity_factors = dict() # Process cross-immunity parameters and indices, if relevant if ns>1: @@ -566,13 +543,20 @@ def step(self): cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by the cross strain cross_immune_inds = cvd.default_int(cvu.true(cross_immune)) # People with some immunity to this strain from a prior infection with another strain - cross_immunity = { - 'sus': cvd.default_float(self['immunity']['sus'][cross_strain, strain]), - 'trans': cvd.default_float(self['immunity']['trans'][strain]), - 'prog': cvd.default_float(self['immunity']['prog'][strain]), - } - immunity_factors = cvu.compute_immunity(immunity_factors, cross_immune_time, cross_immune_inds, cross_immunity, decay_rate) # Calculate cross_immunity factors + # Compute immunity to susceptibility, transmissibility, and progression + for iax, ax in enumerate(cvd.immunity_axes): + half_life = getattr(people,f'{ax}_half_life') + init_immunity = self['init_immunity'][strain][ax] + if ax=='sus': + immunity_factors[ax] = np.full(len(people), 0, dtype=cvd.default_float, order='F') + else: + if ax=='trans': immunity_factors[ax] = people.trans_immunity_factors[strain, :] + elif ax=='prog': immunity_factors[ax] = people.prog_immunity_factors[strain, :] + + immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], immune_time, immune_inds, init_immunity, half_life) # Calculate immunity factors + if ns>1: + immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], cross_immune_time, cross_immune_inds, self['cross_immunity'], half_life) # Calculate cross_immunity factors people.trans_immunity_factors[strain, :] = immunity_factors['trans'] people.prog_immunity_factors[strain, :] = immunity_factors['prog'] @@ -606,7 +590,7 @@ def step(self): # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): if 'by_strain' in key or 'by strain' in key: - for strain in range(self['n_strains']): + for strain in range(ns): self.results[f'n_{key}'][strain][t] = people.count_by_strain(key, strain) else: self.results[f'n_{key}'][t] = people.count(key) @@ -614,7 +598,7 @@ def step(self): # Update counts for this time step: flows for key,count in people.flows.items(): if 'by_strain' in key or 'by strain' in key: - for strain in range(self['n_strains']): + for strain in range(ns): self.results[key][strain][t] += count[strain] else: self.results[key][t] += count diff --git a/covasim/utils.py b/covasim/utils.py index 75016f58c..ede637053 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -69,27 +69,12 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -# @nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat, nbfloat[:]), cache=True, parallel=parallel) -def compute_immunity(immunity_factors, immune_time, immune_inds, init_immunity, decay_rate): # pragma: no cover - ''' - Calculate immunity factors for time t - - Args: - immunity_factors (array of floats): - immune_time: - immune_inds: - init_immunity: - decay_rate: - - Returns: - immunity_factors (float[]): immunity factors - ''' - - immune_type_keys = immunity_factors.keys() - - for key in immune_type_keys: - this_decay_rate = decay_rate[key][immune_inds] - immunity_factors[key][immune_inds] = init_immunity[key] * np.exp(-this_decay_rate * immune_time) +@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat, nbfloat[:]), cache=True, parallel=parallel) +def compute_immunity(immunity_factors, immune_time, immune_inds, init_immunity, half_life): # pragma: no cover + ''' Calculate immunity factors for time t ''' + decay_rate = np.log(2) / half_life + decay_rate[np.isnan(decay_rate)] = 0 + immunity_factors[immune_inds] = init_immunity * np.exp(-decay_rate[immune_inds] * immune_time) return immunity_factors diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 2afccf8d2..afbdbfdd1 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -1,4 +1,5 @@ import covasim as cv +import covasim.defaults as cvd import sciris as sc import matplotlib.pyplot as plt import numpy as np @@ -76,47 +77,6 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): return scens -def test_2strains(do_plot=False, do_show=True, do_save=False): - sc.heading('Run basic sim with 2 strains') - sc.heading('Setting up...') - - imported_strain = { - 'beta': 0.2, - 'half_life': { - 'sus': dict(asymptomatic=25, mild=55, severe=155), - 'trans': dict(asymptomatic=55, mild=100, severe=160), - 'prog': dict(asymptomatic=55, mild=100, severe=160), - }, - } - - imports = cv.import_strain(strain=imported_strain, immunity_to=0.5, immunity_from=0.5, days=1, n_imports=30) - - pars = { - 'beta': 0.016, - 'n_days': 80, - 'rel_severe_prob': 1.3, # 30% more severe across all ages - 'half_life': { - 'sus': dict(asymptomatic=10, mild=30, severe=50), - }, - 'init_immunity': { - 'sus': 1 - } - } - - sim = cv.Sim(pars=pars, interventions=imports) - sim.run() - - strain_labels = [ - f'Strain A: beta {pars["beta"]}', - f'Strain B: beta {imported_strain["beta"]}', - ] - - if do_plot: - # sim.plot_result('new_reinfections', do_show=do_show, do_save=do_save, fig_path=f'results/test_2strains.png') - plot_results(sim, key='incidence_by_strain', title=f'2 strain test', filename='test_2strains2', labels=strain_labels, do_show=do_show, do_save=do_save) - return sim - - def test_strainduration(do_plot=False, do_show=True, do_save=False): sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') sc.heading('Setting up...') @@ -165,7 +125,7 @@ def test_strainduration(do_plot=False, do_show=True, do_save=False): return scens -def test_importstrain1(do_plot=False, do_show=True, do_save=False): +def test_import1strain(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing a new strain partway through a sim') sc.heading('Setting up...') @@ -176,16 +136,13 @@ def test_importstrain1(do_plot=False, do_show=True, do_save=False): pars = { 'n_days': 80, - 'half_life': { - 'sus': dict(asymptomatic=None, mild=None, severe=None) - } + 'half_life': {'sus': dict(asymptomatic=100, mild=None, severe=None)}, + 'init_immunity': {'prog': 0.9} } imported_strain = { 'beta': 0.025, - 'half_life': {'sus': dict(asymptomatic=None, mild=None, severe=None), - }, - 'init_immunity': {'sus': 0.5, 'trans': 0.5, 'prog': 0.5}, + 'init_immunity': {'sus':0.5} } imports = cv.import_strain(strain=imported_strain, days=10, n_imports=30) @@ -198,25 +155,22 @@ def test_importstrain1(do_plot=False, do_show=True, do_save=False): return sim -def test_importstrain2(do_plot=False, do_show=True, do_save=False): +def test_import2strains(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing 2 new strains partway through a sim') sc.heading('Setting up...') strain2 = {'beta': 0.025, 'rel_severe_prob': 1.3, - 'half_life': {'sus': dict(asymptomatic=20, mild=80, severe=200) - }, - 'init_immunity': 0.9 + 'half_life': {'sus': dict(asymptomatic=20, mild=80, severe=200)}, + 'init_immunity': {k:0.9 for k in cvd.immunity_axes}, } - pars = { - 'n_days': 80, - } + pars = {'n_days': 80} + strain3 = { 'beta': 0.05, 'rel_symp_prob': 1.6, - 'half_life': {'sus': dict(asymptomatic=10, mild=50, severe=150), - }, - 'init_immunity': 0.4 + 'half_life': {'sus': dict(asymptomatic=10, mild=50, severe=150)}, + 'init_immunity': {k:0.4 for k in cvd.immunity_axes} } imports = [cv.import_strain(strain=strain2, days=10, n_imports=20), @@ -238,34 +192,6 @@ def test_importstrain2(do_plot=False, do_show=True, do_save=False): return sim -# def test_par_refactor(): -# ''' -# The purpose of this test is to experiment with different representations of the parameter structures -# Still WIP! -# ''' -# -# # Simplest case: add a strain to beta -# p1 = cv.Par(name='beta', val=0.016, by_strain=True) -# print(p1.val) # Prints all the stored values of beta -# print(p1[0]) # Can index beta like an array to pull out strain-specific values -# p1.add_strain(new_val = 0.025) -# -# # Complex case: add a strain that's differentiated by severity for kids 0-20 -# p2 = cv.Par(name='sus_ORs', val=np.array([0.34, 0.67, 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47]), by_strain=True, by_age=True) -# print(p2.val) # Prints all the stored values for the original strain -# print(p2[0]) # Can index beta like an array to pull out strain-specific values -# p2.add_strain(new_val=np.array([1., 1., 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47])) -# -# # Complex case: add a strain that's differentiated by duration of disease -# p3 = cv.Par(name='dur_asym2rec', val=dict(dist='lognormal_int', par1=8.0, par2=2.0), by_strain=True, is_dist=True) -# print(p3.val) # Prints all the stored values for the original strain -# print(p3[0]) # Can index beta like an array to pull out strain-specific values -# p3.add_strain(new_val=dict(dist='lognormal_int', par1=12.0, par2=2.0)) -# p3.get(strain=1, n=6) -# -# return p1, p2, p3 - - def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing a new strain with longer duration partway through a sim') sc.heading('Setting up...') @@ -365,16 +291,14 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - # p1, p2, p3 = test_par_refactor() - sim5 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - # Importing strains is not currently working, so the following tests break - # sim2 = test_importstrain1(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim3 = test_importstrain2(do_plot=do_plot, do_save=do_save, do_show=do_show) - # This next test is deprecated, can be removed + # The next tests are deprecated, can be removed # simX = test_importstrain_args() + # p1, p2, p3 = test_par_refactor() sc.toc() @@ -417,3 +341,31 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab # # return sim # + +# def test_par_refactor(): +# ''' +# The purpose of this test is to experiment with different representations of the parameter structures +# Still WIP! +# ''' +# +# # Simplest case: add a strain to beta +# p1 = cv.Par(name='beta', val=0.016, by_strain=True) +# print(p1.val) # Prints all the stored values of beta +# print(p1[0]) # Can index beta like an array to pull out strain-specific values +# p1.add_strain(new_val = 0.025) +# +# # Complex case: add a strain that's differentiated by severity for kids 0-20 +# p2 = cv.Par(name='sus_ORs', val=np.array([0.34, 0.67, 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47]), by_strain=True, by_age=True) +# print(p2.val) # Prints all the stored values for the original strain +# print(p2[0]) # Can index beta like an array to pull out strain-specific values +# p2.add_strain(new_val=np.array([1., 1., 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47])) +# +# # Complex case: add a strain that's differentiated by duration of disease +# p3 = cv.Par(name='dur_asym2rec', val=dict(dist='lognormal_int', par1=8.0, par2=2.0), by_strain=True, is_dist=True) +# print(p3.val) # Prints all the stored values for the original strain +# print(p3[0]) # Can index beta like an array to pull out strain-specific values +# p3.add_strain(new_val=dict(dist='lognormal_int', par1=12.0, par2=2.0)) +# p3.get(strain=1, n=6) +# +# return p1, p2, p3 + From f580d4004a55711b16c4692f48ba02c422b4e5a3 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 26 Feb 2021 13:50:46 +0100 Subject: [PATCH 091/569] update intervention for duration --- covasim/interventions.py | 5 ++++- covasim/parameters.py | 7 ------- tests/devtests/test_variants.py | 29 +++++++++++++++-------------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 53f21b44b..f6167f032 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1190,7 +1190,10 @@ def apply(self, sim): # Update strain info for strain_key in cvd.strain_pars: if hasattr(self, strain_key): - sim[strain_key].append(getattr(self, strain_key)) + newval = getattr(self, strain_key) + if strain_key=='dur': # Validate durations (make sure there are values for all durations) + newval = sc.mergenested(sim[strain_key][0], newval) + sim[strain_key].append(newval) else: # use default print(f'{strain_key} not provided for this strain, using default value') diff --git a/covasim/parameters.py b/covasim/parameters.py index fb17caabd..83538de7b 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -318,13 +318,6 @@ def listify_strain_pars(pars, default_pars): return pars -def delistify_strain_pars(pars): - ''' Helper function to validate parameters that have been set to vary by strain ''' - for sp in cvd.strain_pars: - pars[sp] = pars[sp][0] - return pars - - def initialize_immunity(pars): ''' Initialize the immunity matrices with default values diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index afbdbfdd1..6b0ccf7bc 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -207,16 +207,17 @@ def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): imported_strain = { 'beta': 0.025, - 'dur': dict(exp2inf=dict(dist='lognormal_int', par1=6.0, par2=2.0), - inf2sym=dict(dist='lognormal_int', par1=4.0, par2=2.0), - sym2sev=dict(dist='lognormal_int', par1=8.0, par2=2.0), - sev2crit=dict(dist='lognormal_int', par1=8.0, par2=2.0), - asym2rec=dict(dist='lognormal_int', par1=5.0, par2=2.0), - mild2rec=dict(dist='lognormal_int', par1=12.0, par2=2.0), - sev2rec=dict(dist='lognormal_int', par1=12.0, par2=2.0), - crit2rec=dict(dist='lognormal_int', par1=12.0, par2=2.0), - crit2die=dict(dist='lognormal_int', par1=12.0, par2=2.0)), - + 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} + # 'dur': dict(exp2inf=dict(dist='lognormal_int', par1=6.0, par2=2.0), + # inf2sym=dict(dist='lognormal_int', par1=4.0, par2=2.0), + # sym2sev=dict(dist='lognormal_int', par1=8.0, par2=2.0), + # sev2crit=dict(dist='lognormal_int', par1=8.0, par2=2.0), + # asym2rec=dict(dist='lognormal_int', par1=5.0, par2=2.0), + # mild2rec=dict(dist='lognormal_int', par1=12.0, par2=2.0), + # sev2rec=dict(dist='lognormal_int', par1=12.0, par2=2.0), + # crit2rec=dict(dist='lognormal_int', par1=12.0, par2=2.0), + # crit2die=dict(dist='lognormal_int', par1=12.0, par2=2.0)), + # } imports = cv.import_strain(strain=imported_strain, days=10, n_imports=30) @@ -289,10 +290,10 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab if __name__ == '__main__': sc.tic() - scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + #scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + #scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) From d3f985d990bd2cd0349f02963f22d07d436c2af0 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 26 Feb 2021 14:41:22 +0100 Subject: [PATCH 092/569] update change beta --- covasim/interventions.py | 13 +++++++- tests/devtests/test_variants.py | 53 ++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index f6167f032..ac2649ab0 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -395,8 +395,10 @@ def initialize(self, sim): for lkey in self.layers: if lkey is None: self.orig_betas['overall'] = sim['beta'] + self.testkey = 'overall' else: self.orig_betas[lkey] = sim['beta_layer'][lkey] + self.testkey = lkey self.initialized = True return @@ -404,15 +406,24 @@ def initialize(self, sim): def apply(self, sim): + # Extend beta if needed + if self.layers[0] is None: + if len(sim['beta'])>len(self.orig_betas['overall']): + prev_change = sim['beta'][0]/self.orig_betas['overall'][0] + self.orig_betas['overall'].append(sim['beta'][-1]) + sim['beta'][-1] *= prev_change + # If this day is found in the list, apply the intervention for ind in find_day(self.days, sim.t): for lkey,new_beta in self.orig_betas.items(): - new_beta = new_beta * self.changes[ind] if lkey == 'overall': + new_beta = [bv * self.changes[ind] for bv in new_beta] sim['beta'] = new_beta else: + new_beta *= self.changes[ind] sim['beta_layer'][lkey] = new_beta + return diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 6b0ccf7bc..fda88f324 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -208,16 +208,6 @@ def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): imported_strain = { 'beta': 0.025, 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} - # 'dur': dict(exp2inf=dict(dist='lognormal_int', par1=6.0, par2=2.0), - # inf2sym=dict(dist='lognormal_int', par1=4.0, par2=2.0), - # sym2sev=dict(dist='lognormal_int', par1=8.0, par2=2.0), - # sev2crit=dict(dist='lognormal_int', par1=8.0, par2=2.0), - # asym2rec=dict(dist='lognormal_int', par1=5.0, par2=2.0), - # mild2rec=dict(dist='lognormal_int', par1=12.0, par2=2.0), - # sev2rec=dict(dist='lognormal_int', par1=12.0, par2=2.0), - # crit2rec=dict(dist='lognormal_int', par1=12.0, par2=2.0), - # crit2die=dict(dist='lognormal_int', par1=12.0, par2=2.0)), - # } imports = cv.import_strain(strain=imported_strain, days=10, n_imports=30) @@ -230,6 +220,46 @@ def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): return sim +def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): + sc.heading('Test introducing 2 new strains partway through a sim, with a change_beta intervention') + sc.heading('Setting up...') + + strain2 = {'beta': 0.025, + 'rel_severe_prob': 1.3, + 'half_life': {'sus': dict(asymptomatic=20, mild=80, severe=200)}, + 'init_immunity': {k:0.9 for k in cvd.immunity_axes}, + } + + pars = {'n_days': 80} + + strain3 = { + 'beta': 0.05, + 'rel_symp_prob': 1.6, + 'half_life': {'sus': dict(asymptomatic=10, mild=50, severe=150)}, + 'init_immunity': {k:0.4 for k in cvd.immunity_axes} + } + + intervs = [ + cv.change_beta(days=[10, 50, 70], changes=[0.8, 0.7, 0.6]), + cv.import_strain(strain=strain2, days=40, n_imports=20), + cv.import_strain(strain=strain3, days=60, n_imports=20), + ] + sim = cv.Sim(pars=pars, interventions=intervs, label='With imported infections') + sim.run() + + strain_labels = [ + 'Strain 1: beta 0.016', + 'Strain 2: beta 0.025, 20 imported day 10', + 'Strain 3: beta 0.05, 20 imported day 30' + ] + + if do_plot: + plot_results(sim, key='incidence_by_strain', title='Imported strains', filename='test_importstrain2', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_shares(sim, key='new_infections', title='Shares of new infections by strain', + filename='test_importstrain2_shares', do_show=do_show, do_save=do_save) + return sim + + def plot_results(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): results = sim.results @@ -294,7 +324,8 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab #scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) #sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) #sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) # The next tests are deprecated, can be removed From 8800e02f029141ac7ead3091c13eb2e663502a41 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 26 Feb 2021 08:50:18 -0500 Subject: [PATCH 093/569] starting vaccine-variants work --- covasim/defaults.py | 2 ++ covasim/interventions.py | 4 ++++ covasim/people.py | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/covasim/defaults.py b/covasim/defaults.py index a8a36533a..5fc67cae9 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -53,6 +53,8 @@ class PeopleMeta(sc.prettyobj): 'sus_half_life', # Float 'trans_half_life', # Float 'prog_half_life', # Float + 'vaccinations', # Number of doses given per person + 'vaccination_dates' # The dates when people are vaccinated ] # Set the states that a person can be in: these are all booleans per person -- used in people.py diff --git a/covasim/interventions.py b/covasim/interventions.py index b95120f5b..6016faa4b 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1115,6 +1115,10 @@ def apply(self, sim): for v_ind in vacc_inds: self.vaccination_dates[v_ind].append(sim.t) + # Update vaccine attributes in sim + sim.people.vaccinations = self.vaccinations + sim.people.vaccination_dates = self.vaccination_dates + return diff --git a/covasim/people.py b/covasim/people.py index f37a97d12..1cdaef39b 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -62,6 +62,10 @@ def __init__(self, pars, strict=True, **kwargs): self[key] = np.full((self.pars['max_strains'], self.pop_size), np.nan, dtype=cvd.default_float, order='F') elif key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. self[key] = np.full((self.pars['max_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') + elif key == 'vaccination_dates': + self[key] = [[] for _ in range(self.pop_size)] + elif key == 'vaccinations': + self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) From e4323f6a08977a00b7da7e11ef5305a371f80466 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Sat, 27 Feb 2021 11:12:03 +0100 Subject: [PATCH 094/569] minor changes to get sim to work --- covasim/base.py | 3 ++- covasim/defaults.py | 11 ++++++----- covasim/interventions.py | 4 ++-- covasim/parameters.py | 11 +++++------ covasim/people.py | 2 -- tests/devtests/test_variants.py | 23 ++++++++++++----------- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 64d352218..2412dd768 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -299,6 +299,7 @@ def _brief(self): def update_pars(self, pars=None, create=False, defaults=None, **kwargs): ''' Ensure that metaparameters get used properly before being updated ''' pars = sc.mergedicts(pars, kwargs) + if pars: if pars.get('pop_type'): cvpar.reset_layer_pars(pars, force=False) @@ -306,7 +307,7 @@ def update_pars(self, pars=None, create=False, defaults=None, **kwargs): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses if defaults is not None: # Defaults have been provided: we are now doing updates - pars = cvpar.listify_strain_pars(pars, defaults) # Strain pars need to be lists + pars = cvpar.listify_strain_pars(pars) # Strain pars need to be lists pars = cvpar.update_sub_key_pars(pars, defaults) # Update dict parameters by sub-key if pars.get('init_immunity'): pars['immunity'] = cvpar.update_init_immunity(defaults['immunity'], pars['init_immunity'][0]) # Update immunity diff --git a/covasim/defaults.py b/covasim/defaults.py index 5fc67cae9..73779509b 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -54,7 +54,6 @@ class PeopleMeta(sc.prettyobj): 'trans_half_life', # Float 'prog_half_life', # Float 'vaccinations', # Number of doses given per person - 'vaccination_dates' # The dates when people are vaccinated ] # Set the states that a person can be in: these are all booleans per person -- used in people.py @@ -85,6 +84,7 @@ class PeopleMeta(sc.prettyobj): dates.append('date_pos_test') # Store the date when a person tested which will come back positive dates.append('date_end_quarantine') # Store the date when a person comes out of quarantine dates.append('date_recovered') # Store the date when a person recovers + dates.append('date_vaccinated') # Store the date when a person is vaccinated # Duration of different states: these are floats per person -- used in people.py durs = [ @@ -146,11 +146,12 @@ class PeopleMeta(sc.prettyobj): 'rel_death_prob', ] -immunity_pars = ['n_strains', - 'max_strains', - 'immunity', +immunity_sources = [ + 'asymptomatic', + 'mild', + 'severe', + 'vaccine' ] - immunity_axes = ['sus', 'trans', 'prog'] # Default age data, based on Seattle 2018 census data -- used in population.py diff --git a/covasim/interventions.py b/covasim/interventions.py index a33dbc107..14cf3903c 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1086,7 +1086,7 @@ def initialize(self, sim): ''' Fix the dates and store the vaccinations ''' self.days = process_days(sim, self.days) self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person - self.vaccination_dates = [[] for p in range(sim.n)] # Store the dates when people are vaccinated + self.date_vaccinated = [[] for p in range(sim.n)] # Store the dates when people are vaccinated self.orig_rel_sus = sc.dcp(sim.people.rel_sus) # Keep a copy of pre-vaccination susceptibility self.orig_symp_prob = sc.dcp(sim.people.symp_prob) # ...and symptom probability self.mod_rel_sus = np.ones(sim.n, dtype=cvd.default_float) # Store the final modifiers @@ -1102,7 +1102,7 @@ def apply(self, sim): for ind in find_day(self.days, sim.t): # TODO -- investigate this, why does it loop over a variable that isn't subsequently used? Also, comments need updating # Construct the testing probabilities piece by piece -- complicated, since need to do it in the right order - vacc_probs = np.full(sim.n, self.prob) # Begin by assigning equal testing probability to everyone + vacc_probs = np.full(sim.n, self.prob) # Begin by assigning equal vaccination probability to everyone if self.subtarget is not None: subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted diff --git a/covasim/parameters.py b/covasim/parameters.py index 83538de7b..70a053917 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -72,10 +72,10 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): for axis in cvd.immunity_axes: if axis == 'sus': pars['init_immunity'][axis] = 1. # Default initial immunity - pars['half_life'][axis] = dict(asymptomatic=180, mild=180, severe=180) + pars['half_life'][axis] = {k: 180 for k in cvd.immunity_sources} #dict(asymptomatic=180, mild=180, severe=180) else: pars['init_immunity'][axis] = 0.5 # Default -- 50% shorter duration and probability of symptoms - pars['half_life'][axis] = dict(asymptomatic=None, mild=None, severe=None) + pars['half_life'][axis] = {k: 180 for k in cvd.immunity_sources} #dict(asymptomatic=None, mild=None, severe=None) pars['dur'] = {} # Duration parameters: time for disease progression @@ -120,6 +120,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses pars = initialize_immunity(pars) # Initialize immunity + pars = listify_strain_pars(pars) # Turn strain parameters into lists # If version is specified, load old parameters if version is not None: @@ -308,13 +309,11 @@ def update_sub_key_pars(pars, default_pars): return pars -def listify_strain_pars(pars, default_pars): - ''' Helper function to validate parameters that have been set to vary by strain ''' +def listify_strain_pars(pars): + ''' Helper function to turn strain parameters into lists ''' for sp in cvd.strain_pars: if sp in pars.keys(): pars[sp] = sc.promotetolist(pars[sp]) - else: - pars[sp] = sc.promotetolist(default_pars[sp]) return pars diff --git a/covasim/people.py b/covasim/people.py index 1cdaef39b..2621a8137 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -62,8 +62,6 @@ def __init__(self, pars, strict=True, **kwargs): self[key] = np.full((self.pars['max_strains'], self.pop_size), np.nan, dtype=cvd.default_float, order='F') elif key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. self[key] = np.full((self.pars['max_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') - elif key == 'vaccination_dates': - self[key] = [[] for _ in range(self.pop_size)] elif key == 'vaccinations': self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) else: diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index fda88f324..2b84e3677 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -81,11 +81,7 @@ def test_strainduration(do_plot=False, do_show=True, do_save=False): sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') sc.heading('Setting up...') - sim = cv.Sim() - dur = sc.dcp(sim['dur']) - dur['inf2sym'] = {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9} # Let's say this strain takes 10 days before you get symptoms - imported_strain = {'dur': dur} - + imported_strain = {'dur':{'inf2sym': {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9}}} imports = cv.import_strain(strain=imported_strain, days=10, n_imports=30) tp = cv.test_prob(symp_prob=0.2) # Add an efficient testing program @@ -320,14 +316,19 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab if __name__ == '__main__': sc.tic() - #scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - #scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + # Run simplest possible test + if 0: + sim = cv.Sim() + sim.run() + + # Run more complex tests + scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - # The next tests are deprecated, can be removed # simX = test_importstrain_args() # p1, p2, p3 = test_par_refactor() From b497c555319bcf6357acb8fb49ad22088320e9aa Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Sat, 27 Feb 2021 11:50:12 +0100 Subject: [PATCH 095/569] major changes needed to do this --- covasim/defaults.py | 5 ++-- covasim/parameters.py | 24 ++++++++------- covasim/people.py | 22 +++++++------- covasim/sim.py | 10 +++++-- covasim/utils.py | 52 +++++++++++++++++++++++++++------ tests/devtests/test_variants.py | 14 ++++----- 6 files changed, 85 insertions(+), 42 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 73779509b..9782dbba2 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -137,8 +137,9 @@ class PeopleMeta(sc.prettyobj): # Parameters that can vary by strain (should be in list format) strain_pars = ['beta', 'asymp_factor', - 'half_life', - 'init_immunity', + 'imm_pars', + # 'half_life', + # 'init_immunity', 'dur', 'rel_symp_prob', 'rel_severe_prob', diff --git a/covasim/parameters.py b/covasim/parameters.py index 70a053917..3268085c7 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -67,15 +67,19 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 - pars['init_immunity'] = {} - pars['half_life'] = {} - for axis in cvd.immunity_axes: - if axis == 'sus': - pars['init_immunity'][axis] = 1. # Default initial immunity - pars['half_life'][axis] = {k: 180 for k in cvd.immunity_sources} #dict(asymptomatic=180, mild=180, severe=180) - else: - pars['init_immunity'][axis] = 0.5 # Default -- 50% shorter duration and probability of symptoms - pars['half_life'][axis] = {k: 180 for k in cvd.immunity_sources} #dict(asymptomatic=None, mild=None, severe=None) + pars['imm_pars'] = {} + for ax in cvd.immunity_axes: + pars['imm_pars'][ax] = dict(form='exp_decay', par1=1., par2=180) + +# pars['init_immunity'] = {} +# pars['half_life'] = {} +# for axis in cvd.immunity_axes: +# if axis == 'sus': +# pars['init_immunity'][axis] = 1. # Default initial immunity +# pars['half_life'][axis] = {k: 180 for k in cvd.immunity_sources} #dict(asymptomatic=180, mild=180, severe=180) +# else: +# pars['init_immunity'][axis] = 0.5 # Default -- 50% shorter duration and probability of symptoms +# pars['half_life'][axis] = {k: 180 for k in cvd.immunity_sources} #dict(asymptomatic=None, mild=None, severe=None) pars['dur'] = {} # Duration parameters: time for disease progression @@ -119,7 +123,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): reset_layer_pars(pars) if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses - pars = initialize_immunity(pars) # Initialize immunity +# pars = initialize_immunity(pars) # Initialize immunity pars = listify_strain_pars(pars) # Turn strain parameters into lists # If version is specified, load old parameters diff --git a/covasim/people.py b/covasim/people.py index 2621a8137..590b97513 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -390,14 +390,14 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str source = source[keep] # Deal with strain parameters - infect_parkeys = ['dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob', 'half_life'] + infect_parkeys = ['dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob']#, 'half_life'] infect_pars = dict() for key in infect_parkeys: infect_pars[key] = self.pars[key][strain] n_infections = len(inds) durpars = infect_pars['dur'] - halflifepars = infect_pars['half_life'] +# halflifepars = infect_pars['half_life'] # Update states, strain info, and flows self.susceptible[inds] = False @@ -428,9 +428,9 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str dur_asym2rec = cvu.sample(**durpars['asym2rec'], size=len(asymp_inds))*(1-self.trans_immunity_factors[strain, asymp_inds]) self.date_recovered[asymp_inds] = self.date_infectious[asymp_inds] + dur_asym2rec # Date they recover self.dur_disease[asymp_inds] = self.dur_exp2inf[asymp_inds] + dur_asym2rec # Store how long this person had COVID-19 - self.sus_half_life[asymp_inds] = halflifepars['sus']['asymptomatic'] - self.trans_half_life[asymp_inds] = halflifepars['trans']['asymptomatic'] - self.prog_half_life[asymp_inds] = halflifepars['prog']['asymptomatic'] +# self.sus_half_life[asymp_inds] = halflifepars['sus']['asymptomatic'] +# self.trans_half_life[asymp_inds] = halflifepars['trans']['asymptomatic'] +# self.prog_half_life[asymp_inds] = halflifepars['prog']['asymptomatic'] # CASE 2: Symptomatic: can either be mild, severe, or critical n_symp_inds = len(symp_inds) @@ -445,16 +445,16 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str dur_mild2rec = cvu.sample(**durpars['mild2rec'], size=len(mild_inds))*(1-self.trans_immunity_factors[strain, mild_inds]) self.date_recovered[mild_inds] = self.date_symptomatic[mild_inds] + dur_mild2rec # Date they recover self.dur_disease[mild_inds] = self.dur_exp2inf[mild_inds] + self.dur_inf2sym[mild_inds] + dur_mild2rec # Store how long this person had COVID-19 - self.sus_half_life[mild_inds] = halflifepars['sus']['mild'] - self.trans_half_life[mild_inds] = halflifepars['trans']['mild'] - self.prog_half_life[mild_inds] = halflifepars['prog']['mild'] +# self.sus_half_life[mild_inds] = halflifepars['sus']['mild'] +# self.trans_half_life[mild_inds] = halflifepars['trans']['mild'] +# self.prog_half_life[mild_inds] = halflifepars['prog']['mild'] # CASE 2.2: Severe cases: hospitalization required, may become critical self.dur_sym2sev[sev_inds] = cvu.sample(**durpars['sym2sev'], size=len(sev_inds))*(1-self.trans_immunity_factors[strain, sev_inds]) # Store how long this person took to develop severe symptoms self.date_severe[sev_inds] = self.date_symptomatic[sev_inds] + self.dur_sym2sev[sev_inds] # Date symptoms become severe - self.sus_half_life[sev_inds] = halflifepars['sus']['severe'] - self.trans_half_life[sev_inds] = halflifepars['trans']['severe'] - self.prog_half_life[sev_inds] = halflifepars['prog']['severe'] +# self.sus_half_life[sev_inds] = halflifepars['sus']['severe'] +# self.trans_half_life[sev_inds] = halflifepars['trans']['severe'] +# self.prog_half_life[sev_inds] = halflifepars['prog']['severe'] crit_probs = infect_pars['rel_crit_prob'] * self.crit_prob[sev_inds] * (self.pars['no_hosp_factor'] if hosp_max else 1.) *(1-self.prog_immunity_factors[strain, sev_inds]) # Probability of these people becoming critical - higher if no beds available is_crit = cvu.binomial_arr(crit_probs) # See if they're a critical case crit_inds = sev_inds[is_crit] diff --git a/covasim/sim.py b/covasim/sim.py index 13755dbd1..2551366bc 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -518,7 +518,7 @@ def step(self): quar = people.quarantined # Initialize temp storage for strain parameters - strain_parkeys = ['beta', 'asymp_factor', 'half_life'] + strain_parkeys = ['beta', 'asymp_factor']#, 'half_life'] strain_pars = dict() # Iterate through n_strains to calculate infections @@ -546,14 +546,18 @@ def step(self): # Compute immunity to susceptibility, transmissibility, and progression for iax, ax in enumerate(cvd.immunity_axes): - half_life = getattr(people,f'{ax}_half_life') - init_immunity = self['init_immunity'][strain][ax] + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() if ax=='sus': immunity_factors[ax] = np.full(len(people), 0, dtype=cvd.default_float, order='F') else: if ax=='trans': immunity_factors[ax] = people.trans_immunity_factors[strain, :] elif ax=='prog': immunity_factors[ax] = people.prog_immunity_factors[strain, :] + half_life = getattr(people,f'{ax}_half_life') + init_immunity = self['init_immunity'][strain][ax] immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], immune_time, immune_inds, init_immunity, half_life) # Calculate immunity factors if ns>1: immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], cross_immune_time, cross_immune_inds, self['cross_immunity'], half_life) # Calculate cross_immunity factors diff --git a/covasim/utils.py b/covasim/utils.py index ede637053..26d368281 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -69,15 +69,6 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat, nbfloat[:]), cache=True, parallel=parallel) -def compute_immunity(immunity_factors, immune_time, immune_inds, init_immunity, half_life): # pragma: no cover - ''' Calculate immunity factors for time t ''' - decay_rate = np.log(2) / half_life - decay_rate[np.isnan(decay_rate)] = 0 - immunity_factors[immune_inds] = init_immunity * np.exp(-decay_rate[immune_inds] * immune_time) - - return immunity_factors - @nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=parallel) def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors): # pragma: no cover @@ -124,6 +115,48 @@ def find_contacts(p1, p2, inds): # pragma: no cover return pairing_partners +#%% Immunity methods +__all__ += ['compute_immunity'] + +def compute_immunity(form, pars, immunity_factors, immune_time, immune_inds): + ''' + Process immunity pars and functional form into a value + + - 'exp_decay' : exponential decay with initial value par1 and half-life=par2 + - 'linear' : linear decay with initial value par1 and per-day decay = par2 + - others TBC! + + Args: + form (str): the functional form to use + args (dict): passed to individual immunity functions + ''' + + choices = [ + 'exp_decay', + 'linear', + ] + + # Process inputs + if form == 'exp_decay': + output = exp_decay(immunity_factors, immune_time, immune_inds, pars) + + else: + choicestr = '\n'.join(choices) + errormsg = f'The selected functional form "{form}" is not implemented; choices are: {choicestr}' + raise NotImplementedError(errormsg) + + return output + + +@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat, nbfloat), cache=True, parallel=parallel) +def exp_decay(y, t, inds, init_val, half_life): # pragma: no cover + ''' Calculate exponential decay ''' + decay_rate = np.log(2) / half_life + decay_rate[np.isnan(decay_rate)] = 0 + y[inds] = init_val * np.exp(-decay_rate[inds] * t) + return y + + #%% Sampling and seed methods __all__ += ['sample', 'get_pdf', 'set_seed'] @@ -208,6 +241,7 @@ def sample(dist=None, par1=None, par2=None, size=None, **kwargs): return samples + def get_pdf(dist=None, par1=None, par2=None): ''' Return a probability density function for the specified distribution. This diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 2b84e3677..123c65430 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -317,17 +317,17 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() # Run simplest possible test - if 0: + if 1: sim = cv.Sim() sim.run() # Run more complex tests - scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) +# scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) +# scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) +# sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) +# sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) +# sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) +# sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) # The next tests are deprecated, can be removed # simX = test_importstrain_args() From d25deb4ca9cbebce7c994351cf94a0d108210a95 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Sat, 27 Feb 2021 11:52:51 +0100 Subject: [PATCH 096/569] adding defaults --- covasim/defaults.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 9782dbba2..94124c4b5 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -147,14 +147,12 @@ class PeopleMeta(sc.prettyobj): 'rel_death_prob', ] -immunity_sources = [ - 'asymptomatic', - 'mild', - 'severe', - 'vaccine' -] +# Immunity is broken down according to 3 axes, as listed here immunity_axes = ['sus', 'trans', 'prog'] +# Immunity protection also varies according to the level of symptoms you had, differentiated by 3 levels: +prior_symptoms = ['asymptomatic','mild','severe'] # TODO: align these with people.states?? Add vaccination?? + # Default age data, based on Seattle 2018 census data -- used in population.py default_age_data = np.array([ [ 0, 4, 0.0605], From 434a4c544ec595a5b1096850fa75bbaec66a071c Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Sat, 27 Feb 2021 12:20:24 +0100 Subject: [PATCH 097/569] towards a generic form of waning --- covasim/defaults.py | 13 ++++++++----- covasim/parameters.py | 2 +- covasim/people.py | 11 ++++++++++- covasim/sim.py | 9 +-------- covasim/utils.py | 11 +++++------ 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 94124c4b5..b9fd76b6e 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -48,11 +48,9 @@ class PeopleMeta(sc.prettyobj): 'death_prob', # Float 'rel_trans', # Float 'rel_sus', # Float + 'prior_symptoms', # Float 'trans_immunity_factors', # Float 'prog_immunity_factors', # Float - 'sus_half_life', # Float - 'trans_half_life', # Float - 'prog_half_life', # Float 'vaccinations', # Number of doses given per person ] @@ -150,8 +148,13 @@ class PeopleMeta(sc.prettyobj): # Immunity is broken down according to 3 axes, as listed here immunity_axes = ['sus', 'trans', 'prog'] -# Immunity protection also varies according to the level of symptoms you had, differentiated by 3 levels: -prior_symptoms = ['asymptomatic','mild','severe'] # TODO: align these with people.states?? Add vaccination?? +# Immunity protection also varies depending on your infection/vaccination history +immunity_sources = [ + 'asymptomatic', + 'mild', + 'severe', +# 'vaccine', +] # Default age data, based on Seattle 2018 census data -- used in population.py default_age_data = np.array([ diff --git a/covasim/parameters.py b/covasim/parameters.py index 3268085c7..765ebf179 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -69,7 +69,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 pars['imm_pars'] = {} for ax in cvd.immunity_axes: - pars['imm_pars'][ax] = dict(form='exp_decay', par1=1., par2=180) + pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val':1., 'half_life':180}) # pars['init_immunity'] = {} # pars['half_life'] = {} diff --git a/covasim/people.py b/covasim/people.py index 590b97513..7a07c1464 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -262,13 +262,22 @@ def check_critical(self): def check_recovery(self): ''' Check for recovery ''' inds = self.check_inds(self.susceptible, self.date_recovered, filter_inds=self.is_exp) + + # Before letting them recover, store information about the strain and symptoms they had + self.recovered_strain[inds] = self.infectious_strain[inds] + mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=inds) + severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=inds) + self.prior_symptoms[inds] = 0 # TODO: don't hard code this. It could come from cvd.prior_symptoms + self.prior_symptoms[mild_inds] = 1 # + self.prior_symptoms[severe_inds] = 2 # + + # Now reset all disease states self.exposed[inds] = False self.infectious[inds] = False self.symptomatic[inds] = False self.severe[inds] = False self.critical[inds] = False self.susceptible[inds] = True - self.recovered_strain[inds] = self.infectious_strain[inds] # TODO: check that this works self.infectious_strain[inds] = np.nan return len(inds) diff --git a/covasim/sim.py b/covasim/sim.py index 2551366bc..7f49d36c1 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -546,19 +546,12 @@ def step(self): # Compute immunity to susceptibility, transmissibility, and progression for iax, ax in enumerate(cvd.immunity_axes): - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() if ax=='sus': immunity_factors[ax] = np.full(len(people), 0, dtype=cvd.default_float, order='F') else: if ax=='trans': immunity_factors[ax] = people.trans_immunity_factors[strain, :] elif ax=='prog': immunity_factors[ax] = people.prog_immunity_factors[strain, :] - - half_life = getattr(people,f'{ax}_half_life') - init_immunity = self['init_immunity'][strain][ax] - immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], immune_time, immune_inds, init_immunity, half_life) # Calculate immunity factors + immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], immune_time, immune_inds, **self['imm_pars'][strain][ax]) # Calculate immunity factors if ns>1: immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], cross_immune_time, cross_immune_inds, self['cross_immunity'], half_life) # Calculate cross_immunity factors diff --git a/covasim/utils.py b/covasim/utils.py index 26d368281..bd0f7fcfd 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -118,7 +118,7 @@ def find_contacts(p1, p2, inds): # pragma: no cover #%% Immunity methods __all__ += ['compute_immunity'] -def compute_immunity(form, pars, immunity_factors, immune_time, immune_inds): +def compute_immunity(immunity_factors, immune_time, immune_inds, form, pars): ''' Process immunity pars and functional form into a value @@ -128,7 +128,7 @@ def compute_immunity(form, pars, immunity_factors, immune_time, immune_inds): Args: form (str): the functional form to use - args (dict): passed to individual immunity functions + kwargs (dict): passed to individual immunity functions ''' choices = [ @@ -138,7 +138,7 @@ def compute_immunity(form, pars, immunity_factors, immune_time, immune_inds): # Process inputs if form == 'exp_decay': - output = exp_decay(immunity_factors, immune_time, immune_inds, pars) + output = exp_decay(immunity_factors, immune_time, immune_inds, **pars) else: choicestr = '\n'.join(choices) @@ -151,9 +151,8 @@ def compute_immunity(form, pars, immunity_factors, immune_time, immune_inds): @nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat, nbfloat), cache=True, parallel=parallel) def exp_decay(y, t, inds, init_val, half_life): # pragma: no cover ''' Calculate exponential decay ''' - decay_rate = np.log(2) / half_life - decay_rate[np.isnan(decay_rate)] = 0 - y[inds] = init_val * np.exp(-decay_rate[inds] * t) + decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. + y[inds] = init_val * np.exp(-decay_rate * t) return y From 9c439e2d651182f7ccefc07293b351ab5b5d4932 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Sun, 28 Feb 2021 17:59:56 +0100 Subject: [PATCH 098/569] losing some generality --- covasim/interventions.py | 34 ++++++++--------- covasim/parameters.py | 68 +++++++++++++++++++++------------ covasim/people.py | 6 +-- covasim/sim.py | 23 ++++++----- covasim/utils.py | 10 ++--- tests/devtests/test_variants.py | 48 +++++++---------------- 6 files changed, 93 insertions(+), 96 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 14cf3903c..e2350b658 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1175,11 +1175,8 @@ def __init__(self, strain=None, days=None, n_imports=1, immunity_to=None, immuni def initialize(self, sim): self.days = process_days(sim, self.days) - self.max_strains = sim['max_strains'] - if not hasattr(self,'init_immunity'): - self.init_immunity = None - if not hasattr(self, 'half_life'): - self.half_life = sim['half_life'][0] + if not hasattr(self, 'imm_pars'): + self.imm_pars = sim['imm_pars'][0] self.initialized = True def apply(self, sim): @@ -1188,19 +1185,12 @@ def apply(self, sim): # Check number of strains prev_strains = sim['n_strains'] - if prev_strains + 1 > self.max_strains: - errormsg = f"Number of existing strains ({sim['n_strains']}) plus new strain exceeds the maximal allowable ({sim['max_strains']}. Increase pars['max_strains'])." - raise ValueError(errormsg) - # Validate half_life and init_immunity (make sure there are values for all cvd.immunity_axes + # Validate immunity pars (make sure there are values for all cvd.immunity_axes for key in cvd.immunity_axes: - if self.init_immunity is not None: - if key not in self.init_immunity: - print(f'initial immunity for imported strain for {key} not provided, using default value') - self.init_immunity[key] = sim['init_immunity'][0][key] - if key not in self.half_life: - print(f'half life for imported strain for {key} not provided, using default value') - self.half_life[key] = sim['half_life'][0][key] + if key not in self.imm_pars: + print(f'Immunity pars for imported strain for {key} not provided, using default value') + self.imm_pars[key] = sim['imm_pars'][0][key] # Update strain info for strain_key in cvd.strain_pars: @@ -1214,10 +1204,16 @@ def apply(self, sim): print(f'{strain_key} not provided for this strain, using default value') sim[strain_key].append(sim[strain_key][0]) + # Create defaults for cross-immunity if not provided + if self.immunity_to is None: + self.immunity_to = [sim['cross_immunity']]*sim['n_strains'] + if self.immunity_from is None: + self.immunity_from = [sim['cross_immunity']]*sim['n_strains'] + sim['n_strains'] += 1 - cvpar.update_immunity(pars=sim.pars, update_strain=prev_strains, - immunity_from=self.immunity_from, immunity_to=self.immunity_to, - init_immunity=self.init_immunity) + + # Update the immunity matrix + sim['immunity'] = cvpar.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) diff --git a/covasim/parameters.py b/covasim/parameters.py index 765ebf179..9781c87f3 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -70,16 +70,10 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['imm_pars'] = {} for ax in cvd.immunity_axes: pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val':1., 'half_life':180}) - -# pars['init_immunity'] = {} -# pars['half_life'] = {} -# for axis in cvd.immunity_axes: -# if axis == 'sus': -# pars['init_immunity'][axis] = 1. # Default initial immunity -# pars['half_life'][axis] = {k: 180 for k in cvd.immunity_sources} #dict(asymptomatic=180, mild=180, severe=180) -# else: -# pars['init_immunity'][axis] = 0.5 # Default -- 50% shorter duration and probability of symptoms -# pars['half_life'][axis] = {k: 180 for k in cvd.immunity_sources} #dict(asymptomatic=None, mild=None, severe=None) + pars['rel_imm'] = {} # Relative immunity scalings depending on the severity of symptoms + pars['rel_imm']['asymptomatic'] = 1. + pars['rel_imm']['mild'] = 1.2 + pars['rel_imm']['severe'] = 1.5 pars['dur'] = {} # Duration parameters: time for disease progression @@ -123,7 +117,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): reset_layer_pars(pars) if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses -# pars = initialize_immunity(pars) # Initialize immunity + pars['immunity'] = initialize_immunity() # Initialize immunity pars = listify_strain_pars(pars) # Turn strain parameters into lists # If version is specified, load old parameters @@ -321,30 +315,56 @@ def listify_strain_pars(pars): return pars -def initialize_immunity(pars): +def initialize_immunity(n_strains=None): ''' Initialize the immunity matrices with default values Susceptibility matrix is of size sim['max_strains']*sim['max_strains'] and is initialized with default values Progression is a matrix of scalars of size sim['max_strains'] initialized with default values Transmission is a matrix of scalars of size sim['max_strains'] initialized with default values ''' - pars['immunity'] = {} - pars['immunity']['sus'] = np.full((pars['max_strains'], pars['max_strains']), np.nan, dtype=cvd.default_float) - pars['immunity']['prog'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - pars['immunity']['trans'] = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - pars['immunity'] = update_init_immunity(pars['immunity'], pars['init_immunity']) - return pars + # Initialize + if n_strains is None: n_strains = 1 + immunity = {} + for ax in cvd.immunity_axes: + if ax == 'sus': + immunity[ax] = np.full((n_strains, n_strains), 1, dtype=cvd.default_float) + else: + immunity[ax] = np.full(n_strains, 1, dtype=cvd.default_float) + return immunity + + +def update_immunity(prev_immunity=None, n_strains=None, immunity_from=None, immunity_to=None): + ''' + Helper function to update the immunity matrices when a new strain is added. + (called by import_strain intervention) + ''' + # Add off-diagonals + immunity_from = sc.promotetolist(immunity_from) + immunity_to = sc.promotetolist(immunity_to) + + immunity = initialize_immunity(n_strains=n_strains) + update_strain = n_strains-1 + for ax in cvd.immunity_axes: + if ax=='sus': + immunity[ax][:update_strain, :update_strain] = prev_immunity[ax] + else: + immunity[ax][:update_strain] = prev_immunity[ax] + + # create the immunity[update_strain,] and immunity[,update_strain] arrays + new_immunity_row = np.full(n_strains, 1, dtype=cvd.default_float) + new_immunity_column = np.full(n_strains, 1, dtype=cvd.default_float) + for i in range(n_strains-1): + new_immunity_row[i] = immunity_from[i] + new_immunity_column[i] = immunity_to[i] + + immunity['sus'][update_strain, :] = new_immunity_row + immunity['sus'][:, update_strain] = new_immunity_column -def update_init_immunity(immunity, init_immunity): - '''Update immunity matrices with initial immunity values''' - for par, val in init_immunity.items(): - if par == 'sus': immunity[par][0,0] = val - else: immunity[par][0] = val return immunity -def update_immunity(pars, update_strain=None, immunity_from=None, immunity_to=None, init_immunity=None): +def update_immunity2(pars, update_strain=None, immunity_from=None, immunity_to=None, init_immunity=None): ''' Helper function to update the immunity matrices when a strain strain is added. If update_strain is not None, it's used to add a new strain with strain-specific values diff --git a/covasim/people.py b/covasim/people.py index 7a07c1464..1faf1c066 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -267,9 +267,9 @@ def check_recovery(self): self.recovered_strain[inds] = self.infectious_strain[inds] mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=inds) severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=inds) - self.prior_symptoms[inds] = 0 # TODO: don't hard code this. It could come from cvd.prior_symptoms - self.prior_symptoms[mild_inds] = 1 # - self.prior_symptoms[severe_inds] = 2 # + self.prior_symptoms[inds] = self.pars['rel_imm']['asymptomatic'] # + self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] # + self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] # # Now reset all disease states self.exposed[inds] = False diff --git a/covasim/sim.py b/covasim/sim.py index 7f49d36c1..1cc525c20 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -531,29 +531,32 @@ def step(self): asymp_factor = cvd.default_float(strain_pars['asymp_factor']) # Determine people with immunity from a past infection from this strain - immune = people.recovered_strain == strain # Whether people have some immunity to this strain from a prior infection with this strain - immune_time = t - date_rec[immune] # Time since recovery for people who were last infected by this strain - immune_inds = cvd.default_int(cvu.true(immune)) # People with some immunity to this strain from a prior infection with this strain - immunity_factors = dict() + immune = people.recovered_strain == strain + immune_inds = cvu.true(immune) # Whether people have some immunity to this strain from a prior infection with this strain + immune_time = t - date_rec[immune_inds] # Time since recovery for people who were last infected by this strain + prior_symptoms = people.prior_symptoms[immune_inds] + immunity_factors = dict() # Process cross-immunity parameters and indices, if relevant if ns>1: for cross_strain in range(ns): if cross_strain != strain: cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain - cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by the cross strain - cross_immune_inds = cvd.default_int(cvu.true(cross_immune)) # People with some immunity to this strain from a prior infection with another strain + cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by the cross strain + cross_immune_inds = cvu.true(cross_immune) # People with some immunity to this strain from a prior infection with another strain + cross_prior_symptoms = people.prior_symptoms[cross_immune_inds] # Compute immunity to susceptibility, transmissibility, and progression - for iax, ax in enumerate(cvd.immunity_axes): + for ax in cvd.immunity_axes: if ax=='sus': immunity_factors[ax] = np.full(len(people), 0, dtype=cvd.default_float, order='F') + immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], immune_time, immune_inds, prior_symptoms, self['immunity'][ax][strain, strain], **self['imm_pars'][strain][ax]) # Calculate immunity factors + if ns > 1: + immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], cross_immune_time, cross_immune_inds, cross_prior_symptoms, self['immunity'][ax][strain, cross_strain], **self['imm_pars'][strain][ax]) # Calculate cross_immunity factors else: if ax=='trans': immunity_factors[ax] = people.trans_immunity_factors[strain, :] elif ax=='prog': immunity_factors[ax] = people.prog_immunity_factors[strain, :] - immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], immune_time, immune_inds, **self['imm_pars'][strain][ax]) # Calculate immunity factors - if ns>1: - immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], cross_immune_time, cross_immune_inds, self['cross_immunity'], half_life) # Calculate cross_immunity factors + immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], immune_time, immune_inds, prior_symptoms, self['immunity'][ax][strain], **self['imm_pars'][strain][ax]) # Calculate immunity factors people.trans_immunity_factors[strain, :] = immunity_factors['trans'] people.prog_immunity_factors[strain, :] = immunity_factors['prog'] diff --git a/covasim/utils.py b/covasim/utils.py index bd0f7fcfd..2c0695060 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -118,7 +118,7 @@ def find_contacts(p1, p2, inds): # pragma: no cover #%% Immunity methods __all__ += ['compute_immunity'] -def compute_immunity(immunity_factors, immune_time, immune_inds, form, pars): +def compute_immunity(immunity_factors, immune_time, immune_inds, prior_symptoms, scale_factor, form, pars): ''' Process immunity pars and functional form into a value @@ -138,7 +138,7 @@ def compute_immunity(immunity_factors, immune_time, immune_inds, form, pars): # Process inputs if form == 'exp_decay': - output = exp_decay(immunity_factors, immune_time, immune_inds, **pars) + output = exp_decay(immunity_factors, immune_time, immune_inds, prior_symptoms, scale_factor, **pars) else: choicestr = '\n'.join(choices) @@ -148,11 +148,11 @@ def compute_immunity(immunity_factors, immune_time, immune_inds, form, pars): return output -@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat, nbfloat), cache=True, parallel=parallel) -def exp_decay(y, t, inds, init_val, half_life): # pragma: no cover +#@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) +def exp_decay(y, t, inds, prior_symptoms, scale_factor, init_val, half_life): # pragma: no cover ''' Calculate exponential decay ''' decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. - y[inds] = init_val * np.exp(-decay_rate * t) + y[inds] = scale_factor * prior_symptoms * init_val * np.exp(-decay_rate * t) return y diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 123c65430..3e55239e5 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -29,12 +29,7 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): 'baseline': { 'name':'No reinfection', 'pars': { - 'half_life': { - 'sus': dict(asymptomatic=None, mild=None, severe=None), # Constant immunity from reinfection, - }, - 'init_immunity': { - 'sus': 1, - }, + 'imm_pars': {'sus': dict(form='exp_decay', pars={'init_val': 1., 'half_life': None})} } }, 'med_halflife': { @@ -130,19 +125,13 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): 'Strain 2: beta 0.025' ] - pars = { - 'n_days': 80, - 'half_life': {'sus': dict(asymptomatic=100, mild=None, severe=None)}, - 'init_immunity': {'prog': 0.9} - } - imported_strain = { 'beta': 0.025, - 'init_immunity': {'sus':0.5} + 'imm_pars': {k: dict(form='exp_decay', pars={'init_val':1., 'half_life':100}) for k in cvd.immunity_axes} } - imports = cv.import_strain(strain=imported_strain, days=10, n_imports=30) - sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') + imports = cv.import_strain(strain=imported_strain, days=1, n_imports=30) + sim = cv.Sim(interventions=imports, label='With imported infections') sim.run() if do_plot: @@ -157,22 +146,17 @@ def test_import2strains(do_plot=False, do_show=True, do_save=False): strain2 = {'beta': 0.025, 'rel_severe_prob': 1.3, - 'half_life': {'sus': dict(asymptomatic=20, mild=80, severe=200)}, - 'init_immunity': {k:0.9 for k in cvd.immunity_axes}, + 'imm_pars': {'sus': dict(form='exp_decay', pars={'init_val': 1., 'half_life': 10})} } - pars = {'n_days': 80} - strain3 = { 'beta': 0.05, 'rel_symp_prob': 1.6, - 'half_life': {'sus': dict(asymptomatic=10, mild=50, severe=150)}, - 'init_immunity': {k:0.4 for k in cvd.immunity_axes} } imports = [cv.import_strain(strain=strain2, days=10, n_imports=20), cv.import_strain(strain=strain3, days=30, n_imports=20), ] - sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') + sim = cv.Sim(interventions=imports, label='With imported infections') sim.run() strain_labels = [ @@ -222,25 +206,19 @@ def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): strain2 = {'beta': 0.025, 'rel_severe_prob': 1.3, - 'half_life': {'sus': dict(asymptomatic=20, mild=80, severe=200)}, - 'init_immunity': {k:0.9 for k in cvd.immunity_axes}, } - pars = {'n_days': 80} - strain3 = { 'beta': 0.05, 'rel_symp_prob': 1.6, - 'half_life': {'sus': dict(asymptomatic=10, mild=50, severe=150)}, - 'init_immunity': {k:0.4 for k in cvd.immunity_axes} } intervs = [ - cv.change_beta(days=[10, 50, 70], changes=[0.8, 0.7, 0.6]), - cv.import_strain(strain=strain2, days=40, n_imports=20), - cv.import_strain(strain=strain3, days=60, n_imports=20), + cv.change_beta(days=[5, 20, 40], changes=[0.8, 0.7, 0.6]), + cv.import_strain(strain=strain2, days=10, n_imports=20), + cv.import_strain(strain=strain3, days=30, n_imports=20), ] - sim = cv.Sim(pars=pars, interventions=intervs, label='With imported infections') + sim = cv.Sim(interventions=intervs, label='With imported infections') sim.run() strain_labels = [ @@ -317,17 +295,17 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() # Run simplest possible test - if 1: + if 0: sim = cv.Sim() sim.run() # Run more complex tests -# scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) -# scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) +# scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # The next tests are deprecated, can be removed # simX = test_importstrain_args() From 81910a2ca20060749ee5ab52c5c360afa2cca6c2 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Sun, 28 Feb 2021 19:03:30 +0100 Subject: [PATCH 099/569] slow again --- covasim/defaults.py | 3 +-- covasim/parameters.py | 6 +++--- covasim/people.py | 12 +++++++----- covasim/utils.py | 2 ++ tests/devtests/test_variants.py | 33 +++++++++++++-------------------- 5 files changed, 26 insertions(+), 30 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index b9fd76b6e..459f940e8 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -136,8 +136,7 @@ class PeopleMeta(sc.prettyobj): strain_pars = ['beta', 'asymp_factor', 'imm_pars', - # 'half_life', - # 'init_immunity', + 'rel_imm', 'dur', 'rel_symp_prob', 'rel_severe_prob', diff --git a/covasim/parameters.py b/covasim/parameters.py index 9781c87f3..5b8389766 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -71,9 +71,9 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): for ax in cvd.immunity_axes: pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val':1., 'half_life':180}) pars['rel_imm'] = {} # Relative immunity scalings depending on the severity of symptoms - pars['rel_imm']['asymptomatic'] = 1. - pars['rel_imm']['mild'] = 1.2 - pars['rel_imm']['severe'] = 1.5 + pars['rel_imm']['asymptomatic'] = 0.7 + pars['rel_imm']['mild'] = 0.9 + pars['rel_imm']['severe'] = 1. pars['dur'] = {} # Duration parameters: time for disease progression diff --git a/covasim/people.py b/covasim/people.py index 1faf1c066..f6733414e 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -265,11 +265,13 @@ def check_recovery(self): # Before letting them recover, store information about the strain and symptoms they had self.recovered_strain[inds] = self.infectious_strain[inds] - mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=inds) - severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=inds) - self.prior_symptoms[inds] = self.pars['rel_imm']['asymptomatic'] # - self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] # - self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] # + for strain in range(self.pars['n_strains']): + this_strain_inds = cvu.true(self.recovered_strain[inds] == strain) + mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=this_strain_inds) + severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=this_strain_inds) + self.prior_symptoms[this_strain_inds] = self.pars['rel_imm'][strain]['asymptomatic'] # + self.prior_symptoms[mild_inds] = self.pars['rel_imm'][strain]['mild'] # + self.prior_symptoms[severe_inds] = self.pars['rel_imm'][strain]['severe'] # # Now reset all disease states self.exposed[inds] = False diff --git a/covasim/utils.py b/covasim/utils.py index 2c0695060..224dd2abb 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -138,6 +138,7 @@ def compute_immunity(immunity_factors, immune_time, immune_inds, prior_symptoms, # Process inputs if form == 'exp_decay': + if pars['half_life'] is None: pars['half_life'] = np.nan output = exp_decay(immunity_factors, immune_time, immune_inds, prior_symptoms, scale_factor, **pars) else: @@ -151,6 +152,7 @@ def compute_immunity(immunity_factors, immune_time, immune_inds, prior_symptoms, #@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) def exp_decay(y, t, inds, prior_symptoms, scale_factor, init_val, half_life): # pragma: no cover ''' Calculate exponential decay ''' + decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. y[inds] = scale_factor * prior_symptoms * init_val * np.exp(-decay_rate * t) return y diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 3e55239e5..8f4a28f3a 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -29,29 +29,22 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): 'baseline': { 'name':'No reinfection', 'pars': { - 'imm_pars': {'sus': dict(form='exp_decay', pars={'init_val': 1., 'half_life': None})} + 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) for k in cvd.immunity_axes} } }, 'med_halflife': { 'name':'3 month waning susceptibility', 'pars': { - 'half_life': { - 'sus': dict(asymptomatic=55, mild=55, severe=55), - }, - 'init_immunity': { - 'sus': 1, - }, + 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 90}) for k in + cvd.immunity_axes} } }, 'med_halflife_bysev': { - 'name':'1, 3, 6 month waning susceptibility, by severity', + 'name':'2 month waning susceptibility for symptomatics only', 'pars': { - 'half_life': { - 'sus': dict(asymptomatic=25, mild=55, severe=155), - }, - 'init_immunity': { - 'sus': 1, - }, + 'rel_imm': {'asymptomatic': 0}, + 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 60}) for k in + cvd.immunity_axes} } }, } @@ -295,17 +288,17 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() # Run simplest possible test - if 0: + if 1: sim = cv.Sim() sim.run() # Run more complex tests -# sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) -# sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) -# sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) -# sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) -# scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # The next tests are deprecated, can be removed # simX = test_importstrain_args() From 0819b47574be879a91737dd4d99d6d2cb8f7e289 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 1 Mar 2021 12:17:51 +0100 Subject: [PATCH 100/569] sketch out default strain structure --- covasim/parameters.py | 61 +++++++++++++++------------------ tests/devtests/test_variants.py | 12 ++----- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 5b8389766..ef48525c0 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -8,7 +8,7 @@ from . import misc as cvm from . import defaults as cvd -__all__ = ['make_pars', 'reset_layer_pars', 'get_prognoses'] +__all__ = ['make_pars', 'reset_layer_pars', 'get_prognoses', 'make_strain'] def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): @@ -364,41 +364,34 @@ def update_immunity(prev_immunity=None, n_strains=None, immunity_from=None, immu return immunity -def update_immunity2(pars, update_strain=None, immunity_from=None, immunity_to=None, init_immunity=None): - ''' - Helper function to update the immunity matrices when a strain strain is added. - If update_strain is not None, it's used to add a new strain with strain-specific values - (called by import_strain intervention) - ''' - immunity = pars['immunity'] - if immunity_from is None: - print('Immunity from pars not provided, using default value') - immunity_from = [pars['cross_immunity']] * pars['n_strains'] - if immunity_to is None: - print('Immunity to pars not provided, using default value') - immunity_to = [pars['cross_immunity']] * pars['n_strains'] - if init_immunity is None: - print('Initial immunity not provided, using default value') - pars['init_immunity'][update_strain] = pars['init_immunity'][0] - init_immunity = pars['init_immunity'][update_strain] - immunity_from = sc.promotetolist(immunity_from) - immunity_to = sc.promotetolist(immunity_to) +#%% Store default strain information - # create the immunity[update_strain,] and immunity[,update_strain] arrays - new_immunity_row = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - new_immunity_column = np.full(pars['max_strains'], np.nan, dtype=cvd.default_float) - for i in range(pars['n_strains']): - if i != update_strain: - new_immunity_row[i] = immunity_from[i] - new_immunity_column[i] = immunity_to[i] - else: - new_immunity_row[i] = new_immunity_column[i] = init_immunity['sus'] +def make_strain(strain=None): + ''' Populates a par dict with known information about circulating strains''' - immunity['sus'][update_strain, :] = new_immunity_row - immunity['sus'][:, update_strain] = new_immunity_column - immunity['prog'][update_strain] = init_immunity['prog'] - immunity['trans'][update_strain] = init_immunity['trans'] - pars['immunity'] = immunity + choices = { + 'b117': ['b117', 'B117', 'B.1.1.7', 'UK', 'uk', 'UK variant', 'uk variant'], + 'sa': ['SA', 'sa', 'SA variant', 'sa variant'], # TODO: add other aliases + } + + # Known parameters on B117 + if strain in choices['b117']: + pars = dict() + pars['rel_beta'] = 1.5 # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 + pars['rel_severe_prob'] = 1.6 # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf + + # Known parameters on South African variant + elif strain in choices['sa']: + pars = dict() + pars['imm_pars'] = dict() + for ax in cvd.immunity_axes: + pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + + else: + choicestr = '\n'.join(choices.values()) + errormsg = f'The selected variant "{strain}" is not implemented; choices are: {choicestr}' + raise NotImplementedError(errormsg) return pars + diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 8f4a28f3a..164603af9 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -137,14 +137,8 @@ def test_import2strains(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing 2 new strains partway through a sim') sc.heading('Setting up...') - strain2 = {'beta': 0.025, - 'rel_severe_prob': 1.3, - 'imm_pars': {'sus': dict(form='exp_decay', pars={'init_val': 1., 'half_life': 10})} - } - strain3 = { - 'beta': 0.05, - 'rel_symp_prob': 1.6, - } + strain2 = cv.make_strain('b117') + strain3 = cv.make_strain('sa variant') imports = [cv.import_strain(strain=strain2, days=10, n_imports=20), cv.import_strain(strain=strain3, days=30, n_imports=20), @@ -288,7 +282,7 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() # Run simplest possible test - if 1: + if 0: sim = cv.Sim() sim.run() From 1b3688252c36254c5009fc637a24f064c1580364 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 1 Mar 2021 12:32:46 +0100 Subject: [PATCH 101/569] clean up --- covasim/people.py | 12 +----------- covasim/sim.py | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index f6733414e..8e3c7e659 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -401,14 +401,13 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str source = source[keep] # Deal with strain parameters - infect_parkeys = ['dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob']#, 'half_life'] + infect_parkeys = ['dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob'] infect_pars = dict() for key in infect_parkeys: infect_pars[key] = self.pars[key][strain] n_infections = len(inds) durpars = infect_pars['dur'] -# halflifepars = infect_pars['half_life'] # Update states, strain info, and flows self.susceptible[inds] = False @@ -439,9 +438,6 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str dur_asym2rec = cvu.sample(**durpars['asym2rec'], size=len(asymp_inds))*(1-self.trans_immunity_factors[strain, asymp_inds]) self.date_recovered[asymp_inds] = self.date_infectious[asymp_inds] + dur_asym2rec # Date they recover self.dur_disease[asymp_inds] = self.dur_exp2inf[asymp_inds] + dur_asym2rec # Store how long this person had COVID-19 -# self.sus_half_life[asymp_inds] = halflifepars['sus']['asymptomatic'] -# self.trans_half_life[asymp_inds] = halflifepars['trans']['asymptomatic'] -# self.prog_half_life[asymp_inds] = halflifepars['prog']['asymptomatic'] # CASE 2: Symptomatic: can either be mild, severe, or critical n_symp_inds = len(symp_inds) @@ -456,16 +452,10 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str dur_mild2rec = cvu.sample(**durpars['mild2rec'], size=len(mild_inds))*(1-self.trans_immunity_factors[strain, mild_inds]) self.date_recovered[mild_inds] = self.date_symptomatic[mild_inds] + dur_mild2rec # Date they recover self.dur_disease[mild_inds] = self.dur_exp2inf[mild_inds] + self.dur_inf2sym[mild_inds] + dur_mild2rec # Store how long this person had COVID-19 -# self.sus_half_life[mild_inds] = halflifepars['sus']['mild'] -# self.trans_half_life[mild_inds] = halflifepars['trans']['mild'] -# self.prog_half_life[mild_inds] = halflifepars['prog']['mild'] # CASE 2.2: Severe cases: hospitalization required, may become critical self.dur_sym2sev[sev_inds] = cvu.sample(**durpars['sym2sev'], size=len(sev_inds))*(1-self.trans_immunity_factors[strain, sev_inds]) # Store how long this person took to develop severe symptoms self.date_severe[sev_inds] = self.date_symptomatic[sev_inds] + self.dur_sym2sev[sev_inds] # Date symptoms become severe -# self.sus_half_life[sev_inds] = halflifepars['sus']['severe'] -# self.trans_half_life[sev_inds] = halflifepars['trans']['severe'] -# self.prog_half_life[sev_inds] = halflifepars['prog']['severe'] crit_probs = infect_pars['rel_crit_prob'] * self.crit_prob[sev_inds] * (self.pars['no_hosp_factor'] if hosp_max else 1.) *(1-self.prog_immunity_factors[strain, sev_inds]) # Probability of these people becoming critical - higher if no beds available is_crit = cvu.binomial_arr(crit_probs) # See if they're a critical case crit_inds = sev_inds[is_crit] diff --git a/covasim/sim.py b/covasim/sim.py index 1cc525c20..b074f8af0 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -518,7 +518,7 @@ def step(self): quar = people.quarantined # Initialize temp storage for strain parameters - strain_parkeys = ['beta', 'asymp_factor']#, 'half_life'] + strain_parkeys = ['beta', 'asymp_factor'] strain_pars = dict() # Iterate through n_strains to calculate infections From 336430b0cb6fc19f0343dff73141c734a0c0ca90 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 1 Mar 2021 16:20:03 -0500 Subject: [PATCH 102/569] merged immunity and cross-immunity calculations into one. moved trans/prog immunity calculation into infect() function --- covasim/base.py | 3 --- covasim/parameters.py | 20 ++++++++++++----- covasim/people.py | 27 ++++++++++++++++++++-- covasim/sim.py | 40 +++++++++++++++++---------------- covasim/utils.py | 4 +++- tests/devtests/test_variants.py | 10 ++++----- 6 files changed, 69 insertions(+), 35 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 2412dd768..6e1dd9691 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -309,9 +309,6 @@ def update_pars(self, pars=None, create=False, defaults=None, **kwargs): if defaults is not None: # Defaults have been provided: we are now doing updates pars = cvpar.listify_strain_pars(pars) # Strain pars need to be lists pars = cvpar.update_sub_key_pars(pars, defaults) # Update dict parameters by sub-key - if pars.get('init_immunity'): - pars['immunity'] = cvpar.update_init_immunity(defaults['immunity'], pars['init_immunity'][0]) # Update immunity - super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/parameters.py b/covasim/parameters.py index ef48525c0..1714ce74b 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -318,9 +318,9 @@ def listify_strain_pars(pars): def initialize_immunity(n_strains=None): ''' Initialize the immunity matrices with default values - Susceptibility matrix is of size sim['max_strains']*sim['max_strains'] and is initialized with default values - Progression is a matrix of scalars of size sim['max_strains'] initialized with default values - Transmission is a matrix of scalars of size sim['max_strains'] initialized with default values + Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] and is initialized with default values + Progression is a matrix of scalars of size sim['n_strains'] initialized with default values + Transmission is a matrix of scalars of size sim['n_strains'] initialized with default values ''' # Initialize @@ -372,7 +372,8 @@ def make_strain(strain=None): choices = { 'b117': ['b117', 'B117', 'B.1.1.7', 'UK', 'uk', 'UK variant', 'uk variant'], - 'sa': ['SA', 'sa', 'SA variant', 'sa variant'], # TODO: add other aliases + 'b1351': ['b1351', 'B1351', 'B.1.351', 'SA', 'sa', 'SA variant', 'sa variant'], # TODO: add other aliases + 'p1': ['p1', 'P1', 'P.1', 'Brazil', 'Brazil variant', 'brazil variant'], } # Known parameters on B117 @@ -382,12 +383,21 @@ def make_strain(strain=None): pars['rel_severe_prob'] = 1.6 # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf # Known parameters on South African variant - elif strain in choices['sa']: + elif strain in choices['b1351']: pars = dict() pars['imm_pars'] = dict() for ax in cvd.immunity_axes: pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + # Known parameters on Brazil variant + elif strain in choices['p1']: + pars = dict() + pars['imm_pars'] = dict() + for ax in cvd.immunity_axes: + pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., + 'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + + else: choicestr = '\n'.join(choices.values()) errormsg = f'The selected variant "{strain}" is not implemented; choices are: {choicestr}' diff --git a/covasim/people.py b/covasim/people.py index 8e3c7e659..639fbd4f5 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -266,7 +266,9 @@ def check_recovery(self): # Before letting them recover, store information about the strain and symptoms they had self.recovered_strain[inds] = self.infectious_strain[inds] for strain in range(self.pars['n_strains']): - this_strain_inds = cvu.true(self.recovered_strain[inds] == strain) + this_strain = self.recovered_strain[inds] == strain + this_strain_inds = cvu.true(this_strain) + this_strain_inds = inds[this_strain_inds] mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=this_strain_inds) severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=this_strain_inds) self.prior_symptoms[this_strain_inds] = self.pars['rel_imm'][strain]['asymptomatic'] # @@ -369,7 +371,7 @@ def make_susceptible(self, inds): return - def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, strain=0): + def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, strain=0, immunity=None): ''' Infect people and determine their eventual outcomes. * Every infected person can infect other people, regardless of whether they develop symptoms @@ -424,6 +426,27 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str for i, target in enumerate(inds): self.infection_log.append(dict(source=source[i] if source is not None else None, target=target, date=self.t, layer=layer)) + if layer is not 'seed_infection' and layer is not 'importation': + # determine people with immunity from this strain and then calculate immunity to trans/prog + date_rec = self.date_recovered + immune = self.recovered_strain == strain + immune_inds = cvu.true( + immune) # Whether people have some immunity to this strain from a prior infection with this strain + immune_time = self.t - date_rec[ + immune_inds] # Time since recovery for people who were last infected by this strain + prior_symptoms = self.prior_symptoms[immune_inds] + + self.trans_immunity_factors[strain, :] = cvu.compute_immunity(self.trans_immunity_factors[strain, :], + immune_time, immune_inds, prior_symptoms, + immunity['immunity']['trans'][strain], + **immunity['imm_pars'][strain][ + 'trans']) # Calculate immunity factors + self.prog_immunity_factors[strain, :] = cvu.compute_immunity(self.prog_immunity_factors[strain, :], + immune_time, immune_inds, prior_symptoms, + immunity['immunity']['prog'][strain], + **immunity['imm_pars'][strain][ + 'prog']) # Calculate immunity factors + # Calculate how long before this person can infect other people self.dur_exp2inf[inds] = cvu.sample(**durpars['exp2inf'], size=n_infections) self.date_infectious[inds] = self.dur_exp2inf[inds] + self.t diff --git a/covasim/sim.py b/covasim/sim.py index b074f8af0..d43ff921e 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -499,7 +499,6 @@ def step(self): raise ValueError(errormsg) people.update_states_post() # Check for state changes after interventions - ns = self['n_strains'] # Shorten number of strains # Compute viral loads frac_time = cvd.default_float(self['viral_dist']['frac_time']) @@ -520,10 +519,15 @@ def step(self): # Initialize temp storage for strain parameters strain_parkeys = ['beta', 'asymp_factor'] strain_pars = dict() + ns = self['n_strains'] # Shorten number of strains # Iterate through n_strains to calculate infections for strain in range(ns): + # Create immunity dictionary to give infect() + immunity=dict() + immunity['immunity'] = self['immunity'] + immunity['imm_pars'] = self['imm_pars'] # Deal with strain parameters for key in strain_parkeys: strain_pars[key] = self[key][strain] @@ -535,31 +539,29 @@ def step(self): immune_inds = cvu.true(immune) # Whether people have some immunity to this strain from a prior infection with this strain immune_time = t - date_rec[immune_inds] # Time since recovery for people who were last infected by this strain prior_symptoms = people.prior_symptoms[immune_inds] - immunity_factors = dict() + immunity_scale_factor = np.full(len(immune_inds), self['immunity']['sus'][strain,strain]) + + # TODO--combine immune and cross-immunity for this operation # Process cross-immunity parameters and indices, if relevant - if ns>1: + if ns > 1: for cross_strain in range(ns): if cross_strain != strain: cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain - cross_immune_time = t - date_rec[cross_immune] # Time since recovery for people who were last infected by the cross strain cross_immune_inds = cvu.true(cross_immune) # People with some immunity to this strain from a prior infection with another strain + cross_immune_inds = np.setdiff1d(cross_immune_inds, immune_inds) # remove anyone who has own-immunity, that supercedes cross-immunity + cross_immune_time = t - date_rec[cross_immune_inds] # Time since recovery for people who were last infected by the cross strain cross_prior_symptoms = people.prior_symptoms[cross_immune_inds] + cross_immunity = np.full(len(cross_immune_inds), self['immunity']['sus'][strain, cross_strain]) + immunity_scale_factor = np.concatenate((immunity_scale_factor, cross_immunity)) + immune_inds = np.concatenate((immune_inds, cross_immune_inds)) + immune_time = np.concatenate((immune_time, cross_immune_time)) + prior_symptoms = np.concatenate((prior_symptoms, cross_prior_symptoms)) - # Compute immunity to susceptibility, transmissibility, and progression - for ax in cvd.immunity_axes: - if ax=='sus': - immunity_factors[ax] = np.full(len(people), 0, dtype=cvd.default_float, order='F') - immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], immune_time, immune_inds, prior_symptoms, self['immunity'][ax][strain, strain], **self['imm_pars'][strain][ax]) # Calculate immunity factors - if ns > 1: - immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], cross_immune_time, cross_immune_inds, cross_prior_symptoms, self['immunity'][ax][strain, cross_strain], **self['imm_pars'][strain][ax]) # Calculate cross_immunity factors - else: - if ax=='trans': immunity_factors[ax] = people.trans_immunity_factors[strain, :] - elif ax=='prog': immunity_factors[ax] = people.prog_immunity_factors[strain, :] - immunity_factors[ax] = cvu.compute_immunity(immunity_factors[ax], immune_time, immune_inds, prior_symptoms, self['immunity'][ax][strain], **self['imm_pars'][strain][ax]) # Calculate immunity factors + # Compute immunity to susceptibility - people.trans_immunity_factors[strain, :] = immunity_factors['trans'] - people.prog_immunity_factors[strain, :] = immunity_factors['prog'] + immunity_factors = np.full(len(people), 0, dtype=cvd.default_float, order='F') + immunity_factors = cvu.compute_immunity(immunity_factors, immune_time, immune_inds, prior_symptoms, immunity_scale_factor, **self['imm_pars'][strain]['sus']) # Calculate immunity factors # Define indices for this strain inf_by_this_strain = sc.dcp(inf) @@ -578,13 +580,13 @@ def step(self): quar_factor = cvd.default_float(self['quar_factor'][lkey]) beta_layer = cvd.default_float(self['beta_layer'][lkey]) rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, - diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors['sus']) + diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors) rel_sus = np.float32(rel_sus) # TODO: why doesn't this get returned in this format already? # Calculate actual transmission for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! - people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, + people.infect(inds=target_inds, immunity=immunity, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey, strain=strain) # Actually infect people # Update counts for this time step: stocks diff --git a/covasim/utils.py b/covasim/utils.py index 224dd2abb..390699a3b 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -127,6 +127,8 @@ def compute_immunity(immunity_factors, immune_time, immune_inds, prior_symptoms, - others TBC! Args: + scale_factor: reduction in immunity due to cross-immunity, if relevant (1 otherwise) (this is from immunity matrix) + prior_symptoms: reduction in immunity based on severity of prior symptoms (by default asymptomatic = 0.7, mild = 0.9, severe = 1.) form (str): the functional form to use kwargs (dict): passed to individual immunity functions ''' @@ -149,7 +151,7 @@ def compute_immunity(immunity_factors, immune_time, immune_inds, prior_symptoms, return output -#@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) +#@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat, nbfloat), cache=True, parallel=parallel) def exp_decay(y, t, inds, prior_symptoms, scale_factor, init_val, half_life): # pragma: no cover ''' Calculate exponential decay ''' diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 164603af9..db8d2c612 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -288,11 +288,11 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab # Run more complex tests sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + # scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # The next tests are deprecated, can be removed # simX = test_importstrain_args() From fdf1a7131aade772acbe16ee7156e3fa0d8f9977 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 1 Mar 2021 17:56:16 -0500 Subject: [PATCH 103/569] I don't think we need this to be by strain? we are using beta and immunity for that --- covasim/people.py | 41 ++++++++++++++++++----------------------- covasim/sim.py | 12 +++--------- 2 files changed, 21 insertions(+), 32 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index 639fbd4f5..ee6885c15 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -58,8 +58,6 @@ def __init__(self, pars, strict=True, **kwargs): for key in self.meta.person: if key == 'uid': self[key] = np.arange(self.pop_size, dtype=cvd.default_int) - elif key == 'rel_trans' or key == 'rel_sus': - self[key] = np.full((self.pars['max_strains'], self.pop_size), np.nan, dtype=cvd.default_float, order='F') elif key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. self[key] = np.full((self.pars['max_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') elif key == 'vaccinations': @@ -150,10 +148,9 @@ def find_cutoff(age_cutoffs, age): self.severe_prob[:] = progs['severe_probs'][inds]*progs['comorbidities'][inds] # Severe disease probability is modified by comorbidities self.crit_prob[:] = progs['crit_probs'][inds] # Probability of developing critical disease self.death_prob[:] = progs['death_probs'][inds] # Probability of death - for strain in range(self.pars['max_strains']): - #TODO -- make this strain specific in inputs if needed? - self.rel_sus[strain, :] = progs['sus_ORs'][inds] # Default susceptibilities - self.rel_trans[strain, :] = progs['trans_ORs'][inds]*cvu.sample(**self.pars['beta_dist'], size=len(inds)) # Default transmissibilities, with viral load drawn from a distribution + self.rel_sus[:] = progs['sus_ORs'][inds] # Default susceptibilities + self.rel_trans[:] = progs['trans_ORs'][inds] * cvu.sample(**self.pars['beta_dist'], size=len(inds)) # Default transmissibilities, with viral load drawn from a distribution + return @@ -371,7 +368,7 @@ def make_susceptible(self, inds): return - def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, strain=0, immunity=None): + def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, strain=0): ''' Infect people and determine their eventual outcomes. * Every infected person can infect other people, regardless of whether they develop symptoms @@ -426,27 +423,25 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str for i, target in enumerate(inds): self.infection_log.append(dict(source=source[i] if source is not None else None, target=target, date=self.t, layer=layer)) - if layer is not 'seed_infection' and layer is not 'importation': - # determine people with immunity from this strain and then calculate immunity to trans/prog - date_rec = self.date_recovered - immune = self.recovered_strain == strain - immune_inds = cvu.true( - immune) # Whether people have some immunity to this strain from a prior infection with this strain - immune_time = self.t - date_rec[ - immune_inds] # Time since recovery for people who were last infected by this strain - prior_symptoms = self.prior_symptoms[immune_inds] - - self.trans_immunity_factors[strain, :] = cvu.compute_immunity(self.trans_immunity_factors[strain, :], + # determine people with immunity from this strain and then calculate immunity to trans/prog + date_rec = self.date_recovered + immune = self.recovered_strain == strain + immune_inds = cvu.true(immune) # Whether people have some immunity to this strain from a prior infection with this strain + immune_time = self.t - date_rec[immune_inds] # Time since recovery for people who were last infected by this strain + prior_symptoms = self.prior_symptoms[immune_inds] + + self.trans_immunity_factors[strain, :] = cvu.compute_immunity(self.trans_immunity_factors[strain, :], immune_time, immune_inds, prior_symptoms, - immunity['immunity']['trans'][strain], - **immunity['imm_pars'][strain][ + self.pars['immunity']['trans'][strain], + **self.pars['imm_pars'][strain][ 'trans']) # Calculate immunity factors - self.prog_immunity_factors[strain, :] = cvu.compute_immunity(self.prog_immunity_factors[strain, :], + self.prog_immunity_factors[strain, :] = cvu.compute_immunity(self.prog_immunity_factors[strain, :], immune_time, immune_inds, prior_symptoms, - immunity['immunity']['prog'][strain], - **immunity['imm_pars'][strain][ + self.pars['immunity']['prog'][strain], + **self.pars['imm_pars'][strain][ 'prog']) # Calculate immunity factors + # Calculate how long before this person can infect other people self.dur_exp2inf[inds] = cvu.sample(**durpars['exp2inf'], size=n_infections) self.date_infectious[inds] = self.dur_exp2inf[inds] + self.t diff --git a/covasim/sim.py b/covasim/sim.py index d43ff921e..e7adce38b 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -524,10 +524,6 @@ def step(self): # Iterate through n_strains to calculate infections for strain in range(ns): - # Create immunity dictionary to give infect() - immunity=dict() - immunity['immunity'] = self['immunity'] - immunity['imm_pars'] = self['imm_pars'] # Deal with strain parameters for key in strain_parkeys: strain_pars[key] = self[key][strain] @@ -541,8 +537,6 @@ def step(self): prior_symptoms = people.prior_symptoms[immune_inds] immunity_scale_factor = np.full(len(immune_inds), self['immunity']['sus'][strain,strain]) - - # TODO--combine immune and cross-immunity for this operation # Process cross-immunity parameters and indices, if relevant if ns > 1: for cross_strain in range(ns): @@ -573,8 +567,8 @@ def step(self): betas = layer['beta'] # Compute relative transmission and susceptibility - rel_trans = people.rel_trans[strain, :] - rel_sus = people.rel_sus[strain, :] + rel_trans = people.rel_trans[:] + rel_sus = people.rel_sus[:] iso_factor = cvd.default_float(self['iso_factor'][lkey]) quar_factor = cvd.default_float(self['quar_factor'][lkey]) @@ -586,7 +580,7 @@ def step(self): # Calculate actual transmission for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! - people.infect(inds=target_inds, immunity=immunity, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, + people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey, strain=strain) # Actually infect people # Update counts for this time step: stocks From 5549e36eb384a2ba5021c55b2f7e9b10d8c74313 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 1 Mar 2021 21:40:39 -0500 Subject: [PATCH 104/569] starting to work on initializing exponential decay arrays to be used in compute immunity instead of recalculating each step. WIP! --- covasim/base.py | 1 + covasim/defaults.py | 3 ++- covasim/interventions.py | 3 ++- covasim/parameters.py | 37 ++++++++++++++++++++++++++------- covasim/people.py | 1 - covasim/sim.py | 4 ++-- covasim/utils.py | 16 ++++++++++++++ tests/devtests/test_variants.py | 18 ++++++++-------- 8 files changed, 61 insertions(+), 22 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 6e1dd9691..d6c42afb2 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -309,6 +309,7 @@ def update_pars(self, pars=None, create=False, defaults=None, **kwargs): if defaults is not None: # Defaults have been provided: we are now doing updates pars = cvpar.listify_strain_pars(pars) # Strain pars need to be lists pars = cvpar.update_sub_key_pars(pars, defaults) # Update dict parameters by sub-key + pars = cvpar.initialize_immune_degree(pars, defaults) super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/defaults.py b/covasim/defaults.py index 459f940e8..d1bb3a8b1 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -133,9 +133,10 @@ class PeopleMeta(sc.prettyobj): cum_result_flows = [f'cum_{key}' for key in result_flows.keys()] # Parameters that can vary by strain (should be in list format) -strain_pars = ['beta', +strain_pars = ['rel_beta', 'asymp_factor', 'imm_pars', + 'immune_degree', 'rel_imm', 'dur', 'rel_symp_prob', diff --git a/covasim/interventions.py b/covasim/interventions.py index e2350b658..08eece33b 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1213,7 +1213,8 @@ def apply(self, sim): sim['n_strains'] += 1 # Update the immunity matrix - sim['immunity'] = cvpar.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from) + sim['immunity'], sim['immune_degree'] = cvpar.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from, + imm_pars_strain=self.imm_pars, sim_immune_degree=sim['immune_degree'], n_days=sim['n_days']) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) diff --git a/covasim/parameters.py b/covasim/parameters.py index 1714ce74b..a3b5be2df 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -7,6 +7,7 @@ from .settings import options as cvo # For setting global options from . import misc as cvm from . import defaults as cvd +from . import utils as cvu __all__ = ['make_pars', 'reset_layer_pars', 'get_prognoses', 'make_strain'] @@ -57,15 +58,17 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Basic disease transmission parameters pars['beta_dist'] = dict(dist='neg_binomial', par1=1.0, par2=0.45, step=0.01) # Distribution to draw individual level transmissibility; dispersion from https://www.researchsquare.com/article/rs-29548/v1 pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 + pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated # Parameters that control settings and defaults for multi-strain runs - pars['n_strains'] = 1 # The number of strains currently circulating in the population + pars['n_strains'] = 1 # The number of strains currently circulating in the population pars['max_strains'] = 30 # For allocating memory with numpy arrays pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor - pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by set_immunity() below + pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by initialize_immunity() below + pars['immune_degree'] = {} # Dictionary with pre-loaded arrays of exponential decays for calculating immune degree # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains - pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated + pars['rel_beta'] = 1.0 pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 pars['imm_pars'] = {} for ax in cvd.immunity_axes: @@ -87,6 +90,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['dur']['sev2rec'] = dict(dist='lognormal_int', par1=14.0, par2=2.4) # Duration for people with severe symptoms to recover, 22.6 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf pars['dur']['crit2rec'] = dict(dist='lognormal_int', par1=14.0, par2=2.4) # Duration for people with critical symptoms to recover, 22.6 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf pars['dur']['crit2die'] = dict(dist='lognormal_int', par1=6.2, par2=1.7) # Duration from critical symptoms to death, 17.8 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf + # Severity parameters: probabilities of symptom progression pars['rel_symp_prob'] = 1.0 # Scale factor for proportion of symptomatic cases pars['rel_severe_prob'] = 1.0 # Scale factor for proportion of symptomatic cases that become severe @@ -118,6 +122,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses pars['immunity'] = initialize_immunity() # Initialize immunity + pars['immune_degree'] = initialize_immune_degree() pars = listify_strain_pars(pars) # Turn strain parameters into lists # If version is specified, load old parameters @@ -334,7 +339,17 @@ def initialize_immunity(n_strains=None): return immunity -def update_immunity(prev_immunity=None, n_strains=None, immunity_from=None, immunity_to=None): +def initialize_immune_degree(pars, defaults): + combined_pars = sc.mergedicts(pars, defaults) + immune_degree = {} + for ax in cvd.immunity_axes: + immune_degree[ax] = cvu.expo_decay(combined_pars['imm_pars'][ax]['pars']['half_life'], combined_pars['n_days']) + pars['immune_degree'] = immune_degree + return pars + + +def update_immunity(prev_immunity=None, n_strains=None, immunity_from=None, immunity_to=None, imm_pars_strain=None, + sim_immune_degree=None, n_days=None): ''' Helper function to update the immunity matrices when a new strain is added. (called by import_strain intervention) @@ -346,7 +361,7 @@ def update_immunity(prev_immunity=None, n_strains=None, immunity_from=None, immu immunity = initialize_immunity(n_strains=n_strains) update_strain = n_strains-1 for ax in cvd.immunity_axes: - if ax=='sus': + if ax == 'sus': immunity[ax][:update_strain, :update_strain] = prev_immunity[ax] else: immunity[ax][:update_strain] = prev_immunity[ax] @@ -361,7 +376,14 @@ def update_immunity(prev_immunity=None, n_strains=None, immunity_from=None, immu immunity['sus'][update_strain, :] = new_immunity_row immunity['sus'][:, update_strain] = new_immunity_column - return immunity + immune_degree = sc.promotetolist(sim_immune_degree) + immune_degree_new = {} + for ax in cvd.immunity_axes: + immune_degree_new[ax] = cvu.expo_decay(imm_pars_strain[ax]['pars']['half_life'], n_days) + + immune_degree.append(immune_degree_new) + + return immunity, immune_degree @@ -394,8 +416,7 @@ def make_strain(strain=None): pars = dict() pars['imm_pars'] = dict() for ax in cvd.immunity_axes: - pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., - 'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) else: diff --git a/covasim/people.py b/covasim/people.py index ee6885c15..a9b4d3b6e 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -151,7 +151,6 @@ def find_cutoff(age_cutoffs, age): self.rel_sus[:] = progs['sus_ORs'][inds] # Default susceptibilities self.rel_trans[:] = progs['trans_ORs'][inds] * cvu.sample(**self.pars['beta_dist'], size=len(inds)) # Default transmissibilities, with viral load drawn from a distribution - return diff --git a/covasim/sim.py b/covasim/sim.py index e7adce38b..0da31236d 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -517,7 +517,7 @@ def step(self): quar = people.quarantined # Initialize temp storage for strain parameters - strain_parkeys = ['beta', 'asymp_factor'] + strain_parkeys = ['rel_beta', 'asymp_factor'] strain_pars = dict() ns = self['n_strains'] # Shorten number of strains @@ -527,7 +527,7 @@ def step(self): # Deal with strain parameters for key in strain_parkeys: strain_pars[key] = self[key][strain] - beta = cvd.default_float(strain_pars['beta']) + beta = cvd.default_float(self['beta'])*cvd.default_float(strain_pars['rel_beta']) asymp_factor = cvd.default_float(strain_pars['asymp_factor']) # Determine people with immunity from a past infection from this strain diff --git a/covasim/utils.py b/covasim/utils.py index 390699a3b..83fe612dd 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -490,6 +490,22 @@ def choose_w(probs, n, unique=True): return np.random.choice(n_choices, n_samples, p=probs, replace=not(unique)) +#%% Waning immunity functions + +__all__ += ['expo_decay'] + + +def expo_decay(half_life, length): + ''' + Returns an array of length t with values for the immunity at each time step after recovery + ''' + + decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. + arr = np.ones(length, dtype=cvd.default_float) + for t in range(length): + arr[t] = np.exp(-decay_rate * t) + return arr + #%% Simple array operations diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index db8d2c612..c57ac2887 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -114,12 +114,12 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): sc.heading('Setting up...') strain_labels = [ - 'Strain 1: beta 0.016', - 'Strain 2: beta 0.025' + 'Strain 1', + 'Strain 2: 1.5x more transmissible' ] imported_strain = { - 'beta': 0.025, + 'rel_beta': 1.5, 'imm_pars': {k: dict(form='exp_decay', pars={'init_val':1., 'half_life':100}) for k in cvd.immunity_axes} } @@ -147,9 +147,9 @@ def test_import2strains(do_plot=False, do_show=True, do_save=False): sim.run() strain_labels = [ - 'Strain 1: beta 0.016', - 'Strain 2: beta 0.025, 20 imported day 10', - 'Strain 3: beta 0.05, 20 imported day 30' + 'Strain 1: Wild Type', + 'Strain 2: UK Variant on day 10', + 'Strain 3: SA Variant on day 30' ] if do_plot: @@ -173,7 +173,7 @@ def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): } imported_strain = { - 'beta': 0.025, + 'rel_beta': 1.5, 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} } @@ -191,12 +191,12 @@ def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing 2 new strains partway through a sim, with a change_beta intervention') sc.heading('Setting up...') - strain2 = {'beta': 0.025, + strain2 = {'rel_beta': 1.5, 'rel_severe_prob': 1.3, } strain3 = { - 'beta': 0.05, + 'rel_beta': 2, 'rel_symp_prob': 1.6, } From 65ff7ac5e551a1332d5fa30dfdbd20c380f603c8 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 2 Mar 2021 12:55:19 +0100 Subject: [PATCH 105/569] running tests --- covasim/interventions.py | 11 ++++++++--- covasim/parameters.py | 13 ++++++++----- tests/devtests/test_variants.py | 10 +++++++--- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 08eece33b..919d3f065 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -394,7 +394,7 @@ def initialize(self, sim): self.orig_betas = {} for lkey in self.layers: if lkey is None: - self.orig_betas['overall'] = sim['beta'] + self.orig_betas['overall'] = [rb*sim['beta'] for rb in sim['rel_beta']] self.testkey = 'overall' else: self.orig_betas[lkey] = sim['beta_layer'][lkey] @@ -406,9 +406,13 @@ def initialize(self, sim): def apply(self, sim): - # Extend beta if needed + # Extend rel_beta if needed if self.layers[0] is None: - if len(sim['beta'])>len(self.orig_betas['overall']): + if len(sim['rel_beta'])>len(self.orig_betas['overall']): + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() prev_change = sim['beta'][0]/self.orig_betas['overall'][0] self.orig_betas['overall'].append(sim['beta'][-1]) sim['beta'][-1] *= prev_change @@ -1202,6 +1206,7 @@ def apply(self, sim): else: # use default print(f'{strain_key} not provided for this strain, using default value') + sim[strain_key].append(sim[strain_key][0]) # Create defaults for cross-immunity if not provided diff --git a/covasim/parameters.py b/covasim/parameters.py index a3b5be2df..8aa7d98ae 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -122,7 +122,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses pars['immunity'] = initialize_immunity() # Initialize immunity - pars['immune_degree'] = initialize_immune_degree() + #pars['immune_degree'] = initialize_immune_degree() pars = listify_strain_pars(pars) # Turn strain parameters into lists # If version is specified, load old parameters @@ -340,10 +340,13 @@ def initialize_immunity(n_strains=None): def initialize_immune_degree(pars, defaults): - combined_pars = sc.mergedicts(pars, defaults) - immune_degree = {} - for ax in cvd.immunity_axes: - immune_degree[ax] = cvu.expo_decay(combined_pars['imm_pars'][ax]['pars']['half_life'], combined_pars['n_days']) + combined_pars = sc.mergedicts(defaults, pars) + immune_degree = [] # List by strain + for s in range(combined_pars['n_strains']): # Need to loop over strains here + ax_dict = {} + for ax in cvd.immunity_axes: + ax_dict[ax] = cvu.expo_decay(combined_pars['imm_pars'][s][ax]['pars']['half_life'], combined_pars['n_days']) + immune_degree.append(ax_dict) pars['immune_degree'] = immune_degree return pars diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index c57ac2887..1f32c38b1 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -282,15 +282,19 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() # Run simplest possible test - if 0: + if 1: sim = cv.Sim() sim.run() # Run more complex tests sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + + # TODO: the next test isn't working, need to check change_beta logic # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + + # TODO: the next two tests aren't working, need to check scenario logic # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) # scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) From 7d377c83979bb307580b6c7ffb74f5c666a392a6 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 2 Mar 2021 15:59:07 +0100 Subject: [PATCH 106/569] stuck with immunity calcs --- covasim/base.py | 3 +- covasim/interventions.py | 6 +- covasim/parameters.py | 42 ++++++------ covasim/people.py | 32 ++++------ covasim/sim.py | 8 +-- covasim/utils.py | 110 +++++++++++++++++++++++--------- tests/devtests/test_variants.py | 9 +-- 7 files changed, 129 insertions(+), 81 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index d6c42afb2..a5138ea21 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -309,7 +309,8 @@ def update_pars(self, pars=None, create=False, defaults=None, **kwargs): if defaults is not None: # Defaults have been provided: we are now doing updates pars = cvpar.listify_strain_pars(pars) # Strain pars need to be lists pars = cvpar.update_sub_key_pars(pars, defaults) # Update dict parameters by sub-key - pars = cvpar.initialize_immune_degree(pars, defaults) + combined_pars = sc.mergedicts(defaults, pars) # Now that subkeys have been updated, can merge the dicts together + pars['immune_degree'] = [cvpar.initialize_immune_degree(n_days=combined_pars['n_days'], imm_pars=combined_pars['imm_pars'][s]) for s in range(combined_pars['n_strains'])] # Precompute immunity waning super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/interventions.py b/covasim/interventions.py index 919d3f065..51f09cc1a 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1218,8 +1218,10 @@ def apply(self, sim): sim['n_strains'] += 1 # Update the immunity matrix - sim['immunity'], sim['immune_degree'] = cvpar.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from, - imm_pars_strain=self.imm_pars, sim_immune_degree=sim['immune_degree'], n_days=sim['n_days']) +# sim['immunity'], sim['immune_degree'] = cvpar.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from, +# imm_pars_strain=self.imm_pars, sim_immune_degree=sim['immune_degree'], n_days=sim['n_days']) + sim['immunity'] = cvpar.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from, + imm_pars_strain=self.imm_pars, sim_immune_degree=sim['immune_degree'], n_days=sim['n_days']) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) diff --git a/covasim/parameters.py b/covasim/parameters.py index 8aa7d98ae..4b2bb0b8f 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -65,7 +65,6 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['max_strains'] = 30 # For allocating memory with numpy arrays pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by initialize_immunity() below - pars['immune_degree'] = {} # Dictionary with pre-loaded arrays of exponential decays for calculating immune degree # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 @@ -122,7 +121,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses pars['immunity'] = initialize_immunity() # Initialize immunity - #pars['immune_degree'] = initialize_immune_degree() + pars['immune_degree'] = initialize_immune_degree(n_days=pars['n_days'], imm_pars=pars['imm_pars']) pars = listify_strain_pars(pars) # Turn strain parameters into lists # If version is specified, load old parameters @@ -339,20 +338,16 @@ def initialize_immunity(n_strains=None): return immunity -def initialize_immune_degree(pars, defaults): - combined_pars = sc.mergedicts(defaults, pars) - immune_degree = [] # List by strain - for s in range(combined_pars['n_strains']): # Need to loop over strains here - ax_dict = {} - for ax in cvd.immunity_axes: - ax_dict[ax] = cvu.expo_decay(combined_pars['imm_pars'][s][ax]['pars']['half_life'], combined_pars['n_days']) - immune_degree.append(ax_dict) - pars['immune_degree'] = immune_degree - return pars +def initialize_immune_degree(n_days=None, imm_pars=None): + ''' Precompute immunity waning ''' + immune_degree = {} + for ax in cvd.immunity_axes: + immune_degree[ax] = cvu.pre_compute_immunity(n_days, **imm_pars[ax]) + return immune_degree -def update_immunity(prev_immunity=None, n_strains=None, immunity_from=None, immunity_to=None, imm_pars_strain=None, - sim_immune_degree=None, n_days=None): +def update_immunity(prev_immunity=None, n_strains=None, immunity_from=None, immunity_to=None, + imm_pars_strain=None, sim_immune_degree=None, n_days=None): ''' Helper function to update the immunity matrices when a new strain is added. (called by import_strain intervention) @@ -379,15 +374,22 @@ def update_immunity(prev_immunity=None, n_strains=None, immunity_from=None, immu immunity['sus'][update_strain, :] = new_immunity_row immunity['sus'][:, update_strain] = new_immunity_column - immune_degree = sc.promotetolist(sim_immune_degree) - immune_degree_new = {} - for ax in cvd.immunity_axes: - immune_degree_new[ax] = cvu.expo_decay(imm_pars_strain[ax]['pars']['half_life'], n_days) + # TODO: figure out how to adapt this next section for generic waning functions. + # Initial thoughts: perhaps we call compute_immunity here, with compute_immunity + # modified to calculate immunity for a generic series of timepoints rather than + # anything specific. But then we'll need to retain the array of (t, immunity) + # so we can do lookups for specific values of t. + +# immune_degree = sc.promotetolist(sim_immune_degree) +# immune_degree_new = {} +# for ax in cvd.immunity_axes: +# immune_degree_new[ax] = cvu.expo_decay(imm_pars_strain[ax]['pars']['half_life'], n_days) - immune_degree.append(immune_degree_new) +# immune_degree.append(immune_degree_new) - return immunity, immune_degree +# return immunity, immune_degree + return immunity #%% Store default strain information diff --git a/covasim/people.py b/covasim/people.py index a9b4d3b6e..719335d92 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -407,6 +407,19 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str n_infections = len(inds) durpars = infect_pars['dur'] + # determine people with immunity from this strain and then calculate immunity to trans/prog + date_rec = self.date_recovered + immune = self.recovered_strain == strain + immune_inds = cvu.true(immune) # Whether people have some immunity to this strain from a prior infection with this strain + immune_time = cvd.default_int(self.t - date_rec[immune_inds]) # Time since recovery for people who were last infected by this strain + prior_symptoms = self.prior_symptoms[immune_inds] + + if len(immune_inds): + self.trans_immunity_factors[strain, immune_inds] = self['pars']['immune_degree'][strain]['trans'][immune_time] * \ + prior_symptoms * self.pars['immunity']['trans'][strain] + self.prog_immunity_factors[strain, immune_inds] = self['pars']['immune_degree'][strain]['prog'][immune_time] * \ + prior_symptoms * self.pars['immunity']['prog'][strain] + # Update states, strain info, and flows self.susceptible[inds] = False self.exposed[inds] = True @@ -422,25 +435,6 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str for i, target in enumerate(inds): self.infection_log.append(dict(source=source[i] if source is not None else None, target=target, date=self.t, layer=layer)) - # determine people with immunity from this strain and then calculate immunity to trans/prog - date_rec = self.date_recovered - immune = self.recovered_strain == strain - immune_inds = cvu.true(immune) # Whether people have some immunity to this strain from a prior infection with this strain - immune_time = self.t - date_rec[immune_inds] # Time since recovery for people who were last infected by this strain - prior_symptoms = self.prior_symptoms[immune_inds] - - self.trans_immunity_factors[strain, :] = cvu.compute_immunity(self.trans_immunity_factors[strain, :], - immune_time, immune_inds, prior_symptoms, - self.pars['immunity']['trans'][strain], - **self.pars['imm_pars'][strain][ - 'trans']) # Calculate immunity factors - self.prog_immunity_factors[strain, :] = cvu.compute_immunity(self.prog_immunity_factors[strain, :], - immune_time, immune_inds, prior_symptoms, - self.pars['immunity']['prog'][strain], - **self.pars['imm_pars'][strain][ - 'prog']) # Calculate immunity factors - - # Calculate how long before this person can infect other people self.dur_exp2inf[inds] = cvu.sample(**durpars['exp2inf'], size=n_infections) self.date_infectious[inds] = self.dur_exp2inf[inds] + self.t diff --git a/covasim/sim.py b/covasim/sim.py index 0da31236d..298a0ca65 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -533,7 +533,7 @@ def step(self): # Determine people with immunity from a past infection from this strain immune = people.recovered_strain == strain immune_inds = cvu.true(immune) # Whether people have some immunity to this strain from a prior infection with this strain - immune_time = t - date_rec[immune_inds] # Time since recovery for people who were last infected by this strain + immune_time = cvd.default_int(t - date_rec[immune_inds]) # Time since recovery for people who were last infected by this strain prior_symptoms = people.prior_symptoms[immune_inds] immunity_scale_factor = np.full(len(immune_inds), self['immunity']['sus'][strain,strain]) @@ -544,7 +544,7 @@ def step(self): cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain cross_immune_inds = cvu.true(cross_immune) # People with some immunity to this strain from a prior infection with another strain cross_immune_inds = np.setdiff1d(cross_immune_inds, immune_inds) # remove anyone who has own-immunity, that supercedes cross-immunity - cross_immune_time = t - date_rec[cross_immune_inds] # Time since recovery for people who were last infected by the cross strain + cross_immune_time = cvd.default_int(t - date_rec[cross_immune_inds]) # Time since recovery for people who were last infected by the cross strain cross_prior_symptoms = people.prior_symptoms[cross_immune_inds] cross_immunity = np.full(len(cross_immune_inds), self['immunity']['sus'][strain, cross_strain]) immunity_scale_factor = np.concatenate((immunity_scale_factor, cross_immunity)) @@ -553,9 +553,9 @@ def step(self): prior_symptoms = np.concatenate((prior_symptoms, cross_prior_symptoms)) # Compute immunity to susceptibility - immunity_factors = np.full(len(people), 0, dtype=cvd.default_float, order='F') - immunity_factors = cvu.compute_immunity(immunity_factors, immune_time, immune_inds, prior_symptoms, immunity_scale_factor, **self['imm_pars'][strain]['sus']) # Calculate immunity factors + if len(immune_inds): + immunity_factors[immune_inds] = self['immune_degree'][strain]['sus'][immune_time] * prior_symptoms * self.pars['immunity']['sus'][strain, strain] # Define indices for this strain inf_by_this_strain = sc.dcp(inf) diff --git a/covasim/utils.py b/covasim/utils.py index 83fe612dd..d8500268b 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -116,32 +116,35 @@ def find_contacts(p1, p2, inds): # pragma: no cover #%% Immunity methods -__all__ += ['compute_immunity'] +__all__ += ['pre_compute_immunity'] -def compute_immunity(immunity_factors, immune_time, immune_inds, prior_symptoms, scale_factor, form, pars): +def pre_compute_immunity(length, form, pars): ''' Process immunity pars and functional form into a value - - - 'exp_decay' : exponential decay with initial value par1 and half-life=par2 - - 'linear' : linear decay with initial value par1 and per-day decay = par2 + - 'exp_decay' : exponential decay (TODO fill in details) + - 'logistic_decay' : logistic decay (TODO fill in details) + - 'linear' : linear decay (TODO fill in details) - others TBC! Args: - scale_factor: reduction in immunity due to cross-immunity, if relevant (1 otherwise) (this is from immunity matrix) - prior_symptoms: reduction in immunity based on severity of prior symptoms (by default asymptomatic = 0.7, mild = 0.9, severe = 1.) form (str): the functional form to use - kwargs (dict): passed to individual immunity functions + pars (dict): passed to individual immunity functions + length (float): length of time to compute immunity ''' choices = [ 'exp_decay', + 'logistic_decay', 'linear', ] # Process inputs if form == 'exp_decay': if pars['half_life'] is None: pars['half_life'] = np.nan - output = exp_decay(immunity_factors, immune_time, immune_inds, prior_symptoms, scale_factor, **pars) + output = exp_decay(length, **pars) + + elif form == 'logistic_decay': + output = logistic_decay(**pars) else: choicestr = '\n'.join(choices) @@ -151,13 +154,74 @@ def compute_immunity(immunity_factors, immune_time, immune_inds, prior_symptoms, return output -#@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat, nbfloat), cache=True, parallel=parallel) -def exp_decay(y, t, inds, prior_symptoms, scale_factor, init_val, half_life): # pragma: no cover - ''' Calculate exponential decay ''' - +def exp_decay(length, init_val, half_life): + ''' + Returns an array of length t with values for the immunity at each time step after recovery + ''' decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. - y[inds] = scale_factor * prior_symptoms * init_val * np.exp(-decay_rate * t) - return y + t = np.arange(length, dtype=cvd.default_int) + return init_val * np.exp(-decay_rate * t) + + +def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp): + ''' Calculate logistic decay ''' + t = np.arange(length, dtype=cvd.default_int) + return (init_val + (lower_asymp-init_val) / (1 + (t/half_val) ** decay_rate)) # TODO: make this robust to /0 errors + + + +####### PREVIOUS VERSIONS +# def compute_immunity(immunity_factors, immune_time, immune_inds, prior_symptoms, scale_factor, form, pars): +# ''' +# Process immunity pars and functional form into a value +# +# - 'exp_decay' : exponential decay with initial value par1 and half-life=par2 +# - 'linear' : linear decay with initial value par1 and per-day decay = par2 +# - others TBC! +# +# Args: +# scale_factor: reduction in immunity due to cross-immunity, if relevant (1 otherwise) (this is from immunity matrix) +# prior_symptoms: reduction in immunity based on severity of prior symptoms (by default asymptomatic = 0.7, mild = 0.9, severe = 1.) +# form (str): the functional form to use +# kwargs (dict): passed to individual immunity functions +# ''' +# +# choices = [ +# 'exp_decay', +# 'logistic_decay', +# 'linear', +# ] +# +# # Process inputs +# if form == 'exp_decay': +# if pars['half_life'] is None: pars['half_life'] = np.nan +# output = exp_decay(immunity_factors, immune_time, immune_inds, prior_symptoms, scale_factor, **pars) +# +# elif form == 'logistic_decay': +# output = logistic_decay(immunity_factors, immune_time, immune_inds, prior_symptoms, scale_factor, **pars) +# +# else: +# choicestr = '\n'.join(choices) +# errormsg = f'The selected functional form "{form}" is not implemented; choices are: {choicestr}' +# raise NotImplementedError(errormsg) +# +# return output +# +# +# #@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat, nbfloat), cache=True, parallel=parallel) +# def exp_decay(y, t, inds, prior_symptoms, scale_factor, init_val, half_life): # pragma: no cover +# ''' Calculate exponential decay ''' +# decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. +# y[inds] = scale_factor * prior_symptoms * init_val * np.exp(-decay_rate * t) +# y[inds] = scale_factor * prior_symptoms * init_val * np.exp(-decay_rate * t) +# return y +# +# #@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat, nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) +# def logistic_decay(y, t, inds, prior_symptoms, scale_factor, init_val, decay_rate, half_val, lower_asymp): # pragma: no cover +# ''' Calculate logistic decay ''' +# y[inds] = scale_factor * prior_symptoms * (init_val + (lower_asymp-init_val) / (1 + (t/half_val) ** decay_rate)) +# return y +# #%% Sampling and seed methods @@ -490,22 +554,6 @@ def choose_w(probs, n, unique=True): return np.random.choice(n_choices, n_samples, p=probs, replace=not(unique)) -#%% Waning immunity functions - -__all__ += ['expo_decay'] - - -def expo_decay(half_life, length): - ''' - Returns an array of length t with values for the immunity at each time step after recovery - ''' - - decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. - arr = np.ones(length, dtype=cvd.default_float) - for t in range(length): - arr[t] = np.exp(-decay_rate * t) - return arr - #%% Simple array operations diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 1f32c38b1..5d6ad6b56 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -120,7 +120,8 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): imported_strain = { 'rel_beta': 1.5, - 'imm_pars': {k: dict(form='exp_decay', pars={'init_val':1., 'half_life':100}) for k in cvd.immunity_axes} +# 'imm_pars': {k: dict(form='exp_decay', pars={'init_val':1., 'half_life':100}) for k in cvd.immunity_axes} + 'imm_pars': {k: dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 10, 'lower_asymp': 0.1, 'decay_rate': -5}) for k in cvd.immunity_axes} } imports = cv.import_strain(strain=imported_strain, days=1, n_imports=30) @@ -282,14 +283,14 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() # Run simplest possible test - if 1: + if 0: sim = cv.Sim() sim.run() # Run more complex tests sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) +# sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) +# sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # TODO: the next test isn't working, need to check change_beta logic # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) From 8ca4cc38a226ed4223b48c5d9ec14bb9cd1155a4 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 2 Mar 2021 16:18:01 +0100 Subject: [PATCH 107/569] attempting to simplify indices --- covasim/interventions.py | 4 +--- covasim/parameters.py | 19 +++++++------------ covasim/sim.py | 15 ++++++--------- covasim/utils.py | 2 +- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 51f09cc1a..295b8f9fe 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1218,9 +1218,7 @@ def apply(self, sim): sim['n_strains'] += 1 # Update the immunity matrix -# sim['immunity'], sim['immune_degree'] = cvpar.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from, -# imm_pars_strain=self.imm_pars, sim_immune_degree=sim['immune_degree'], n_days=sim['n_days']) - sim['immunity'] = cvpar.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from, + sim['immunity'], sim['immune_degree'] = cvpar.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from, imm_pars_strain=self.imm_pars, sim_immune_degree=sim['immune_degree'], n_days=sim['n_days']) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) diff --git a/covasim/parameters.py b/covasim/parameters.py index 4b2bb0b8f..ea512584f 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -377,19 +377,14 @@ def update_immunity(prev_immunity=None, n_strains=None, immunity_from=None, immu # TODO: figure out how to adapt this next section for generic waning functions. # Initial thoughts: perhaps we call compute_immunity here, with compute_immunity # modified to calculate immunity for a generic series of timepoints rather than - # anything specific. But then we'll need to retain the array of (t, immunity) - # so we can do lookups for specific values of t. - -# immune_degree = sc.promotetolist(sim_immune_degree) -# immune_degree_new = {} -# for ax in cvd.immunity_axes: -# immune_degree_new[ax] = cvu.expo_decay(imm_pars_strain[ax]['pars']['half_life'], n_days) - -# immune_degree.append(immune_degree_new) - -# return immunity, immune_degree + # anything specific. + immune_degree = sc.promotetolist(sim_immune_degree) + immune_degree_new = {} + for ax in cvd.immunity_axes: + immune_degree_new[ax] = cvu.pre_compute_immunity(n_days, **imm_pars_strain[ax]) + immune_degree.append(immune_degree_new) - return immunity + return immunity, immune_degree #%% Store default strain information diff --git a/covasim/sim.py b/covasim/sim.py index 298a0ca65..294a23fac 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -527,14 +527,12 @@ def step(self): # Deal with strain parameters for key in strain_parkeys: strain_pars[key] = self[key][strain] - beta = cvd.default_float(self['beta'])*cvd.default_float(strain_pars['rel_beta']) + beta = cvd.default_float(self['beta'] * strain_pars['rel_beta']) asymp_factor = cvd.default_float(strain_pars['asymp_factor']) # Determine people with immunity from a past infection from this strain immune = people.recovered_strain == strain immune_inds = cvu.true(immune) # Whether people have some immunity to this strain from a prior infection with this strain - immune_time = cvd.default_int(t - date_rec[immune_inds]) # Time since recovery for people who were last infected by this strain - prior_symptoms = people.prior_symptoms[immune_inds] immunity_scale_factor = np.full(len(immune_inds), self['immunity']['sus'][strain,strain]) # Process cross-immunity parameters and indices, if relevant @@ -544,18 +542,17 @@ def step(self): cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain cross_immune_inds = cvu.true(cross_immune) # People with some immunity to this strain from a prior infection with another strain cross_immune_inds = np.setdiff1d(cross_immune_inds, immune_inds) # remove anyone who has own-immunity, that supercedes cross-immunity - cross_immune_time = cvd.default_int(t - date_rec[cross_immune_inds]) # Time since recovery for people who were last infected by the cross strain - cross_prior_symptoms = people.prior_symptoms[cross_immune_inds] cross_immunity = np.full(len(cross_immune_inds), self['immunity']['sus'][strain, cross_strain]) - immunity_scale_factor = np.concatenate((immunity_scale_factor, cross_immunity)) immune_inds = np.concatenate((immune_inds, cross_immune_inds)) - immune_time = np.concatenate((immune_time, cross_immune_time)) - prior_symptoms = np.concatenate((prior_symptoms, cross_prior_symptoms)) + immunity_scale_factor = np.concatenate((immunity_scale_factor, cross_immunity)) + + immune_time = cvd.default_int(t - date_rec[immune_inds]) # Time since recovery for people who were last infected by this strain + prior_symptoms = people.prior_symptoms[immune_inds] # Compute immunity to susceptibility immunity_factors = np.full(len(people), 0, dtype=cvd.default_float, order='F') if len(immune_inds): - immunity_factors[immune_inds] = self['immune_degree'][strain]['sus'][immune_time] * prior_symptoms * self.pars['immunity']['sus'][strain, strain] + immunity_factors[immune_inds] = self['immune_degree'][strain]['sus'][immune_time] * prior_symptoms * immunity_scale_factor # Define indices for this strain inf_by_this_strain = sc.dcp(inf) diff --git a/covasim/utils.py b/covasim/utils.py index d8500268b..46cc97c39 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -144,7 +144,7 @@ def pre_compute_immunity(length, form, pars): output = exp_decay(length, **pars) elif form == 'logistic_decay': - output = logistic_decay(**pars) + output = logistic_decay(length, **pars) else: choicestr = '\n'.join(choices) From 2bc8515d9c874314c68aaab1d819f8d825f53af9 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 2 Mar 2021 16:31:04 +0100 Subject: [PATCH 108/569] still stuck on indices --- covasim/people.py | 4 +--- covasim/sim.py | 8 +++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index 719335d92..ab28888d3 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -262,9 +262,7 @@ def check_recovery(self): # Before letting them recover, store information about the strain and symptoms they had self.recovered_strain[inds] = self.infectious_strain[inds] for strain in range(self.pars['n_strains']): - this_strain = self.recovered_strain[inds] == strain - this_strain_inds = cvu.true(this_strain) - this_strain_inds = inds[this_strain_inds] + this_strain_inds = cvu.itrue(self.recovered_strain[inds] == strain, inds) mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=this_strain_inds) severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=this_strain_inds) self.prior_symptoms[this_strain_inds] = self.pars['rel_imm'][strain]['asymptomatic'] # diff --git a/covasim/sim.py b/covasim/sim.py index 294a23fac..dcb091d3a 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -552,7 +552,13 @@ def step(self): # Compute immunity to susceptibility immunity_factors = np.full(len(people), 0, dtype=cvd.default_float, order='F') if len(immune_inds): - immunity_factors[immune_inds] = self['immune_degree'][strain]['sus'][immune_time] * prior_symptoms * immunity_scale_factor + try: + immunity_factors[immune_inds] = self['immune_degree'][strain]['sus'][immune_time] * prior_symptoms * immunity_scale_factor + except: + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() # Define indices for this strain inf_by_this_strain = sc.dcp(inf) From 08ca35c55cecc21c609002457640df483f68d681 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 2 Mar 2021 11:14:30 -0500 Subject: [PATCH 109/569] filtering out people who are actively infected from immune function --- covasim/sim.py | 4 ++++ tests/devtests/test_variants.py | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index dcb091d3a..0858f2176 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -521,6 +521,9 @@ def step(self): strain_pars = dict() ns = self['n_strains'] # Shorten number of strains + # Determine who is currently infected and cannot get another infection + inf_inds = cvu.false(sus) + # Iterate through n_strains to calculate infections for strain in range(ns): @@ -533,6 +536,7 @@ def step(self): # Determine people with immunity from a past infection from this strain immune = people.recovered_strain == strain immune_inds = cvu.true(immune) # Whether people have some immunity to this strain from a prior infection with this strain + immune_inds = np.setdiff1d(immune_inds, inf_inds) immunity_scale_factor = np.full(len(immune_inds), self['immunity']['sus'][strain,strain]) # Process cross-immunity parameters and indices, if relevant diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 5d6ad6b56..b6c92f643 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -283,9 +283,9 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() # Run simplest possible test - if 0: - sim = cv.Sim() - sim.run() + # if 0: + # sim = cv.Sim() + # sim.run() # Run more complex tests sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) From b38cb2ba38c97ade54b2c4630af4522855938d35 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 2 Mar 2021 12:29:41 -0500 Subject: [PATCH 110/569] fixed bug that was leading to negative recovery times --- covasim/people.py | 6 +++--- covasim/sim.py | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index ab28888d3..3c1ea2177 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -407,12 +407,12 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str # determine people with immunity from this strain and then calculate immunity to trans/prog date_rec = self.date_recovered - immune = self.recovered_strain == strain - immune_inds = cvu.true(immune) # Whether people have some immunity to this strain from a prior infection with this strain + immune = self.recovered_strain[inds] == strain + immune_inds = cvu.itrue(immune, inds) # Whether people have some immunity to this strain from a prior infection with this strain immune_time = cvd.default_int(self.t - date_rec[immune_inds]) # Time since recovery for people who were last infected by this strain prior_symptoms = self.prior_symptoms[immune_inds] - if len(immune_inds): + if len(immune_inds)>0: self.trans_immunity_factors[strain, immune_inds] = self['pars']['immune_degree'][strain]['trans'][immune_time] * \ prior_symptoms * self.pars['immunity']['trans'][strain] self.prog_immunity_factors[strain, immune_inds] = self['pars']['immune_degree'][strain]['prog'][immune_time] * \ diff --git a/covasim/sim.py b/covasim/sim.py index 0858f2176..2312ba437 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -521,12 +521,12 @@ def step(self): strain_pars = dict() ns = self['n_strains'] # Shorten number of strains - # Determine who is currently infected and cannot get another infection - inf_inds = cvu.false(sus) - # Iterate through n_strains to calculate infections for strain in range(ns): + # Determine who is currently infected and cannot get another infection + inf_inds = cvu.false(sus) + # Deal with strain parameters for key in strain_parkeys: strain_pars[key] = self[key][strain] @@ -545,6 +545,7 @@ def step(self): if cross_strain != strain: cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain cross_immune_inds = cvu.true(cross_immune) # People with some immunity to this strain from a prior infection with another strain + cross_immune_inds = np.setdiff1d(cross_immune_inds, inf_inds) # remove anyone who is currently exposed cross_immune_inds = np.setdiff1d(cross_immune_inds, immune_inds) # remove anyone who has own-immunity, that supercedes cross-immunity cross_immunity = np.full(len(cross_immune_inds), self['immunity']['sus'][strain, cross_strain]) immune_inds = np.concatenate((immune_inds, cross_immune_inds)) From c2e6b6ae67ebb80eccde0238e5b876dc198da0aa Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 2 Mar 2021 13:36:57 -0500 Subject: [PATCH 111/569] changing strain-specific attributes of person to be default 1 strain and then appended if new strain is imported --- covasim/interventions.py | 5 +++++ covasim/people.py | 7 ++++--- covasim/utils.py | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 295b8f9fe..318d616c6 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -13,6 +13,7 @@ from . import defaults as cvd from . import base as cvb from . import parameters as cvpar +from . import people as cvppl from collections import defaultdict @@ -1220,6 +1221,10 @@ def apply(self, sim): # Update the immunity matrix sim['immunity'], sim['immune_degree'] = cvpar.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from, imm_pars_strain=self.imm_pars, sim_immune_degree=sim['immune_degree'], n_days=sim['n_days']) + + # Update strain-specific people attributes + + cvu.update_strain_attributes(sim.people) importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) diff --git a/covasim/people.py b/covasim/people.py index 3c1ea2177..9e119732c 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -59,7 +59,7 @@ def __init__(self, pars, strict=True, **kwargs): if key == 'uid': self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. - self[key] = np.full((self.pars['max_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') + self[key] = np.full((self.pars['n_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') elif key == 'vaccinations': self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) else: @@ -75,7 +75,7 @@ def __init__(self, pars, strict=True, **kwargs): # Set strain states, which store info about which strain a person is exposed to for key in self.meta.strain_states: if 'by' in key: - self[key] = np.full((self.pars['max_strains'], self.pop_size), False, dtype=bool, order='F') + self[key] = np.full((self.pars['n_strains'], self.pop_size), False, dtype=bool, order='F') else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -91,7 +91,7 @@ def __init__(self, pars, strict=True, **kwargs): self.flows = {key:0 for key in cvd.new_result_flows} for key in cvd.new_result_flows: if 'by_strain' in key: - self.flows[key] = np.full(self.pars['max_strains'], 0, dtype=cvd.default_float) + self.flows[key] = np.full(self.pars['n_strains'], 0, dtype=cvd.default_float) # Although we have called init(), we still need to call initialize() @@ -679,3 +679,4 @@ def label_lkey(lkey): else: print(f'Nothing happened to {uid} during the simulation.') return + diff --git a/covasim/utils.py b/covasim/utils.py index 46cc97c39..7963db1eb 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -115,6 +115,22 @@ def find_contacts(p1, p2, inds): # pragma: no cover return pairing_partners +def update_strain_attributes(people): + for key in people.meta.person: + if key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. + people[key] = np.append(people[key], np.full((1, people.pop_size), 0, dtype=cvd.default_float, order='F'), axis=0) + + # Set strain states, which store info about which strain a person is exposed to + for key in people.meta.strain_states: + if 'by' in key: + people[key] = np.append(people[key], np.full((1, people.pop_size), False, dtype=bool, order='F'), axis=0) + + for key in cvd.new_result_flows: + if 'by_strain' in key: + people.flows[key] = np.append(people.flows[key], np.full(1, 0, dtype=cvd.default_float), axis=0) + return + + #%% Immunity methods __all__ += ['pre_compute_immunity'] From d19be68fe8238572a6772df9a81c01312628c08b Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 2 Mar 2021 13:58:48 -0500 Subject: [PATCH 112/569] fixed updating immune degree twice in intervention --- covasim/interventions.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 318d616c6..7a312a1f1 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1199,16 +1199,17 @@ def apply(self, sim): # Update strain info for strain_key in cvd.strain_pars: - if hasattr(self, strain_key): - newval = getattr(self, strain_key) - if strain_key=='dur': # Validate durations (make sure there are values for all durations) - newval = sc.mergenested(sim[strain_key][0], newval) - sim[strain_key].append(newval) - else: - # use default - print(f'{strain_key} not provided for this strain, using default value') + if strain_key != 'immune_degree': + if hasattr(self, strain_key): + newval = getattr(self, strain_key) + if strain_key == 'dur': # Validate durations (make sure there are values for all durations) + newval = sc.mergenested(sim[strain_key][0], newval) + sim[strain_key].append(newval) + else: + # use default + print(f'{strain_key} not provided for this strain, using default value') - sim[strain_key].append(sim[strain_key][0]) + sim[strain_key].append(sim[strain_key][0]) # Create defaults for cross-immunity if not provided if self.immunity_to is None: From 65212cfad964a38d7caee9b43d421c1ace09c51b Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 3 Mar 2021 11:39:09 +0100 Subject: [PATCH 113/569] bid refactor --- covasim/__init__.py | 1 + covasim/base.py | 2 +- covasim/interventions.py | 93 ------------------------- covasim/parameters.py | 117 ++------------------------------ covasim/people.py | 2 +- covasim/sim.py | 60 +++++++++++----- covasim/utils.py | 108 ----------------------------- covasim/version.py | 4 +- tests/devtests/test_variants.py | 13 ++-- 9 files changed, 61 insertions(+), 339 deletions(-) diff --git a/covasim/__init__.py b/covasim/__init__.py index 23efc68d0..b098c9a07 100644 --- a/covasim/__init__.py +++ b/covasim/__init__.py @@ -17,6 +17,7 @@ from .people import * # Depends on utils, defaults, base, plotting from .population import * # Depends on people et al. from .interventions import * # Depends on defaults, utils, base +from .immunity import * # Depends on utils, parameters, defaults from .analysis import * # Depends on utils, misc, interventions from .sim import * # Depends on almost everything from .run import * # Depends on sim diff --git a/covasim/base.py b/covasim/base.py index a5138ea21..a1d7ca647 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -310,7 +310,7 @@ def update_pars(self, pars=None, create=False, defaults=None, **kwargs): pars = cvpar.listify_strain_pars(pars) # Strain pars need to be lists pars = cvpar.update_sub_key_pars(pars, defaults) # Update dict parameters by sub-key combined_pars = sc.mergedicts(defaults, pars) # Now that subkeys have been updated, can merge the dicts together - pars['immune_degree'] = [cvpar.initialize_immune_degree(n_days=combined_pars['n_days'], imm_pars=combined_pars['imm_pars'][s]) for s in range(combined_pars['n_strains'])] # Precompute immunity waning +# pars['immune_degree'] = [cvpar.initialize_immune_degree(n_days=combined_pars['n_days'], imm_pars=combined_pars['imm_pars'][s]) for s in range(combined_pars['n_strains'])] # Precompute immunity waning super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/interventions.py b/covasim/interventions.py index 7a312a1f1..a79a7bbeb 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1137,96 +1137,3 @@ def apply(self, sim): return - -#%% Multi-strain interventions - -__all__+= ['import_strain'] - -class import_strain(Intervention): - ''' - Introduce a new variant to the population through an importation at a given time point. - - Args: - days (int): day on which new variant is introduced. - n_imports (int): the number of imports of strain - beta (float): per contact transmission of strain - init_immunity (floats): initial immunity against strain once recovered; 1 = perfect, 0 = no immunity - half_life (dicts): determines decay rate of immunity against strain broken down by severity; If half_life is None immunity is constant - immunity_to (list of floats): cross immunity to existing strains in model - immunity_from (list of floats): cross immunity from existing strains in model - kwargs (dict): passed to Intervention() - - **Examples**:: - - interv = cv.import_strain(days=50, beta=0.03) # On day 50, import new strain (one case) - interv = cv.import_strain(days=[10, 20], n_imports=[5, 10], beta=0.03, init_immunity=1, - half_life=180, immunity_to=[0, 0], immunity_from=[0, 0]) # On day 10, import 5 cases of new strain, on day 20, import 10 cases - ''' - - def __init__(self, strain=None, days=None, n_imports=1, immunity_to=None, immunity_from=None, **kwargs): - # Do standard initialization - super().__init__(**kwargs) # Initialize the Intervention object - self._store_args() # Store the input arguments so the intervention can be recreated - - # Handle inputs - self.days = days - self.n_imports = n_imports - self.immunity_to = immunity_to - self.immunity_from = immunity_from - self.strain = {par: val for par, val in strain.items()} - for par, val in self.strain.items(): - setattr(self, par, val) - return - - def initialize(self, sim): - self.days = process_days(sim, self.days) - if not hasattr(self, 'imm_pars'): - self.imm_pars = sim['imm_pars'][0] - self.initialized = True - - def apply(self, sim): - - if sim.t == self.days: # Time to introduce strain - - # Check number of strains - prev_strains = sim['n_strains'] - - # Validate immunity pars (make sure there are values for all cvd.immunity_axes - for key in cvd.immunity_axes: - if key not in self.imm_pars: - print(f'Immunity pars for imported strain for {key} not provided, using default value') - self.imm_pars[key] = sim['imm_pars'][0][key] - - # Update strain info - for strain_key in cvd.strain_pars: - if strain_key != 'immune_degree': - if hasattr(self, strain_key): - newval = getattr(self, strain_key) - if strain_key == 'dur': # Validate durations (make sure there are values for all durations) - newval = sc.mergenested(sim[strain_key][0], newval) - sim[strain_key].append(newval) - else: - # use default - print(f'{strain_key} not provided for this strain, using default value') - - sim[strain_key].append(sim[strain_key][0]) - - # Create defaults for cross-immunity if not provided - if self.immunity_to is None: - self.immunity_to = [sim['cross_immunity']]*sim['n_strains'] - if self.immunity_from is None: - self.immunity_from = [sim['cross_immunity']]*sim['n_strains'] - - sim['n_strains'] += 1 - - # Update the immunity matrix - sim['immunity'], sim['immune_degree'] = cvpar.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from, - imm_pars_strain=self.imm_pars, sim_immune_degree=sim['immune_degree'], n_days=sim['n_days']) - - # Update strain-specific people attributes - - cvu.update_strain_attributes(sim.people) - importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely - sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) - - return diff --git a/covasim/parameters.py b/covasim/parameters.py index ea512584f..65878ce57 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -9,7 +9,7 @@ from . import defaults as cvd from . import utils as cvu -__all__ = ['make_pars', 'reset_layer_pars', 'get_prognoses', 'make_strain'] +__all__ = ['make_pars', 'reset_layer_pars', 'get_prognoses'] def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): @@ -62,9 +62,10 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Parameters that control settings and defaults for multi-strain runs pars['n_strains'] = 1 # The number of strains currently circulating in the population - pars['max_strains'] = 30 # For allocating memory with numpy arrays + pars['total_strains'] = None # Set during sim initialization, once strains have been specified and processed pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by initialize_immunity() below + pars['immune_degree'] = None # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 @@ -106,6 +107,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Events and interventions pars['interventions'] = [] # The interventions present in this simulation; populated by the user pars['analyzers'] = [] # Custom analysis functions; populated by the user + pars['strains'] = [] # Additional strains of the virus; populated by the user, see immunity.py pars['timelimit'] = None # Time limit for the simulation (seconds) pars['stopping_func'] = None # A function to call to stop the sim partway through @@ -120,8 +122,8 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): reset_layer_pars(pars) if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses - pars['immunity'] = initialize_immunity() # Initialize immunity - pars['immune_degree'] = initialize_immune_degree(n_days=pars['n_days'], imm_pars=pars['imm_pars']) +# pars['immunity'] = initialize_immunity() # Initialize immunity +# pars['immune_degree'] = initialize_immune_degree(n_days=pars['n_days'], imm_pars=pars['imm_pars']) pars = listify_strain_pars(pars) # Turn strain parameters into lists # If version is specified, load old parameters @@ -319,110 +321,3 @@ def listify_strain_pars(pars): return pars -def initialize_immunity(n_strains=None): - ''' - Initialize the immunity matrices with default values - Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] and is initialized with default values - Progression is a matrix of scalars of size sim['n_strains'] initialized with default values - Transmission is a matrix of scalars of size sim['n_strains'] initialized with default values - ''' - - # Initialize - if n_strains is None: n_strains = 1 - immunity = {} - for ax in cvd.immunity_axes: - if ax == 'sus': - immunity[ax] = np.full((n_strains, n_strains), 1, dtype=cvd.default_float) - else: - immunity[ax] = np.full(n_strains, 1, dtype=cvd.default_float) - return immunity - - -def initialize_immune_degree(n_days=None, imm_pars=None): - ''' Precompute immunity waning ''' - immune_degree = {} - for ax in cvd.immunity_axes: - immune_degree[ax] = cvu.pre_compute_immunity(n_days, **imm_pars[ax]) - return immune_degree - - -def update_immunity(prev_immunity=None, n_strains=None, immunity_from=None, immunity_to=None, - imm_pars_strain=None, sim_immune_degree=None, n_days=None): - ''' - Helper function to update the immunity matrices when a new strain is added. - (called by import_strain intervention) - ''' - # Add off-diagonals - immunity_from = sc.promotetolist(immunity_from) - immunity_to = sc.promotetolist(immunity_to) - - immunity = initialize_immunity(n_strains=n_strains) - update_strain = n_strains-1 - for ax in cvd.immunity_axes: - if ax == 'sus': - immunity[ax][:update_strain, :update_strain] = prev_immunity[ax] - else: - immunity[ax][:update_strain] = prev_immunity[ax] - - # create the immunity[update_strain,] and immunity[,update_strain] arrays - new_immunity_row = np.full(n_strains, 1, dtype=cvd.default_float) - new_immunity_column = np.full(n_strains, 1, dtype=cvd.default_float) - for i in range(n_strains-1): - new_immunity_row[i] = immunity_from[i] - new_immunity_column[i] = immunity_to[i] - - immunity['sus'][update_strain, :] = new_immunity_row - immunity['sus'][:, update_strain] = new_immunity_column - - # TODO: figure out how to adapt this next section for generic waning functions. - # Initial thoughts: perhaps we call compute_immunity here, with compute_immunity - # modified to calculate immunity for a generic series of timepoints rather than - # anything specific. - immune_degree = sc.promotetolist(sim_immune_degree) - immune_degree_new = {} - for ax in cvd.immunity_axes: - immune_degree_new[ax] = cvu.pre_compute_immunity(n_days, **imm_pars_strain[ax]) - immune_degree.append(immune_degree_new) - - return immunity, immune_degree - - -#%% Store default strain information - -def make_strain(strain=None): - ''' Populates a par dict with known information about circulating strains''' - - choices = { - 'b117': ['b117', 'B117', 'B.1.1.7', 'UK', 'uk', 'UK variant', 'uk variant'], - 'b1351': ['b1351', 'B1351', 'B.1.351', 'SA', 'sa', 'SA variant', 'sa variant'], # TODO: add other aliases - 'p1': ['p1', 'P1', 'P.1', 'Brazil', 'Brazil variant', 'brazil variant'], - } - - # Known parameters on B117 - if strain in choices['b117']: - pars = dict() - pars['rel_beta'] = 1.5 # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 - pars['rel_severe_prob'] = 1.6 # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf - - # Known parameters on South African variant - elif strain in choices['b1351']: - pars = dict() - pars['imm_pars'] = dict() - for ax in cvd.immunity_axes: - pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) - - # Known parameters on Brazil variant - elif strain in choices['p1']: - pars = dict() - pars['imm_pars'] = dict() - for ax in cvd.immunity_axes: - pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) - - - else: - choicestr = '\n'.join(choices.values()) - errormsg = f'The selected variant "{strain}" is not implemented; choices are: {choicestr}' - raise NotImplementedError(errormsg) - - return pars - diff --git a/covasim/people.py b/covasim/people.py index 9e119732c..8dc5368fa 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -165,7 +165,7 @@ def update_states_pre(self, t): self.flows = {key:0 for key in cvd.new_result_flows} for key in cvd.new_result_flows: if 'by_strain' in key: - self.flows[key] += np.full(self.pars['max_strains'], 0, dtype=cvd.default_float) + self.flows[key] += np.full(self.pars['total_strains'], 0, dtype=cvd.default_float) self.flows['new_infectious'] += self.check_infectious() # For people who are exposed and not infectious, check if they begin being infectious self.flows['new_symptomatic'] += self.check_symptomatic() self.flows['new_severe'] += self.check_severe() diff --git a/covasim/sim.py b/covasim/sim.py index 2312ba437..37ec330a9 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -14,6 +14,7 @@ from . import population as cvpop from . import plotting as cvplt from . import interventions as cvi +from . import immunity as cvimm from . import analysis as cva # Almost everything in this file is contained in the Sim class @@ -107,11 +108,13 @@ def initialize(self, reset=False, **kwargs): self.t = 0 # The current time index self.validate_pars() # Ensure parameters have valid values self.set_seed() # Reset the random seed before the population is created - self.init_results() # Create the results structure self.init_people(save_pop=self.save_pop, load_pop=self.load_pop, popfile=self.popfile, reset=reset, **kwargs) # Create all the people (slow) self.validate_layer_pars() # Once the population is initialized, validate the layer parameters again - self.init_interventions() # Initialize the interventions - self.init_analyzers() # ...and the interventions + self.init_interventions() # Initialize the interventions... + self.init_analyzers() # ...and the analyzers... + self.init_strains() # ...and the strains.... + self.init_immunity() # ... and information about immunity/cross-immunity. + self.init_results() # Create the results structure - do this after initializing strains self.set_seed() # Reset the random seed again so the random number stream is consistent self.initialized = True self.complete = False @@ -234,12 +237,13 @@ def validate_pars(self, validate_layers=True): errormsg = f'Population type "{choice}" not available; choices are: {choicestr}' raise ValueError(errormsg) - # Handle interventions and analyzers + # Handle interventions, analyzers, and strains self['interventions'] = sc.promotetolist(self['interventions'], keepnone=False) for i,interv in enumerate(self['interventions']): if isinstance(interv, dict): # It's a dictionary representation of an intervention self['interventions'][i] = cvi.InterventionDict(**interv) self['analyzers'] = sc.promotetolist(self['analyzers'], keepnone=False) + self['strains'] = sc.promotetolist(self['strains'], keepnone=False) # Optionally handle layer parameters if validate_layers: @@ -274,21 +278,21 @@ def init_res(*args, **kwargs): # Flows and cumulative flows for key,label in cvd.result_flows.items(): - self.results[f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key], max_strains=self['max_strains']) # Cumulative variables -- e.g. "Cumulative infections" + self.results[f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key], max_strains=self['total_strains']) # Cumulative variables -- e.g. "Cumulative infections" for key,label in cvd.result_flows.items(): # Repeat to keep all the cumulative keys together - self.results[f'new_{key}'] = init_res(f'Number of new {label}', color=dcols[key], max_strains=self['max_strains']) # Flow variables -- e.g. "Number of new infections" + self.results[f'new_{key}'] = init_res(f'Number of new {label}', color=dcols[key], max_strains=self['total_strains']) # Flow variables -- e.g. "Number of new infections" # Stock variables for key,label in cvd.result_stocks.items(): - self.results[f'n_{key}'] = init_res(label, color=dcols[key], max_strains=self['max_strains']) + self.results[f'n_{key}'] = init_res(label, color=dcols[key], max_strains=self['total_strains']) # Other variables self.results['n_alive'] = init_res('Number of people alive', scale=False) self.results['prevalence'] = init_res('Prevalence', scale=False) - self.results['prevalence_by_strain'] = init_res('Prevalence by strain', scale=False, max_strains=self['max_strains']) + self.results['prevalence_by_strain'] = init_res('Prevalence by strain', scale=False, max_strains=self['total_strains']) self.results['incidence'] = init_res('Incidence', scale=False) - self.results['incidence_by_strain'] = init_res('Incidence by strain', scale=False, max_strains=self['max_strains']) + self.results['incidence_by_strain'] = init_res('Incidence by strain', scale=False, max_strains=self['total_strains']) self.results['r_eff'] = init_res('Effective reproduction number', scale=False) self.results['doubling_time'] = init_res('Doubling time', scale=False) self.results['test_yield'] = init_res('Testing yield', scale=False) @@ -436,6 +440,25 @@ def init_analyzers(self): analyzer.initialize(self) return + def init_strains(self): + ''' Initialize the strains ''' + if self._orig_pars and 'strains' in self._orig_pars: + self['strains'] = self._orig_pars.pop('strains') # Restore + + for strain in self['strains']: + if isinstance(strain, cvimm.Strain): + strain.initialize(self) + + # Calculate the total number of strains that will be active at some point in the sim + self['total_strains'] = self['n_strains'] + len(self['strains']) + return + + + def init_immunity(self): + ''' Initialize immunity matrices and precompute immunity waning for each strain ''' + cvimm.init_immunity(self) + return + def rescale(self): ''' Dynamically rescale the population -- used during step() ''' @@ -488,6 +511,11 @@ def step(self): people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation', strain=strain) + # Add strains + for strain in self['strains']: + if isinstance(strain, cvimm.Strain): + strain.apply(self) + # Apply interventions for intervention in self['interventions']: if isinstance(intervention, cvi.Intervention): @@ -539,6 +567,12 @@ def step(self): immune_inds = np.setdiff1d(immune_inds, inf_inds) immunity_scale_factor = np.full(len(immune_inds), self['immunity']['sus'][strain,strain]) + if len(immune_inds): + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() + # Process cross-immunity parameters and indices, if relevant if ns > 1: for cross_strain in range(ns): @@ -557,13 +591,7 @@ def step(self): # Compute immunity to susceptibility immunity_factors = np.full(len(people), 0, dtype=cvd.default_float, order='F') if len(immune_inds): - try: - immunity_factors[immune_inds] = self['immune_degree'][strain]['sus'][immune_time] * prior_symptoms * immunity_scale_factor - except: - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() + immunity_factors[immune_inds] = self['immune_degree'][strain]['sus'][immune_time] * prior_symptoms * immunity_scale_factor # Define indices for this strain inf_by_this_strain = sc.dcp(inf) diff --git a/covasim/utils.py b/covasim/utils.py index 7963db1eb..b7c373d1d 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -131,114 +131,6 @@ def update_strain_attributes(people): return -#%% Immunity methods -__all__ += ['pre_compute_immunity'] - -def pre_compute_immunity(length, form, pars): - ''' - Process immunity pars and functional form into a value - - 'exp_decay' : exponential decay (TODO fill in details) - - 'logistic_decay' : logistic decay (TODO fill in details) - - 'linear' : linear decay (TODO fill in details) - - others TBC! - - Args: - form (str): the functional form to use - pars (dict): passed to individual immunity functions - length (float): length of time to compute immunity - ''' - - choices = [ - 'exp_decay', - 'logistic_decay', - 'linear', - ] - - # Process inputs - if form == 'exp_decay': - if pars['half_life'] is None: pars['half_life'] = np.nan - output = exp_decay(length, **pars) - - elif form == 'logistic_decay': - output = logistic_decay(length, **pars) - - else: - choicestr = '\n'.join(choices) - errormsg = f'The selected functional form "{form}" is not implemented; choices are: {choicestr}' - raise NotImplementedError(errormsg) - - return output - - -def exp_decay(length, init_val, half_life): - ''' - Returns an array of length t with values for the immunity at each time step after recovery - ''' - decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. - t = np.arange(length, dtype=cvd.default_int) - return init_val * np.exp(-decay_rate * t) - - -def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp): - ''' Calculate logistic decay ''' - t = np.arange(length, dtype=cvd.default_int) - return (init_val + (lower_asymp-init_val) / (1 + (t/half_val) ** decay_rate)) # TODO: make this robust to /0 errors - - - -####### PREVIOUS VERSIONS -# def compute_immunity(immunity_factors, immune_time, immune_inds, prior_symptoms, scale_factor, form, pars): -# ''' -# Process immunity pars and functional form into a value -# -# - 'exp_decay' : exponential decay with initial value par1 and half-life=par2 -# - 'linear' : linear decay with initial value par1 and per-day decay = par2 -# - others TBC! -# -# Args: -# scale_factor: reduction in immunity due to cross-immunity, if relevant (1 otherwise) (this is from immunity matrix) -# prior_symptoms: reduction in immunity based on severity of prior symptoms (by default asymptomatic = 0.7, mild = 0.9, severe = 1.) -# form (str): the functional form to use -# kwargs (dict): passed to individual immunity functions -# ''' -# -# choices = [ -# 'exp_decay', -# 'logistic_decay', -# 'linear', -# ] -# -# # Process inputs -# if form == 'exp_decay': -# if pars['half_life'] is None: pars['half_life'] = np.nan -# output = exp_decay(immunity_factors, immune_time, immune_inds, prior_symptoms, scale_factor, **pars) -# -# elif form == 'logistic_decay': -# output = logistic_decay(immunity_factors, immune_time, immune_inds, prior_symptoms, scale_factor, **pars) -# -# else: -# choicestr = '\n'.join(choices) -# errormsg = f'The selected functional form "{form}" is not implemented; choices are: {choicestr}' -# raise NotImplementedError(errormsg) -# -# return output -# -# -# #@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat, nbfloat), cache=True, parallel=parallel) -# def exp_decay(y, t, inds, prior_symptoms, scale_factor, init_val, half_life): # pragma: no cover -# ''' Calculate exponential decay ''' -# decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. -# y[inds] = scale_factor * prior_symptoms * init_val * np.exp(-decay_rate * t) -# y[inds] = scale_factor * prior_symptoms * init_val * np.exp(-decay_rate * t) -# return y -# -# #@nb.njit( (nbfloat[:], nbfloat[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat, nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) -# def logistic_decay(y, t, inds, prior_symptoms, scale_factor, init_val, decay_rate, half_val, lower_asymp): # pragma: no cover -# ''' Calculate logistic decay ''' -# y[inds] = scale_factor * prior_symptoms * (init_val + (lower_asymp-init_val) / (1 + (t/half_val) ** decay_rate)) -# return y -# - #%% Sampling and seed methods diff --git a/covasim/version.py b/covasim/version.py index d1febf74f..1e11a1e8e 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,6 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '2.0.2' -__versiondate__ = '2020-02-01' +__version__ = '3.0' +__versiondate__ = '2020-03-01' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index b6c92f643..4298841ec 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -118,14 +118,13 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): 'Strain 2: 1.5x more transmissible' ] - imported_strain = { + strain_pars = { 'rel_beta': 1.5, # 'imm_pars': {k: dict(form='exp_decay', pars={'init_val':1., 'half_life':100}) for k in cvd.immunity_axes} 'imm_pars': {k: dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 10, 'lower_asymp': 0.1, 'decay_rate': -5}) for k in cvd.immunity_axes} } - - imports = cv.import_strain(strain=imported_strain, days=1, n_imports=30) - sim = cv.Sim(interventions=imports, label='With imported infections') + strain = cv.Strain(strain_pars, days=1) + sim = cv.Sim(strains=strain) sim.run() if do_plot: @@ -283,9 +282,9 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() # Run simplest possible test - # if 0: - # sim = cv.Sim() - # sim.run() + if 0: + sim = cv.Sim() + sim.run() # Run more complex tests sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) From 46c407009917fbb6160e7c7861d71fbc8d7eebcc Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 3 Mar 2021 11:39:28 +0100 Subject: [PATCH 114/569] add immunity file --- covasim/immunity.py | 267 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 covasim/immunity.py diff --git a/covasim/immunity.py b/covasim/immunity.py new file mode 100644 index 000000000..3540c470b --- /dev/null +++ b/covasim/immunity.py @@ -0,0 +1,267 @@ +''' +Defines classes and methods for calculating immunity +''' + +import numpy as np +import pylab as pl +import sciris as sc +import datetime as dt +from . import utils as cvu +from . import defaults as cvd +from . import base as cvb +from . import parameters as cvpar +from . import people as cvppl +from collections import defaultdict + +__all__ = [] + + +#%% Define strain class + +__all__+= ['Strain'] + +class Strain(): + ''' + Add a new strain to the sim + + Args: + day (int): day on which new variant is introduced. + n_imports (int): the number of imports of the strain to be added + strain (dict): dictionary of parameters specifying information about the strain + immunity_to (list of floats): cross immunity to existing strains in model + immunity_from (list of floats): cross immunity from existing strains in model + kwargs (dict): passed to Intervention() + + **Example**:: + b117 = cv.Strain('b117', days=10) # Make strain B117 active from day 10 + p1 = cv.Strain('p1', days=15) # Make strain P1 active from day 15 + my_var = cv.Strain(strain={'rel_beta': 2.5}, strain_label='My strain', days=20) # Make a custom strain active from day 20 + sim = cv.Sim(strains=[b117, p1, my_var]) # Add them all to the sim + ''' + + def __init__(self, strain=None, strain_label=None, days=None, n_imports=1, **kwargs): + + # Handle inputs + self.days = days + self.n_imports = n_imports + + # Strains can be defined in different ways: process these here + self.strain_pars = self.parse_strain_pars(strain=strain, strain_label=strain_label) + for par, val in self.strain_pars.items(): + setattr(self, par, val) + return + + + def parse_strain_pars(self, strain=None, strain_label=None): + ''' Unpack strain information, which may be given in different ways''' + + # Option 1: strains can be chosen from a list of pre-defined strains + if isinstance(strain, str): + + # List of choices currently available: new ones can be added to the list along with their aliases + choices = { + 'default': ['default', 'wild', 'pre-existing'], + 'b117': ['b117', 'B117', 'B.1.1.7', 'UK', 'uk', 'UK variant', 'uk variant'], + 'b1351': ['b1351', 'B1351', 'B.1.351', 'SA', 'sa', 'SA variant', 'sa variant'], # TODO: add other aliases + 'p1': ['p1', 'P1', 'P.1', 'Brazil', 'Brazil variant', 'brazil variant'], + } + + # Empty pardict for wild strain + if strain in choices['default']: + strain_pars = dict() + self.strain_label = strain + + # Known parameters on B117 + elif strain in choices['b117']: + strain_pars = dict() + strain_pars['rel_beta'] = 1.5 # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 + strain_pars['rel_severe_prob'] = 1.6 # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf + self.strain_label = strain + + # Known parameters on South African variant + elif strain in choices['b1351']: + strain_pars = dict() + strain_pars['imm_pars'] = dict() + for ax in cvd.immunity_axes: + strain_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1.,'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + self.strain_label = strain + + # Known parameters on Brazil variant + elif strain in choices['p1']: + strain_pars = dict() + strain_pars['imm_pars'] = dict() + for ax in cvd.immunity_axes: + strain_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1.,'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + self.strain_label = strain + + else: + choicestr = '\n'.join(choices.values()) + errormsg = f'The selected variant "{strain}" is not implemented; choices are: {choicestr}' + raise NotImplementedError(errormsg) + + # Option 2: strains can be specified as a dict of pars + elif isinstance(strain, dict): + strain_pars = strain + self.strain_label = strain_label + + else: + errormsg = f'Could not understand {type(strain)}, please specify as a string indexing a predefined strain or a dict.' + raise ValueError(errormsg) + + return strain_pars + + + def initialize(self, sim): + if not hasattr(self, 'imm_pars'): + self.imm_pars = sim['imm_pars'][0] + + # Validate immunity pars (make sure there are values for all cvd.immunity_axes) + for key in cvd.immunity_axes: + if key not in self.imm_pars: + print(f'Immunity pars for imported strain for {key} not provided, using default value') + self.imm_pars[key] = sim['imm_pars'][0][key] + + # Update strain info + for strain_key in cvd.strain_pars: + if strain_key != 'immune_degree': + if hasattr(self, strain_key): + newval = getattr(self, strain_key) + if strain_key == 'dur': # Validate durations (make sure there are values for all durations) + newval = sc.mergenested(sim[strain_key][0], newval) + sim[strain_key].append(newval) + else: + # use default + print(f'{strain_key} not provided for this strain, using default value') + sim[strain_key].append(sim[strain_key][0]) + + self.initialized = True + + + def apply(self, sim): + + if sim.t == self.days: # Time to introduce strain + + # Check number of strains + prev_strains = sim['n_strains'] + sim['n_strains'] += 1 + + # Update strain-specific people attributes + cvu.update_strain_attributes(sim.people) + importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely + sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) + + return + + +# if self.immunity_to is None: +# self.immunity_to = [sim['cross_immunity']]*sim['n_strains'] +# if self.immunity_from is None: +# self.immunity_from = [sim['cross_immunity']]*sim['n_strains'] + + # Update the immunity matrix +# sim['immunity'], sim['immune_degree'] = self.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from, +# imm_pars_strain=self.imm_pars, sim_immune_degree=sim['immune_degree'], n_days=sim['n_days']) + + + + +#%% Immunity methods +__all__ += ['init_immunity', 'pre_compute_waning'] + +def init_immunity(sim): + ''' Initialize immunity matrices with all strains that will eventually be in the sim''' + ns = sim['n_strains'] + ts = sim['total_strains'] + immunity = {} + + # If immunity values have been provided, process them + if sim['immunity'] is not None: + if sc.checktype(sim['immunity']['sus'], 'arraylike'): + correct_size = sim['immunity']['sus'].shape == (sim['total_strains'], sim['total_strains']) + if not correct_size: + errormsg = f'Wrong dimensions for immunity["sus"]: you provided a matrix sized {sim["immunity"]["sus"].shape}, but it should be sized {(sim["total_strains"], sim["total_strains"])}' + raise ValueError(errormsg) + elif sc.checktype(sim['immunity']['sus'], dict): + # TODO: make it possible to specify this as something like: + # imm = {'b117': {'wild': 0.4, 'p1': 0.3}, + # 'wild': {'b117': 0.6, 'p1': 0.7}, + # 'p1': {'wild': 0.9, 'b117': 0.65}} + # per Dan's suggestion, by using [s.strain_label for s in sim['strains']]. + # Would need lots of validation!! + raise NotImplementedError + else: + errormsg = f'Type of immunity["sus"] not understood: you provided {type(sim["immunity"]["sus"])}, but it should be an array or dict.' + raise ValueError(errormsg) + + else: + # Initialize immunity + for ax in cvd.immunity_axes: + if ax == 'sus': # Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] + immunity[ax] = np.full((ts, ts), sim['cross_immunity'], dtype=cvd.default_float) # Default for off-diagnonals + np.fill_diagonal(immunity[ax], 1) # Default for own-immunity + else: # Progression and transmission are matrices of scalars of size sim['n_strains'] + immunity[ax] = np.full(ts, 1, dtype=cvd.default_float) + sim['immunity'] = immunity + + # Precompute waning + immune_degree = [] # Stored as a list by strain + for s in range(ts): + strain_immune_degree = {} + for ax in cvd.immunity_axes: + strain_immune_degree[ax] = pre_compute_waning(sim['n_days'], **sim['imm_pars'][s][ax]) + immune_degree.append(strain_immune_degree) + sim['immune_degree'] = immune_degree + + +def pre_compute_waning(length, form, pars): + ''' + Process immunity pars and functional form into a value + - 'exp_decay' : exponential decay (TODO fill in details) + - 'logistic_decay' : logistic decay (TODO fill in details) + - 'linear' : linear decay (TODO fill in details) + - others TBC! + + Args: + form (str): the functional form to use + pars (dict): passed to individual immunity functions + length (float): length of time to compute immunity + ''' + + choices = [ + 'exp_decay', + 'logistic_decay', + 'linear', + ] + + # Process inputs + if form == 'exp_decay': + if pars['half_life'] is None: pars['half_life'] = np.nan + output = exp_decay(length, **pars) + + elif form == 'logistic_decay': + output = logistic_decay(length, **pars) + + else: + choicestr = '\n'.join(choices) + errormsg = f'The selected functional form "{form}" is not implemented; choices are: {choicestr}' + raise NotImplementedError(errormsg) + + return output + +# SPecific waning functions are listed here +def exp_decay(length, init_val, half_life): + ''' + Returns an array of length t with values for the immunity at each time step after recovery + ''' + decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. + t = np.arange(length, dtype=cvd.default_int) + return init_val * np.exp(-decay_rate * t) + + +def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp): + ''' Calculate logistic decay ''' + t = np.arange(length, dtype=cvd.default_int) + return (init_val + (lower_asymp-init_val) / (1 + (t/half_val) ** decay_rate)) # TODO: make this robust to /0 errors + + + From 0c3c724e4c008540be2d2de8a5c85ccac990506a Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 3 Mar 2021 11:44:38 +0100 Subject: [PATCH 115/569] clean up --- covasim/base.py | 1 - covasim/immunity.py | 16 ++-------------- covasim/parameters.py | 2 -- covasim/sim.py | 6 ------ 4 files changed, 2 insertions(+), 23 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index a1d7ca647..6eac7174e 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -310,7 +310,6 @@ def update_pars(self, pars=None, create=False, defaults=None, **kwargs): pars = cvpar.listify_strain_pars(pars) # Strain pars need to be lists pars = cvpar.update_sub_key_pars(pars, defaults) # Update dict parameters by sub-key combined_pars = sc.mergedicts(defaults, pars) # Now that subkeys have been updated, can merge the dicts together -# pars['immune_degree'] = [cvpar.initialize_immune_degree(n_days=combined_pars['n_days'], imm_pars=combined_pars['imm_pars'][s]) for s in range(combined_pars['n_strains'])] # Precompute immunity waning super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/immunity.py b/covasim/immunity.py index 3540c470b..8ba22a1c5 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -153,33 +153,21 @@ def apply(self, sim): return -# if self.immunity_to is None: -# self.immunity_to = [sim['cross_immunity']]*sim['n_strains'] -# if self.immunity_from is None: -# self.immunity_from = [sim['cross_immunity']]*sim['n_strains'] - - # Update the immunity matrix -# sim['immunity'], sim['immune_degree'] = self.update_immunity(prev_immunity=sim['immunity'], n_strains=sim['n_strains'], immunity_to=self.immunity_to, immunity_from=self.immunity_from, -# imm_pars_strain=self.imm_pars, sim_immune_degree=sim['immune_degree'], n_days=sim['n_days']) - - - #%% Immunity methods __all__ += ['init_immunity', 'pre_compute_waning'] def init_immunity(sim): ''' Initialize immunity matrices with all strains that will eventually be in the sim''' - ns = sim['n_strains'] ts = sim['total_strains'] immunity = {} # If immunity values have been provided, process them if sim['immunity'] is not None: if sc.checktype(sim['immunity']['sus'], 'arraylike'): - correct_size = sim['immunity']['sus'].shape == (sim['total_strains'], sim['total_strains']) + correct_size = sim['immunity']['sus'].shape == (ts, ts) if not correct_size: - errormsg = f'Wrong dimensions for immunity["sus"]: you provided a matrix sized {sim["immunity"]["sus"].shape}, but it should be sized {(sim["total_strains"], sim["total_strains"])}' + errormsg = f'Wrong dimensions for immunity["sus"]: you provided a matrix sized {sim["immunity"]["sus"].shape}, but it should be sized {(ts, ts)}' raise ValueError(errormsg) elif sc.checktype(sim['immunity']['sus'], dict): # TODO: make it possible to specify this as something like: diff --git a/covasim/parameters.py b/covasim/parameters.py index 65878ce57..b1a94d29f 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -122,8 +122,6 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): reset_layer_pars(pars) if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses -# pars['immunity'] = initialize_immunity() # Initialize immunity -# pars['immune_degree'] = initialize_immune_degree(n_days=pars['n_days'], imm_pars=pars['imm_pars']) pars = listify_strain_pars(pars) # Turn strain parameters into lists # If version is specified, load old parameters diff --git a/covasim/sim.py b/covasim/sim.py index 37ec330a9..d4df4582f 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -567,12 +567,6 @@ def step(self): immune_inds = np.setdiff1d(immune_inds, inf_inds) immunity_scale_factor = np.full(len(immune_inds), self['immunity']['sus'][strain,strain]) - if len(immune_inds): - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() - # Process cross-immunity parameters and indices, if relevant if ns > 1: for cross_strain in range(ns): From fa149bd6996b831a7a0fac18fe90701d32ab23d5 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 3 Mar 2021 12:01:58 +0100 Subject: [PATCH 116/569] begin testing - need to investigate speed --- tests/devtests/test_variants.py | 159 +++++++++----------------------- 1 file changed, 46 insertions(+), 113 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 4298841ec..8591444ac 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -27,25 +27,30 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): scenarios = { 'baseline': { - 'name':'No reinfection', - 'pars': { - 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) for k in cvd.immunity_axes} - } + 'name':'No reinfection', + 'pars': { + 'strains': { + 'imm_pars': { + k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) for k in cvd.immunity_axes} + } + } }, 'med_halflife': { - 'name':'3 month waning susceptibility', - 'pars': { - 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 90}) for k in - cvd.immunity_axes} - } + 'name':'3 month waning susceptibility', + 'pars': { + 'strains': { + 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 90}) for k in cvd.immunity_axes} + } + } }, 'med_halflife_bysev': { - 'name':'2 month waning susceptibility for symptomatics only', - 'pars': { - 'rel_imm': {'asymptomatic': 0}, - 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 60}) for k in - cvd.immunity_axes} - } + 'name':'2 month waning susceptibility for symptomatics only', + 'pars': { + 'strains': { + 'rel_imm': {'asymptomatic': 0}, + 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 60}) for k in cvd.immunity_axes} + } + } }, } @@ -69,13 +74,14 @@ def test_strainduration(do_plot=False, do_show=True, do_save=False): sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') sc.heading('Setting up...') - imported_strain = {'dur':{'inf2sym': {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9}}} - imports = cv.import_strain(strain=imported_strain, days=10, n_imports=30) + strain_pars = {'dur':{'inf2sym': {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9}}} + strains = cv.Strain(strain=strain_pars, strain_label='10 days til symptoms', days=10, n_imports=30) tp = cv.test_prob(symp_prob=0.2) # Add an efficient testing program base_pars = { 'beta': 0.015, # Make beta higher than usual so people get infected quickly 'n_days': 120, + 'interventions': tp } n_runs = 1 @@ -84,12 +90,12 @@ def test_strainduration(do_plot=False, do_show=True, do_save=False): # Define the scenarios scenarios = { 'baseline': { - 'name':'1 day to symptoms', - 'pars': {'interventions': [tp]} + 'name':'1 day to symptoms', + 'pars': {} }, 'slowsymp': { - 'name':'10 days to symptoms', - 'pars': {'interventions': [imports, tp]} + 'name':'10 days to symptoms', + 'pars': {'strains': strains} } } @@ -120,7 +126,6 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): strain_pars = { 'rel_beta': 1.5, -# 'imm_pars': {k: dict(form='exp_decay', pars={'init_val':1., 'half_life':100}) for k in cvd.immunity_axes} 'imm_pars': {k: dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 10, 'lower_asymp': 0.1, 'decay_rate': -5}) for k in cvd.immunity_axes} } strain = cv.Strain(strain_pars, days=1) @@ -137,13 +142,9 @@ def test_import2strains(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing 2 new strains partway through a sim') sc.heading('Setting up...') - strain2 = cv.make_strain('b117') - strain3 = cv.make_strain('sa variant') - - imports = [cv.import_strain(strain=strain2, days=10, n_imports=20), - cv.import_strain(strain=strain3, days=30, n_imports=20), - ] - sim = cv.Sim(interventions=imports, label='With imported infections') + b117 = cv.Strain('b117', days=10, n_imports=20) + p1 = cv.Strain('sa variant', days=30, n_imports=20) + sim = cv.Sim(strains=[b117, p1], label='With imported infections') sim.run() strain_labels = [ @@ -172,13 +173,13 @@ def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): 'n_days': 120, } - imported_strain = { + strain_pars = { 'rel_beta': 1.5, 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} } - imports = cv.import_strain(strain=imported_strain, days=10, n_imports=30) - sim = cv.Sim(pars=pars, interventions=imports, label='With imported infections') + strain = cv.Strain(strain=strain_pars, strain_label='Custom strain', days=10, n_imports=30) + sim = cv.Sim(pars=pars, strains=strain, label='With imported infections') sim.run() if do_plot: @@ -192,20 +193,16 @@ def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): sc.heading('Setting up...') strain2 = {'rel_beta': 1.5, - 'rel_severe_prob': 1.3, - } + 'rel_severe_prob': 1.3} - strain3 = { - 'rel_beta': 2, - 'rel_symp_prob': 1.6, - } + strain3 = {'rel_beta': 2, + 'rel_symp_prob': 1.6} - intervs = [ - cv.change_beta(days=[5, 20, 40], changes=[0.8, 0.7, 0.6]), - cv.import_strain(strain=strain2, days=10, n_imports=20), - cv.import_strain(strain=strain3, days=30, n_imports=20), + intervs = cv.change_beta(days=[5, 20, 40], changes=[0.8, 0.7, 0.6]), + strains = [cv.Strain(strain=strain2, days=10, n_imports=20), + cv.Strain(strain=strain3, days=30, n_imports=20), ] - sim = cv.Sim(interventions=intervs, label='With imported infections') + sim = cv.Sim(interventions=intervs, strains=strains, label='With imported infections') sim.run() strain_labels = [ @@ -282,21 +279,21 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() # Run simplest possible test - if 0: + if 1: sim = cv.Sim() sim.run() # Run more complex tests sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) -# sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) -# sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # TODO: the next test isn't working, need to check change_beta logic - # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) +# sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) # TODO: the next two tests aren't working, need to check scenario logic - # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - # scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) +# scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) +# scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # The next tests are deprecated, can be removed # simX = test_importstrain_args() @@ -307,67 +304,3 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab print('Done.') - - -# DEPRECATED -# def test_importstrain_args(): -# sc.heading('Test flexibility of arguments for the import strain "intervention"') -# -# # Run sim with a single strain initially, then introduce a new strain that's more transmissible on day 10 -# immunity = [ -# {'init_immunity': 1., 'half_life': 180, 'cross_factor': 1}, -# ] -# pars = { -# 'n_strains': 1, -# 'beta': [0.016], -# 'immunity': immunity -# } -# -# # All these should run -# imports = cv.import_strain(days=50, beta=0.03) -# #imports = cv.import_strain(days=[10, 50], beta=0.03) -# #imports = cv.import_strain(days=50, beta=[0.03, 0.05]) -# #imports = cv.import_strain(days=[10, 20], beta=[0.03, 0.05]) -# #imports = cv.import_strain(days=50, beta=[0.03, 0.05, 0.06]) -# #imports = cv.import_strain(days=[10, 20], n_imports=[5, 10], beta=[0.03, 0.05], init_immunity=[1, 1], -# # half_life=[180, 180], cross_factor=[0, 0]) -# #imports = cv.import_strain(days=[10, 50], beta=0.03, cross_factor=[0.4, 0.6]) -# #imports = cv.import_strain(days=['2020-04-01', '2020-05-01'], beta=0.03) -# -# # This should fail -# #imports = cv.import_strain(days=[20, 50], beta=[0.03, 0.05, 0.06]) -# -# sim = cv.Sim(pars=pars, interventions=imports) -# sim.run() -# -# -# return sim -# - -# def test_par_refactor(): -# ''' -# The purpose of this test is to experiment with different representations of the parameter structures -# Still WIP! -# ''' -# -# # Simplest case: add a strain to beta -# p1 = cv.Par(name='beta', val=0.016, by_strain=True) -# print(p1.val) # Prints all the stored values of beta -# print(p1[0]) # Can index beta like an array to pull out strain-specific values -# p1.add_strain(new_val = 0.025) -# -# # Complex case: add a strain that's differentiated by severity for kids 0-20 -# p2 = cv.Par(name='sus_ORs', val=np.array([0.34, 0.67, 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47]), by_strain=True, by_age=True) -# print(p2.val) # Prints all the stored values for the original strain -# print(p2[0]) # Can index beta like an array to pull out strain-specific values -# p2.add_strain(new_val=np.array([1., 1., 1., 1., 1., 1., 1.24, 1.47, 1.47, 1.47])) -# -# # Complex case: add a strain that's differentiated by duration of disease -# p3 = cv.Par(name='dur_asym2rec', val=dict(dist='lognormal_int', par1=8.0, par2=2.0), by_strain=True, is_dist=True) -# print(p3.val) # Prints all the stored values for the original strain -# print(p3[0]) # Can index beta like an array to pull out strain-specific values -# p3.add_strain(new_val=dict(dist='lognormal_int', par1=12.0, par2=2.0)) -# p3.get(strain=1, n=6) -# -# return p1, p2, p3 - From 10517477e17a2d4b17ed76c52410c77ecbf4c962 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 3 Mar 2021 13:03:21 +0100 Subject: [PATCH 117/569] fix scenario logic --- covasim/immunity.py | 23 ++++++++++++----------- covasim/parameters.py | 6 ++++++ covasim/run.py | 10 ++++++++-- covasim/sim.py | 4 ++-- tests/devtests/test_variants.py | 29 ++++++++++++++--------------- 5 files changed, 42 insertions(+), 30 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 8ba22a1c5..ea97b4445 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -157,13 +157,23 @@ def apply(self, sim): #%% Immunity methods __all__ += ['init_immunity', 'pre_compute_waning'] -def init_immunity(sim): +def init_immunity(sim, create=False): ''' Initialize immunity matrices with all strains that will eventually be in the sim''' ts = sim['total_strains'] immunity = {} # If immunity values have been provided, process them - if sim['immunity'] is not None: + if sim['immunity'] is None or create: + # Initialize immunity + for ax in cvd.immunity_axes: + if ax == 'sus': # Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] + immunity[ax] = np.full((ts, ts), sim['cross_immunity'], dtype=cvd.default_float) # Default for off-diagnonals + np.fill_diagonal(immunity[ax], 1) # Default for own-immunity + else: # Progression and transmission are matrices of scalars of size sim['n_strains'] + immunity[ax] = np.full(ts, 1, dtype=cvd.default_float) + sim['immunity'] = immunity + + else: if sc.checktype(sim['immunity']['sus'], 'arraylike'): correct_size = sim['immunity']['sus'].shape == (ts, ts) if not correct_size: @@ -181,15 +191,6 @@ def init_immunity(sim): errormsg = f'Type of immunity["sus"] not understood: you provided {type(sim["immunity"]["sus"])}, but it should be an array or dict.' raise ValueError(errormsg) - else: - # Initialize immunity - for ax in cvd.immunity_axes: - if ax == 'sus': # Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] - immunity[ax] = np.full((ts, ts), sim['cross_immunity'], dtype=cvd.default_float) # Default for off-diagnonals - np.fill_diagonal(immunity[ax], 1) # Default for own-immunity - else: # Progression and transmission are matrices of scalars of size sim['n_strains'] - immunity[ax] = np.full(ts, 1, dtype=cvd.default_float) - sim['immunity'] = immunity # Precompute waning immune_degree = [] # Stored as a list by strain diff --git a/covasim/parameters.py b/covasim/parameters.py index b1a94d29f..fcde2f5f1 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -308,6 +308,12 @@ def update_sub_key_pars(pars, default_pars): else: if isinstance(val, dict): # Update the dictionary, don't just overwrite it pars[par] = sc.mergenested(default_pars[par], val) +# try: pars[par] = sc.mergenested(default_pars[par], val) +# except: +# import traceback; +# traceback.print_exc(); +# import pdb; +# pdb.set_trace() return pars diff --git a/covasim/run.py b/covasim/run.py index d66fd4b52..0bdbf822d 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -11,6 +11,7 @@ from . import defaults as cvd from . import base as cvb from . import sim as cvs +from . import immunity as cvimm from . import plotting as cvplt from .settings import options as cvo @@ -861,6 +862,8 @@ def __init__(self, sim=None, metapars=None, scenarios=None, basepars=None, scenf self.basepars = sc.mergedicts({}, basepars) self.base_sim.update_pars(self.basepars) self.base_sim.validate_pars() + self.base_sim.init_strains() + self.base_sim.init_immunity() self.base_sim.init_results() # Copy quantities from the base sim to the main object @@ -926,13 +929,16 @@ def print_heading(string): raise ValueError(errormsg) # Create and run the simulations - print_heading(f'Multirun for {scenkey}') scen_sim = sc.dcp(self.base_sim) scen_sim.label = scenkey + defaults = {par: scen_sim[par] for par in cvd.strain_pars} - defaults['immunity'] = scen_sim['immunity'] scen_sim.update_pars(scenpars, defaults=defaults, **kwargs) # Update the parameters, if provided + if 'strains' in scenpars: # Process strains + scen_sim.init_strains() + scen_sim.init_immunity(create=True) + run_args = dict(n_runs=self['n_runs'], noise=self['noise'], noisepar=self['noisepar'], keep_people=keep_people, verbose=verbose) if debug: print('Running in debug mode (not parallelized)') diff --git a/covasim/sim.py b/covasim/sim.py index d4df4582f..c9ea2adac 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -454,9 +454,9 @@ def init_strains(self): return - def init_immunity(self): + def init_immunity(self, create=False): ''' Initialize immunity matrices and precompute immunity waning for each strain ''' - cvimm.init_immunity(self) + cvimm.init_immunity(self, create=create) return diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 8591444ac..ff940034c 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -29,27 +29,26 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): 'baseline': { 'name':'No reinfection', 'pars': { - 'strains': { - 'imm_pars': { - k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) for k in cvd.immunity_axes} + 'strains': [cv.Strain( + strain = {'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) for k in cvd.immunity_axes}}, + days = 1)] } - } - }, + }, 'med_halflife': { 'name':'3 month waning susceptibility', 'pars': { - 'strains': { - 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 90}) for k in cvd.immunity_axes} - } + 'strains': [cv.Strain( + strain = {'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 90}) for k in cvd.immunity_axes}}, + days=1)] } }, 'med_halflife_bysev': { 'name':'2 month waning susceptibility for symptomatics only', 'pars': { - 'strains': { - 'rel_imm': {'asymptomatic': 0}, - 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 60}) for k in cvd.immunity_axes} - } + 'strains': [cv.Strain( + strain = {'rel_imm': {'asymptomatic': 0, 'mild': 1, 'severe': 1}, + 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 60}) for k in cvd.immunity_axes}}, + days=1)] } }, } @@ -95,7 +94,7 @@ def test_strainduration(do_plot=False, do_show=True, do_save=False): }, 'slowsymp': { 'name':'10 days to symptoms', - 'pars': {'strains': strains} + 'pars': {'strains': [strains]} } } @@ -287,13 +286,13 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # TODO: the next test isn't working, need to check change_beta logic # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) # TODO: the next two tests aren't working, need to check scenario logic -# scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) -# scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # The next tests are deprecated, can be removed # simX = test_importstrain_args() From cb601c176fa24bd29b86a283ec19879d5326b669 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 3 Mar 2021 13:18:04 +0100 Subject: [PATCH 118/569] all tests pass --- covasim/interventions.py | 18 ++---------------- covasim/parameters.py | 11 ++++------- tests/devtests/test_variants.py | 19 ++++++++----------- 3 files changed, 14 insertions(+), 34 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index a79a7bbeb..b8b8effec 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -395,11 +395,9 @@ def initialize(self, sim): self.orig_betas = {} for lkey in self.layers: if lkey is None: - self.orig_betas['overall'] = [rb*sim['beta'] for rb in sim['rel_beta']] - self.testkey = 'overall' + self.orig_betas['overall'] = sim['beta'] else: self.orig_betas[lkey] = sim['beta_layer'][lkey] - self.testkey = lkey self.initialized = True return @@ -407,25 +405,13 @@ def initialize(self, sim): def apply(self, sim): - # Extend rel_beta if needed - if self.layers[0] is None: - if len(sim['rel_beta'])>len(self.orig_betas['overall']): - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() - prev_change = sim['beta'][0]/self.orig_betas['overall'][0] - self.orig_betas['overall'].append(sim['beta'][-1]) - sim['beta'][-1] *= prev_change - # If this day is found in the list, apply the intervention for ind in find_day(self.days, sim.t): for lkey,new_beta in self.orig_betas.items(): + new_beta = new_beta * self.changes[ind] if lkey == 'overall': - new_beta = [bv * self.changes[ind] for bv in new_beta] sim['beta'] = new_beta else: - new_beta *= self.changes[ind] sim['beta_layer'][lkey] = new_beta diff --git a/covasim/parameters.py b/covasim/parameters.py index fcde2f5f1..b7977be3c 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -307,13 +307,10 @@ def update_sub_key_pars(pars, default_pars): pars[par] = sc.promotetolist(sc.mergenested(oldval, newval)) else: if isinstance(val, dict): # Update the dictionary, don't just overwrite it - pars[par] = sc.mergenested(default_pars[par], val) -# try: pars[par] = sc.mergenested(default_pars[par], val) -# except: -# import traceback; -# traceback.print_exc(); -# import pdb; -# pdb.set_trace() + if isinstance(default_pars[par], dict): + pars[par] = sc.mergenested(default_pars[par], val) + else: # If the default isn't a disctionary, just overwrite it (TODO: could make this more robust) + pars[par] = val return pars diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index ff940034c..edbdf4c18 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -127,8 +127,13 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): 'rel_beta': 1.5, 'imm_pars': {k: dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 10, 'lower_asymp': 0.1, 'decay_rate': -5}) for k in cvd.immunity_axes} } + immunity = {} + immunity['sus'] = np.array([[1,0.4],[0.9,1.]]) + immunity['prog'] = np.array([1,1]) + immunity['trans'] = np.array([1,1]) + pars = {'immunity': immunity} strain = cv.Strain(strain_pars, days=1) - sim = cv.Sim(strains=strain) + sim = cv.Sim(pars=pars, strains=strain) sim.run() if do_plot: @@ -197,7 +202,7 @@ def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): strain3 = {'rel_beta': 2, 'rel_symp_prob': 1.6} - intervs = cv.change_beta(days=[5, 20, 40], changes=[0.8, 0.7, 0.6]), + intervs = cv.change_beta(days=[5, 20, 40], changes=[0.8, 0.7, 0.6]) strains = [cv.Strain(strain=strain2, days=10, n_imports=20), cv.Strain(strain=strain3, days=30, n_imports=20), ] @@ -286,18 +291,10 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) - # TODO: the next test isn't working, need to check change_beta logic -# sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - - # TODO: the next two tests aren't working, need to check scenario logic - - # The next tests are deprecated, can be removed - # simX = test_importstrain_args() - # p1, p2, p3 = test_par_refactor() - sc.toc() From 71aff215b4dbec4a2e0ec6a74f306e551c895de8 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 3 Mar 2021 14:14:10 +0100 Subject: [PATCH 119/569] investigating tests some more --- covasim/immunity.py | 5 ++++ covasim/run.py | 2 ++ tests/devtests/test_variants.py | 51 ++++++++++++++------------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index ea97b4445..1bb58ed8a 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -141,6 +141,11 @@ def apply(self, sim): if sim.t == self.days: # Time to introduce strain + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() + # Check number of strains prev_strains = sim['n_strains'] sim['n_strains'] += 1 diff --git a/covasim/run.py b/covasim/run.py index 0bdbf822d..b5ad4beac 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -938,6 +938,8 @@ def print_heading(string): if 'strains' in scenpars: # Process strains scen_sim.init_strains() scen_sim.init_immunity(create=True) + if 'imm_pars' in scenpars: # Process strains + scen_sim.init_immunity(create=True) run_args = dict(n_runs=self['n_runs'], noise=self['noise'], noisepar=self['noisepar'], keep_people=keep_people, verbose=verbose) if debug: diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index edbdf4c18..524972fef 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -28,27 +28,18 @@ def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): scenarios = { 'baseline': { 'name':'No reinfection', - 'pars': { - 'strains': [cv.Strain( - strain = {'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) for k in cvd.immunity_axes}}, - days = 1)] - } - }, + 'pars': {'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) for k in cvd.immunity_axes}, + 'rel_imm': {k: 1 for k in cvd.immunity_sources} + }, + }, 'med_halflife': { 'name':'3 month waning susceptibility', - 'pars': { - 'strains': [cv.Strain( - strain = {'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 90}) for k in cvd.immunity_axes}}, - days=1)] - } + 'pars': {'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 90}) for k in cvd.immunity_axes}}, }, 'med_halflife_bysev': { 'name':'2 month waning susceptibility for symptomatics only', - 'pars': { - 'strains': [cv.Strain( - strain = {'rel_imm': {'asymptomatic': 0, 'mild': 1, 'severe': 1}, - 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 60}) for k in cvd.immunity_axes}}, - days=1)] + 'pars': {'rel_imm': {'asymptomatic': 0, 'mild': 1, 'severe': 1}, + 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 60}) for k in cvd.immunity_axes} } }, } @@ -131,8 +122,10 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): immunity['sus'] = np.array([[1,0.4],[0.9,1.]]) immunity['prog'] = np.array([1,1]) immunity['trans'] = np.array([1,1]) - pars = {'immunity': immunity} - strain = cv.Strain(strain_pars, days=1) + pars = { + 'immunity': immunity + } + strain = cv.Strain(strain_pars, days=1, n_imports=20) sim = cv.Sim(pars=pars, strains=strain) sim.run() @@ -210,15 +203,15 @@ def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): sim.run() strain_labels = [ - 'Strain 1: beta 0.016', - 'Strain 2: beta 0.025, 20 imported day 10', - 'Strain 3: beta 0.05, 20 imported day 30' + f'Strain 1: beta {sim["beta"]}', + f'Strain 2: beta {sim["beta"]*sim["rel_beta"][1]}, 20 imported day 10', + f'Strain 3: beta {sim["beta"]*sim["rel_beta"][2]}, 20 imported day 30' ] if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strains', filename='test_importstrain2', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_results(sim, key='incidence_by_strain', title='Imported strains', filename='test_import2strains_changebeta', labels=strain_labels, do_show=do_show, do_save=do_save) plot_shares(sim, key='new_infections', title='Shares of new infections by strain', - filename='test_importstrain2_shares', do_show=do_show, do_save=do_save) + filename='test_import2strains_changebeta_shares', do_show=do_show, do_save=do_save) return sim @@ -283,17 +276,17 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() # Run simplest possible test - if 1: + if 0: sim = cv.Sim() sim.run() # Run more complex tests sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + #sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + #scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + #scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) sc.toc() From f752dadb3b8cd1671c59f6d7637b3b33f7e61b64 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 3 Mar 2021 15:37:30 +0100 Subject: [PATCH 120/569] returning to debugging --- covasim/immunity.py | 5 ----- covasim/people.py | 9 ++++----- covasim/sim.py | 6 +++--- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 1bb58ed8a..ea97b4445 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -141,11 +141,6 @@ def apply(self, sim): if sim.t == self.days: # Time to introduce strain - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() - # Check number of strains prev_strains = sim['n_strains'] sim['n_strains'] += 1 diff --git a/covasim/people.py b/covasim/people.py index 8dc5368fa..2d2457faf 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -91,8 +91,7 @@ def __init__(self, pars, strict=True, **kwargs): self.flows = {key:0 for key in cvd.new_result_flows} for key in cvd.new_result_flows: if 'by_strain' in key: - self.flows[key] = np.full(self.pars['n_strains'], 0, dtype=cvd.default_float) - + self.flows[key] = np.full(self.pars['total_strains'], 0, dtype=cvd.default_float) # Although we have called init(), we still need to call initialize() self.initialized = False @@ -148,8 +147,8 @@ def find_cutoff(age_cutoffs, age): self.severe_prob[:] = progs['severe_probs'][inds]*progs['comorbidities'][inds] # Severe disease probability is modified by comorbidities self.crit_prob[:] = progs['crit_probs'][inds] # Probability of developing critical disease self.death_prob[:] = progs['death_probs'][inds] # Probability of death - self.rel_sus[:] = progs['sus_ORs'][inds] # Default susceptibilities - self.rel_trans[:] = progs['trans_ORs'][inds] * cvu.sample(**self.pars['beta_dist'], size=len(inds)) # Default transmissibilities, with viral load drawn from a distribution + self.rel_sus[:] = progs['sus_ORs'][inds] # Default susceptibilities + self.rel_trans[:] = progs['trans_ORs'][inds] * cvu.sample(**self.pars['beta_dist'], size=len(inds)) # Default transmissibilities, with viral load drawn from a distribution return @@ -165,7 +164,7 @@ def update_states_pre(self, t): self.flows = {key:0 for key in cvd.new_result_flows} for key in cvd.new_result_flows: if 'by_strain' in key: - self.flows[key] += np.full(self.pars['total_strains'], 0, dtype=cvd.default_float) + self.flows[key] = np.full(self.pars['total_strains'], 0, dtype=cvd.default_float) self.flows['new_infectious'] += self.check_infectious() # For people who are exposed and not infectious, check if they begin being infectious self.flows['new_symptomatic'] += self.check_symptomatic() self.flows['new_severe'] += self.check_severe() diff --git a/covasim/sim.py b/covasim/sim.py index c9ea2adac..2356d861c 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -108,13 +108,13 @@ def initialize(self, reset=False, **kwargs): self.t = 0 # The current time index self.validate_pars() # Ensure parameters have valid values self.set_seed() # Reset the random seed before the population is created - self.init_people(save_pop=self.save_pop, load_pop=self.load_pop, popfile=self.popfile, reset=reset, **kwargs) # Create all the people (slow) - self.validate_layer_pars() # Once the population is initialized, validate the layer parameters again self.init_interventions() # Initialize the interventions... self.init_analyzers() # ...and the analyzers... self.init_strains() # ...and the strains.... self.init_immunity() # ... and information about immunity/cross-immunity. - self.init_results() # Create the results structure - do this after initializing strains + self.init_results() # After initializing the strain, create the results structure + self.init_people(save_pop=self.save_pop, load_pop=self.load_pop, popfile=self.popfile, reset=reset, **kwargs) # Create all the people (slow) + self.validate_layer_pars() # Once the population is initialized, validate the layer parameters again self.set_seed() # Reset the random seed again so the random number stream is consistent self.initialized = True self.complete = False From 5420dcf86d99cb17f1a1c59b61362d3795259890 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 3 Mar 2021 11:08:06 -0500 Subject: [PATCH 121/569] added in infectious by strain counter --- covasim/immunity.py | 2 +- covasim/people.py | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index ea97b4445..4926afca5 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -146,7 +146,7 @@ def apply(self, sim): sim['n_strains'] += 1 # Update strain-specific people attributes - cvu.update_strain_attributes(sim.people) + # cvu.update_strain_attributes(sim.people) # don't think we need to do this if we just create people arrays with number of total strains in sim importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) diff --git a/covasim/people.py b/covasim/people.py index 2d2457faf..eea8b49e9 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -59,7 +59,7 @@ def __init__(self, pars, strict=True, **kwargs): if key == 'uid': self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. - self[key] = np.full((self.pars['n_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') + self[key] = np.full((self.pars['total_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') elif key == 'vaccinations': self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) else: @@ -75,7 +75,7 @@ def __init__(self, pars, strict=True, **kwargs): # Set strain states, which store info about which strain a person is exposed to for key in self.meta.strain_states: if 'by' in key: - self[key] = np.full((self.pars['n_strains'], self.pop_size), False, dtype=bool, order='F') + self[key] = np.full((self.pars['total_strains'], self.pop_size), False, dtype=bool, order='F') else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -230,6 +230,9 @@ def check_infectious(self): inds = self.check_inds(self.infectious, self.date_infectious, filter_inds=self.is_exp) self.infectious[inds] = True self.infectious_strain[inds] = self.exposed_strain[inds] + for strain in range(self.pars['n_strains']): + this_strain_inds = cvu.itrue(self.infectious_strain[inds] == strain, inds) + self.flows['new_infectious_by_strain'][strain] += len(this_strain_inds) return len(inds) @@ -291,11 +294,6 @@ def check_death(self): self.dead[inds] = True return len(inds) - # TODO-- add in immunity instead of recovery - # def check_immunity(self): - # '''Update immunity by strain based on time since recovery''' - # just_recovered_inds = self.check_inds(self.recovered, self.date_recovered, filter_inds=self.is_exp) - def check_diagnosed(self): ''' From e60931d81833ab5eb70cc7eb676c97df4275c04c Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 3 Mar 2021 14:28:20 -0500 Subject: [PATCH 122/569] adding in vaccines, still WIP --- covasim/immunity.py | 200 +++++++++++++++++++++++++++++++++------ covasim/interventions.py | 95 ++++++++++++++++++- covasim/parameters.py | 1 + 3 files changed, 268 insertions(+), 28 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 4926afca5..1e0d401e0 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -15,10 +15,10 @@ __all__ = [] +# %% Define strain class -#%% Define strain class +__all__ += ['Strain'] -__all__+= ['Strain'] class Strain(): ''' @@ -28,9 +28,7 @@ class Strain(): day (int): day on which new variant is introduced. n_imports (int): the number of imports of the strain to be added strain (dict): dictionary of parameters specifying information about the strain - immunity_to (list of floats): cross immunity to existing strains in model - immunity_from (list of floats): cross immunity from existing strains in model - kwargs (dict): passed to Intervention() + kwargs (dict): **Example**:: b117 = cv.Strain('b117', days=10) # Make strain B117 active from day 10 @@ -42,16 +40,15 @@ class Strain(): def __init__(self, strain=None, strain_label=None, days=None, n_imports=1, **kwargs): # Handle inputs - self.days = days - self.n_imports = n_imports + self.days = days + self.n_imports = n_imports # Strains can be defined in different ways: process these here - self.strain_pars = self.parse_strain_pars(strain=strain, strain_label=strain_label) + self.strain_pars = self.parse_strain_pars(strain=strain, strain_label=strain_label) for par, val in self.strain_pars.items(): setattr(self, par, val) return - def parse_strain_pars(self, strain=None, strain_label=None): ''' Unpack strain information, which may be given in different ways''' @@ -62,7 +59,8 @@ def parse_strain_pars(self, strain=None, strain_label=None): choices = { 'default': ['default', 'wild', 'pre-existing'], 'b117': ['b117', 'B117', 'B.1.1.7', 'UK', 'uk', 'UK variant', 'uk variant'], - 'b1351': ['b1351', 'B1351', 'B.1.351', 'SA', 'sa', 'SA variant', 'sa variant'], # TODO: add other aliases + 'b1351': ['b1351', 'B1351', 'B.1.351', 'SA', 'sa', 'SA variant', 'sa variant'], + # TODO: add other aliases 'p1': ['p1', 'P1', 'P.1', 'Brazil', 'Brazil variant', 'brazil variant'], } @@ -74,8 +72,10 @@ def parse_strain_pars(self, strain=None, strain_label=None): # Known parameters on B117 elif strain in choices['b117']: strain_pars = dict() - strain_pars['rel_beta'] = 1.5 # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 - strain_pars['rel_severe_prob'] = 1.6 # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf + strain_pars[ + 'rel_beta'] = 1.5 # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 + strain_pars[ + 'rel_severe_prob'] = 1.6 # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf self.strain_label = strain # Known parameters on South African variant @@ -83,7 +83,8 @@ def parse_strain_pars(self, strain=None, strain_label=None): strain_pars = dict() strain_pars['imm_pars'] = dict() for ax in cvd.immunity_axes: - strain_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1.,'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + strain_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., + 'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) self.strain_label = strain # Known parameters on Brazil variant @@ -91,7 +92,8 @@ def parse_strain_pars(self, strain=None, strain_label=None): strain_pars = dict() strain_pars['imm_pars'] = dict() for ax in cvd.immunity_axes: - strain_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1.,'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + strain_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., + 'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) self.strain_label = strain else: @@ -110,7 +112,6 @@ def parse_strain_pars(self, strain=None, strain_label=None): return strain_pars - def initialize(self, sim): if not hasattr(self, 'imm_pars'): self.imm_pars = sim['imm_pars'][0] @@ -136,7 +137,6 @@ def initialize(self, sim): self.initialized = True - def apply(self, sim): if sim.t == self.days: # Time to introduce strain @@ -147,15 +147,162 @@ def apply(self, sim): # Update strain-specific people attributes # cvu.update_strain_attributes(sim.people) # don't think we need to do this if we just create people arrays with number of total strains in sim - importation_inds = cvu.choose(max_n=len(sim.people), n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely + importation_inds = cvu.choose(max_n=len(sim.people), + n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) return +class Vaccine(): + ''' + Add a new vaccine to the sim (called by interventions.py vaccinate() + + Args: + vaccine (dict): dictionary of parameters specifying information about the vaccine + kwargs (dict): + + **Example**:: + moderna = cv.Vaccine('moderna') # Create Moderna vaccine + pfizer = cv.Vaccine('pfizer) # Create Pfizer vaccine + j&j = cv.Vaccine('j&j') # Create J&J vaccine + az = cv.Vaccine('az) # Create AstraZeneca vaccine + interventions += [cv.vaccinate(vaccines=[moderna, pfizer, j&j, az], days=[1, 10, 10, 30])] # Add them all to the sim + sim = cv.Sim(interventions=interventions) + ''' + + def __init__(self, vaccine=None, vaccine_label=None, **kwargs): + + self.vaccine_immune_degree = None + self.vaccine_immunity = None + self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine, vaccine_label=vaccine_label) + for par, val in self.vaccine_pars.items(): + setattr(self, par, val) + return + + def parse_vaccine_pars(self, vaccine=None, vaccine_label=None): + ''' Unpack vaccine information, which may be given in different ways''' + + # Option 1: strains can be chosen from a list of pre-defined strains + if isinstance(vaccine, str): + + # List of choices currently available: new ones can be added to the list along with their aliases + choices = { + 'default': ['default', 'pre-existing'], + 'pfizer': ['pfizer', 'Pfizer', 'Pfizer-BionTech'], + 'moderna': ['moderna', 'Moderna'], + 'az': ['az', 'AstraZeneca', 'astrazeneca'], + 'j&j': ['j&j', 'johnson & johnson', 'Johnson & Johnson'], + } + + # Empty pardict for wild strain + if vaccine in choices['default']: + vaccine_pars = dict() + self.vaccine_label = vaccine + + # Known parameters on pfizer + elif vaccine in choices['pfizer']: + vaccine_pars = dict() + vaccine_pars['imm_pars'] = {} + for ax in cvd.immunity_axes: + vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 120}) # (TODO: link to actual evidence) + self.vaccine_label = vaccine + + # Known parameters on moderna + elif vaccine in choices['moderna']: + vaccine_pars = dict() + vaccine_pars['imm_pars'] = {} + for ax in cvd.immunity_axes: + vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., + 'half_life': 120}) # (TODO: link to actual evidence) + self.vaccine_label = vaccine + + # Known parameters on az + elif vaccine in choices['az']: + vaccine_pars = dict() + vaccine_pars['imm_pars'] = {} + for ax in cvd.immunity_axes: + vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., + 'half_life': 120}) # (TODO: link to actual evidence) + self.vaccine_label = vaccine + + # Known parameters on j&j + elif vaccine in choices['j&j']: + vaccine_pars = dict() + vaccine_pars['imm_pars'] = {} + for ax in cvd.immunity_axes: + vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., + 'half_life': 120}) # (TODO: link to actual evidence) + self.vaccine_label = vaccine + + + else: + choicestr = '\n'.join(choices.values()) + errormsg = f'The selected vaccine "{vaccine}" is not implemented; choices are: {choicestr}' + raise NotImplementedError(errormsg) + + # Option 2: strains can be specified as a dict of pars + elif isinstance(vaccine, dict): + vaccine_pars = vaccine + self.vaccine_label = vaccine_label + + else: + errormsg = f'Could not understand {type(vaccine)}, please specify as a string indexing a predefined vaccine or a dict.' + raise ValueError(errormsg) + + return vaccine_pars + + def initialize(self): + if not hasattr(self, 'imm_pars'): + print('Immunity pars not provided, using defaults') + self.imm_pars = {} + for ax in cvd.immunity_axes: + self.imm_pars[ax] = dict(form='exp_decay', pars={'init_val':1., 'half_life':None}) + + # Validate immunity pars (make sure there are values for all cvd.immunity_axes) + for key in cvd.immunity_axes: + if key not in self.imm_pars: + print(f'Immunity pars for vaccine for {key} not provided, using default value') + self.imm_pars[key] = dict(form='exp_decay', pars={'init_val':1., 'half_life':None}) + + self.initialized = True + + def init_vaccine_immunity(self, sim): + ''' Initialize vaccine immunity and pre-loaded immune_degree with all strains that will eventually be in the sim''' + ts = sim['total_strains'] + immunity = {} + + # If immunity values have been provided, process them + if not hasattr(self, 'vaccine_immunity'): + # Initialize immunity + for ax in cvd.immunity_axes: + immunity[ax] = np.full(ts, 1, dtype=cvd.default_float) + self.vaccine_immunity = immunity + + else: + if sc.checktype(self.vaccine_immunity['sus'], 'arraylike'): + correct_size = self.vaccine_immunity['sus']['sus'].shape == (ts) + if not correct_size: + errormsg = f'Wrong dimensions for immunity["sus"]: you provided a matrix sized {self.vaccine_immunity["sus"].shape}, but it should be sized {(ts)}' + raise ValueError(errormsg) + else: + errormsg = f'Type of immunity["sus"] not understood: you provided {type(self.vaccine_immunity["sus"])}, but it should be an array or dict.' + raise ValueError(errormsg) + + # Precompute waning + immune_degree = [] # Stored as a list by strain + for s in range(ts): + strain_immune_degree = {} + for ax in cvd.immunity_axes: + strain_immune_degree[ax] = pre_compute_waning(sim['n_days'], **self.imm_pars[s][ax]) + immune_degree.append(strain_immune_degree) + self.vaccine_immune_degree = immune_degree + + return + +# %% Immunity methods +__all__ += ['init_immunity', 'pre_compute_waning', 'init_vaccine_immunity'] -#%% Immunity methods -__all__ += ['init_immunity', 'pre_compute_waning'] def init_immunity(sim, create=False): ''' Initialize immunity matrices with all strains that will eventually be in the sim''' @@ -167,8 +314,9 @@ def init_immunity(sim, create=False): # Initialize immunity for ax in cvd.immunity_axes: if ax == 'sus': # Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] - immunity[ax] = np.full((ts, ts), sim['cross_immunity'], dtype=cvd.default_float) # Default for off-diagnonals - np.fill_diagonal(immunity[ax], 1) # Default for own-immunity + immunity[ax] = np.full((ts, ts), sim['cross_immunity'], + dtype=cvd.default_float) # Default for off-diagnonals + np.fill_diagonal(immunity[ax], 1) # Default for own-immunity else: # Progression and transmission are matrices of scalars of size sim['n_strains'] immunity[ax] = np.full(ts, 1, dtype=cvd.default_float) sim['immunity'] = immunity @@ -191,7 +339,6 @@ def init_immunity(sim, create=False): errormsg = f'Type of immunity["sus"] not understood: you provided {type(sim["immunity"]["sus"])}, but it should be an array or dict.' raise ValueError(errormsg) - # Precompute waning immune_degree = [] # Stored as a list by strain for s in range(ts): @@ -220,7 +367,7 @@ def pre_compute_waning(length, form, pars): 'exp_decay', 'logistic_decay', 'linear', - ] + ] # Process inputs if form == 'exp_decay': @@ -237,6 +384,7 @@ def pre_compute_waning(length, form, pars): return output + # SPecific waning functions are listed here def exp_decay(length, init_val, half_life): ''' @@ -250,7 +398,5 @@ def exp_decay(length, init_val, half_life): def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp): ''' Calculate logistic decay ''' t = np.arange(length, dtype=cvd.default_int) - return (init_val + (lower_asymp-init_val) / (1 + (t/half_val) ** decay_rate)) # TODO: make this robust to /0 errors - - - + return (init_val + (lower_asymp - init_val) / ( + 1 + (t / half_val) ** decay_rate)) # TODO: make this robust to /0 errors diff --git a/covasim/interventions.py b/covasim/interventions.py index b8b8effec..df772c7ef 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1027,7 +1027,7 @@ def notify_contacts(self, sim, contacts): #%% Treatment and prevention interventions -__all__+= ['vaccine'] +__all__+= ['vaccine', 'vaccinate'] class vaccine(Intervention): @@ -1123,3 +1123,96 @@ def apply(self, sim): return + +class vaccinate(Intervention): + ''' + Apply a vaccine to a subset of the population. In addition to changing the + relative susceptibility and the probability of developing symptoms if still + infected, this intervention stores several types of data: + + - ``vaccinations``: the number of vaccine doses per person + - ``vaccination_dates``: list of dates per person + - ``orig_rel_sus``: relative susceptibility per person at the beginning of the simulation + - ``orig_symp_prob``: probability of developing symptoms per person at the beginning of the simulation + - ``mod_rel_sus``: modifier on default susceptibility due to the vaccine + - ``mod_symp_prob``: modifier on default symptom probability due to the vaccine + + Args: + days (int or array): the day or array of days to apply the interventions + prob (float): probability of being vaccinated (i.e., fraction of the population) + rel_sus (float): relative change in susceptibility; 0 = perfect, 1 = no effect + rel_symp (float): relative change in symptom probability for people who still get infected; 0 = perfect, 1 = no effect + subtarget (dict): subtarget intervention to people with particular indices (see test_num() for details) + cumulative (bool): whether cumulative doses have cumulative effects (default false); can also be an array for efficacy per dose, with the last entry used for multiple doses; thus True = [1] and False = [1,0] + kwargs (dict): passed to Intervention() + + **Examples**:: + + interv = cv.vaccine(days=50, prob=0.3, rel_sus=0.5, rel_symp=0.1) + interv = cv.vaccine(days=[10,20,30,40], prob=0.8, rel_sus=0.5, cumulative=[1, 0.3, 0.1, 0]) # A vaccine with efficacy up to the 3rd dose + ''' + def __init__(self, days, prob=1.0, rel_sus=0.0, rel_symp=0.0, subtarget=None, cumulative=False, **kwargs): + super().__init__(**kwargs) # Initialize the Intervention object + self._store_args() # Store the input arguments so the intervention can be recreated + self.days = sc.dcp(days) + self.prob = prob + self.rel_sus = rel_sus + self.rel_symp = rel_symp + self.subtarget = subtarget + if cumulative in [0, False]: + cumulative = [1,0] # First dose has full efficacy, second has none + elif cumulative in [1, True]: + cumulative = [1] # All doses have full efficacy + self.cumulative = np.array(cumulative, dtype=cvd.default_float) # Ensure it's an array + return + + + def initialize(self, sim): + ''' Fix the dates and store the vaccinations ''' + self.days = process_days(sim, self.days) + self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person + self.date_vaccinated = [[] for p in range(sim.n)] # Store the dates when people are vaccinated + self.orig_rel_sus = sc.dcp(sim.people.rel_sus) # Keep a copy of pre-vaccination susceptibility + self.orig_symp_prob = sc.dcp(sim.people.symp_prob) # ...and symptom probability + self.mod_rel_sus = np.ones(sim.n, dtype=cvd.default_float) # Store the final modifiers + self.mod_symp_prob = np.ones(sim.n, dtype=cvd.default_float) # Store the final modifiers + self.initialized = True + return + + + def apply(self, sim): + ''' Perform vaccination ''' + + # If this day is found in the list, apply the intervention + for ind in find_day(self.days, sim.t): # TODO -- investigate this, why does it loop over a variable that isn't subsequently used? Also, comments need updating + + # Construct the testing probabilities piece by piece -- complicated, since need to do it in the right order + vacc_probs = np.full(sim.n, self.prob) # Begin by assigning equal vaccination probability to everyone + if self.subtarget is not None: + subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) + vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted + vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated + + # Calculate the effect per person + vacc_doses = self.vaccinations[vacc_inds] # Calculate current doses + eff_doses = np.minimum(vacc_doses, len(self.cumulative)-1) # Convert to a valid index + vacc_eff = self.cumulative[eff_doses] # Pull out corresponding effect sizes + rel_sus_eff = (1.0 - vacc_eff) + vacc_eff*self.rel_sus + rel_symp_eff = (1.0 - vacc_eff) + vacc_eff*self.rel_symp + + # Apply the vaccine to people + sim.people.rel_sus[vacc_inds] *= rel_sus_eff + sim.people.symp_prob[vacc_inds] *= rel_symp_eff + + # Update counters + self.mod_rel_sus[vacc_inds] *= rel_sus_eff + self.mod_symp_prob[vacc_inds] *= rel_symp_eff + self.vaccinations[vacc_inds] += 1 + for v_ind in vacc_inds: + self.vaccination_dates[v_ind].append(sim.t) + + # Update vaccine attributes in sim + sim.people.vaccinations = self.vaccinations + sim.people.vaccination_dates = self.vaccination_dates + + return \ No newline at end of file diff --git a/covasim/parameters.py b/covasim/parameters.py index b7977be3c..13f3db9d0 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -108,6 +108,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['interventions'] = [] # The interventions present in this simulation; populated by the user pars['analyzers'] = [] # Custom analysis functions; populated by the user pars['strains'] = [] # Additional strains of the virus; populated by the user, see immunity.py + pars['vaccines'] = [] # Vaccines that are being used; populated by user pars['timelimit'] = None # Time limit for the simulation (seconds) pars['stopping_func'] = None # A function to call to stop the sim partway through From f72fa280e43c6351abdfa484fa64e489d84c15c7 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 3 Mar 2021 14:59:59 -0500 Subject: [PATCH 123/569] updated intervention to speak to Vaccine() class --- covasim/defaults.py | 1 + covasim/interventions.py | 57 ++++++++++++---------------------------- 2 files changed, 18 insertions(+), 40 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index d1bb3a8b1..fba6551d3 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -52,6 +52,7 @@ class PeopleMeta(sc.prettyobj): 'trans_immunity_factors', # Float 'prog_immunity_factors', # Float 'vaccinations', # Number of doses given per person + 'vaccine_source' # index of vaccine that individual received ] # Set the states that a person can be in: these are all booleans per person -- used in people.py diff --git a/covasim/interventions.py b/covasim/interventions.py index df772c7ef..fcfd99df7 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -13,7 +13,7 @@ from . import defaults as cvd from . import base as cvb from . import parameters as cvpar -from . import people as cvppl +from . import immunity as cvi from collections import defaultdict @@ -1132,38 +1132,29 @@ class vaccinate(Intervention): - ``vaccinations``: the number of vaccine doses per person - ``vaccination_dates``: list of dates per person - - ``orig_rel_sus``: relative susceptibility per person at the beginning of the simulation - - ``orig_symp_prob``: probability of developing symptoms per person at the beginning of the simulation - - ``mod_rel_sus``: modifier on default susceptibility due to the vaccine - - ``mod_symp_prob``: modifier on default symptom probability due to the vaccine + - ``pars``: vaccine pars that are given to Vaccine() class Args: days (int or array): the day or array of days to apply the interventions prob (float): probability of being vaccinated (i.e., fraction of the population) - rel_sus (float): relative change in susceptibility; 0 = perfect, 1 = no effect - rel_symp (float): relative change in symptom probability for people who still get infected; 0 = perfect, 1 = no effect + vaccine_pars (dict): passed to Vaccine() + vaccine_label (str): passed to Vaccine() subtarget (dict): subtarget intervention to people with particular indices (see test_num() for details) - cumulative (bool): whether cumulative doses have cumulative effects (default false); can also be an array for efficacy per dose, with the last entry used for multiple doses; thus True = [1] and False = [1,0] kwargs (dict): passed to Intervention() **Examples**:: - interv = cv.vaccine(days=50, prob=0.3, rel_sus=0.5, rel_symp=0.1) - interv = cv.vaccine(days=[10,20,30,40], prob=0.8, rel_sus=0.5, cumulative=[1, 0.3, 0.1, 0]) # A vaccine with efficacy up to the 3rd dose + interv = cv.vaccine(days=50, prob=0.3, ) ''' - def __init__(self, days, prob=1.0, rel_sus=0.0, rel_symp=0.0, subtarget=None, cumulative=False, **kwargs): + def __init__(self, days, prob=1.0, vaccine_pars=None, subtarget=None, vaccine_label=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object self._store_args() # Store the input arguments so the intervention can be recreated self.days = sc.dcp(days) self.prob = prob - self.rel_sus = rel_sus - self.rel_symp = rel_symp + self.vaccine_pars = vaccine_pars + self.vaccine_label = vaccine_label self.subtarget = subtarget - if cumulative in [0, False]: - cumulative = [1,0] # First dose has full efficacy, second has none - elif cumulative in [1, True]: - cumulative = [1] # All doses have full efficacy - self.cumulative = np.array(cumulative, dtype=cvd.default_float) # Ensure it's an array + self.vaccine_ind = None return @@ -1171,11 +1162,9 @@ def initialize(self, sim): ''' Fix the dates and store the vaccinations ''' self.days = process_days(sim, self.days) self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person - self.date_vaccinated = [[] for p in range(sim.n)] # Store the dates when people are vaccinated - self.orig_rel_sus = sc.dcp(sim.people.rel_sus) # Keep a copy of pre-vaccination susceptibility - self.orig_symp_prob = sc.dcp(sim.people.symp_prob) # ...and symptom probability - self.mod_rel_sus = np.ones(sim.n, dtype=cvd.default_float) # Store the final modifiers - self.mod_symp_prob = np.ones(sim.n, dtype=cvd.default_float) # Store the final modifiers + self.vaccination_dates = [[] for _ in range(sim.n)] # Store the dates when people are vaccinated + self.vaccine_ind = len(sim['vaccines']) + sim['vaccines'].append(cvi.Vaccine(self.vaccine_pars, self.vaccine_label)) self.initialized = True return @@ -1184,35 +1173,23 @@ def apply(self, sim): ''' Perform vaccination ''' # If this day is found in the list, apply the intervention - for ind in find_day(self.days, sim.t): # TODO -- investigate this, why does it loop over a variable that isn't subsequently used? Also, comments need updating + for _ in find_day(self.days, sim.t): - # Construct the testing probabilities piece by piece -- complicated, since need to do it in the right order + # Construct the vaccine probabilities piece by piece -- complicated, since need to do it in the right order vacc_probs = np.full(sim.n, self.prob) # Begin by assigning equal vaccination probability to everyone if self.subtarget is not None: subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated - # Calculate the effect per person - vacc_doses = self.vaccinations[vacc_inds] # Calculate current doses - eff_doses = np.minimum(vacc_doses, len(self.cumulative)-1) # Convert to a valid index - vacc_eff = self.cumulative[eff_doses] # Pull out corresponding effect sizes - rel_sus_eff = (1.0 - vacc_eff) + vacc_eff*self.rel_sus - rel_symp_eff = (1.0 - vacc_eff) + vacc_eff*self.rel_symp - # Apply the vaccine to people - sim.people.rel_sus[vacc_inds] *= rel_sus_eff - sim.people.symp_prob[vacc_inds] *= rel_symp_eff - - # Update counters - self.mod_rel_sus[vacc_inds] *= rel_sus_eff - self.mod_symp_prob[vacc_inds] *= rel_symp_eff self.vaccinations[vacc_inds] += 1 for v_ind in vacc_inds: self.vaccination_dates[v_ind].append(sim.t) # Update vaccine attributes in sim - sim.people.vaccinations = self.vaccinations - sim.people.vaccination_dates = self.vaccination_dates + sim.people.vaccine_source[vacc_inds] = self.vaccine_ind + sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] + sim.people.vaccination_dates = self.vaccination_dates[vacc_inds] return \ No newline at end of file From 39dc184d7e7e493b1ac606ee5d1426109d0dfc20 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 3 Mar 2021 16:55:58 -0500 Subject: [PATCH 124/569] vaccine functionality/integration improving --- covasim/defaults.py | 1 + covasim/immunity.py | 108 +++++++++++++++++---------------------- covasim/interventions.py | 9 ++-- covasim/parameters.py | 2 +- covasim/sim.py | 17 +++++- 5 files changed, 70 insertions(+), 67 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index fba6551d3..c45a55794 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -68,6 +68,7 @@ class PeopleMeta(sc.prettyobj): 'dead', 'known_contact', 'quarantined', + 'vaccinated' ] strain_states = [ diff --git a/covasim/immunity.py b/covasim/immunity.py index 1e0d401e0..6d2c7c85a 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -158,8 +158,10 @@ class Vaccine(): ''' Add a new vaccine to the sim (called by interventions.py vaccinate() + stores number of doses for vaccine and a dictionary to pass to init_immunity for each dose + Args: - vaccine (dict): dictionary of parameters specifying information about the vaccine + vaccine (dict or str): dictionary of parameters specifying information about the vaccine or label for loading pre-defined vaccine kwargs (dict): **Example**:: @@ -171,70 +173,66 @@ class Vaccine(): sim = cv.Sim(interventions=interventions) ''' - def __init__(self, vaccine=None, vaccine_label=None, **kwargs): + def __init__(self, vaccine=None): - self.vaccine_immune_degree = None - self.vaccine_immunity = None - self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine, vaccine_label=vaccine_label) + self.vaccine_immune_degree = None # dictionary of pre-loaded decay to by imm_axis and dose + self.rel_imm = None # list of length total_strains with relative immunity factor + self.doses = None + self.imm_pars = None + self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine) for par, val in self.vaccine_pars.items(): setattr(self, par, val) return - def parse_vaccine_pars(self, vaccine=None, vaccine_label=None): + def parse_vaccine_pars(self, vaccine=None): ''' Unpack vaccine information, which may be given in different ways''' - # Option 1: strains can be chosen from a list of pre-defined strains + # Option 1: vaccines can be chosen from a list of pre-defined strains if isinstance(vaccine, str): # List of choices currently available: new ones can be added to the list along with their aliases choices = { - 'default': ['default', 'pre-existing'], 'pfizer': ['pfizer', 'Pfizer', 'Pfizer-BionTech'], 'moderna': ['moderna', 'Moderna'], 'az': ['az', 'AstraZeneca', 'astrazeneca'], 'j&j': ['j&j', 'johnson & johnson', 'Johnson & Johnson'], } - # Empty pardict for wild strain - if vaccine in choices['default']: - vaccine_pars = dict() - self.vaccine_label = vaccine - + # (TODO: link to actual evidence) # Known parameters on pfizer - elif vaccine in choices['pfizer']: + if vaccine in choices['pfizer']: vaccine_pars = dict() vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 120}) # (TODO: link to actual evidence) - self.vaccine_label = vaccine + vaccine_pars['imm_pars'][ax] = [dict(form='exp_decay', pars={'init_val': 0.5, 'half_life': 30}), + dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})] + vaccine_pars['doses'] = 2 # Known parameters on moderna elif vaccine in choices['moderna']: vaccine_pars = dict() vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., - 'half_life': 120}) # (TODO: link to actual evidence) - self.vaccine_label = vaccine + vaccine_pars['imm_pars'][ax] = [dict(form='exp_decay', pars={'init_val': 0.5, 'half_life': 30}), + dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})] + vaccine_pars['doses'] = 2 # Known parameters on az elif vaccine in choices['az']: vaccine_pars = dict() vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., - 'half_life': 120}) # (TODO: link to actual evidence) - self.vaccine_label = vaccine + vaccine_pars['imm_pars'][ax] = [dict(form='exp_decay', pars={'init_val': 0.5, 'half_life': 30}), + dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})] + vaccine_pars['doses'] = 2 # Known parameters on j&j elif vaccine in choices['j&j']: vaccine_pars = dict() vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., - 'half_life': 120}) # (TODO: link to actual evidence) - self.vaccine_label = vaccine - + vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 120}) + vaccine_pars['doses'] = 1 else: choicestr = '\n'.join(choices.values()) @@ -244,7 +242,6 @@ def parse_vaccine_pars(self, vaccine=None, vaccine_label=None): # Option 2: strains can be specified as a dict of pars elif isinstance(vaccine, dict): vaccine_pars = vaccine - self.vaccine_label = vaccine_label else: errormsg = f'Could not understand {type(vaccine)}, please specify as a string indexing a predefined vaccine or a dict.' @@ -252,53 +249,42 @@ def parse_vaccine_pars(self, vaccine=None, vaccine_label=None): return vaccine_pars - def initialize(self): - if not hasattr(self, 'imm_pars'): - print('Immunity pars not provided, using defaults') - self.imm_pars = {} - for ax in cvd.immunity_axes: - self.imm_pars[ax] = dict(form='exp_decay', pars={'init_val':1., 'half_life':None}) + def initialize(self, sim): - # Validate immunity pars (make sure there are values for all cvd.immunity_axes) - for key in cvd.immunity_axes: - if key not in self.imm_pars: - print(f'Immunity pars for vaccine for {key} not provided, using default value') - self.imm_pars[key] = dict(form='exp_decay', pars={'init_val':1., 'half_life':None}) + ts = sim['total_strains'] - self.initialized = True + if self.imm_pars is None: + errormsg = f'Did not provide parameters for this vaccine' + raise ValueError(errormsg) - def init_vaccine_immunity(self, sim): - ''' Initialize vaccine immunity and pre-loaded immune_degree with all strains that will eventually be in the sim''' - ts = sim['total_strains'] - immunity = {} + if self.rel_imm is None: + errormsg = f'Did not provide rel_imm parameters for this vaccine' + raise ValueError(errormsg) - # If immunity values have been provided, process them - if not hasattr(self, 'vaccine_immunity'): - # Initialize immunity - for ax in cvd.immunity_axes: - immunity[ax] = np.full(ts, 1, dtype=cvd.default_float) - self.vaccine_immunity = immunity + correct_size = self.rel_imm.shape == ts + if not correct_size: + errormsg = f'Did not provide relative immunity for each strain' + raise ValueError(errormsg) - else: - if sc.checktype(self.vaccine_immunity['sus'], 'arraylike'): - correct_size = self.vaccine_immunity['sus']['sus'].shape == (ts) - if not correct_size: - errormsg = f'Wrong dimensions for immunity["sus"]: you provided a matrix sized {self.vaccine_immunity["sus"].shape}, but it should be sized {(ts)}' - raise ValueError(errormsg) - else: - errormsg = f'Type of immunity["sus"] not understood: you provided {type(self.vaccine_immunity["sus"])}, but it should be an array or dict.' + # Validate immunity pars (make sure there are values for all cvd.immunity_axes) + for key in cvd.immunity_axes: + if key not in self.imm_pars: + errormsg = f'Immunity pars for vaccine for {key} not provided' raise ValueError(errormsg) + ''' Initialize immune_degree with all strains that will eventually be in the sim''' + doses = self.doses + # Precompute waning - immune_degree = [] # Stored as a list by strain - for s in range(ts): + immune_degree = [] # Stored as a list by dose + for dose in range(doses): strain_immune_degree = {} for ax in cvd.immunity_axes: - strain_immune_degree[ax] = pre_compute_waning(sim['n_days'], **self.imm_pars[s][ax]) + strain_immune_degree[ax] = pre_compute_waning(sim['n_days'], **self.imm_pars[ax][dose]) immune_degree.append(strain_immune_degree) self.vaccine_immune_degree = immune_degree - return + # %% Immunity methods __all__ += ['init_immunity', 'pre_compute_waning', 'init_vaccine_immunity'] diff --git a/covasim/interventions.py b/covasim/interventions.py index fcfd99df7..ba7c8e861 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1164,7 +1164,9 @@ def initialize(self, sim): self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person self.vaccination_dates = [[] for _ in range(sim.n)] # Store the dates when people are vaccinated self.vaccine_ind = len(sim['vaccines']) - sim['vaccines'].append(cvi.Vaccine(self.vaccine_pars, self.vaccine_label)) + vaccine = cvi.Vaccine(self.vaccine_pars, self.vaccine_label) + sim['vaccines'].append(vaccine) + vaccine.initialize(sim) self.initialized = True return @@ -1182,12 +1184,11 @@ def apply(self, sim): vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated - self.vaccinations[vacc_inds] += 1 - for v_ind in vacc_inds: - self.vaccination_dates[v_ind].append(sim.t) + self.vaccination_dates[vacc_inds] = sim.t # Update vaccine attributes in sim + sim.people.vaccinated[vacc_inds] = True sim.people.vaccine_source[vacc_inds] = self.vaccine_ind sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] sim.people.vaccination_dates = self.vaccination_dates[vacc_inds] diff --git a/covasim/parameters.py b/covasim/parameters.py index 13f3db9d0..e3eb80e18 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -64,7 +64,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['n_strains'] = 1 # The number of strains currently circulating in the population pars['total_strains'] = None # Set during sim initialization, once strains have been specified and processed pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor - pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by initialize_immunity() below + pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py pars['immune_degree'] = None # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains diff --git a/covasim/sim.py b/covasim/sim.py index 2356d861c..8590dcf31 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -552,9 +552,23 @@ def step(self): # Iterate through n_strains to calculate infections for strain in range(ns): + immunity_factors = np.full(len(people), 0, dtype=cvd.default_float, order='F') + # Determine who is currently infected and cannot get another infection inf_inds = cvu.false(sus) + # Determine who is vaccinated and has some immunity from vaccine + vaccinated = people.vaccinated + vacc_inds = cvu.true(vaccinated) + vacc_inds = np.setdiff1d(vacc_inds, inf_inds) + if len(vacc_inds): + date_vacc = people.vaccination_dates + vaccine_scale_factor = np.full(len(vacc_inds), self['vaccines'][vacc_inds.vaccine_source]['rel_imm_by_strain'][strain]) + doses = people.vaccinations[vacc_inds] + vaccine_time = cvd.default_int(t - date_vacc[vacc_inds]) + vaccine_immunity = self['vaccines'][vacc_inds.vaccine_source]['vaccine_immune_degree'][doses]['sus'] + immunity_factors[vacc_inds] = vaccine_scale_factor * vaccine_immunity[vaccine_time] + # Deal with strain parameters for key in strain_parkeys: strain_pars[key] = self[key][strain] @@ -565,6 +579,7 @@ def step(self): immune = people.recovered_strain == strain immune_inds = cvu.true(immune) # Whether people have some immunity to this strain from a prior infection with this strain immune_inds = np.setdiff1d(immune_inds, inf_inds) + immune_inds = np.setdiff1d(immune_inds, vacc_inds) immunity_scale_factor = np.full(len(immune_inds), self['immunity']['sus'][strain,strain]) # Process cross-immunity parameters and indices, if relevant @@ -575,6 +590,7 @@ def step(self): cross_immune_inds = cvu.true(cross_immune) # People with some immunity to this strain from a prior infection with another strain cross_immune_inds = np.setdiff1d(cross_immune_inds, inf_inds) # remove anyone who is currently exposed cross_immune_inds = np.setdiff1d(cross_immune_inds, immune_inds) # remove anyone who has own-immunity, that supercedes cross-immunity + cross_immune_inds = np.setdiff1d(cross_immune_inds, vacc_inds) cross_immunity = np.full(len(cross_immune_inds), self['immunity']['sus'][strain, cross_strain]) immune_inds = np.concatenate((immune_inds, cross_immune_inds)) immunity_scale_factor = np.concatenate((immunity_scale_factor, cross_immunity)) @@ -583,7 +599,6 @@ def step(self): prior_symptoms = people.prior_symptoms[immune_inds] # Compute immunity to susceptibility - immunity_factors = np.full(len(people), 0, dtype=cvd.default_float, order='F') if len(immune_inds): immunity_factors[immune_inds] = self['immune_degree'][strain]['sus'][immune_time] * prior_symptoms * immunity_scale_factor From 67f8660526b07a99f375610a7ab9948baf13e439 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 3 Mar 2021 18:13:37 -0500 Subject: [PATCH 125/569] for cliff to review --- covasim/defaults.py | 1 + covasim/immunity.py | 14 +++++++++----- covasim/interventions.py | 27 ++++++++++++++------------- covasim/sim.py | 9 +++++---- tests/devtests/test_variants.py | 30 +++++++++++++++++++++++++----- 5 files changed, 54 insertions(+), 27 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index c45a55794..8e14cd4f4 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -52,6 +52,7 @@ class PeopleMeta(sc.prettyobj): 'trans_immunity_factors', # Float 'prog_immunity_factors', # Float 'vaccinations', # Number of doses given per person + 'vaccine_source' # index of vaccine that individual received ] diff --git a/covasim/immunity.py b/covasim/immunity.py index 6d2c7c85a..26392a989 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -207,6 +207,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['imm_pars'][ax] = [dict(form='exp_decay', pars={'init_val': 0.5, 'half_life': 30}), dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})] vaccine_pars['doses'] = 2 + vaccine_pars['label'] = vaccine # Known parameters on moderna elif vaccine in choices['moderna']: @@ -216,6 +217,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['imm_pars'][ax] = [dict(form='exp_decay', pars={'init_val': 0.5, 'half_life': 30}), dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})] vaccine_pars['doses'] = 2 + vaccine_pars['label'] = vaccine # Known parameters on az elif vaccine in choices['az']: @@ -225,6 +227,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['imm_pars'][ax] = [dict(form='exp_decay', pars={'init_val': 0.5, 'half_life': 30}), dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})] vaccine_pars['doses'] = 2 + vaccine_pars['label'] = vaccine # Known parameters on j&j elif vaccine in choices['j&j']: @@ -233,6 +236,7 @@ def parse_vaccine_pars(self, vaccine=None): for ax in cvd.immunity_axes: vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 120}) vaccine_pars['doses'] = 1 + vaccine_pars['label'] = vaccine else: choicestr = '\n'.join(choices.values()) @@ -258,10 +262,10 @@ def initialize(self, sim): raise ValueError(errormsg) if self.rel_imm is None: - errormsg = f'Did not provide rel_imm parameters for this vaccine' - raise ValueError(errormsg) + print(f'Did not provide rel_imm parameters for this vaccine, assuming all the same') + self.rel_imm = [1]*ts - correct_size = self.rel_imm.shape == ts + correct_size = len(self.rel_imm) == ts if not correct_size: errormsg = f'Did not provide relative immunity for each strain' raise ValueError(errormsg) @@ -272,7 +276,7 @@ def initialize(self, sim): errormsg = f'Immunity pars for vaccine for {key} not provided' raise ValueError(errormsg) - ''' Initialize immune_degree with all strains that will eventually be in the sim''' + ''' Initialize immune_degree''' doses = self.doses # Precompute waning @@ -287,7 +291,7 @@ def initialize(self, sim): # %% Immunity methods -__all__ += ['init_immunity', 'pre_compute_waning', 'init_vaccine_immunity'] +__all__ += ['init_immunity', 'pre_compute_waning'] def init_immunity(sim, create=False): diff --git a/covasim/interventions.py b/covasim/interventions.py index ba7c8e861..751a1c60c 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1137,8 +1137,7 @@ class vaccinate(Intervention): Args: days (int or array): the day or array of days to apply the interventions prob (float): probability of being vaccinated (i.e., fraction of the population) - vaccine_pars (dict): passed to Vaccine() - vaccine_label (str): passed to Vaccine() + vaccine_pars (dict or label): passed to Vaccine() subtarget (dict): subtarget intervention to people with particular indices (see test_num() for details) kwargs (dict): passed to Intervention() @@ -1146,14 +1145,13 @@ class vaccinate(Intervention): interv = cv.vaccine(days=50, prob=0.3, ) ''' - def __init__(self, days, prob=1.0, vaccine_pars=None, subtarget=None, vaccine_label=None, **kwargs): + def __init__(self, days, prob=1.0, vaccine_pars=None, subtarget=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object self._store_args() # Store the input arguments so the intervention can be recreated self.days = sc.dcp(days) - self.prob = prob - self.vaccine_pars = vaccine_pars - self.vaccine_label = vaccine_label + self.prob = sc.promotetolist(prob) self.subtarget = subtarget + self.vaccine_pars = vaccine_pars self.vaccine_ind = None return @@ -1162,9 +1160,11 @@ def initialize(self, sim): ''' Fix the dates and store the vaccinations ''' self.days = process_days(sim, self.days) self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person - self.vaccination_dates = [[] for _ in range(sim.n)] # Store the dates when people are vaccinated + self.vaccination_dates = np.full(sim.n, np.nan) # Store the dates when people are vaccinated + if self.subtarget is not None: + self.subtarget = sc.promotetolist(self.subtarget) self.vaccine_ind = len(sim['vaccines']) - vaccine = cvi.Vaccine(self.vaccine_pars, self.vaccine_label) + vaccine = cvi.Vaccine(self.vaccine_pars) sim['vaccines'].append(vaccine) vaccine.initialize(sim) self.initialized = True @@ -1175,12 +1175,12 @@ def apply(self, sim): ''' Perform vaccination ''' # If this day is found in the list, apply the intervention - for _ in find_day(self.days, sim.t): + for day in find_day(self.days, sim.t): - # Construct the vaccine probabilities piece by piece -- complicated, since need to do it in the right order - vacc_probs = np.full(sim.n, self.prob) # Begin by assigning equal vaccination probability to everyone + # Determine who gets vaccinated today + vacc_probs = np.full(sim.n, self.prob[day]) # Begin by assigning equal vaccination probability to everyone if self.subtarget is not None: - subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) + subtarget_inds, subtarget_vals = get_subtargets(self.subtarget[day], sim) vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated @@ -1191,6 +1191,7 @@ def apply(self, sim): sim.people.vaccinated[vacc_inds] = True sim.people.vaccine_source[vacc_inds] = self.vaccine_ind sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] - sim.people.vaccination_dates = self.vaccination_dates[vacc_inds] + sim.people.date_vaccinated[vacc_inds] = self.vaccination_dates[vacc_inds] + return \ No newline at end of file diff --git a/covasim/sim.py b/covasim/sim.py index 8590dcf31..9808b5471 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -108,12 +108,12 @@ def initialize(self, reset=False, **kwargs): self.t = 0 # The current time index self.validate_pars() # Ensure parameters have valid values self.set_seed() # Reset the random seed before the population is created - self.init_interventions() # Initialize the interventions... - self.init_analyzers() # ...and the analyzers... self.init_strains() # ...and the strains.... self.init_immunity() # ... and information about immunity/cross-immunity. self.init_results() # After initializing the strain, create the results structure self.init_people(save_pop=self.save_pop, load_pop=self.load_pop, popfile=self.popfile, reset=reset, **kwargs) # Create all the people (slow) + self.init_interventions() # Initialize the interventions... + self.init_analyzers() # ...and the analyzers... self.validate_layer_pars() # Once the population is initialized, validate the layer parameters again self.set_seed() # Reset the random seed again so the random number stream is consistent self.initialized = True @@ -562,8 +562,9 @@ def step(self): vacc_inds = cvu.true(vaccinated) vacc_inds = np.setdiff1d(vacc_inds, inf_inds) if len(vacc_inds): - date_vacc = people.vaccination_dates - vaccine_scale_factor = np.full(len(vacc_inds), self['vaccines'][vacc_inds.vaccine_source]['rel_imm_by_strain'][strain]) + date_vacc = people.date_vaccinated + # HOW DO I DO THIS WITHOUT A LIST OPERATION + vaccine_scale_factor = np.full(len(vacc_inds), self['vaccines'][people.vaccine_source[vacc_inds]]['rel_imm'][strain]) doses = people.vaccinations[vacc_inds] vaccine_time = cvd.default_int(t - date_vacc[vacc_inds]) vaccine_immunity = self['vaccines'][vacc_inds.vaccine_source]['vaccine_immune_degree'][doses]['sus'] diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 524972fef..04f993731 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -9,6 +9,25 @@ do_show = 0 do_save = 1 +def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): + sc.heading('Run a basic sim with 1 strain, moderna vaccine') + + sc.heading('Setting up...') + + pfizer = cv.vaccinate(days=10, vaccine_pars='pfizer') + sim = cv.Sim(interventions=[pfizer]) + sim.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'Cumulative infections': ['cum_infections'], + 'New reinfections': ['new_reinfections'], + # 'Cumulative reinfections': ['cum_reinfections'], + }) + if do_plot: + sim.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_vaccine_1strain.png', to_plot=to_plot) + return sim + def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): sc.heading('Run a basic sim with 1 strain, varying reinfection risk') @@ -276,16 +295,17 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab sc.tic() # Run simplest possible test - if 0: - sim = cv.Sim() - sim.run() + # if 0: + # sim = cv.Sim() + # sim.run() # Run more complex tests - sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim0 = test_vaccine_1strain() + # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) #sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) #sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) #sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - #scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) #scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) sc.toc() From adf14f815c7969148cebd0f6ab1e3209a64cebc4 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 3 Mar 2021 21:01:44 -0500 Subject: [PATCH 126/569] functional vaccines!! --- covasim/interventions.py | 3 +-- covasim/parameters.py | 2 ++ covasim/people.py | 18 ++++++++++++++++-- covasim/sim.py | 33 +++++++++++++++++++++++++++------ tests/devtests/test_variants.py | 2 +- 5 files changed, 47 insertions(+), 11 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 751a1c60c..e9faf1aea 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1155,7 +1155,6 @@ def __init__(self, days, prob=1.0, vaccine_pars=None, subtarget=None, **kwargs): self.vaccine_ind = None return - def initialize(self, sim): ''' Fix the dates and store the vaccinations ''' self.days = process_days(sim, self.days) @@ -1165,8 +1164,8 @@ def initialize(self, sim): self.subtarget = sc.promotetolist(self.subtarget) self.vaccine_ind = len(sim['vaccines']) vaccine = cvi.Vaccine(self.vaccine_pars) - sim['vaccines'].append(vaccine) vaccine.initialize(sim) + sim['vaccines'].append(vaccine) self.initialized = True return diff --git a/covasim/parameters.py b/covasim/parameters.py index e3eb80e18..d8ff61eea 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -66,6 +66,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py pars['immune_degree'] = None + pars['vaccine_info'] = None # Vaccine info in a more easily accessible format # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 @@ -109,6 +110,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['analyzers'] = [] # Custom analysis functions; populated by the user pars['strains'] = [] # Additional strains of the virus; populated by the user, see immunity.py pars['vaccines'] = [] # Vaccines that are being used; populated by user + pars['timelimit'] = None # Time limit for the simulation (seconds) pars['stopping_func'] = None # A function to call to stop the sim partway through diff --git a/covasim/people.py b/covasim/people.py index eea8b49e9..a39dbaaf9 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -402,14 +402,28 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str n_infections = len(inds) durpars = infect_pars['dur'] - # determine people with immunity from this strain and then calculate immunity to trans/prog + # Determine who is vaccinated and has some immunity from vaccine + vaccinated = self.vaccinated + vacc_inds = cvu.true(vaccinated) + if len(vacc_inds): + vaccine_info = self['pars']['vaccine_info'] + date_vacc = self.date_vaccinated + vaccine_source = cvd.default_int(self.vaccine_source[vacc_inds]) + vaccine_scale_factor = vaccine_info['rel_imm'][vaccine_source, strain] + doses = cvd.default_int(self.vaccinations[vacc_inds]) + vaccine_time = cvd.default_int(self.t - date_vacc[vacc_inds]) + self.trans_immunity_factors[strain, vacc_inds] = vaccine_scale_factor * vaccine_info['vaccine_immune_degree']['trans'][vaccine_source, doses, vaccine_time] + self.prog_immunity_factors[strain, vacc_inds] = vaccine_scale_factor * vaccine_info['vaccine_immune_degree']['prog'][vaccine_source, doses, vaccine_time] + + # determine people with immunity from this strain date_rec = self.date_recovered immune = self.recovered_strain[inds] == strain immune_inds = cvu.itrue(immune, inds) # Whether people have some immunity to this strain from a prior infection with this strain + immune_inds = np.setdiff1d(immune_inds, vacc_inds) immune_time = cvd.default_int(self.t - date_rec[immune_inds]) # Time since recovery for people who were last infected by this strain prior_symptoms = self.prior_symptoms[immune_inds] - if len(immune_inds)>0: + if len(immune_inds): self.trans_immunity_factors[strain, immune_inds] = self['pars']['immune_degree'][strain]['trans'][immune_time] * \ prior_symptoms * self.pars['immunity']['trans'][strain] self.prog_immunity_factors[strain, immune_inds] = self['pars']['immune_degree'][strain]['prog'][immune_time] * \ diff --git a/covasim/sim.py b/covasim/sim.py index 9808b5471..129141f3c 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -113,6 +113,7 @@ def initialize(self, reset=False, **kwargs): self.init_results() # After initializing the strain, create the results structure self.init_people(save_pop=self.save_pop, load_pop=self.load_pop, popfile=self.popfile, reset=reset, **kwargs) # Create all the people (slow) self.init_interventions() # Initialize the interventions... + self.init_vaccines() # Initialize vaccine information self.init_analyzers() # ...and the analyzers... self.validate_layer_pars() # Once the population is initialized, validate the layer parameters again self.set_seed() # Reset the random seed again so the random number stream is consistent @@ -459,6 +460,25 @@ def init_immunity(self, create=False): cvimm.init_immunity(self, create=create) return + def init_vaccines(self): + ''' Check if there are any vaccines in simulation, if so initialize vaccine info param''' + if len(self['vaccines']): + nv = len(self['vaccines']) + ns = self['total_strains'] + nd = 2 + days = self['n_days'] + self['vaccine_info'] = {} + self['vaccine_info']['rel_imm'] = np.full((nv, ns), np.nan, dtype=cvd.default_float) + self['vaccine_info']['vaccine_immune_degree'] = {} + for ax in cvd.immunity_axes: + self['vaccine_info']['vaccine_immune_degree'][ax] =np.full((nv, nd, days), np.nan, dtype=cvd.default_float) + + for ind, vacc in enumerate(self['vaccines']): + self['vaccine_info']['rel_imm'][ind,:] = vacc.rel_imm + for dose in range(vacc.doses): + for ax in cvd.immunity_axes: + self['vaccine_info']['vaccine_immune_degree'][ax][ind, dose, :] = vacc.vaccine_immune_degree[dose][ax] + return def rescale(self): ''' Dynamically rescale the population -- used during step() ''' @@ -552,7 +572,7 @@ def step(self): # Iterate through n_strains to calculate infections for strain in range(ns): - immunity_factors = np.full(len(people), 0, dtype=cvd.default_float, order='F') + immunity_factors = np.zeros(len(people), dtype=cvd.default_float) # Determine who is currently infected and cannot get another infection inf_inds = cvu.false(sus) @@ -562,13 +582,14 @@ def step(self): vacc_inds = cvu.true(vaccinated) vacc_inds = np.setdiff1d(vacc_inds, inf_inds) if len(vacc_inds): + vaccine_info = self['vaccine_info'] date_vacc = people.date_vaccinated - # HOW DO I DO THIS WITHOUT A LIST OPERATION - vaccine_scale_factor = np.full(len(vacc_inds), self['vaccines'][people.vaccine_source[vacc_inds]]['rel_imm'][strain]) - doses = people.vaccinations[vacc_inds] + vaccine_source = cvd.default_int(people.vaccine_source[vacc_inds]) + vaccine_scale_factor = vaccine_info['rel_imm'][vaccine_source, strain] + doses = cvd.default_int(people.vaccinations[vacc_inds]) vaccine_time = cvd.default_int(t - date_vacc[vacc_inds]) - vaccine_immunity = self['vaccines'][vacc_inds.vaccine_source]['vaccine_immune_degree'][doses]['sus'] - immunity_factors[vacc_inds] = vaccine_scale_factor * vaccine_immunity[vaccine_time] + vaccine_immunity = vaccine_info['vaccine_immune_degree']['sus'][vaccine_source, doses, vaccine_time] + immunity_factors[vacc_inds] = vaccine_scale_factor * vaccine_immunity # Deal with strain parameters for key in strain_parkeys: diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 04f993731..eca8e46be 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -14,7 +14,7 @@ def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): sc.heading('Setting up...') - pfizer = cv.vaccinate(days=10, vaccine_pars='pfizer') + pfizer = cv.vaccinate(days=10, vaccine_pars='pfizer', prob = 0.15) sim = cv.Sim(interventions=[pfizer]) sim.run() From ca734ddb664677ec3a08cdbe840863004a8b160a Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 3 Mar 2021 21:20:18 -0500 Subject: [PATCH 127/569] updated tests --- tests/devtests/test_variants.py | 35 +++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index eca8e46be..667a4b3ca 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -6,15 +6,15 @@ do_plot = 1 -do_show = 0 -do_save = 1 +do_show = 1 +do_save = 0 -def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): - sc.heading('Run a basic sim with 1 strain, moderna vaccine') +def test_vaccine_1strain(do_plot=True, do_show=True, do_save=False): + sc.heading('Run a basic sim with 1 strain, pfizer vaccine') sc.heading('Setting up...') - pfizer = cv.vaccinate(days=10, vaccine_pars='pfizer', prob = 0.15) + pfizer = cv.vaccinate(days=10, vaccine_pars='pfizer', prob = 0.5) sim = cv.Sim(interventions=[pfizer]) sim.run() @@ -29,6 +29,27 @@ def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): return sim +def test_vaccine_2strains(do_plot=True, do_show=True, do_save=False): + sc.heading('Run a basic sim with b117 strain on day 10, pfizer vaccine day 20') + + sc.heading('Setting up...') + + b117 = cv.Strain('b117', days=10, n_imports=20) + pfizer = cv.vaccinate(days=20, vaccine_pars='pfizer', prob = 0.5) + sim = cv.Sim(strains=[b117], interventions=[pfizer]) + sim.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'Cumulative infections': ['cum_infections'], + 'New reinfections': ['new_reinfections'], + # 'Cumulative reinfections': ['cum_reinfections'], + }) + if do_plot: + sim.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_vaccine_2strain.png', to_plot=to_plot) + return sim + + def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): sc.heading('Run a basic sim with 1 strain, varying reinfection risk') sc.heading('Setting up...') @@ -300,7 +321,6 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab # sim.run() # Run more complex tests - sim0 = test_vaccine_1strain() # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) #sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) #sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) @@ -308,6 +328,9 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) #scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + # Run Vaccine tests + sim5 = test_vaccine_1strain() + sim6 = test_vaccine_2strains() sc.toc() From 79550ba4bb56f953192cd28b58a80e5bd3eea5a5 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 4 Mar 2021 15:00:41 -0500 Subject: [PATCH 128/569] vaccine subtarget working! thanks @cliff --- covasim/immunity.py | 37 +++++++++++++++++++++++----- covasim/interventions.py | 43 ++++++++++++++++++++++++--------- covasim/people.py | 4 +-- covasim/sim.py | 2 +- tests/devtests/test_variants.py | 32 +++++++++++++++++++++--- 5 files changed, 93 insertions(+), 25 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 26392a989..487142a89 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -178,6 +178,7 @@ def __init__(self, vaccine=None): self.vaccine_immune_degree = None # dictionary of pre-loaded decay to by imm_axis and dose self.rel_imm = None # list of length total_strains with relative immunity factor self.doses = None + self.interval = None self.imm_pars = None self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine) for par, val in self.vaccine_pars.items(): @@ -204,9 +205,10 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars = dict() vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = [dict(form='exp_decay', pars={'init_val': 0.5, 'half_life': 30}), + vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 0.5/22}), dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})] vaccine_pars['doses'] = 2 + vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine # Known parameters on moderna @@ -214,9 +216,10 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars = dict() vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = [dict(form='exp_decay', pars={'init_val': 0.5, 'half_life': 30}), + vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 0.5/29}), dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})] vaccine_pars['doses'] = 2 + vaccine_pars['interval'] = 29 vaccine_pars['label'] = vaccine # Known parameters on az @@ -224,9 +227,10 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars = dict() vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = [dict(form='exp_decay', pars={'init_val': 0.5, 'half_life': 30}), + vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 0.5/22}), dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})] vaccine_pars['doses'] = 2 + vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine # Known parameters on j&j @@ -234,8 +238,9 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars = dict() vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 120}) + vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180}) vaccine_pars['doses'] = 1 + vaccine_pars['interval'] = None vaccine_pars['label'] = vaccine else: @@ -356,7 +361,8 @@ def pre_compute_waning(length, form, pars): choices = [ 'exp_decay', 'logistic_decay', - 'linear', + 'linear_growth', + 'linear_decay' ] # Process inputs @@ -367,6 +373,12 @@ def pre_compute_waning(length, form, pars): elif form == 'logistic_decay': output = logistic_decay(length, **pars) + elif form == 'linear_growth': + output = linear_growth(length, **pars) + + elif form == 'linear_decay': + output = linear_decay(length, **pars) + else: choicestr = '\n'.join(choices) errormsg = f'The selected functional form "{form}" is not implemented; choices are: {choicestr}' @@ -375,7 +387,7 @@ def pre_compute_waning(length, form, pars): return output -# SPecific waning functions are listed here +# Specific waning and growth functions are listed here def exp_decay(length, init_val, half_life): ''' Returns an array of length t with values for the immunity at each time step after recovery @@ -390,3 +402,16 @@ def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp): t = np.arange(length, dtype=cvd.default_int) return (init_val + (lower_asymp - init_val) / ( 1 + (t / half_val) ** decay_rate)) # TODO: make this robust to /0 errors + +def linear_decay(length, init_val, slope): + ''' Calculate linear decay ''' + t = np.arange(length, dtype=cvd.default_int) + result = init_val - slope*t + if result < 0: + result = 0 + return result + +def linear_growth(length, slope): + ''' Calculate linear growth ''' + t = np.arange(length, dtype=cvd.default_int) + return (slope * t) \ No newline at end of file diff --git a/covasim/interventions.py b/covasim/interventions.py index e9faf1aea..740a16724 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1149,7 +1149,7 @@ def __init__(self, days, prob=1.0, vaccine_pars=None, subtarget=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object self._store_args() # Store the input arguments so the intervention can be recreated self.days = sc.dcp(days) - self.prob = sc.promotetolist(prob) + self.prob = prob self.subtarget = subtarget self.vaccine_pars = vaccine_pars self.vaccine_ind = None @@ -1157,15 +1157,20 @@ def __init__(self, days, prob=1.0, vaccine_pars=None, subtarget=None, **kwargs): def initialize(self, sim): ''' Fix the dates and store the vaccinations ''' - self.days = process_days(sim, self.days) + self.first_dose_days = process_days(sim, self.days) + self.vaccinated = [None]*len(self.first_dose_days) # keep track of inds of people vaccinated on each day self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person self.vaccination_dates = np.full(sim.n, np.nan) # Store the dates when people are vaccinated - if self.subtarget is not None: - self.subtarget = sc.promotetolist(self.subtarget) self.vaccine_ind = len(sim['vaccines']) vaccine = cvi.Vaccine(self.vaccine_pars) vaccine.initialize(sim) sim['vaccines'].append(vaccine) + self.doses = vaccine.doses + self.interval = vaccine.interval + if self.interval is not None: + self.second_dose_days = self.first_dose_days + self.interval + else: + self.second_dose_days = [] self.initialized = True return @@ -1174,23 +1179,37 @@ def apply(self, sim): ''' Perform vaccination ''' # If this day is found in the list, apply the intervention - for day in find_day(self.days, sim.t): + for day in find_day(self.first_dose_days, sim.t): # Determine who gets vaccinated today - vacc_probs = np.full(sim.n, self.prob[day]) # Begin by assigning equal vaccination probability to everyone + if self.subtarget is not None: - subtarget_inds, subtarget_vals = get_subtargets(self.subtarget[day], sim) + subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) + vacc_probs = np.zeros(sim.n)# Begin by assigning equal vaccination probability to everyone vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted + else: + vacc_probs = np.full(sim.n, self.prob) # Assign equal vaccination probability to everyone vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated - - self.vaccinations[vacc_inds] += 1 - self.vaccination_dates[vacc_inds] = sim.t + self.vaccinated[day] = vacc_inds # Update vaccine attributes in sim sim.people.vaccinated[vacc_inds] = True sim.people.vaccine_source[vacc_inds] = self.vaccine_ind - sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] - sim.people.date_vaccinated[vacc_inds] = self.vaccination_dates[vacc_inds] + self.update_vaccine_info(sim, vacc_inds) + + for day2 in find_day(self.second_dose_days, sim.t): + # Determine who gets vaccinated today + vacc_inds = self.vaccinated[day2-1] + self.update_vaccine_info(sim, vacc_inds) + + + return + def update_vaccine_info(self, sim, vacc_inds): + self.vaccinations[vacc_inds] += 1 + self.vaccination_dates[vacc_inds] = sim.t + # Update vaccine attributes in sim + sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] + sim.people.date_vaccinated[vacc_inds] = self.vaccination_dates[vacc_inds] return \ No newline at end of file diff --git a/covasim/people.py b/covasim/people.py index a39dbaaf9..ff1639dc1 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -412,8 +412,8 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str vaccine_scale_factor = vaccine_info['rel_imm'][vaccine_source, strain] doses = cvd.default_int(self.vaccinations[vacc_inds]) vaccine_time = cvd.default_int(self.t - date_vacc[vacc_inds]) - self.trans_immunity_factors[strain, vacc_inds] = vaccine_scale_factor * vaccine_info['vaccine_immune_degree']['trans'][vaccine_source, doses, vaccine_time] - self.prog_immunity_factors[strain, vacc_inds] = vaccine_scale_factor * vaccine_info['vaccine_immune_degree']['prog'][vaccine_source, doses, vaccine_time] + self.trans_immunity_factors[strain, vacc_inds] = vaccine_scale_factor * vaccine_info['vaccine_immune_degree']['trans'][vaccine_source, doses-1, vaccine_time] + self.prog_immunity_factors[strain, vacc_inds] = vaccine_scale_factor * vaccine_info['vaccine_immune_degree']['prog'][vaccine_source, doses-1, vaccine_time] # determine people with immunity from this strain date_rec = self.date_recovered diff --git a/covasim/sim.py b/covasim/sim.py index 129141f3c..d8dcc6a61 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -588,7 +588,7 @@ def step(self): vaccine_scale_factor = vaccine_info['rel_imm'][vaccine_source, strain] doses = cvd.default_int(people.vaccinations[vacc_inds]) vaccine_time = cvd.default_int(t - date_vacc[vacc_inds]) - vaccine_immunity = vaccine_info['vaccine_immune_degree']['sus'][vaccine_source, doses, vaccine_time] + vaccine_immunity = vaccine_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, vaccine_time] immunity_factors[vacc_inds] = vaccine_scale_factor * vaccine_immunity # Deal with strain parameters diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 667a4b3ca..a96e07358 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -14,8 +14,15 @@ def test_vaccine_1strain(do_plot=True, do_show=True, do_save=False): sc.heading('Setting up...') - pfizer = cv.vaccinate(days=10, vaccine_pars='pfizer', prob = 0.5) - sim = cv.Sim(interventions=[pfizer]) + sim = cv.Sim() + # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 + sim.vxsubtarg = sc.objdict() + sim.vxsubtarg.age = [75, 65, 50, 18] + sim.vxsubtarg.prob = [.5, .5, .5, .5] + sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] + pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) + sim['interventions'] += [pfizer] + sim.initialize() sim.run() to_plot = sc.objdict({ @@ -34,9 +41,16 @@ def test_vaccine_2strains(do_plot=True, do_show=True, do_save=False): sc.heading('Setting up...') + sim = cv.Sim() + # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 + sim.vxsubtarg = sc.objdict() + sim.vxsubtarg.age = [75, 65, 50, 18] + sim.vxsubtarg.prob = [.5, .5, .5, .5] + sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] + pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) b117 = cv.Strain('b117', days=10, n_imports=20) - pfizer = cv.vaccinate(days=20, vaccine_pars='pfizer', prob = 0.5) - sim = cv.Sim(strains=[b117], interventions=[pfizer]) + sim['strains'] = [b117] + sim['interventions'] = pfizer sim.run() to_plot = sc.objdict({ @@ -311,6 +325,16 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab return +def vacc_subtarg(sim): + ''' Subtarget by age''' + ind = sim.vxsubtarg.days.index(sim.t) + age = sim.vxsubtarg.age[ind] + prob = sim.vxsubtarg.prob[ind] + inds = sc.findinds((sim.people.age>=age) * ~sim.people.vaccinated) + vals = prob*np.ones(len(inds)) + return {'inds':inds, 'vals':vals} + + #%% Run as a script if __name__ == '__main__': sc.tic() From 39aefa396207099e85cd91139a0148949ab2c15e Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 4 Mar 2021 15:59:02 -0500 Subject: [PATCH 129/569] pre loading more vaccine info for known strains --- covasim/immunity.py | 86 +++++++++++++++++++++++++++------ covasim/interventions.py | 1 - covasim/sim.py | 3 -- tests/devtests/test_variants.py | 2 +- 4 files changed, 73 insertions(+), 19 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 487142a89..732a54cdd 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -180,11 +180,36 @@ def __init__(self, vaccine=None): self.doses = None self.interval = None self.imm_pars = None + self.vaccine_strain_info = self.init_strain_vaccine_info() self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine) for par, val in self.vaccine_pars.items(): setattr(self, par, val) return + def init_strain_vaccine_info(self): + + rel_imm = {} + rel_imm['known_vaccines'] = ['pfizer', 'moderna', 'az', 'j&j'] + rel_imm['known_strains'] = ['wild', 'b117', 'b1351', 'p1'] + for vx in rel_imm['known_vaccines']: + rel_imm[vx] = {} + rel_imm[vx]['wild'] = 1 + rel_imm[vx]['b117'] = 1 + + rel_imm['pfizer']['b1351'] = 1 + rel_imm['pfizer']['p1'] = 1 + + rel_imm['moderna']['b1351'] = 1 + rel_imm['moderna']['p1'] = 1 + + rel_imm['az']['b1351'] = 1 + rel_imm['az']['p1'] = 1 + + rel_imm['j&j']['b1351'] = 1 + rel_imm['j&j']['p1'] = 1 + + return rel_imm + def parse_vaccine_pars(self, vaccine=None): ''' Unpack vaccine information, which may be given in different ways''' @@ -206,7 +231,8 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 0.5/22}), - dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})] + dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 30, + 'lower_asymp': 0.3, 'decay_rate': -5})] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -217,7 +243,9 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 0.5/29}), - dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})] + dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 30, + 'lower_asymp': 0.3, + 'decay_rate': -5})] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 29 vaccine_pars['label'] = vaccine @@ -228,7 +256,9 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 0.5/22}), - dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})] + dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 30, + 'lower_asymp': 0.3, + 'decay_rate': -5})] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -238,7 +268,12 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars = dict() vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180}) + if ax == 'sus': + vaccine_pars['imm_pars'][ax] = [dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 30, + 'lower_asymp': 0.3, 'decay_rate': -5, + 'delay': 30})] + else: + vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180}) vaccine_pars['doses'] = 1 vaccine_pars['interval'] = None vaccine_pars['label'] = vaccine @@ -261,14 +296,22 @@ def parse_vaccine_pars(self, vaccine=None): def initialize(self, sim): ts = sim['total_strains'] + circulating_strains = ['wild'] # assume wild is circulating + for strain in ts: + circulating_strains.append(sim['strains'][strain].strain_label) if self.imm_pars is None: errormsg = f'Did not provide parameters for this vaccine' raise ValueError(errormsg) if self.rel_imm is None: - print(f'Did not provide rel_imm parameters for this vaccine, assuming all the same') - self.rel_imm = [1]*ts + print(f'Did not provide rel_imm parameters for this vaccine, trying to find values') + self.rel_imm = [] + for strain in circulating_strains: + if strain in self.vaccine_strain_info['known_strains']: + self.rel_imm.append(self.vaccine_strain_info[self.label][strain]) + else: + self.rel_imm.append(1) correct_size = len(self.rel_imm) == ts if not correct_size: @@ -388,20 +431,35 @@ def pre_compute_waning(length, form, pars): # Specific waning and growth functions are listed here -def exp_decay(length, init_val, half_life): +def exp_decay(length, init_val, half_life, delay=None): ''' Returns an array of length t with values for the immunity at each time step after recovery ''' decay_rate = np.log(2) / half_life if ~np.isnan(half_life) else 0. - t = np.arange(length, dtype=cvd.default_int) - return init_val * np.exp(-decay_rate * t) - + if delay is not None: + t = np.arange(length-delay, dtype=cvd.default_int) + growth = linear_growth(delay, init_val/delay) + decay = init_val * np.exp(-decay_rate * t) + result = np.concatenate(growth, decay, axis=None) + else: + t = np.arange(length, dtype=cvd.default_int) + result = init_val * np.exp(-decay_rate * t) + return result -def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp): +def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp, delay=None): ''' Calculate logistic decay ''' - t = np.arange(length, dtype=cvd.default_int) - return (init_val + (lower_asymp - init_val) / ( - 1 + (t / half_val) ** decay_rate)) # TODO: make this robust to /0 errors + + if delay is not None: + t = np.arange(length - delay, dtype=cvd.default_int) + growth = linear_growth(delay, init_val / delay) + decay = (init_val + (lower_asymp - init_val) / ( + 1 + (t / half_val) ** decay_rate)) + result = np.concatenate(growth, decay, axis=None) + else: + t = np.arange(length, dtype=cvd.default_int) + result = (init_val + (lower_asymp - init_val) / ( + 1 + (t / half_val) ** decay_rate)) + return result # TODO: make this robust to /0 errors def linear_decay(length, init_val, slope): ''' Calculate linear decay ''' diff --git a/covasim/interventions.py b/covasim/interventions.py index 740a16724..b1e70d96a 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1202,7 +1202,6 @@ def apply(self, sim): vacc_inds = self.vaccinated[day2-1] self.update_vaccine_info(sim, vacc_inds) - return def update_vaccine_info(self, sim, vacc_inds): diff --git a/covasim/sim.py b/covasim/sim.py index d8dcc6a61..07ca18539 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -780,10 +780,7 @@ def finalize(self, verbose=None, restore_pars=True): self.results[reskey].values = self.results[reskey].values[:self['n_strains'], :] if self.results[reskey].scale: # Scale the result dynamically if 'by_strain' in reskey: - # self.results[reskey].values = np.rot90(self.results[reskey].values) self.results[reskey].values = np.einsum('ij,j->ij',self.results[reskey].values,self.rescale_vec) - # self.results[reskey].values = np.flipud(self.results[reskey].values) - # self.results[reskey].values = np.rot90(self.results[reskey].values) else: self.results[reskey].values *= self.rescale_vec diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index a96e07358..102f017d3 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -353,7 +353,7 @@ def vacc_subtarg(sim): #scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # Run Vaccine tests - sim5 = test_vaccine_1strain() + # sim5 = test_vaccine_1strain() sim6 = test_vaccine_2strains() sc.toc() From c339a0e69ebcbbdc3271730d0d0404314e22ed1c Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 4 Mar 2021 17:02:34 -0500 Subject: [PATCH 130/569] some updates --- covasim/immunity.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 732a54cdd..a17ee854f 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -187,7 +187,7 @@ def __init__(self, vaccine=None): return def init_strain_vaccine_info(self): - + # TODO-- populate this with data! rel_imm = {} rel_imm['known_vaccines'] = ['pfizer', 'moderna', 'az', 'j&j'] rel_imm['known_strains'] = ['wild', 'b117', 'b1351', 'p1'] @@ -196,17 +196,17 @@ def init_strain_vaccine_info(self): rel_imm[vx]['wild'] = 1 rel_imm[vx]['b117'] = 1 - rel_imm['pfizer']['b1351'] = 1 - rel_imm['pfizer']['p1'] = 1 + rel_imm['pfizer']['b1351'] = .5 + rel_imm['pfizer']['p1'] = .5 - rel_imm['moderna']['b1351'] = 1 - rel_imm['moderna']['p1'] = 1 + rel_imm['moderna']['b1351'] = .5 + rel_imm['moderna']['p1'] = .5 - rel_imm['az']['b1351'] = 1 - rel_imm['az']['p1'] = 1 + rel_imm['az']['b1351'] = .5 + rel_imm['az']['p1'] = .5 - rel_imm['j&j']['b1351'] = 1 - rel_imm['j&j']['p1'] = 1 + rel_imm['j&j']['b1351'] = .5 + rel_imm['j&j']['p1'] = .5 return rel_imm From 3c464695fe52c90cba1e3fb51301100672b61b4f Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 5 Mar 2021 20:41:44 -0500 Subject: [PATCH 131/569] some vaccine improvements --- covasim/immunity.py | 8 ++++---- covasim/interventions.py | 35 +++++++++++++-------------------- covasim/sim.py | 6 ++++++ tests/devtests/test_variants.py | 24 ++++++++++++++++------ 4 files changed, 42 insertions(+), 31 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index a17ee854f..95a760ffe 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -271,9 +271,9 @@ def parse_vaccine_pars(self, vaccine=None): if ax == 'sus': vaccine_pars['imm_pars'][ax] = [dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 30, 'lower_asymp': 0.3, 'decay_rate': -5, - 'delay': 30})] + 'delay': 30})]*2 else: - vaccine_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180}) + vaccine_pars['imm_pars'][ax] = [dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})]*2 vaccine_pars['doses'] = 1 vaccine_pars['interval'] = None vaccine_pars['label'] = vaccine @@ -297,7 +297,7 @@ def initialize(self, sim): ts = sim['total_strains'] circulating_strains = ['wild'] # assume wild is circulating - for strain in ts: + for strain in range(ts-1): circulating_strains.append(sim['strains'][strain].strain_label) if self.imm_pars is None: @@ -454,7 +454,7 @@ def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp, delay=No growth = linear_growth(delay, init_val / delay) decay = (init_val + (lower_asymp - init_val) / ( 1 + (t / half_val) ** decay_rate)) - result = np.concatenate(growth, decay, axis=None) + result = np.concatenate((growth, decay), axis=None) else: t = np.arange(length, dtype=cvd.default_int) result = (init_val + (lower_asymp - init_val) / ( diff --git a/covasim/interventions.py b/covasim/interventions.py index b1e70d96a..f5e697c2b 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1157,8 +1157,9 @@ def __init__(self, days, prob=1.0, vaccine_pars=None, subtarget=None, **kwargs): def initialize(self, sim): ''' Fix the dates and store the vaccinations ''' - self.first_dose_days = process_days(sim, self.days) - self.vaccinated = [None]*len(self.first_dose_days) # keep track of inds of people vaccinated on each day + self.first_dose_eligible = process_days(sim, self.days) # days that group becomes eligible + self.second_dose_days = [None] * (sim['n_days']+1) # inds who get second dose (if relevant) + self.vaccinated = [None]*(sim['n_days']+1) # keep track of inds of people vaccinated on each day self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person self.vaccination_dates = np.full(sim.n, np.nan) # Store the dates when people are vaccinated self.vaccine_ind = len(sim['vaccines']) @@ -1167,10 +1168,6 @@ def initialize(self, sim): sim['vaccines'].append(vaccine) self.doses = vaccine.doses self.interval = vaccine.interval - if self.interval is not None: - self.second_dose_days = self.first_dose_days + self.interval - else: - self.second_dose_days = [] self.initialized = True return @@ -1178,30 +1175,26 @@ def initialize(self, sim): def apply(self, sim): ''' Perform vaccination ''' - # If this day is found in the list, apply the intervention - for day in find_day(self.first_dose_days, sim.t): - - # Determine who gets vaccinated today - + if sim.t >= min(self.first_dose_eligible): + # Determine who gets first dose of vaccine today if self.subtarget is not None: subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) - vacc_probs = np.zeros(sim.n)# Begin by assigning equal vaccination probability to everyone - vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted + vacc_probs = np.zeros(sim.n) # Begin by assigning equal vaccination probability to everyone + vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted else: - vacc_probs = np.full(sim.n, self.prob) # Assign equal vaccination probability to everyone - vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated - self.vaccinated[day] = vacc_inds + vacc_probs = np.full(sim.n, self.prob) # Assign equal vaccination probability to everyone + vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated + self.vaccinated[sim.t] = vacc_inds + if self.interval is not None: + self.second_dose_days[sim.t + self.interval] = vacc_inds + vacc_inds_dose2 = self.second_dose_days[sim.t] + vacc_inds = np.concatenate((vacc_inds, vacc_inds_dose2), axis=None) # Update vaccine attributes in sim sim.people.vaccinated[vacc_inds] = True sim.people.vaccine_source[vacc_inds] = self.vaccine_ind self.update_vaccine_info(sim, vacc_inds) - for day2 in find_day(self.second_dose_days, sim.t): - # Determine who gets vaccinated today - vacc_inds = self.vaccinated[day2-1] - self.update_vaccine_info(sim, vacc_inds) - return def update_vaccine_info(self, sim, vacc_inds): diff --git a/covasim/sim.py b/covasim/sim.py index 07ca18539..c8258b8f1 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -587,6 +587,12 @@ def step(self): vaccine_source = cvd.default_int(people.vaccine_source[vacc_inds]) vaccine_scale_factor = vaccine_info['rel_imm'][vaccine_source, strain] doses = cvd.default_int(people.vaccinations[vacc_inds]) + + # pull out inds who have a prior infection + prior_inf = cvu.false(np.isnan(date_rec)) + prior_inf_vacc = np.intersect1d(prior_inf, vacc_inds) + prior_inf_vacc = np.where(vacc_inds == prior_inf_vacc)[0] + doses[prior_inf_vacc] = 2 vaccine_time = cvd.default_int(t - date_vacc[vacc_inds]) vaccine_immunity = vaccine_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, vaccine_time] immunity_factors[vacc_inds] = vaccine_scale_factor * vaccine_immunity diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 102f017d3..f4526c69f 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -46,11 +46,11 @@ def test_vaccine_2strains(do_plot=True, do_show=True, do_save=False): sim.vxsubtarg = sc.objdict() sim.vxsubtarg.age = [75, 65, 50, 18] sim.vxsubtarg.prob = [.5, .5, .5, .5] - sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] - pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) - b117 = cv.Strain('b117', days=10, n_imports=20) - sim['strains'] = [b117] - sim['interventions'] = pfizer + sim.vxsubtarg.days = subtarg_days = [20, 80, 120, 180] + jnj = cv.vaccinate(days=subtarg_days, vaccine_pars='j&j', subtarget=vacc_subtarg) + b1351 = cv.Strain('b1351', days=0, n_imports=20) + sim['strains'] = [b1351] + sim['interventions'] = jnj sim.run() to_plot = sc.objdict({ @@ -327,13 +327,25 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab def vacc_subtarg(sim): ''' Subtarget by age''' - ind = sim.vxsubtarg.days.index(sim.t) + + # Want to adjust this so that it retrieves the first ind that is = or < sim.t + ind = get_ind_of_min_value(sim.vxsubtarg.days, sim.t) age = sim.vxsubtarg.age[ind] prob = sim.vxsubtarg.prob[ind] inds = sc.findinds((sim.people.age>=age) * ~sim.people.vaccinated) vals = prob*np.ones(len(inds)) return {'inds':inds, 'vals':vals} +def get_ind_of_min_value(list, time): + ind = None + for place, t in enumerate(list): + if time >= t: + ind = place + + if ind is None: + errormsg = f'{time} is not within the list of times' + raise ValueError(errormsg) + return ind #%% Run as a script if __name__ == '__main__': From 49059ead739b477c47f8faff776c6a86d8b52e6e Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 5 Mar 2021 20:51:01 -0500 Subject: [PATCH 132/569] few more fixes --- covasim/interventions.py | 7 +++++-- tests/devtests/test_variants.py | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index f5e697c2b..3fb17534e 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1186,9 +1186,12 @@ def apply(self, sim): vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated self.vaccinated[sim.t] = vacc_inds if self.interval is not None: - self.second_dose_days[sim.t + self.interval] = vacc_inds + next_dose_day = sim.t + self.interval + if next_dose_day < sim['n_days']: + self.second_dose_days[next_dose_day] = vacc_inds vacc_inds_dose2 = self.second_dose_days[sim.t] - vacc_inds = np.concatenate((vacc_inds, vacc_inds_dose2), axis=None) + if vacc_inds_dose2 is not None: + vacc_inds = np.concatenate((vacc_inds, vacc_inds_dose2), axis=None) # Update vaccine attributes in sim sim.people.vaccinated[vacc_inds] = True diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index f4526c69f..30710231c 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -22,7 +22,7 @@ def test_vaccine_1strain(do_plot=True, do_show=True, do_save=False): sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) sim['interventions'] += [pfizer] - sim.initialize() + # sim.initialize() sim.run() to_plot = sc.objdict({ @@ -365,8 +365,8 @@ def get_ind_of_min_value(list, time): #scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # Run Vaccine tests - # sim5 = test_vaccine_1strain() - sim6 = test_vaccine_2strains() + sim5 = test_vaccine_1strain() + # sim6 = test_vaccine_2strains() sc.toc() From fc7093c9d1025eebe847250452a7fe8c5617e7af Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 8 Mar 2021 12:37:27 -0500 Subject: [PATCH 133/569] fixed a bug with vaccine efficacy in people with prior infection --- covasim/immunity.py | 10 ++++---- covasim/parameters.py | 2 +- covasim/run.py | 6 ++--- covasim/sim.py | 11 +++++--- tests/devtests/test_variants.py | 45 +++++++++++++++++++++++++-------- 5 files changed, 51 insertions(+), 23 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 95a760ffe..865843c5e 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -230,8 +230,8 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars = dict() vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 0.5/22}), - dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 30, + vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 1/22}), + dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 50, 'lower_asymp': 0.3, 'decay_rate': -5})] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 @@ -243,7 +243,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 0.5/29}), - dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 30, + dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 50, 'lower_asymp': 0.3, 'decay_rate': -5})] vaccine_pars['doses'] = 2 @@ -256,7 +256,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 0.5/22}), - dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 30, + dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 50, 'lower_asymp': 0.3, 'decay_rate': -5})] vaccine_pars['doses'] = 2 @@ -269,7 +269,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['imm_pars'] = {} for ax in cvd.immunity_axes: if ax == 'sus': - vaccine_pars['imm_pars'][ax] = [dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 30, + vaccine_pars['imm_pars'][ax] = [dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 50, 'lower_asymp': 0.3, 'decay_rate': -5, 'delay': 30})]*2 else: diff --git a/covasim/parameters.py b/covasim/parameters.py index d8ff61eea..13c277be8 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -73,7 +73,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 pars['imm_pars'] = {} for ax in cvd.immunity_axes: - pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val':1., 'half_life':180}) + pars['imm_pars'][ax] = dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 50, 'lower_asymp': 0.3, 'decay_rate': -5}) pars['rel_imm'] = {} # Relative immunity scalings depending on the severity of symptoms pars['rel_imm']['asymptomatic'] = 0.7 pars['rel_imm']['mild'] = 0.9 diff --git a/covasim/run.py b/covasim/run.py index b5ad4beac..aac53d307 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -1353,9 +1353,9 @@ def single_run(sim, ind=0, reseed=True, noise=0.0, noisepar=None, keep_people=Fa # noisefactor = 1/(1-noiseval) # sim[noisepar] *= noisefactor - if verbose>=1: - verb = 'Running' if do_run else 'Creating' - print(f'{verb} a simulation using seed={sim["rand_seed"]} and noise={noiseval}') + # if verbose>=1: + # verb = 'Running' if do_run else 'Creating' + # print(f'{verb} a simulation using seed={sim["rand_seed"]} and noise={noiseval}') # Handle additional arguments for key,val in sim_args.items(): diff --git a/covasim/sim.py b/covasim/sim.py index c8258b8f1..99ffe1b0f 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -580,19 +580,22 @@ def step(self): # Determine who is vaccinated and has some immunity from vaccine vaccinated = people.vaccinated vacc_inds = cvu.true(vaccinated) - vacc_inds = np.setdiff1d(vacc_inds, inf_inds) + vacc_inds = np.setdiff1d(vacc_inds, inf_inds) # Take out anyone currently infected if len(vacc_inds): vaccine_info = self['vaccine_info'] date_vacc = people.date_vaccinated vaccine_source = cvd.default_int(people.vaccine_source[vacc_inds]) vaccine_scale_factor = vaccine_info['rel_imm'][vaccine_source, strain] - doses = cvd.default_int(people.vaccinations[vacc_inds]) + doses_all = cvd.default_int(people.vaccinations) + + # doses = cvd.default_int(people.vaccinations[vacc_inds]) # pull out inds who have a prior infection prior_inf = cvu.false(np.isnan(date_rec)) prior_inf_vacc = np.intersect1d(prior_inf, vacc_inds) - prior_inf_vacc = np.where(vacc_inds == prior_inf_vacc)[0] - doses[prior_inf_vacc] = 2 + # prior_inf_vacc = np.where(vacc_inds == prior_inf_vacc)[0] + doses_all[prior_inf_vacc] = 2 + doses = doses_all[vacc_inds] vaccine_time = cvd.default_int(t - date_vacc[vacc_inds]) vaccine_immunity = vaccine_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, vaccine_time] immunity_factors[vacc_inds] = vaccine_scale_factor * vaccine_immunity diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 30710231c..2bea4476a 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -14,16 +14,39 @@ def test_vaccine_1strain(do_plot=True, do_show=True, do_save=False): sc.heading('Setting up...') - sim = cv.Sim() + # Define baseline parameters + base_pars = { + 'n_days': 200, + } + + n_runs = 3 + base_sim = cv.Sim(base_pars) + # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 - sim.vxsubtarg = sc.objdict() - sim.vxsubtarg.age = [75, 65, 50, 18] - sim.vxsubtarg.prob = [.5, .5, .5, .5] - sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] + base_sim.vxsubtarg = sc.objdict() + base_sim.vxsubtarg.age = [75, 65, 50, 18] + base_sim.vxsubtarg.prob = [.5, .5, .5, .5] + base_sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) - sim['interventions'] += [pfizer] - # sim.initialize() - sim.run() + + # Define the scenarios + + scenarios = { + 'baseline': { + 'name': 'No Vaccine', + 'pars': {} + }, + 'pfizer': { + 'name': 'Pfizer starting on day 20', + 'pars': { + 'interventions': [pfizer], + } + }, + } + + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run() to_plot = sc.objdict({ 'New infections': ['new_infections'], @@ -32,8 +55,9 @@ def test_vaccine_1strain(do_plot=True, do_show=True, do_save=False): # 'Cumulative reinfections': ['cum_reinfections'], }) if do_plot: - sim.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_vaccine_1strain.png', to_plot=to_plot) - return sim + scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_basic_reinfection.png', to_plot=to_plot) + + return scens def test_vaccine_2strains(do_plot=True, do_show=True, do_save=False): @@ -365,6 +389,7 @@ def get_ind_of_min_value(list, time): #scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # Run Vaccine tests + # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) sim5 = test_vaccine_1strain() # sim6 = test_vaccine_2strains() sc.toc() From ffe2f4606b97d01c75dcc7fdd2ab2092dd484ed0 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 8 Mar 2021 20:08:51 -0500 Subject: [PATCH 134/569] pre populated cross immunity for known strains --- covasim/immunity.py | 57 ++++++++++++++++++++++++--- covasim/sim.py | 2 + tests/devtests/test_variants.py | 69 +++++++++++++++++++++++++-------- 3 files changed, 107 insertions(+), 21 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 865843c5e..49043a41c 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -83,8 +83,7 @@ def parse_strain_pars(self, strain=None, strain_label=None): strain_pars = dict() strain_pars['imm_pars'] = dict() for ax in cvd.immunity_axes: - strain_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., - 'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + strain_pars['imm_pars'][ax] = dict(form='logistic_decay', pars={'init_val': .8, 'half_val': 30, 'lower_asymp': 0.2, 'decay_rate': -5}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) self.strain_label = strain # Known parameters on Brazil variant @@ -92,8 +91,7 @@ def parse_strain_pars(self, strain=None, strain_label=None): strain_pars = dict() strain_pars['imm_pars'] = dict() for ax in cvd.immunity_axes: - strain_pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., - 'half_life': 120}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + strain_pars['imm_pars'][ax] = dict(form='logistic_decay', pars={'init_val': .8, 'half_val': 30, 'lower_asymp': 0.2, 'decay_rate': -5}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) self.strain_label = strain else: @@ -347,6 +345,11 @@ def init_immunity(sim, create=False): ts = sim['total_strains'] immunity = {} + # Pull out all of the circulating strains for cross-immunity + circulating_strains = ['wild'] + for strain in sim['strains']: + circulating_strains.append(strain.strain_label) + # If immunity values have been provided, process them if sim['immunity'] is None or create: # Initialize immunity @@ -360,11 +363,18 @@ def init_immunity(sim, create=False): sim['immunity'] = immunity else: + # if we know all the circulating strains, then update, otherwise use defaults + cross_immunity = create_cross_immunity(circulating_strains) if sc.checktype(sim['immunity']['sus'], 'arraylike'): correct_size = sim['immunity']['sus'].shape == (ts, ts) if not correct_size: errormsg = f'Wrong dimensions for immunity["sus"]: you provided a matrix sized {sim["immunity"]["sus"].shape}, but it should be sized {(ts, ts)}' raise ValueError(errormsg) + for i in range(ts): + for j in range(ts): + if i != j: + sim['immunity']['sus'][j][i] = cross_immunity[circulating_strains[j]][circulating_strains[i]] + elif sc.checktype(sim['immunity']['sus'], dict): # TODO: make it possible to specify this as something like: # imm = {'b117': {'wild': 0.4, 'p1': 0.3}, @@ -446,6 +456,7 @@ def exp_decay(length, init_val, half_life, delay=None): result = init_val * np.exp(-decay_rate * t) return result + def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp, delay=None): ''' Calculate logistic decay ''' @@ -461,6 +472,7 @@ def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp, delay=No 1 + (t / half_val) ** decay_rate)) return result # TODO: make this robust to /0 errors + def linear_decay(length, init_val, slope): ''' Calculate linear decay ''' t = np.arange(length, dtype=cvd.default_int) @@ -469,7 +481,42 @@ def linear_decay(length, init_val, slope): result = 0 return result + def linear_growth(length, slope): ''' Calculate linear growth ''' t = np.arange(length, dtype=cvd.default_int) - return (slope * t) \ No newline at end of file + return (slope * t) + + +def create_cross_immunity(circulating_strains): + known_strains = ['wild', 'b117', 'b1351', 'p1'] + known_cross_immunity = dict() + known_cross_immunity['wild'] = {} # cross-immunity to wild + known_cross_immunity['wild']['b117'] = .5 + known_cross_immunity['wild']['b1351'] = .5 + known_cross_immunity['wild']['p1'] = .5 + known_cross_immunity['b117'] = {} # cross-immunity to b117 + known_cross_immunity['b117']['wild'] = 1 + known_cross_immunity['b117']['b1351'] = 1 + known_cross_immunity['b117']['p1'] = 1 + known_cross_immunity['b1351'] = {} # cross-immunity to b1351 + known_cross_immunity['b1351']['wild'] = 0.1 + known_cross_immunity['b1351']['b117'] = 0.1 + known_cross_immunity['b1351']['p1'] = 0.1 + known_cross_immunity['p1'] = {} # cross-immunity to p1 + known_cross_immunity['p1']['wild'] = 0.2 + known_cross_immunity['p1']['b117'] = 0.2 + known_cross_immunity['p1']['b1351'] = 0.2 + + cross_immunity = {} + cs = len(circulating_strains) + for i in range(cs): + cross_immunity[circulating_strains[i]] = {} + for j in range(cs): + if circulating_strains[j] in known_strains: + if i != j: + if circulating_strains[i] in known_strains: + cross_immunity[circulating_strains[i]][circulating_strains[j]] = \ + known_cross_immunity[circulating_strains[i]][circulating_strains[j]] + + return cross_immunity diff --git a/covasim/sim.py b/covasim/sim.py index 99ffe1b0f..053021923 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -611,6 +611,8 @@ def step(self): immune_inds = cvu.true(immune) # Whether people have some immunity to this strain from a prior infection with this strain immune_inds = np.setdiff1d(immune_inds, inf_inds) immune_inds = np.setdiff1d(immune_inds, vacc_inds) + + # Pull out own immunity immunity_scale_factor = np.full(len(immune_inds), self['immunity']['sus'][strain,strain]) # Process cross-immunity parameters and indices, if relevant diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 2bea4476a..f9cbb6453 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -25,7 +25,7 @@ def test_vaccine_1strain(do_plot=True, do_show=True, do_save=False): # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 base_sim.vxsubtarg = sc.objdict() base_sim.vxsubtarg.age = [75, 65, 50, 18] - base_sim.vxsubtarg.prob = [.5, .5, .5, .5] + base_sim.vxsubtarg.prob = [.05, .05, .05, .05] base_sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) @@ -55,7 +55,7 @@ def test_vaccine_1strain(do_plot=True, do_show=True, do_save=False): # 'Cumulative reinfections': ['cum_reinfections'], }) if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_basic_reinfection.png', to_plot=to_plot) + scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_basic_vaccination.png', to_plot=to_plot) return scens @@ -65,17 +65,51 @@ def test_vaccine_2strains(do_plot=True, do_show=True, do_save=False): sc.heading('Setting up...') - sim = cv.Sim() + # Define baseline parameters + base_pars = { + 'n_days': 250, + } + + n_runs = 3 + base_sim = cv.Sim(base_pars) + # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 - sim.vxsubtarg = sc.objdict() - sim.vxsubtarg.age = [75, 65, 50, 18] - sim.vxsubtarg.prob = [.5, .5, .5, .5] - sim.vxsubtarg.days = subtarg_days = [20, 80, 120, 180] + base_sim.vxsubtarg = sc.objdict() + base_sim.vxsubtarg.age = [75, 65, 50, 18] + base_sim.vxsubtarg.prob = [.01, .01, .01, .01] + base_sim.vxsubtarg.days = subtarg_days = [60, 150, 200, 220] jnj = cv.vaccinate(days=subtarg_days, vaccine_pars='j&j', subtarget=vacc_subtarg) - b1351 = cv.Strain('b1351', days=0, n_imports=20) - sim['strains'] = [b1351] - sim['interventions'] = jnj - sim.run() + b1351 = cv.Strain('b1351', days=10, n_imports=20) + p1 = cv.Strain('p1', days=100, n_imports=100) + + # Define the scenarios + + scenarios = { + 'baseline': { + 'name': 'B1351 on day 10, No Vaccine', + 'pars': { + 'strains': [b1351] + } + }, + 'b1351': { + 'name': 'B1351 on day 10, J&J starting on day 60', + 'pars': { + 'interventions': [jnj], + 'strains': [b1351], + } + }, + 'p1': { + 'name': 'B1351 on day 10, J&J starting on day 60, p1 on day 100', + 'pars': { + 'interventions': [jnj], + 'strains': [b1351, p1], + } + }, + } + + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run() to_plot = sc.objdict({ 'New infections': ['new_infections'], @@ -84,8 +118,9 @@ def test_vaccine_2strains(do_plot=True, do_show=True, do_save=False): # 'Cumulative reinfections': ['cum_reinfections'], }) if do_plot: - sim.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_vaccine_2strain.png', to_plot=to_plot) - return sim + scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_vaccine_b1351.png', to_plot=to_plot) + + return scens def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): @@ -352,7 +387,7 @@ def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, lab def vacc_subtarg(sim): ''' Subtarget by age''' - # Want to adjust this so that it retrieves the first ind that is = or < sim.t + # retrieves the first ind that is = or < sim.t ind = get_ind_of_min_value(sim.vxsubtarg.days, sim.t) age = sim.vxsubtarg.age[ind] prob = sim.vxsubtarg.prob[ind] @@ -360,6 +395,7 @@ def vacc_subtarg(sim): vals = prob*np.ones(len(inds)) return {'inds':inds, 'vals':vals} + def get_ind_of_min_value(list, time): ind = None for place, t in enumerate(list): @@ -371,6 +407,7 @@ def get_ind_of_min_value(list, time): raise ValueError(errormsg) return ind + #%% Run as a script if __name__ == '__main__': sc.tic() @@ -390,8 +427,8 @@ def get_ind_of_min_value(list, time): # Run Vaccine tests # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim5 = test_vaccine_1strain() - # sim6 = test_vaccine_2strains() + # sim5 = test_vaccine_1strain() + sim6 = test_vaccine_2strains() sc.toc() From 5ed01c00a051cdd084f04f0687261f67ecd6687f Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 9 Mar 2021 12:03:49 -0500 Subject: [PATCH 135/569] minor fixes and updating exposed_strain --- covasim/immunity.py | 6 ++---- covasim/people.py | 15 ++++++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 49043a41c..04f894de6 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -72,10 +72,8 @@ def parse_strain_pars(self, strain=None, strain_label=None): # Known parameters on B117 elif strain in choices['b117']: strain_pars = dict() - strain_pars[ - 'rel_beta'] = 1.5 # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 - strain_pars[ - 'rel_severe_prob'] = 1.6 # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf + strain_pars['rel_beta'] = 1.5 # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 + strain_pars['rel_severe_prob'] = 1.6 # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf self.strain_label = strain # Known parameters on South African variant diff --git a/covasim/people.py b/covasim/people.py index ff1639dc1..be649a746 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -272,13 +272,14 @@ def check_recovery(self): self.prior_symptoms[severe_inds] = self.pars['rel_imm'][strain]['severe'] # # Now reset all disease states - self.exposed[inds] = False - self.infectious[inds] = False - self.symptomatic[inds] = False - self.severe[inds] = False - self.critical[inds] = False - self.susceptible[inds] = True - self.infectious_strain[inds] = np.nan + self.exposed[inds] = False + self.infectious[inds] = False + self.symptomatic[inds] = False + self.severe[inds] = False + self.critical[inds] = False + self.susceptible[inds] = True + self.infectious_strain[inds]= np.nan + self.exposed_strain[inds] = np.nan return len(inds) From f4ec5651462aa14c891acc915356f63c0dcbb941 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 9 Mar 2021 14:28:18 -0500 Subject: [PATCH 136/569] check that imports are susceptible --- covasim/immunity.py | 10 ++++++---- covasim/parameters.py | 9 ++++++++- covasim/sim.py | 3 ++- tests/devtests/test_variants.py | 4 ++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 04f894de6..3d836fefc 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -61,7 +61,7 @@ def parse_strain_pars(self, strain=None, strain_label=None): 'b117': ['b117', 'B117', 'B.1.1.7', 'UK', 'uk', 'UK variant', 'uk variant'], 'b1351': ['b1351', 'B1351', 'B.1.351', 'SA', 'sa', 'SA variant', 'sa variant'], # TODO: add other aliases - 'p1': ['p1', 'P1', 'P.1', 'Brazil', 'Brazil variant', 'brazil variant'], + 'p1': ['p1', 'P1', 'P.1', 'B.1.1.248', 'b11248', 'Brazil', 'Brazil variant', 'brazil variant'], } # Empty pardict for wild strain @@ -143,8 +143,8 @@ def apply(self, sim): # Update strain-specific people attributes # cvu.update_strain_attributes(sim.people) # don't think we need to do this if we just create people arrays with number of total strains in sim - importation_inds = cvu.choose(max_n=len(sim.people), - n=self.n_imports) # TODO: do we need to check these people aren't infected? Or just consider it unlikely + susceptible_inds = cvu.true(sim.people.susceptible) + importation_inds = np.random.choice(susceptible_inds, self.n_imports) sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) return @@ -371,7 +371,9 @@ def init_immunity(sim, create=False): for i in range(ts): for j in range(ts): if i != j: - sim['immunity']['sus'][j][i] = cross_immunity[circulating_strains[j]][circulating_strains[i]] + if circulating_strains[i] != None and circulating_strains[j] != None: + sim['immunity']['sus'][j][i] = cross_immunity[circulating_strains[j]][ + circulating_strains[i]] elif sc.checktype(sim['immunity']['sus'], dict): # TODO: make it possible to specify this as something like: diff --git a/covasim/parameters.py b/covasim/parameters.py index 13c277be8..b83db9891 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -307,7 +307,14 @@ def update_sub_key_pars(pars, default_pars): newval = val[0] oldval = sc.promotetolist(default_pars[par])[0] # Might be a list or not! if isinstance(newval, dict): # Update the dictionary, don't just overwrite it - pars[par] = sc.promotetolist(sc.mergenested(oldval, newval)) + if par == 'imm_pars': + for type, valoftype in newval.items(): + if valoftype['form'] == oldval[type]['form']: + pars[par][0][type] = sc.mergenested(oldval[type], valoftype) + else: + pars[par][0][type] = valoftype + else: + pars[par] = sc.promotetolist(sc.mergenested(oldval, newval)) else: if isinstance(val, dict): # Update the dictionary, don't just overwrite it if isinstance(default_pars[par], dict): diff --git a/covasim/sim.py b/covasim/sim.py index 053021923..0d9c94a71 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -527,7 +527,8 @@ def step(self): imports = cvu.n_poisson(self['n_imports'], self['n_strains']) # Imported cases for strain, n_imports in enumerate(imports): if n_imports>0: - importation_inds = cvu.choose(max_n=len(people), n=n_imports) + susceptible_inds = cvu.true(people.susceptible) + importation_inds = np.random.choice(susceptible_inds, n_imports) people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation', strain=strain) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index f9cbb6453..96baf6a16 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -426,9 +426,9 @@ def get_ind_of_min_value(list, time): #scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # Run Vaccine tests - # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim5 = test_vaccine_1strain() - sim6 = test_vaccine_2strains() + # sim6 = test_vaccine_2strains() sc.toc() From 53306b84ac22bd008fb73ca9ed50555a0d61a1a3 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 9 Mar 2021 15:03:33 -0500 Subject: [PATCH 137/569] all tests run --- covasim/immunity.py | 3 ++- tests/devtests/test_variants.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 3d836fefc..e9e74c808 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -362,6 +362,7 @@ def init_immunity(sim, create=False): else: # if we know all the circulating strains, then update, otherwise use defaults + known_strains = ['wild', 'b117', 'b1351', 'p1'] cross_immunity = create_cross_immunity(circulating_strains) if sc.checktype(sim['immunity']['sus'], 'arraylike'): correct_size = sim['immunity']['sus'].shape == (ts, ts) @@ -371,7 +372,7 @@ def init_immunity(sim, create=False): for i in range(ts): for j in range(ts): if i != j: - if circulating_strains[i] != None and circulating_strains[j] != None: + if circulating_strains[i] in known_strains and circulating_strains[j] in known_strains: sim['immunity']['sus'][j][i] = cross_immunity[circulating_strains[j]][ circulating_strains[i]] diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 96baf6a16..693d87a71 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -413,22 +413,22 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - # if 0: - # sim = cv.Sim() - # sim.run() + if 0: + sim = cv.Sim() + sim.run() # Run more complex tests - # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - #sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - #scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # Run Vaccine tests sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim5 = test_vaccine_1strain() - # sim6 = test_vaccine_2strains() + sim5 = test_vaccine_1strain() + sim6 = test_vaccine_2strains() sc.toc() From 6653ed082b59a1b7eaad75486dd4dd8c5bcad38f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 10 Mar 2021 00:42:58 -0800 Subject: [PATCH 138/569] update readme --- README.rst | 6 ++---- setup.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 34a4fe9d4..45919e23d 100644 --- a/README.rst +++ b/README.rst @@ -58,12 +58,10 @@ If you have written a paper or report using Covasim, we'd love to know about it! Requirements ============ -Python >=3.6 (64-bit). (Note: Python 2 is not supported.) +Python 3.7 or 3.8 (64-bit). (Note: Python 2.7 and Python 3.9 are not supported.) -We also recommend, but do not require, using Python virtual environments. For -more information, see documentation for venv_ or Anaconda_. +We also recommend, but do not require, installing Covasim in a virtual environment. For more information, see documentation for e.g. Anaconda_. -.. _venv: https://docs.python.org/3/tutorial/venv.html .. _Anaconda: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html diff --git a/setup.py b/setup.py index 3b2fd187f..e98aeaef2 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules", "Development Status :: 5 - Production/Stable", - "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", ] setup( From e2910ab4e704e31cd5612a060b46d8ec3d45b43f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 10 Mar 2021 03:36:33 -0800 Subject: [PATCH 139/569] update test coverage and change parallelization --- covasim/analysis.py | 32 ++++++------ covasim/base.py | 44 ++++++++-------- covasim/defaults.py | 6 +-- covasim/interventions.py | 18 +++---- covasim/misc.py | 34 ++++++------ covasim/parameters.py | 2 +- covasim/population.py | 22 ++++---- covasim/requirements.py | 6 +-- covasim/settings.py | 15 ++++-- covasim/utils.py | 31 ++++++----- tests/.coveragerc | 24 +++++++++ tests/check_coverage | 2 +- tests/devtests/test_numba_parallelization.py | 27 ++++++++++ tests/requirements_frozen.txt | 52 +++++++++++++++++++ tests/requirements_test.txt | 3 ++ tests/test_other.py | 3 ++ ...ions.py => test_specific_interventions.py} | 0 17 files changed, 220 insertions(+), 101 deletions(-) create mode 100644 tests/.coveragerc create mode 100644 tests/devtests/test_numba_parallelization.py create mode 100644 tests/requirements_frozen.txt create mode 100644 tests/requirements_test.txt rename tests/unittests/{test_interventions.py => test_specific_interventions.py} (100%) diff --git a/covasim/analysis.py b/covasim/analysis.py index 3b68b4342..e9fc24799 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -62,7 +62,7 @@ def validate_recorded_dates(sim, requested_dates, recorded_dates, die=True): ''' requested_dates = sorted(list(requested_dates)) recorded_dates = sorted(list(recorded_dates)) - if recorded_dates != requested_dates: + if recorded_dates != requested_dates: # pragma: no cover errormsg = f'The dates {requested_dates} were requested but only {recorded_dates} were recorded: please check the dates fall between {sim.date(sim["start_day"])} and {sim.date(sim["start_day"])} and the sim was actually run' if die: raise RuntimeError(errormsg) @@ -114,7 +114,7 @@ def initialize(self, sim): self.days, self.dates = cvi.process_days(sim, self.days, return_dates=True) # Ensure days are in the right format max_snapshot_day = self.days[-1] max_sim_day = sim.day(sim['end_day']) - if max_snapshot_day > max_sim_day: + if max_snapshot_day > max_sim_day: # pragma: no cover errormsg = f'Cannot create snapshot for {self.dates[-1]} (day {max_snapshot_day}) because the simulation ends on {self.end_day} (day {max_sim_day})' raise ValueError(errormsg) self.initialized = True @@ -141,7 +141,7 @@ def get(self, key=None): date = sc.date(day, start_date=self.start_day, as_date=False) if date in self.snapshots: snapshot = self.snapshots[date] - else: + else: # pragma: no cover dates = ', '.join(list(self.snapshots.keys())) errormsg = f'Could not find snapshot date {date} (day {day}): choices are {dates}' raise sc.KeyNotFoundError(errormsg) @@ -194,7 +194,7 @@ def __init__(self, days=None, states=None, edges=None, datafile=None, sim=None, def from_sim(self, sim): ''' Create an age histogram from an already run sim ''' - if self.days is not None: + if self.days is not None: # pragma: no cover errormsg = 'If a simulation is being analyzed post-run, no day can be supplied: only the last day of the simulation is available' raise ValueError(errormsg) self.initialize(sim) @@ -212,7 +212,7 @@ def initialize(self, sim): self.days, self.dates = cvi.process_days(sim, self.days, return_dates=True) # Ensure days are in the right format max_hist_day = self.days[-1] max_sim_day = sim.day(self.end_day) - if max_hist_day > max_sim_day: + if max_hist_day > max_sim_day: # pragma: no cover errormsg = f'Cannot create histogram for {self.dates[-1]} (day {max_hist_day}) because the simulation ends on {self.end_day} (day {max_sim_day})' raise ValueError(errormsg) @@ -267,7 +267,7 @@ def get(self, key=None): date = sc.date(day, start_date=self.start_day, as_date=False) if date in self.hists: hists = self.hists[date] - else: + else: # pragma: no cover dates = ', '.join(list(self.hists.keys())) errormsg = f'Could not find histogram date {date} (day {day}): choices are {dates}' raise sc.KeyNotFoundError(errormsg) @@ -326,7 +326,7 @@ def plot(self, windows=False, width=0.8, color='#F8A493', fig_args=None, axis_ar histsdict = self.window_hists else: histsdict = self.hists - if not len(histsdict): + if not len(histsdict): # pragma: no cover errormsg = f'Cannot plot since no histograms were recorded (schuled days: {self.days})' raise ValueError(errormsg) @@ -708,13 +708,13 @@ def __init__(self, sim, weights=None, keys=None, custom=None, compute=True, verb self.gof_kwargs = kwargs # Copy data - if sim.data is None: + if sim.data is None: # pragma: no cover errormsg = 'Model fit cannot be calculated until data are loaded' raise RuntimeError(errormsg) self.data = sim.data # Copy sim results - if not sim.results_ready: + if not sim.results_ready: # pragma: no cover errormsg = 'Model fit cannot be calculated until results are run' raise RuntimeError(errormsg) self.sim_results = sc.objdict() @@ -761,11 +761,11 @@ def reconcile_inputs(self): sim_keys = self.sim_results.keys() intersection = list(set(sim_keys).intersection(data_cols)) # Find keys in both the sim and data self.keys = [key for key in sim_keys if key in intersection and key.startswith('cum_')] # Only keep cumulative keys - if not len(self.keys): + if not len(self.keys): # pragma: no cover errormsg = f'No matches found between simulation result keys ({sim_keys}) and data columns ({data_cols})' raise sc.KeyNotFoundError(errormsg) mismatches = [key for key in self.keys if key not in data_cols] - if len(mismatches): + if len(mismatches): # pragma: no cover mismatchstr = ', '.join(mismatches) errormsg = f'The following requested key(s) were not found in the data: {mismatchstr}' raise sc.KeyNotFoundError(errormsg) @@ -811,10 +811,10 @@ def reconcile_inputs(self): c_sim = custom['sim'] try: assert len(c_data) == len(c_sim) - except: + except: # pragma: no cover errormsg = f'Custom data and sim must be arrays, and be of the same length: data = {c_data}, sim = {c_sim} could not be processed' raise ValueError(errormsg) - if key in self.pair: + if key in self.pair: # pragma: no cover errormsg = f'You cannot use a custom key "{key}" that matches one of the existing keys: {self.pair.keys()}' raise ValueError(errormsg) @@ -863,7 +863,7 @@ def compute_losses(self): pass elif len_wt == len_sim: # Most typical case: it's the length of the simulation, must trim weight = weight[self.inds.sim[key]] # Trim to matching indices - else: + else: # pragma: no cover errormsg = f'Could not map weight array of length {len_wt} onto simulation of length {len_sim} or data-model matches of length {len_match}' raise ValueError(errormsg) else: @@ -1071,7 +1071,7 @@ def __len__(self): ''' try: return len(self.infection_log) - except: + except: # pragma: no cover return 0 @@ -1222,7 +1222,7 @@ def r0(self, recovered_only=False): if i is None or np.isnan(node['date_exposed']) or (recovered_only and node['date_recovered']>self.n_days): continue n_infected.append(self.graph.out_degree(i)) - except Exception as E: + except Exception as E: # pragma: no cover errormsg = f'Unable to compute r0 ({str(E)}): you may need to reinitialize the transmission tree with to_networkx=True' raise RuntimeError(errormsg) return np.mean(n_infected) diff --git a/covasim/base.py b/covasim/base.py index 5621234ef..8db367173 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -233,7 +233,7 @@ def _brief(self): string = f'Sim({labelstr}; {start} to {end}; pop: {pop_size:n} {pop_type}; epi: {results})' # ...but if anything goes wrong, return the default with a warning - except Exception as E: + except Exception as E: # pragma: no cover string = sc.objectid(self) string += f'Warning, sim appears to be malformed:\n{str(E)}' @@ -279,7 +279,7 @@ def n(self): ''' Count the number of people -- if it fails, assume none ''' try: # By default, the length of the people dict return len(self.people) - except: # If it's None or missing + except: # pragma: no cover # If it's None or missing return 0 @property @@ -287,7 +287,7 @@ def scaled_pop_size(self): ''' Get the total population size, i.e. the number of agents times the scale factor -- if it fails, assume none ''' try: return self['pop_size']*self['pop_scale'] - except: # If it's None or missing + except: # pragma: no cover # If it's None or missing return 0 @property @@ -295,7 +295,7 @@ def npts(self): ''' Count the number of time points ''' try: return int(self['n_days'] + 1) - except: + except: # pragma: no cover return 0 @property @@ -303,7 +303,7 @@ def tvec(self): ''' Create a time vector ''' try: return np.arange(self.npts) - except: + except: # pragma: no cover return np.array([]) @property @@ -318,7 +318,7 @@ def datevec(self): ''' try: return self['start_day'] + self.tvec * dt.timedelta(days=1) - except: + except: # pragma: no cover return np.array([]) @@ -420,7 +420,7 @@ def export_results(self, for_json=True, filename=None, indent=2, *args, **kwargs ''' - if not self.results_ready: + if not self.results_ready: # pragma: no cover errormsg = 'Please run the sim before exporting the results' raise RuntimeError(errormsg) @@ -511,7 +511,7 @@ def to_json(self, filename=None, keys=None, tostring=False, indent=2, verbose=Fa d['parameters'] = pardict elif key == 'summary': d['summary'] = dict(sc.dcp(self.summary)) - else: + else: # pragma: no cover try: d[key] = sc.sanitizejson(getattr(self, key)) except Exception as E: @@ -644,7 +644,7 @@ def load(filename, *args, **kwargs): sim = cv.Sim.load('my-simulation.sim') ''' sim = cvm.load(filename, *args, **kwargs) - if not isinstance(sim, BaseSim): + if not isinstance(sim, BaseSim): # pragma: no cover errormsg = f'Cannot load object of {type(sim)} as a Sim object' raise TypeError(errormsg) return sim @@ -654,7 +654,7 @@ def _get_ia(self, which, label=None, partial=False, as_list=False, as_inds=False ''' Helper method for get_interventions() and get_analyzers(); see get_interventions() docstring ''' # Handle inputs - if which not in ['interventions', 'analyzers']: + if which not in ['interventions', 'analyzers']: # pragma: no cover errormsg = f'This method is only defined for interventions and analyzers, not "{which}"' raise ValueError(errormsg) @@ -691,7 +691,7 @@ def _get_ia(self, which, label=None, partial=False, as_list=False, as_inds=False elif isinstance(label, type) and isinstance(ia_obj, label): matches.append(ia_obj) match_inds.append(ind) - else: + else: # pragma: no cover errormsg = f'Could not interpret label type "{type(label)}": should be str, int, or {which} class' raise TypeError(errormsg) @@ -701,7 +701,7 @@ def _get_ia(self, which, label=None, partial=False, as_list=False, as_inds=False elif as_list: output = matches else: # Normal case, return actual interventions - if len(matches) == 0: + if len(matches) == 0: # pragma: no cover if die: errormsg = f'No {which} matching "{label}" were found' raise ValueError(errormsg) @@ -798,14 +798,14 @@ def __getitem__(self, key): try: return self.__dict__[key] - except: + except: # pragma: no cover errormsg = f'Key "{key}" is not a valid attribute of people' raise AttributeError(errormsg) def __setitem__(self, key, value): ''' Ditto ''' - if self._lock and key not in self.__dict__: + if self._lock and key not in self.__dict__: # pragma: no cover errormsg = f'Key "{key}" is not a valid attribute of people' raise AttributeError(errormsg) self.__dict__[key] = value @@ -847,7 +847,7 @@ def _brief(self): try: layerstr = ', '.join([str(k) for k in self.layer_keys()]) string = f'People(n={len(self):0n}; layers: {layerstr})' - except Exception as E: + except Exception as E: # pragma: no cover string = sc.objectid(self) string += f'Warning, multisim appears to be malformed:\n{str(E)}' return string @@ -862,7 +862,7 @@ def set(self, key, value, die=True): ''' Ensure sizes and dtypes match ''' current = self[key] value = np.array(value, dtype=self._dtypes[key]) # Ensure it's the right type - if die and len(value) != len(current): + if die and len(value) != len(current): # pragma: no cover errormsg = f'Length of new array does not match current ({len(value)} vs. {len(current)})' raise IndexError(errormsg) self[key] = value @@ -951,7 +951,7 @@ def layer_keys(self): except: # If not fully initialized try: keys = list(self.pars['beta_layer'].keys()) - except: # If not even partially initialized + except: # pragma: no cover # If not even partially initialized keys = [] return keys @@ -974,7 +974,7 @@ def validate(self, die=True, verbose=False): expected_len = len(self) for key in self.keys(): actual_len = len(self[key]) - if actual_len != expected_len: + if actual_len != expected_len: # pragma: no cover if die: errormsg = f'Length of key "{key}" did not match population size ({actual_len} vs. {expected_len})' raise IndexError(errormsg) @@ -1092,7 +1092,7 @@ def add_contacts(self, contacts, lkey=None, beta=None): new_contacts[lkey] = pd.DataFrame.from_dict(contacts) elif isinstance(contacts, list): # Assume it's a list of contacts by person, not an edgelist new_contacts = self.make_edgelist(contacts) # Assume contains key info - else: + else: # pragma: no cover errormsg = f'Cannot understand contacts of type {type(contacts)}; expecting dataframe, array, or dict' raise TypeError(errormsg) @@ -1234,7 +1234,7 @@ def __len__(self): for key in self.keys(): try: output += len(self[key]) - except: + except: # pragma: no cover pass return output @@ -1296,7 +1296,7 @@ def __init__(self, **kwargs): def __len__(self): try: return len(self[self.basekey]) - except: + except: # pragma: no cover return 0 @@ -1424,7 +1424,7 @@ def find_contacts(self, inds, as_array=True): # Check types if not isinstance(inds, np.ndarray): inds = sc.promotetoarray(inds) - if inds.dtype != np.int64: # This is int64 since indices often come from cv.true(), which returns int64 + if inds.dtype != np.int64: # pragma: no cover # This is int64 since indices often come from cv.true(), which returns int64 inds = np.array(inds, dtype=np.int64) # Find the contacts diff --git a/covasim/defaults.py b/covasim/defaults.py index 124bef548..6012589ea 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -23,7 +23,7 @@ default_int = np.int32 nbfloat = nb.float32 nbint = nb.int32 -elif cvo.precision == 64: +elif cvo.precision == 64: # pragma: no cover default_float = np.float64 default_int = np.int64 nbfloat = nb.float64 @@ -215,7 +215,7 @@ def get_sim_plots(which='default'): }) elif which == 'overview': plots = sc.dcp(overview_plots) - else: + else: # pragma: no cover errormsg = f'The choice which="{which}" is not supported' raise ValueError(errormsg) return plots @@ -237,7 +237,7 @@ def get_scen_plots(which='default'): }) elif which == 'overview': plots = sc.dcp(overview_plots) - else: + else: # pragma: no cover errormsg = f'The choice which="{which}" is not supported' raise ValueError(errormsg) return plots diff --git a/covasim/interventions.py b/covasim/interventions.py index 9832388c2..40d5824ee 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -42,7 +42,7 @@ def find_day(arr, t=None, which='first'): inds = [all_inds[0]] elif which == 'last': inds = [all_inds[-1]] - else: + else: # pragma: no cover errormsg = f'Argument "which" must be "first", "last", or "all", not "{which}"' raise ValueError(errormsg) return inds @@ -250,14 +250,14 @@ def __init__(self, pars=None, **kwargs): subkeys = ['days', 'vals'] for parkey in pars.keys(): for subkey in subkeys: - if subkey not in pars[parkey].keys(): + if subkey not in pars[parkey].keys(): # pragma: no cover errormsg = f'Parameter {parkey} is missing subkey {subkey}' raise sc.KeyNotFoundError(errormsg) if sc.isnumber(pars[parkey][subkey]): # Allow scalar values or dicts, but leave everything else unchanged pars[parkey][subkey] = sc.promotetoarray(pars[parkey][subkey]) len_days = len(pars[parkey]['days']) len_vals = len(pars[parkey]['vals']) - if len_days != len_vals: + if len_days != len_vals: # pragma: no cover raise ValueError(f'Length of days ({len_days}) does not match length of values ({len_vals}) for parameter {parkey}') self.pars = pars return @@ -350,7 +350,7 @@ def process_changes(sim, changes, days): Ensure lists of changes are in consistent format. Used by change_beta and clip_edges. ''' changes = sc.promotetoarray(changes) - if len(days) != len(changes): + if len(days) != len(changes): # pragma: no cover errormsg = f'Number of days supplied ({len(days)}) does not match number of changes ({len(changes)})' raise ValueError(errormsg) return changes @@ -491,7 +491,7 @@ def apply(self, sim): inds = cvu.choose(max_n=n_int, n=abs(n_to_move)) to_move = i_layer.pop_inds(inds) s_layer.append(to_move) - else: + else: # pragma: no cover print(f'Warning: clip_edges() was applied to layer "{lkey}", but no edges were found; please check sim.people.contacts["{lkey}"]') # Ensure the edges get deleted at the end @@ -527,7 +527,7 @@ def process_daily_data(daily_data, sim, start_day, as_int=False): if daily_data == 'data': daily_data = sim.data['new_tests'] # Use default name else: - try: + try: # pragma: no cover daily_data = sim.data[daily_data] except Exception as E: errormsg = f'Tried to load testing data from sim.data["{daily_data}"], but that failed: {str(E)}.\nPlease ensure data are loaded into the sim and the column exists.' @@ -563,7 +563,7 @@ def get_subtargets(subtarget, sim): if callable(subtarget): subtarget = subtarget(sim) - if 'inds' not in subtarget: + if 'inds' not in subtarget: # pragma: no cover errormsg = f'The subtarget dict must have keys "inds" and "vals", but you supplied {subtarget}' raise ValueError(errormsg) @@ -579,7 +579,7 @@ def get_subtargets(subtarget, sim): else: subtarget_vals = subtarget['vals'] # The indices are supplied directly if sc.isiterable(subtarget_vals): - if len(subtarget_vals) != len(subtarget_inds): + if len(subtarget_vals) != len(subtarget_inds): # pragma: no cover errormsg = f'Length of subtargeting indices ({len(subtarget_inds)}) does not match length of values ({len(subtarget_vals)})' raise ValueError(errormsg) @@ -611,7 +611,7 @@ def get_quar_inds(quar_policy, sim): quar_test_inds = np.unique(np.concatenate([cvu.true(sim.people.date_quarantined==t-1-q) for q in quar_policy])) elif callable(quar_policy): quar_test_inds = quar_policy(sim) - else: + else: # pragma: no cover errormsg = f'Quarantine policy "{quar_policy}" not recognized: must be a string (start, end, both, daily), int, list, array, set, tuple, or function' raise ValueError(errormsg) return quar_test_inds diff --git a/covasim/misc.py b/covasim/misc.py index 3927c6dc6..5d99a62f7 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -54,14 +54,14 @@ def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=T raise NotImplementedError(errormsg) elif isinstance(datafile, pd.DataFrame): raw_data = datafile - else: + else: # pragma: no cover errormsg = f'Could not interpret data {type(datafile)}: must be a string or a dataframe' raise TypeError(errormsg) # Confirm data integrity and simplify if columns is not None: for col in columns: - if col not in raw_data.columns: + if col not in raw_data.columns: # pragma: no cover errormsg = f'Column "{col}" is missing from the loaded data' raise ValueError(errormsg) data = raw_data[columns] @@ -189,7 +189,7 @@ def migrate(obj, update=True, verbose=True, die=False): # Rename intervention attribute tps = sim.get_interventions(cvi.test_prob) - for tp in tps: + for tp in tps: # pragma: no cover try: tp.sensitivity = tp.test_sensitivity del tp.test_sensitivity @@ -197,7 +197,7 @@ def migrate(obj, update=True, verbose=True, die=False): pass # Migrations for People - elif isinstance(obj, cvb.BasePeople): + elif isinstance(obj, cvb.BasePeople): # pragma: no cover ppl = obj if not hasattr(ppl, 'version'): # For people prior to 2.0 if verbose: print(f'Migrating people from version <2.0 to version {cvv.__version__}') @@ -229,7 +229,7 @@ def migrate(obj, update=True, verbose=True, die=False): errormsg = f'Object {obj} of type {type(obj)} is not understood and cannot be migrated: must be a sim, multisim, scenario, or people object' if die: raise TypeError(errormsg) - elif verbose: + elif verbose: # pragma: no cover print(errormsg) return @@ -264,7 +264,7 @@ def savefig(filename=None, comments=None, **kwargs): dpi = kwargs.pop('dpi', 150) metadata = kwargs.pop('metadata', {}) - if filename is None: + if filename is None: # pragma: no cover now = sc.getdate(dateformat='%Y-%b-%d_%H.%M.%S') filename = f'covasim_{now}.png' @@ -458,7 +458,7 @@ def get_version_pars(version, verbose=True): if version in verlist: match = ver break - if match is None: + if match is None: # pragma: no cover options = '\n'.join(sum(match_map.values(), [])) errormsg = f'Could not find version "{version}" among options:\n{options}' raise ValueError(errormsg) @@ -490,7 +490,7 @@ def get_png_metadata(filename, output=False): ''' try: import PIL - except ImportError as E: + except ImportError as E: # pragma: no cover errormsg = f'Pillow import failed ({str(E)}), please install first (pip install pillow)' raise ImportError(errormsg) from E im = PIL.Image.open(filename) @@ -528,7 +528,7 @@ def get_doubling_time(sim, series=None, interval=None, start_day=None, end_day=N # Validate inputs: series if series is None or isinstance(series, str): - if not sim.results_ready: + if not sim.results_ready: # pragma: no cover raise Exception("Results not ready, cannot calculate doubling time") else: if series is None or series not in sim.result_keys(): @@ -576,7 +576,7 @@ def get_doubling_time(sim, series=None, interval=None, start_day=None, end_day=N if not exp_approx: try: import statsmodels.api as sm - except ModuleNotFoundError as E: + except ModuleNotFoundError as E: # pragma: no cover errormsg = f'Could not import statsmodels ({E}), falling back to exponential approximation' print(errormsg) exp_approx = True @@ -586,7 +586,7 @@ def get_doubling_time(sim, series=None, interval=None, start_day=None, end_day=N if r > 1: doubling_time = int_length * np.log(2) / np.log(r) doubling_time = min(doubling_time, max_doubling_time) # Otherwise, it's unbounded - else: + else: # pragma: no cover raise ValueError("Can't calculate doubling time with exponential approximation when initial value is zero.") else: @@ -601,9 +601,9 @@ def get_doubling_time(sim, series=None, interval=None, start_day=None, end_day=N doubling_time = 1.0 / doubling_rate else: doubling_time = max_doubling_time - else: + else: # pragma: no cover raise ValueError(f"Can't calculate doubling time for series {series[start_day:end_day]}. Check whether series is growing.") - else: + else: # pragma: no cover raise ValueError(f"Can't calculate doubling time for series {series[start_day:end_day]}. Check whether series is growing.") return doubling_time @@ -691,9 +691,9 @@ def zstat_generic2(value, std_diff, alternative): pvalue = sps.norm.sf(zstat) elif alternative in ['smaller', 's']: pvalue = sps.norm.cdf(zstat) - else: - raise ValueError(f'invalid alternative "{alternative}"') - return pvalue# zstat + else: # pragma: no cover + raise ValueError(f'Invalid alternative "{alternative}"') + return pvalue # shortcut names y1, n1, y2, n2 = count1, exposure1, count2, exposure2 @@ -767,7 +767,7 @@ def compute_gof(actual, predicted, normalize=True, use_frac=False, use_squared=F predicted = np.array(sc.dcp(predicted), dtype=float) # Custom estimator is supplied: use that - if skestimator is not None: + if skestimator is not None: # pragma: no cover try: import sklearn.metrics as sm sklearn_gof = getattr(sm, skestimator) # Shortcut to e.g. sklearn.metrics.max_error diff --git a/covasim/parameters.py b/covasim/parameters.py index 5b71306bf..f1390c574 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -238,7 +238,7 @@ def get_prognoses(by_age=True, version=None): expected_len = len(prognoses['age_cutoffs']) for key,val in prognoses.items(): this_len = len(prognoses[key]) - if this_len != expected_len: + if this_len != expected_len: # pragma: no cover errormsg = f'Lengths mismatch in prognoses: {expected_len} age bins specified, but key "{key}" has {this_len} entries' raise ValueError(errormsg) diff --git a/covasim/population.py b/covasim/population.py index 539bad5b4..1a152df7d 100644 --- a/covasim/population.py +++ b/covasim/population.py @@ -50,7 +50,7 @@ def make_people(sim, popdict=None, save_pop=False, popfile=None, die=True, reset # Check which type of population to produce if pop_type == 'synthpops': - if not cvreq.check_synthpops(): + if not cvreq.check_synthpops(): # pragma: no cover errormsg = f'You have requested "{pop_type}" population, but synthpops is not available; please use random, clustered, or hybrid' if die: raise ValueError(errormsg) @@ -59,7 +59,7 @@ def make_people(sim, popdict=None, save_pop=False, popfile=None, die=True, reset pop_type = 'random' location = sim['location'] - if location: + if location: # pragma: no cover print(f'Warning: not setting ages or contacts for "{location}" since synthpops contacts are pre-generated') # Actually create the population @@ -74,10 +74,10 @@ def make_people(sim, popdict=None, save_pop=False, popfile=None, die=True, reset popdict = make_randpop(sim, microstructure=pop_type, **kwargs) elif pop_type == 'synthpops': popdict = make_synthpop(sim, **kwargs) - elif pop_type is None: + elif pop_type is None: # pragma: no cover errormsg = 'You have set pop_type=None. This is fine, but you must ensure sim.popdict exists before calling make_people().' raise ValueError(errormsg) - else: + else: # pragma: no cover errormsg = f'Population type "{pop_type}" not found; choices are random, clustered, hybrid, or synthpops' raise ValueError(errormsg) @@ -92,7 +92,7 @@ def make_people(sim, popdict=None, save_pop=False, popfile=None, die=True, reset sc.printv(f'Created {pop_size} people, average age {average_age:0.2f} years', 2, verbose) if save_pop: - if popfile is None: + if popfile is None: # pragma: no cover errormsg = 'Please specify a file to save to using the popfile kwarg' raise FileNotFoundError(errormsg) else: @@ -173,7 +173,7 @@ def make_randpop(sim, use_age_data=True, use_household_data=True, sex_ratio=0.5, if microstructure == 'random': contacts, layer_keys = make_random_contacts(pop_size, sim['contacts']) elif microstructure == 'clustered': contacts, layer_keys, _ = make_microstructured_contacts(pop_size, sim['contacts']) elif microstructure == 'hybrid': contacts, layer_keys, _ = make_hybrid_contacts(pop_size, ages, sim['contacts']) - else: + else: # pragma: no cover errormsg = f'Microstructure type "{microstructure}" not found; choices are random, clustered, or hybrid' raise NotImplementedError(errormsg) @@ -338,8 +338,8 @@ def make_synthpop(sim=None, population=None, layer_mapping=None, community_conta ''' try: import synthpops as sp # Optional import - except ModuleNotFoundError as E: - errormsg = f'Please install the optional SynthPops module first, e.g. pip install synthpops' # Also caught in make_people() + except ModuleNotFoundError as E: # pragma: no cover + errormsg = 'Please install the optional SynthPops module first, e.g. pip install synthpops' # Also caught in make_people() raise ModuleNotFoundError(errormsg) from E # Handle layer mapping @@ -348,7 +348,7 @@ def make_synthpop(sim=None, population=None, layer_mapping=None, community_conta # Handle other input arguments if population is None: - if sim is None: + if sim is None: # pragma: no cover errormsg = 'Either a simulation or a population must be supplied' raise ValueError(errormsg) pop_size = sim['pop_size'] @@ -357,7 +357,7 @@ def make_synthpop(sim=None, population=None, layer_mapping=None, community_conta if community_contacts is None: if sim is not None: community_contacts = sim['contacts']['c'] - else: + else: # pragma: no cover errormsg = 'If a simulation is not supplied, the number of community contacts must be specified' raise ValueError(errormsg) @@ -379,7 +379,7 @@ def make_synthpop(sim=None, population=None, layer_mapping=None, community_conta for spkey in uid_contacts.keys(): try: lkey = layer_mapping[spkey] # Map the SynthPops key into a Covasim layer key - except KeyError: + except KeyError: # pragma: no cover errormsg = f'Could not find key "{spkey}" in layer mapping "{layer_mapping}"' raise sc.KeyNotFoundError(errormsg) int_contacts[lkey] = [] diff --git a/covasim/requirements.py b/covasim/requirements.py index a44f9245c..1df1a441a 100644 --- a/covasim/requirements.py +++ b/covasim/requirements.py @@ -16,7 +16,7 @@ def check_sciris(): ''' Check that Sciris is available and the right version ''' try: import sciris as sc - except ModuleNotFoundError: + except ModuleNotFoundError: # pragma: no cover errormsg = 'Sciris is a required dependency but is not found; please install via "pip install sciris"' raise ModuleNotFoundError(errormsg) ver = sc.__version__ @@ -34,10 +34,10 @@ def check_synthpops(verbose=False, die=False): try: import synthpops return synthpops - except ImportError as E: + except ModuleNotFoundError as E: # pragma: no cover import_error = f'Synthpops (for detailed demographic data) is not available ({str(E)})\n' if die: - raise ImportError(import_error) + raise ModuleNotFoundError(import_error) elif verbose: print(import_error) return False diff --git a/covasim/settings.py b/covasim/settings.py index 598adc5cd..3d054c497 100644 --- a/covasim/settings.py +++ b/covasim/settings.py @@ -53,8 +53,11 @@ def set_default_options(): optdesc.precision = 'Set arithmetic precision for Numba -- 32-bit by default for efficiency' options.precision = int(os.getenv('COVASIM_PRECISION', 32)) - optdesc.numba_parallel = 'Set Numba multithreading -- about 20% faster, but simulations become nondeterministic' - options.numba_parallel = bool(int(os.getenv('COVASIM_NUMBA_PARALLEL', 0))) + optdesc.numba_parallel = 'Set Numba multithreading -- 0=no, 1=partial, 2=full; full multithreading is ~20% faster, but results become nondeterministic' + options.numba_parallel = int(os.getenv('COVASIM_NUMBA_PARALLEL', 0)) + + optdesc.numba_cache = 'Set Numba caching -- saves on compilation time, but harder to update' + options.numba_cache = bool(int(os.getenv('COVASIM_NUMBA_CACHE', 1))) return options, optdesc @@ -65,7 +68,7 @@ def set_default_options(): # Specify which keys require a reload matplotlib_keys = ['font_size', 'font_family', 'dpi', 'backend'] -numba_keys = ['precision', 'numba_parallel'] +numba_keys = ['precision', 'numba_parallel', 'numba_cache'] def set_option(key=None, value=None, **kwargs): @@ -90,7 +93,8 @@ def set_option(key=None, value=None, **kwargs): - backend: which Matplotlib backend to use - interactive: convenience method to set show, close, and backend - precision: the arithmetic to use in calculations - - numba_parallel: whether to parallelize Numba + - numba_parallel: whether to parallelize Numba functions + - numba_cache: whether to cache (precompile) Numba functions **Examples**:: @@ -207,7 +211,7 @@ def handle_show(do_show): def reload_numba(): ''' Apply changes to Numba functions -- reloading modules is necessary for - changes to propagate. Not necessary if cv.options.set() is used. + changes to propagate. Not necessary to call directly if cv.options.set() is used. **Example**:: @@ -223,6 +227,7 @@ def reload_numba(): importlib.reload(cv.defaults) importlib.reload(cv.utils) importlib.reload(cv) + print('Reload complete. Note: for some changes you may also need to delete the __pycache__ folder.') return diff --git a/covasim/utils.py b/covasim/utils.py index 364e15442..447cadfb3 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -20,13 +20,18 @@ nbint = cvd.nbint nbfloat = cvd.nbfloat -# Specify whether to allow parallel Numba calculation -- about 20% faster, but the random number stream becomes nondeterministic -parallel = cvo.numba_parallel +# Specify whether to allow parallel Numba calculation -- 10% faster for safe and 20% faster for random, but the random number stream becomes nondeterministic for the latter +safe_parallel = cvo.numba_parallel >= 1 +rand_parallel = cvo.numba_parallel == 2 +if cvo.numba_parallel not in [0,1,2]: + errormsg = f'Numba parallel must be 0, 1, or 2, not {cvo.numba_parallel}' + raise ValueError(errormsg) +cache = cvo.numba_cache #%% The core Covasim functions -- compute the infections -@nb.njit( (nbint, nbfloat[:], nbfloat[:], nbfloat[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) +@nb.njit( (nbint, nbfloat[:], nbfloat[:], nbfloat[:], nbfloat, nbfloat, nbfloat), cache=cache, parallel=safe_parallel) def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, load_ratio, high_cap): # pragma: no cover ''' Calculate relative transmissibility for time t. Includes time varying @@ -69,7 +74,7 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat), cache=True, parallel=parallel) +@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat), cache=cache, parallel=safe_parallel) def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] @@ -80,9 +85,9 @@ def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, return rel_trans, rel_sus -@nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=True, parallel=parallel) -def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): - ''' The heaviest step of the model -- figure out who gets infected on this timestep ''' +@nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) +def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): # pragma: no cover + ''' The heaviest step of the model -- figure out who gets infected on this timestep. Cannot be parallelized since random numbers are used ''' betas = beta * layer_betas * rel_trans[sources] * rel_sus[targets] # Calculate the raw transmission probabilities nonzero_inds = betas.nonzero()[0] # Find nonzero entries nonzero_betas = betas[nonzero_inds] # Remove zero entries from beta @@ -94,7 +99,7 @@ def compute_infections(beta, sources, targets, layer_betas, rel_trans, r return source_inds, target_inds -@nb.njit((nbint[:], nbint[:], nb.int64[:]), cache=True) +@nb.njit((nbint[:], nbint[:], nb.int64[:]), cache=cache) def find_contacts(p1, p2, inds): # pragma: no cover """ Numba for Layer.find_contacts() @@ -237,7 +242,7 @@ def set_seed(seed=None): seed (int): the random seed ''' - @nb.njit((nbint,), cache=True) + @nb.njit((nbint,), cache=cache) def set_seed_numba(seed): return np.random.seed(seed) @@ -334,7 +339,7 @@ def n_multinomial(probs, n): # No speed gain from Numba return np.searchsorted(np.cumsum(probs), np.random.random(n)) -@nb.njit((nbfloat,), cache=True) # This hugely increases performance +@nb.njit((nbfloat,), cache=cache, parallel=rand_parallel) # Numba hugely increases performance def poisson(rate): ''' A Poisson trial. @@ -349,7 +354,7 @@ def poisson(rate): return np.random.poisson(rate, 1)[0] -@nb.njit((nbfloat, nbint), cache=True) # Numba hugely increases performance +@nb.njit((nbfloat, nbint), cache=cache, parallel=rand_parallel) # Numba hugely increases performance def n_poisson(rate, n): ''' An array of Poisson trials. @@ -386,7 +391,7 @@ def n_neg_binomial(rate, dispersion, n, step=1): # Numba not used due to incompa return samples -@nb.njit((nbint, nbint), cache=True) # This hugely increases performance +@nb.njit((nbint, nbint), cache=cache) # This hugely increases performance def choose(max_n, n): ''' Choose a subset of items (e.g., people) without replacement. @@ -402,7 +407,7 @@ def choose(max_n, n): return np.random.choice(max_n, n, replace=False) -@nb.njit((nbint, nbint), cache=True) # This hugely increases performance +@nb.njit((nbint, nbint), cache=cache) # This hugely increases performance def choose_r(max_n, n): ''' Choose a subset of items (e.g., people), with replacement. diff --git a/tests/.coveragerc b/tests/.coveragerc new file mode 100644 index 000000000..82c2d2afb --- /dev/null +++ b/tests/.coveragerc @@ -0,0 +1,24 @@ +# .coveragerc to control coverage.py +[run] +branch = True +source = covasim +omit = *census* + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if __name__ == .__main__.: + + +ignore_errors = True \ No newline at end of file diff --git a/tests/check_coverage b/tests/check_coverage index d53159424..54105af49 100755 --- a/tests/check_coverage +++ b/tests/check_coverage @@ -2,7 +2,7 @@ # Note that although the script runs when parallelized, the coverage results are wrong. echo 'Running tests...' -coverage run --source=../covasim -m pytest test_*.py +pytest test_*.py --cov-config=.coveragerc --cov=../covasim --workers auto --durations=0 echo 'Creating HTML report...' coverage html diff --git a/tests/devtests/test_numba_parallelization.py b/tests/devtests/test_numba_parallelization.py new file mode 100644 index 000000000..c5c936137 --- /dev/null +++ b/tests/devtests/test_numba_parallelization.py @@ -0,0 +1,27 @@ +''' +Test different parallelization options +''' + +import covasim as cv +import sciris as sc + +# Set the parallelization to use -- 0 = none, 1 = safe, 2 = rand +parallel = 1 + +pars = dict( + pop_size = 1e6, + n_days = 200, + verbose = 0.1, +) + +cv.options.set(numba_cache=0, numba_parallel=parallel) + +parstr = f'Parallel={cv.options.numba_parallel}' +print('Initializing (always single core)') +sim = cv.Sim(**pars, label=parstr) +sim.initialize() + +print(f'Running ({parstr})') +sc.tic() +sim.run() +sc.toc(label=parstr) \ No newline at end of file diff --git a/tests/requirements_frozen.txt b/tests/requirements_frozen.txt new file mode 100644 index 000000000..6e7672ac8 --- /dev/null +++ b/tests/requirements_frozen.txt @@ -0,0 +1,52 @@ +backcall==0.2.0 +certifi==2020.12.5 +chardet==4.0.0 +ConnPlotter==0.7a0 +cycler==0.10.0 +decorator==4.4.2 +dill==0.3.3 +et-xmlfile==1.0.1 +gitdb==4.0.5 +GitPython==3.1.14 +idna==2.10 +ipython==7.21.0 +ipython-genutils==0.2.0 +jdcal==1.4.1 +jedi==0.18.0 +jellyfish==0.8.2 +jsonpickle==2.0.0 +kiwisolver==1.3.1 +line-profiler==3.1.0 +llvmlite==0.35.0 +matplotlib==3.3.4 +memory-profiler==0.58.0 +multiprocess==0.70.11.1 +numba==0.52.0 +numpy==1.20.1 +openpyexcel==2.5.14 +pandas==1.2.3 +parso==0.8.1 +patsy==0.5.1 +pexpect==4.8.0 +pickleshare==0.7.5 +Pillow==8.1.2 +prompt-toolkit==3.0.16 +psutil==5.8.0 +ptyprocess==0.7.0 +Pygments==2.8.1 +PyNEST==2.16.0 +pyparsing==2.4.7 +python-dateutil==2.8.1 +pytz==2021.1 +requests==2.25.1 +scipy==1.6.1 +sciris==1.0.2 +six==1.15.0 +smmap==3.0.5 +statsmodels==0.12.2 +Topology==2.16.0 +traitlets==5.0.5 +urllib3==1.26.3 +wcwidth==0.2.5 +xlrd==1.2.0 +XlsxWriter==1.3.7 \ No newline at end of file diff --git a/tests/requirements_test.txt b/tests/requirements_test.txt new file mode 100644 index 000000000..f44ca7f56 --- /dev/null +++ b/tests/requirements_test.txt @@ -0,0 +1,3 @@ +pytest +pytest-cov +pytest-parallel \ No newline at end of file diff --git a/tests/test_other.py b/tests/test_other.py index c76abd367..5bebf6126 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -393,6 +393,9 @@ def test_sim(): sim['interventions'] = {'which': 'change_beta', 'pars': {'days': 10, 'changes': 0.5}} sim.validate_pars() + # Check conversion to absolute parameters + cv.parameters.absolute_prognoses(sim['prognoses']) + # Test intervention functions and results analyses cv.Sim(pop_size=100, verbose=0, interventions=lambda sim: (sim.t==20 and (sim.__setitem__('beta', 0) or print(f'Applying lambda intervention to set beta=0 on day {sim.t}')))).run() # ...This is not the recommended way of defining interventions. diff --git a/tests/unittests/test_interventions.py b/tests/unittests/test_specific_interventions.py similarity index 100% rename from tests/unittests/test_interventions.py rename to tests/unittests/test_specific_interventions.py From 671718c66aa763da0008412b29c0bdb7b9351185 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 10 Mar 2021 03:44:14 -0800 Subject: [PATCH 140/569] update readme --- README.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 45919e23d..a66777855 100644 --- a/README.rst +++ b/README.rst @@ -38,19 +38,19 @@ Covasim has been used for analyses in over a dozen countries, both to inform pol 3. **Modelling the impact of reducing control measures on the COVID-19 pandemic in a low transmission setting**. Scott N, Palmer A, Delport D, Abeysuriya RG, Stuart RM, Kerr CC, Mistry D, Klein DJ, Sacks-Davis R, Heath K, Hainsworth S, Pedrana A, Stoove M, Wilson DP, Hellard M (in press; accepted 2020-09-02). *Medical Journal of Australia* [`Preprint `__]; doi: https://doi.org/10.1101/2020.06.11.20127027. -4. **The role of masks, testing and contact tracing in preventing COVID-19 resurgences: a case study from New South Wales, Australia**. Stuart RM, Abeysuriya RG, Kerr CC, Mistry D, Klein DJ, Gray R, Hellard M, Scott N (under review; posted 2020-09-03). *medRxiv* 2020.09.02.20186742; doi: https://doi.org/10.1101/2020.09.02.20186742. +4. **Lessons learned from Vietnam's COVID-19 response: the role of adaptive behaviour change and testing in epidemic control**. Pham QD, Stuart RM, Nguyen TV, Luong QC, Tran DQ, Phan LT, Dang TQ, Tran DN, Mistry D, Klein DJ, Abeysuriya RG, Oron AP, Kerr CC (in press; accepted 2021-02-25). *Lancet Global Health*; doi: https://doi.org/10.1101/2020.12.18.20248454. -5. **Schools are not islands: Balancing COVID-19 risk and educational benefits using structural and temporal countermeasures**. Cohen JA, Mistry D, Kerr CC, Klein DJ (under review; posted 2020-09-10). *medRxiv* 2020.09.08.20190942; doi: https://doi.org/10.1101/2020.09.08.20190942. +5. **The role of masks, testing and contact tracing in preventing COVID-19 resurgences: a case study from New South Wales, Australia**. Stuart RM, Abeysuriya RG, Kerr CC, Mistry D, Klein DJ, Gray R, Hellard M, Scott N (under review; posted 2020-09-03). *medRxiv* 2020.09.02.20186742; doi: https://doi.org/10.1101/2020.09.02.20186742. -6. **The potential contribution of face coverings to the control of SARS-CoV-2 transmission in schools and broader society in the UK: a modelling study**. Panovska-Griffiths J, Kerr CC, Waites W, Stuart RM, Mistry D, Foster D, Klein DJ, Viner R, Bonnell C (under review; posted 2020-10-08). *medRxiv* 2020.09.28.20202937; doi: https://doi.org/10.1101/2020.09.28.20202937. +6. **Schools are not islands: Balancing COVID-19 risk and educational benefits using structural and temporal countermeasures**. Cohen JA, Mistry D, Kerr CC, Klein DJ (under review; posted 2020-09-10). *medRxiv* 2020.09.08.20190942; doi: https://doi.org/10.1101/2020.09.08.20190942. -7. **COVID-19 reopening strategies at the county level in the face of uncertainty: Multiple Models for Outbreak Decision Support**. Shea K, Borchering RK, Probert WJM, et al. (under review; posted 2020-11-05). *medRxiv* 2020.11.03.20225409; doi: https://doi.org/10.1101/2020.11.03.20225409. +7. **The potential contribution of face coverings to the control of SARS-CoV-2 transmission in schools and broader society in the UK: a modelling study**. Panovska-Griffiths J, Kerr CC, Waites W, Stuart RM, Mistry D, Foster D, Klein DJ, Viner R, Bonnell C (under review; posted 2020-10-08). *medRxiv* 2020.09.28.20202937; doi: https://doi.org/10.1101/2020.09.28.20202937. -8. **Lessons learned from Vietnam's COVID-19 response: the role of adaptive behaviour change and testing in epidemic control**. Pham QD, Stuart RM, Nguyen TV, Luong QC, Tran DQ, Phan LT, Dang TQ, Tran DN, Mistry D, Klein DJ, Abeysuriya RG, Oron AP, Kerr CC (under review; posted 2020-12-19). *medRxiv* 2020.12.18.20248454; doi: https://doi.org/10.1101/2020.12.18.20248454. +8. **COVID-19 reopening strategies at the county level in the face of uncertainty: Multiple Models for Outbreak Decision Support**. Shea K, Borchering RK, Probert WJM, et al. (under review; posted 2020-11-05). *medRxiv* 2020.11.03.20225409; doi: https://doi.org/10.1101/2020.11.03.20225409. 9. **Preventing a cluster from becoming a new wave in settings with zero community COVID-19 cases**. Abeysuriya RG, Delport D, Stuart RM, Sacks-Davis R, Kerr CC, Mistry D, Klein DJ, Hellard M, Scott N (under review; posted 2020-12-22). *medRxiv* 2020.12.21.20248595; doi: https://doi.org/10.1101/2020.12.21.20248595. -10. **Modelling the impact of reopening schools in early 2021 in the presence of the new SARS-CoV-2 variant in the UK**. Panovska-Griffiths J, Kerr CC, Waites W, Stuart RM, Mistry D, Foster D, Klein DJ, Viner R, Bonnell C (under review). +10. **Modelling the impact of reopening schools in early 2021 in the presence of the new SARS-CoV-2 variant in the UK**. Panovska-Griffiths J, Kerr CC, Waites W, Stuart RM, Mistry D, Foster D, Klein DJ, Viner R, Bonnell C (under review; posted 2021-02-09). *medRxiv* 2021.02.07.21251287; doi: https://doi.org/10.1101/2021.02.07.21251287. If you have written a paper or report using Covasim, we'd love to know about it! Please write to us `here `__. From d43272aa67f6bab191663fe0721cc21f9c18a8d7 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 10 Mar 2021 09:30:22 -0500 Subject: [PATCH 141/569] fix so that we can cache populations in advance --- covasim/immunity.py | 6 +++--- covasim/people.py | 6 +++--- tests/devtests/test_variants.py | 35 ++++++++++++++++++++++++--------- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index e9e74c808..73259e349 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -57,7 +57,7 @@ def parse_strain_pars(self, strain=None, strain_label=None): # List of choices currently available: new ones can be added to the list along with their aliases choices = { - 'default': ['default', 'wild', 'pre-existing'], + 'wild': ['default', 'wild', 'pre-existing'], 'b117': ['b117', 'B117', 'B.1.1.7', 'UK', 'uk', 'UK variant', 'uk variant'], 'b1351': ['b1351', 'B1351', 'B.1.351', 'SA', 'sa', 'SA variant', 'sa variant'], # TODO: add other aliases @@ -65,7 +65,7 @@ def parse_strain_pars(self, strain=None, strain_label=None): } # Empty pardict for wild strain - if strain in choices['default']: + if strain in choices['wild']: strain_pars = dict() self.strain_label = strain @@ -142,7 +142,7 @@ def apply(self, sim): sim['n_strains'] += 1 # Update strain-specific people attributes - # cvu.update_strain_attributes(sim.people) # don't think we need to do this if we just create people arrays with number of total strains in sim + cvu.update_strain_attributes(sim.people) # don't think we need to do this if we just create people arrays with number of total strains in sim susceptible_inds = cvu.true(sim.people.susceptible) importation_inds = np.random.choice(susceptible_inds, self.n_imports) sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) diff --git a/covasim/people.py b/covasim/people.py index be649a746..6ea20f55c 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -59,7 +59,7 @@ def __init__(self, pars, strict=True, **kwargs): if key == 'uid': self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. - self[key] = np.full((self.pars['total_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') + self[key] = np.full((self.pars['n_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') elif key == 'vaccinations': self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) else: @@ -75,7 +75,7 @@ def __init__(self, pars, strict=True, **kwargs): # Set strain states, which store info about which strain a person is exposed to for key in self.meta.strain_states: if 'by' in key: - self[key] = np.full((self.pars['total_strains'], self.pop_size), False, dtype=bool, order='F') + self[key] = np.full((self.pars['n_strains'], self.pop_size), False, dtype=bool, order='F') else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -91,7 +91,7 @@ def __init__(self, pars, strict=True, **kwargs): self.flows = {key:0 for key in cvd.new_result_flows} for key in cvd.new_result_flows: if 'by_strain' in key: - self.flows[key] = np.full(self.pars['total_strains'], 0, dtype=cvd.default_float) + self.flows[key] = np.full(self.pars['n_strains'], 0, dtype=cvd.default_float) # Although we have called init(), we still need to call initialize() self.initialized = False diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 693d87a71..2b92b53cf 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -9,6 +9,22 @@ do_show = 1 do_save = 0 +def test_synthpops(): + sim = cv.Sim(pop_size=5000, pop_type='synthpops') + sim.popdict = cv.make_synthpop(sim, with_facilities=True, layer_mapping={'LTCF': 'f'}) + sim.reset_layer_pars() + + # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 + sim.vxsubtarg = sc.objdict() + sim.vxsubtarg.age = [75, 65, 50, 18] + sim.vxsubtarg.prob = [.05, .05, .05, .05] + sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] + pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) + sim['interventions'] += [pfizer] + + sim.run() + return sim + def test_vaccine_1strain(do_plot=True, do_show=True, do_save=False): sc.heading('Run a basic sim with 1 strain, pfizer vaccine') @@ -417,18 +433,19 @@ def get_ind_of_min_value(list, time): sim = cv.Sim() sim.run() + sim0 = test_synthpops() + # Run more complex tests - sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) - scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) + # scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # Run Vaccine tests - sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim5 = test_vaccine_1strain() - sim6 = test_vaccine_2strains() + # sim5 = test_vaccine_1strain() + # sim6 = test_vaccine_2strains() sc.toc() From 10cd5367d3d085b5a63b01cc6281a8a8721b6751 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 10 Mar 2021 13:13:58 -0500 Subject: [PATCH 142/569] so that msims run --- covasim/run.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/covasim/run.py b/covasim/run.py index aac53d307..436d83152 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -246,7 +246,11 @@ def reduce(self, quantiles=None, use_mean=False, bounds=None, output=False): raw[reskey] = np.zeros((reduced_sim.npts, len(self.sims))) for s,sim in enumerate(self.sims): vals = sim.results[reskey].values - if len(vals) != reduced_sim.npts: + if 'by_strain' in reskey: + length = vals.shape[1] + else: + length = len(vals) + if length != reduced_sim.npts: errormsg = f'Cannot reduce sims with inconsistent numbers of days: {reduced_sim.npts} vs. {len(vals)}' raise ValueError(errormsg) raw[reskey][:,s] = vals From 180c59ece03ec87c1f5075be4a2366ef4c8d3dd3 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 10 Mar 2021 13:15:07 -0500 Subject: [PATCH 143/569] better fix, thanks katherine! --- covasim/run.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index 436d83152..59b101b8d 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -246,11 +246,7 @@ def reduce(self, quantiles=None, use_mean=False, bounds=None, output=False): raw[reskey] = np.zeros((reduced_sim.npts, len(self.sims))) for s,sim in enumerate(self.sims): vals = sim.results[reskey].values - if 'by_strain' in reskey: - length = vals.shape[1] - else: - length = len(vals) - if length != reduced_sim.npts: + if vals.size != reduced_sim.npts: errormsg = f'Cannot reduce sims with inconsistent numbers of days: {reduced_sim.npts} vs. {len(vals)}' raise ValueError(errormsg) raw[reskey][:,s] = vals From 94a2efe911a14bf5caeb226364a0ae8c5def061a Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 10 Mar 2021 13:26:02 -0500 Subject: [PATCH 144/569] nvm we need this ugliness --- covasim/run.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/covasim/run.py b/covasim/run.py index 59b101b8d..436d83152 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -246,7 +246,11 @@ def reduce(self, quantiles=None, use_mean=False, bounds=None, output=False): raw[reskey] = np.zeros((reduced_sim.npts, len(self.sims))) for s,sim in enumerate(self.sims): vals = sim.results[reskey].values - if vals.size != reduced_sim.npts: + if 'by_strain' in reskey: + length = vals.shape[1] + else: + length = len(vals) + if length != reduced_sim.npts: errormsg = f'Cannot reduce sims with inconsistent numbers of days: {reduced_sim.npts} vs. {len(vals)}' raise ValueError(errormsg) raw[reskey][:,s] = vals From f399dd7441e105726570012402a768eac689f98c Mon Sep 17 00:00:00 2001 From: Jen Schripsema Date: Wed, 10 Mar 2021 11:56:20 -0700 Subject: [PATCH 145/569] added blank lines to fix docs build break in people.infect docstring --- covasim/people.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/covasim/people.py b/covasim/people.py index 0000cc37e..d2216a5ea 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -325,10 +325,12 @@ def make_susceptible(self, inds): def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None): ''' Infect people and determine their eventual outcomes. + * Every infected person can infect other people, regardless of whether they develop symptoms * Infected people that develop symptoms are disaggregated into mild vs. severe (=requires hospitalization) vs. critical (=requires ICU) * Every asymptomatic, mildly symptomatic, and severely symptomatic person recovers * Critical cases either recover or die + Method also deduplicates input arrays in case one agent is infected many times and stores who infected whom in infection_log list. From 39ba430c11983553e61f6ec13a1070b83bbbef20 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 10 Mar 2021 12:35:14 -0800 Subject: [PATCH 146/569] update get_interventions --- covasim/base.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 8db367173..6aa55b84f 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -671,8 +671,10 @@ def _get_ia(self, which, label=None, partial=False, as_list=False, as_inds=False else: # Standard usage case position = 0 if first else -1 # Choose either the first or last element - if label is None: - label = position # Get the last element + if label is None: # Get all interventions if no label is supplied, e.g. sim.get_interventions() + label = np.arange(n_ia) + if isinstance(label, np.ndarray): # Allow arrays to be provided + label = label.tolist() labels = sc.promotetolist(label) # Calculate the matches @@ -692,15 +694,15 @@ def _get_ia(self, which, label=None, partial=False, as_list=False, as_inds=False matches.append(ia_obj) match_inds.append(ind) else: # pragma: no cover - errormsg = f'Could not interpret label type "{type(label)}": should be str, int, or {which} class' + errormsg = f'Could not interpret label type "{type(label)}": should be str, int, list, or {which} class' raise TypeError(errormsg) # Parse the output options if as_inds: output = match_inds - elif as_list: + elif as_list: # Used by get_interventions() output = matches - else: # Normal case, return actual interventions + else: if len(matches) == 0: # pragma: no cover if die: errormsg = f'No {which} matching "{label}" were found' @@ -708,7 +710,7 @@ def _get_ia(self, which, label=None, partial=False, as_list=False, as_inds=False else: output = None else: - output = matches[position] # Return either the first or last match + output = matches[position] # Return either the first or last match (usually), used by get_intervention() return output From 19341473360bbd9b8387bcff4a8d2ab174c6d87d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 10 Mar 2021 12:47:09 -0800 Subject: [PATCH 147/569] catch uninitialized interventions --- covasim/sim.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 0c405cf3f..f51ef0574 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -483,13 +483,16 @@ def step(self): people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation') # Apply interventions - for intervention in self['interventions']: + for i,intervention in enumerate(self['interventions']): if isinstance(intervention, cvi.Intervention): + if not intervention.initialized: # pragma: no cover + errormsg = f'Intervention {i} (label={intervention.label}, {type(intervention)}) has not been initialized' + raise RuntimeError(errormsg) intervention.apply(self) # If it's an intervention, call the apply() method elif callable(intervention): intervention(self) # If it's a function, call it directly else: - errormsg = f'Intervention {intervention} is neither callable nor an Intervention object' + errormsg = f'Intervention {i} ({intervention}) is neither callable nor an Intervention object' raise ValueError(errormsg) people.update_states_post() # Check for state changes after interventions @@ -537,13 +540,16 @@ def step(self): self.results[key][t] += count # Apply analyzers -- same syntax as interventions - for analyzer in self['analyzers']: + for i,analyzer in enumerate(self['analyzers']): if isinstance(analyzer, cva.Analyzer): + if not analyzer.initialized: # pragma: no cover + errormsg = f'Analyzer {i} (label={analyzer.label}, {type(analyzer)}) has not been initialized' + raise RuntimeError(errormsg) analyzer.apply(self) # If it's an intervention, call the apply() method elif callable(analyzer): analyzer(self) # If it's a function, call it directly else: - errormsg = f'Analyzer {analyzer} is neither callable nor an Analyzer object' + errormsg = f'Analyzer {i} ({analyzer}) is neither callable nor an Analyzer object' raise ValueError(errormsg) # Tidy up From cdc049e1c254f87691bd400d6403b029473b2f1c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 10 Mar 2021 13:02:19 -0800 Subject: [PATCH 148/569] add custom estimator support --- covasim/misc.py | 16 +++++++++++++--- tests/test_analysis.py | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index 5d99a62f7..9ee380ffb 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -730,7 +730,7 @@ def zstat_generic2(value, std_diff, alternative): return pvalue#, stat -def compute_gof(actual, predicted, normalize=True, use_frac=False, use_squared=False, as_scalar='none', eps=1e-9, skestimator=None, **kwargs): +def compute_gof(actual, predicted, normalize=True, use_frac=False, use_squared=False, as_scalar='none', eps=1e-9, skestimator=None, estimator=None, **kwargs): ''' Calculate the goodness of fit. By default use normalized absolute error, but highly customizable. For example, mean squared error is equivalent to @@ -745,7 +745,8 @@ def compute_gof(actual, predicted, normalize=True, use_frac=False, use_squared=F as_scalar (str): return as a scalar instead of a time series: choices are sum, mean, median eps (float): to avoid divide-by-zero skestimator (str): if provided, use this scikit-learn estimator instead - kwargs (dict): passed to the scikit-learn estimator + estimator (func): if provided, use this custom estimator instead + kwargs (dict): passed to the scikit-learn or custom estimator Returns: gofs (arr): array of goodness-of-fit values, or a single value if as_scalar is True @@ -766,7 +767,7 @@ def compute_gof(actual, predicted, normalize=True, use_frac=False, use_squared=F actual = np.array(sc.dcp(actual), dtype=float) predicted = np.array(sc.dcp(predicted), dtype=float) - # Custom estimator is supplied: use that + # Scikit-learn estimator is supplied: use that if skestimator is not None: # pragma: no cover try: import sklearn.metrics as sm @@ -778,6 +779,15 @@ def compute_gof(actual, predicted, normalize=True, use_frac=False, use_squared=F gof = sklearn_gof(actual, predicted, **kwargs) return gof + # Custom estimator is supplied: use that + if estimator is not None: # pragma: no cover + try: + gof = estimator(actual, predicted, **kwargs) + except Exception as E: + errormsg = f'Custom estimator "{estimator}" must be a callable function that accepts actual and predicted arrays, plus optional kwargs' + raise RuntimeError(errormsg) from E + return gof + # Default case: calculate it manually else: # Key step -- calculate the mismatch! diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 6700393e4..a88ee4f82 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -5,6 +5,7 @@ import numpy as np import sciris as sc import covasim as cv +import pytest #%% General settings @@ -90,6 +91,21 @@ def test_fit(): assert fit1.mismatch != fit2.mismatch, "Differences between fit and data remains unchanged after changing sim seed" + # Test custom analyzers + actual = np.array([1,2,4]) + predicted = np.array([1,2,3]) + + def simple(actual, predicted, scale=2): + return np.sum(abs(actual - predicted))*scale + + gof1 = cv.compute_gof(actual, predicted, normalize=False, as_scalar='sum') + gof2 = cv.compute_gof(actual, predicted, estimator=simple, scale=1.0) + assert gof1 == gof2 + with pytest.raises(Exception): + cv.compute_gof(actual, predicted, skestimator='not an estimator') + with pytest.raises(Exception): + cv.compute_gof(actual, predicted, estimator='not an estimator') + if do_plot: fit1.plot() From 48a88e390751814c2554b5ae6a773fe9900281d5 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 10 Mar 2021 16:59:50 -0800 Subject: [PATCH 149/569] fix reprs --- covasim/base.py | 14 +++++--------- covasim/run.py | 6 ++++-- covasim/sim.py | 15 +++++++++++---- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 6aa55b84f..d8fa46bd5 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -13,7 +13,6 @@ from . import misc as cvm from . import defaults as cvd from . import parameters as cvpar -from .settings import options as cvo # Specify all externally visible classes this file defines __all__ = ['ParsObj', 'Result', 'BaseSim', 'BasePeople', 'Person', 'FlexDict', 'Contacts', 'Layer'] @@ -23,17 +22,14 @@ class FlexPretty(sc.prettyobj): ''' - A class that by default changes the display type depending on the current level - of verbosity. + A class that supports multiple different display options: namely obj.brief() + for a one-line description and obj.disp() for a full description. ''' def __repr__(self): - ''' Set display options based on current level of verbosity ''' + ''' Use brief repr by default ''' try: - if cvo['verbose']: - string = self._disp() - else: - string = self._brief() + string = self._brief() except Exception as E: string = sc.objectid(self) string += f'Warning, something went wrong printing object:\n{str(E)}' @@ -235,7 +231,7 @@ def _brief(self): # ...but if anything goes wrong, return the default with a warning except Exception as E: # pragma: no cover string = sc.objectid(self) - string += f'Warning, sim appears to be malformed:\n{str(E)}' + string += f'Warning, sim appears to be malformed; use sim.disp() for details:\n{str(E)}' return string diff --git a/covasim/run.py b/covasim/run.py index cd8ecc0b4..07a649bc3 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -780,7 +780,8 @@ def _brief(self): ''' try: labelstr = f'"{self.label}"; ' if self.label else '' - string = f'MultiSim({labelstr}n_sims: {len(self.sims)}; base: {self.base_sim.brief(output=True)})' + n_sims = 0 if not self.sims else len(self.sims) + string = f'MultiSim({labelstr}n_sims: {n_sims}; base: {self.base_sim.brief(output=True)})' except Exception as E: string = sc.objectid(self) string += f'Warning, multisim appears to be malformed:\n{str(E)}' @@ -1247,7 +1248,8 @@ def _brief(self): ''' try: labelstr = f'"{self.label}"; ' if self.label else '' - string = f'Scenarios({labelstr}n_scenarios: {len(self.sims)}; base: {self.base_sim.brief(output=True)})' + n_scenarios = 0 if not self.scenarios else len(self.scenarios) + string = f'Scenarios({labelstr}n_scenarios: {n_scenarios}; base: {self.base_sim.brief(output=True)})' except Exception as E: string = sc.objectid(self) string += f'Warning, scenarios appear to be malformed:\n{str(E)}' diff --git a/covasim/sim.py b/covasim/sim.py index f51ef0574..d5b87ca06 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -570,11 +570,11 @@ def run(self, do_plot=False, until=None, restore_pars=True, reset_seed=True, ver restore_pars (bool): whether to make a copy of the parameters before the run and restore it after, so runs are repeatable reset_seed (bool): whether to reset the random number stream immediately before run verbose (float): level of detail to print, e.g. -1 = one-line output, 0 = no output, 0.1 = print every 10th day, 1 = print every day - output (bool): whether to return the results dictionary as output - kwargs (dict): passed to sim.plot() + output (bool/str): whether to return the results dictionary as output, or the sim object if output='sim' + kwargs (dict): passed to sim.plot() if do_plot is True Returns: - results (dict): the results object (also modifies in-place) + None if output=False, the results object (also modifies in-place) if output=True, or the sim object if output='sim' ''' # Initialization steps -- start the timer, initialize the sim and the seed, and check that the sim hasn't been run @@ -633,7 +633,14 @@ def run(self, do_plot=False, until=None, restore_pars=True, reset_seed=True, ver sc.printv(f'Run finished after {elapsed:0.2f} s.\n', 1, verbose) if do_plot: # Optionally plot self.plot(**kwargs) - if output: + else: + if len(kwargs): # pragma: no cover + keys = '", "'.join(list(kwargs.keys())) + errormsg = f'Kwargs "{keys}" were not processed since plotting is not enabled; this is treated as an error' + raise RuntimeError(errormsg) + if output == 'sim': + return self + elif output: return self.results else: return From 8e67c09a2fc0b13a82f1b11557c6d5fbd511b259 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 11 Mar 2021 12:51:28 -0500 Subject: [PATCH 150/569] defaults should be no waning for now --- covasim/parameters.py | 2 +- covasim/people.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index b83db9891..d12944834 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -73,7 +73,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 pars['imm_pars'] = {} for ax in cvd.immunity_axes: - pars['imm_pars'][ax] = dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 50, 'lower_asymp': 0.3, 'decay_rate': -5}) + pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) pars['rel_imm'] = {} # Relative immunity scalings depending on the severity of symptoms pars['rel_imm']['asymptomatic'] = 0.7 pars['rel_imm']['mild'] = 0.9 diff --git a/covasim/people.py b/covasim/people.py index 6ea20f55c..1b625eabc 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -326,7 +326,7 @@ def check_quar(self): for ind,end_day in self._pending_quarantine[self.t]: if self.quarantined[ind]: self.date_end_quarantine[ind] = max(self.date_end_quarantine[ind], end_day) # Extend quarantine if required - elif not (self.dead[ind] or self.recovered[ind] or self.diagnosed[ind]): # Unclear whether recovered should be included here + elif not (self.dead[ind] or self.diagnosed[ind]): # Unclear whether recovered should be included here self.quarantined[ind] = True self.date_quarantined[ind] = self.t self.date_end_quarantine[ind] = end_day From 16be194558d50158fda24ae38d989f0f7c2213d4 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 11 Mar 2021 12:36:39 -0800 Subject: [PATCH 151/569] update message --- covasim/misc.py | 2 +- covasim/settings.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index 9ee380ffb..79df1c32c 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -780,7 +780,7 @@ def compute_gof(actual, predicted, normalize=True, use_frac=False, use_squared=F return gof # Custom estimator is supplied: use that - if estimator is not None: # pragma: no cover + if estimator is not None: try: gof = estimator(actual, predicted, **kwargs) except Exception as E: diff --git a/covasim/settings.py b/covasim/settings.py index 3d054c497..e090c2cb4 100644 --- a/covasim/settings.py +++ b/covasim/settings.py @@ -53,7 +53,7 @@ def set_default_options(): optdesc.precision = 'Set arithmetic precision for Numba -- 32-bit by default for efficiency' options.precision = int(os.getenv('COVASIM_PRECISION', 32)) - optdesc.numba_parallel = 'Set Numba multithreading -- 0=no, 1=partial, 2=full; full multithreading is ~20% faster, but results become nondeterministic' + optdesc.numba_parallel = 'Set Numba multithreading -- 0=no, 1=safe, 2=full; full multithreading is ~20% faster, but results become nondeterministic' options.numba_parallel = int(os.getenv('COVASIM_NUMBA_PARALLEL', 0)) optdesc.numba_cache = 'Set Numba caching -- saves on compilation time, but harder to update' @@ -227,7 +227,7 @@ def reload_numba(): importlib.reload(cv.defaults) importlib.reload(cv.utils) importlib.reload(cv) - print('Reload complete. Note: for some changes you may also need to delete the __pycache__ folder.') + print("Reload complete. Note: for some options to take effect, you may also need to delete Covasim's __pycache__ folder.") return From b910f63b866108348b7b530017d286c7845ea558 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 11 Mar 2021 12:39:14 -0800 Subject: [PATCH 152/569] update comments --- covasim/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/utils.py b/covasim/utils.py index 447cadfb3..5a8df6c92 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -391,7 +391,7 @@ def n_neg_binomial(rate, dispersion, n, step=1): # Numba not used due to incompa return samples -@nb.njit((nbint, nbint), cache=cache) # This hugely increases performance +@nb.njit((nbint, nbint), cache=cache) # Numba hugely increases performance def choose(max_n, n): ''' Choose a subset of items (e.g., people) without replacement. @@ -407,7 +407,7 @@ def choose(max_n, n): return np.random.choice(max_n, n, replace=False) -@nb.njit((nbint, nbint), cache=cache) # This hugely increases performance +@nb.njit((nbint, nbint), cache=cache) # Numba hugely increases performance def choose_r(max_n, n): ''' Choose a subset of items (e.g., people), with replacement. @@ -423,7 +423,7 @@ def choose_r(max_n, n): return np.random.choice(max_n, n, replace=True) -def choose_w(probs, n, unique=True): +def choose_w(probs, n, unique=True): # No performance gain from Numba ''' Choose n items (e.g. people), each with a probability from the distribution probs. From b1558704f3fff8ab47184d4eb34bffba5f125aae Mon Sep 17 00:00:00 2001 From: Romesh Abeysuriya Date: Fri, 12 Mar 2021 11:25:01 +1100 Subject: [PATCH 153/569] Add Layer.update() --- covasim/base.py | 46 +++++++++++++++++++++++++++++++++++++++++++++- covasim/people.py | 25 +++---------------------- covasim/version.py | 4 ++-- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 5621234ef..6e489cc04 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1107,7 +1107,12 @@ def add_contacts(self, contacts, lkey=None, beta=None): # Create the layer if it doesn't yet exist if lkey not in self.contacts: - self.contacts[lkey] = Layer() + if self.pars['dynam_layer'].get(lkey, False): + # Equivalent to previous functionality, but might be better if make_randpop() returned Layer objects instead of just dicts, that + # way the population creation function could have control over both the contacts and the update algorithm + self.contacts[lkey] = RandomLayer() + else: + self.contacts[lkey] = Layer() # Actually include them, and update properties if supplied for col in self.contacts[lkey].keys(): # Loop over the supplied columns @@ -1434,3 +1439,42 @@ def find_contacts(self, inds, as_array=True): contact_inds.sort() # Sorting ensures that the results are reproducible for a given seed as well as being identical to previous versions of Covasim return contact_inds + + + def update(self, people): + '''Regenerate contacts + + This method gets called if the layer appears in ``sim.pars['dynam_lkeys']``. The Layer implements + the update procedure so that derived classes can customize the update e.g. implementing + over-dispersion/other distributions, random clusters, etc. + + This method also takes in the ``people`` object so that the update can depend on person attributes + that may change over time (e.g. changing contacts for people that are severe/critical). + ''' + pass + + +def RandomLayer(Layer): + ''' + Randomly sampled layer + + Args: + Layer: + + Returns: + + ''' + def __init__(self, n_contacts): + self.n_contacts = n_contacts + + def update(self, people): + # Choose how many contacts to make + pop_size = len(people) + n_new = int(self.n_contacts*pop_size/2) # Since these get looped over in both directions later + + # Create the contacts + new_contacts = {} # Initialize + self['p1'] = np.array(cvu.choose_r(max_n=pop_size, n=n_new), dtype=cvd.default_int) # Choose with replacement + self['p2'] = np.array(cvu.choose_r(max_n=pop_size, n=n_new), dtype=cvd.default_int) + self['beta'] = np.ones(n_new, dtype=cvd.default_float) + diff --git a/covasim/people.py b/covasim/people.py index 87a3af34a..fe880fd66 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -168,29 +168,10 @@ def update_states_post(self): def update_contacts(self): ''' Refresh dynamic contacts, e.g. community ''' - # Figure out if anything needs to be done -- e.g. {'h':False, 'c':True} - dynam_keys = [lkey for lkey,is_dynam in self.pars['dynam_layer'].items() if is_dynam] - - # Loop over dynamic keys - for lkey in dynam_keys: - # Remove existing contacts - self.contacts.pop(lkey) - - # Choose how many contacts to make - pop_size = len(self) - n_contacts = self.pars['contacts'][lkey] - n_new = int(n_contacts*pop_size/2) # Since these get looped over in both directions later - - # Create the contacts - new_contacts = {} # Initialize - new_contacts['p1'] = np.array(cvu.choose_r(max_n=pop_size, n=n_new), dtype=cvd.default_int) # Choose with replacement - new_contacts['p2'] = np.array(cvu.choose_r(max_n=pop_size, n=n_new), dtype=cvd.default_int) - new_contacts['beta'] = np.ones(n_new, dtype=cvd.default_float) - - # Add to contacts - self.add_contacts(new_contacts, lkey=lkey) - self.contacts[lkey].validate() + for lkey, is_dynam in self.pars['dynam_layer'].items(): + if is_dynam: + self.contacts[lkey].update(self) return self.contacts diff --git a/covasim/version.py b/covasim/version.py index d1febf74f..0df8230b4 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,6 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '2.0.2' -__versiondate__ = '2020-02-01' +__version__ = '2.0.3' +__versiondate__ = '2020-03-12' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From 463d5c41ab396ce7240a748a184defeac8eeefe4 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 11 Mar 2021 21:28:16 -0800 Subject: [PATCH 154/569] update colors --- CHANGELOG.rst | 9 +++++++ covasim/base.py | 2 +- covasim/defaults.py | 45 ++++++++++++++++++++------------- covasim/sim.py | 61 +++++++++++++++++++-------------------------- covasim/version.py | 2 +- 5 files changed, 64 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7ca4524a6..f7fe63b1a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,6 +26,15 @@ Latest versions (2.0.x) ~~~~~~~~~~~~~~~~~~~~~~~ +Version 2.0.3 (2021-03-11) +-------------------------- +- Previously, the way a sim was printed (e.g. ``print(sim)``) depended on what the global ``verbose`` parameter was set to (e.g. ``cv.options.set(verbose=0.1)``), which used ``sim.brief()`` if verbosity was 0, or ``sim.disp()`` otherwise. This has been changed to always use the ``sim.brief()`` representation regardless of verbosity. To restore the previous behavior, use ``sim.disp()`` instead of ``print(sim)``. +- ``sim.get_interventions()`` and ``sim.get_analyzers()`` have been changed to return all interventions/analyzers if no arguments are supplied. Previously, they would return only the last intervention. To restore the previous behavior, call ``sim.get_intervention()`` or ``sim.get_analyzer()`` instead. +- ``sim.run()`` now returns a pointer to the sim object rather than either nothing (the current default) or the ``sim.results`` object. This means you can now do e.g. ``sim.run().plot()`` or ``sim.run().results`` rather than ``sim.run(do_plot=True)`` or ``sim.run(output=True)``. +- *Regression information*: Calls to ``sim.run(do_plot=True, **kwargs)`` should be changed to ``sim.run().plot(**kwargs)``. Calls to ``sim.get_interventions()``/``sim.get_analyzers()`` (with no arguments) should be changed to ``sim.get_intervention()``/``sim.get_analyzer()``. Calls to ``results = sim.run(output=True)`` should be replaced with ``results = sim.run().results``. +- *GitHub info*: PR `788 `__ + + Version 2.0.2 (2021-02-01) -------------------------- - Added a new option to easily turn on/off interactive plotting: e.g., simply set ``cv.options.set(interactive=False)`` to turn off interactive plotting. This meta-option sets the other options ``show``, ``close``, and ``backend``. diff --git a/covasim/base.py b/covasim/base.py index d8fa46bd5..3f3eee47f 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -136,7 +136,7 @@ def __init__(self, name=None, npts=None, scale=True, color=None): self.name = name # Name of this result self.scale = scale # Whether or not to scale the result by the scale factor if color is None: - color = '#000000' + color = cvd.get_colors()['default'] self.color = color # Default color if npts is None: npts = 0 diff --git a/covasim/defaults.py b/covasim/defaults.py index 6012589ea..c17cb4e0d 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -144,24 +144,24 @@ def get_colors(): NB, includes duplicates since stocks and flows are named differently. ''' - colors = sc.objdict( - susceptible = '#5e7544', - infectious = '#c78f65', - infections = '#c75649', - exposed = '#c75649', # Duplicate - tests = '#aaa8ff', - diagnoses = '#8886cc', - diagnosed = '#8886cc', # Duplicate - recoveries = '#799956', - recovered = '#799956', # Duplicate - symptomatic = '#c1ad71', - severe = '#c1981d', - quarantined = '#5f1914', - critical = '#b86113', - deaths = '#000000', - dead = '#000000', # Duplicate - ) - return colors + c = sc.objdict() + c.susceptible = '#4d771e' + c.exposed = '#c78f65' + c.infectious = '#e45226' + c.infections = '#b62413' + c.tests = '#aaa8ff' + c.diagnoses = '#5f5cd2' + c.diagnosed = c.diagnoses + c.quarantined = '#5c399c' + c.recoveries = '#9e1149' + c.recovered = c.recoveries + c.symptomatic = '#c1ad71' + c.severe = '#c1981d' + c.critical = '#b86113' + c.deaths = '#000000' + c.dead = c.deaths + c.default = '#000000' + return c # Define the 'overview plots', i.e. the most useful set of plots to explore different aspects of a simulation @@ -215,6 +215,15 @@ def get_sim_plots(which='default'): }) elif which == 'overview': plots = sc.dcp(overview_plots) + elif which == 'seir': + plots = sc.odict({ + 'SEIR states': [ + 'n_susceptible', + 'n_preinfectious', + 'n_infectious', + 'n_removed', + ], + }) else: # pragma: no cover errormsg = f'The choice which="{which}" is not supported' raise ValueError(errormsg) diff --git a/covasim/sim.py b/covasim/sim.py index d5b87ca06..ced0a221d 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -274,7 +274,7 @@ def init_res(*args, **kwargs): # Flows and cumulative flows for key,label in cvd.result_flows.items(): - self.results[f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key]) # Cumulative variables -- e.g. "Cumulative infections" + self.results[f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key]) # Cumulative variables -- e.g. "Cumulative infections" for key,label in cvd.result_flows.items(): # Repeat to keep all the cumulative keys together self.results[f'new_{key}'] = init_res(f'Number of new {label}', color=dcols[key]) # Flow variables -- e.g. "Number of new infections" @@ -284,13 +284,15 @@ def init_res(*args, **kwargs): self.results[f'n_{key}'] = init_res(label, color=dcols[key]) # Other variables - self.results['n_alive'] = init_res('Number of people alive', scale=False) - self.results['prevalence'] = init_res('Prevalence', scale=False) - self.results['incidence'] = init_res('Incidence', scale=False) - self.results['r_eff'] = init_res('Effective reproduction number', scale=False) - self.results['doubling_time'] = init_res('Doubling time', scale=False) - self.results['test_yield'] = init_res('Testing yield', scale=False) - self.results['rel_test_yield'] = init_res('Relative testing yield', scale=False) + self.results['n_alive'] = init_res('Number alive', scale=False, color=dcols.susceptible) + self.results['n_preinfectious'] = init_res('Number preinfectious', scale=False, color=dcols.exposed) + self.results['n_removed'] = init_res('Number removed', scale=False, color=dcols.recovered) + self.results['prevalence'] = init_res('Prevalence', scale=False, color=dcols.exposed) + self.results['incidence'] = init_res('Incidence', scale=False, color=dcols.infections) + self.results['r_eff'] = init_res('Effective reproduction number', scale=False, color=dcols.default) + self.results['doubling_time'] = init_res('Doubling time', scale=False, color=dcols.default) + self.results['test_yield'] = init_res('Testing yield', scale=False, color=dcols.tests) + self.results['rel_test_yield'] = init_res('Relative testing yield', scale=False, color=dcols.tests) # Populate the rest of the results if self['rescale']: @@ -376,7 +378,10 @@ def init_people(self, save_pop=False, load_pop=False, popfile=None, reset=False, if verbose is None: verbose = self['verbose'] if verbose>0: - print(f'Initializing sim with {self["pop_size"]:0n} people for {self["n_days"]} days') + resetstr= '' + if self.people: + resetstr = ' (resetting people)' if reset else ' (warning: not resetting sim.people)' + print(f'Initializing sim{resetstr} with {self["pop_size"]:0n} people for {self["n_days"]} days') if load_pop and self.popdict is None: self.load_population(popfile=popfile) @@ -560,7 +565,7 @@ def step(self): return - def run(self, do_plot=False, until=None, restore_pars=True, reset_seed=True, verbose=None, output=False, **kwargs): + def run(self, do_plot=False, until=None, restore_pars=True, reset_seed=True, verbose=None): ''' Run the simulation. @@ -570,11 +575,9 @@ def run(self, do_plot=False, until=None, restore_pars=True, reset_seed=True, ver restore_pars (bool): whether to make a copy of the parameters before the run and restore it after, so runs are repeatable reset_seed (bool): whether to reset the random number stream immediately before run verbose (float): level of detail to print, e.g. -1 = one-line output, 0 = no output, 0.1 = print every 10th day, 1 = print every day - output (bool/str): whether to return the results dictionary as output, or the sim object if output='sim' - kwargs (dict): passed to sim.plot() if do_plot is True Returns: - None if output=False, the results object (also modifies in-place) if output=True, or the sim object if output='sim' + A pointer to the sim object (with results modified in-place) ''' # Initialization steps -- start the timer, initialize the sim and the seed, and check that the sim hasn't been run @@ -631,21 +634,7 @@ def run(self, do_plot=False, until=None, restore_pars=True, reset_seed=True, ver if self.complete: self.finalize(verbose=verbose, restore_pars=restore_pars) sc.printv(f'Run finished after {elapsed:0.2f} s.\n', 1, verbose) - if do_plot: # Optionally plot - self.plot(**kwargs) - else: - if len(kwargs): # pragma: no cover - keys = '", "'.join(list(kwargs.keys())) - errormsg = f'Kwargs "{keys}" were not processed since plotting is not enabled; this is treated as an error' - raise RuntimeError(errormsg) - if output == 'sim': - return self - elif output: - return self.results - else: - return - else: - return # If not complete, return nothing + return self def finalize(self, verbose=None, restore_pars=True): @@ -693,7 +682,7 @@ def finalize(self, verbose=None, restore_pars=True): def compute_results(self, verbose=None): ''' Perform final calculations on the results ''' - self.compute_prev_inci() + self.compute_states() self.compute_yield() self.compute_doubling() self.compute_r_eff() @@ -701,18 +690,20 @@ def compute_results(self, verbose=None): return - def compute_prev_inci(self): + def compute_states(self): ''' - Compute prevalence and incidence. Prevalence is the current number of infected + Compute prevalence, incidence, and other states. Prevalence is the current number of infected people divided by the number of people who are alive. Incidence is the number of new infections per day divided by the susceptible population. Also calculate the number of people alive, and recalculate susceptibles to handle scaling. ''' res = self.results - self.results['n_alive'][:] = self.scaled_pop_size - res['cum_deaths'][:] # Number of people still alive - self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents - self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence - self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence + self.results['n_alive'][:] = self.scaled_pop_size - res['cum_deaths'][:] # Number of people still alive + self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents + self.results['n_preinfectious'][:] = res['n_exposed'][:] - res['n_infectious'][:] # Calculate the number not yet infectious + self.results['n_removed'][:] = res['cum_recoveries'][:] + res['cum_deaths'][:] # Calculate the number not yet infectious + self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence + self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence return diff --git a/covasim/version.py b/covasim/version.py index a965358cb..383145c20 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -5,5 +5,5 @@ __all__ = ['__version__', '__versiondate__', '__license__'] __version__ = '2.0.3' -__versiondate__ = '2021-02-06' +__versiondate__ = '2021-03-11' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From 91caa6419225c0218cba35d3a8f09e0d53d18f07 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 11 Mar 2021 21:30:43 -0800 Subject: [PATCH 155/569] update changelog --- CHANGELOG.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f7fe63b1a..c098beaa0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -29,8 +29,11 @@ Latest versions (2.0.x) Version 2.0.3 (2021-03-11) -------------------------- - Previously, the way a sim was printed (e.g. ``print(sim)``) depended on what the global ``verbose`` parameter was set to (e.g. ``cv.options.set(verbose=0.1)``), which used ``sim.brief()`` if verbosity was 0, or ``sim.disp()`` otherwise. This has been changed to always use the ``sim.brief()`` representation regardless of verbosity. To restore the previous behavior, use ``sim.disp()`` instead of ``print(sim)``. -- ``sim.get_interventions()`` and ``sim.get_analyzers()`` have been changed to return all interventions/analyzers if no arguments are supplied. Previously, they would return only the last intervention. To restore the previous behavior, call ``sim.get_intervention()`` or ``sim.get_analyzer()`` instead. - ``sim.run()`` now returns a pointer to the sim object rather than either nothing (the current default) or the ``sim.results`` object. This means you can now do e.g. ``sim.run().plot()`` or ``sim.run().results`` rather than ``sim.run(do_plot=True)`` or ``sim.run(output=True)``. +- ``sim.get_interventions()`` and ``sim.get_analyzers()`` have been changed to return all interventions/analyzers if no arguments are supplied. Previously, they would return only the last intervention. To restore the previous behavior, call ``sim.get_intervention()`` or ``sim.get_analyzer()`` instead. +- Two new results have been added, ``n_preinfectious`` and ``n_removed``, corresponding to the E and R compartments of the SEIR model, respectively. +- A new shortcut plotting option has been introduced, ``sim.plot(to_plot='seir')``. +- Plotting colors have been revised. - *Regression information*: Calls to ``sim.run(do_plot=True, **kwargs)`` should be changed to ``sim.run().plot(**kwargs)``. Calls to ``sim.get_interventions()``/``sim.get_analyzers()`` (with no arguments) should be changed to ``sim.get_intervention()``/``sim.get_analyzer()``. Calls to ``results = sim.run(output=True)`` should be replaced with ``results = sim.run().results``. - *GitHub info*: PR `788 `__ From 21e0058f4076477845f9ce8317f6f099cb1726ad Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 11 Mar 2021 21:35:50 -0800 Subject: [PATCH 156/569] update baseline --- CHANGELOG.rst | 2 +- tests/baseline.json | 2 ++ tests/benchmark.json | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c098beaa0..f70038e2c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -33,7 +33,7 @@ Version 2.0.3 (2021-03-11) - ``sim.get_interventions()`` and ``sim.get_analyzers()`` have been changed to return all interventions/analyzers if no arguments are supplied. Previously, they would return only the last intervention. To restore the previous behavior, call ``sim.get_intervention()`` or ``sim.get_analyzer()`` instead. - Two new results have been added, ``n_preinfectious`` and ``n_removed``, corresponding to the E and R compartments of the SEIR model, respectively. - A new shortcut plotting option has been introduced, ``sim.plot(to_plot='seir')``. -- Plotting colors have been revised. +- Plotting colors have been revised to have greater contrast. - *Regression information*: Calls to ``sim.run(do_plot=True, **kwargs)`` should be changed to ``sim.run().plot(**kwargs)``. Calls to ``sim.get_interventions()``/``sim.get_analyzers()`` (with no arguments) should be changed to ``sim.get_intervention()``/``sim.get_analyzer()``. Calls to ``results = sim.run(output=True)`` should be replaced with ``results = sim.run().results``. - *GitHub info*: PR `788 `__ diff --git a/tests/baseline.json b/tests/baseline.json index ceeb00d4d..4ddd7caad 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -29,6 +29,8 @@ "n_diagnosed": 3385.0, "n_quarantined": 4279.0, "n_alive": 19978.0, + "n_preinfectious": 198.0, + "n_removed": 7600.0, "prevalence": 0.06372009210131144, "incidence": 0.001437943740451155, "r_eff": 0.13863684353019246, diff --git a/tests/benchmark.json b/tests/benchmark.json index 653036b43..e93ac84d1 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,12 +1,12 @@ { "time": { - "initialize": 0.471, - "run": 0.495 + "initialize": 0.429, + "run": 0.496 }, "parameters": { "pop_size": 20000, "pop_type": "hybrid", "n_days": 60 }, - "cpu_performance": 0.9940972211252934 + "cpu_performance": 0.9461125294823827 } \ No newline at end of file From c70135c8f62d7c0fb1cd3e6e641f3c2e04634068 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 11 Mar 2021 21:37:00 -0800 Subject: [PATCH 157/569] added devtests --- tests/devtests/one_line.py | 18 ++++++++++++++++++ tests/devtests/show_colors.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tests/devtests/one_line.py create mode 100644 tests/devtests/show_colors.py diff --git a/tests/devtests/one_line.py b/tests/devtests/one_line.py new file mode 100644 index 000000000..f4c2af9fb --- /dev/null +++ b/tests/devtests/one_line.py @@ -0,0 +1,18 @@ +''' +Runs a fairly complex analysis using a single line of code. Equivalent to: + + pars = dict( + pop_size = 1e3, + pop_infected = 10, + pop_type = 'hybrid', + n_days = 180, + ) + cb = cv.change_beta([30, 50], [0.0, 1.0], layers=['w','c']) + + sim = cv.Sim(**pars, interventions=cb) + sim.run() + sim.plot(to_plot='seir') +''' +import covasim as cv + +cv.Sim(pop_size=1e3, pop_infected=10, pop_type='hybrid', n_days=180, interventions=cv.change_beta([30, 50], [0.0, 1.0], layers=['w','c'])).run().plot(to_plot='seir') \ No newline at end of file diff --git a/tests/devtests/show_colors.py b/tests/devtests/show_colors.py new file mode 100644 index 000000000..14eedd242 --- /dev/null +++ b/tests/devtests/show_colors.py @@ -0,0 +1,36 @@ +import covasim as cv +import pylab as pl +import numpy as np +import sciris as sc + +cv.options.set(dpi=150) +sim = cv.Sim(pop_size=1e3, verbose=0).run() +colors = {k:res.color for k,res in sim.results.items() if isinstance(res, cv.Result)} # colors = cv.get_colors() +d = sc.objdict() +for key in ['cum', 'new', 'n', 'other']: + d[key] = sc.objdict() + +for k,v in colors.items(): + if k.startswith('cum_'): + d.cum[k] = v + elif k.startswith('n_'): + d.n[k] = v + elif k.startswith('new_'): + d.new[k] = v + else: + d.other[k] = v + + +pl.figure(figsize=(24,18)) + +for i,k,colors in d.enumitems(): + pl.subplot(2,2,i+1) + pl.title(k) + n = len(colors) + y = n-np.arange(n) + # pl.axes([0.25,0.05,0.6,0.9]) + pl.barh(y, width=1, color=colors.values()) + pl.gca().set_yticks(y) + pl.gca().set_yticklabels(colors.keys()) + +pl.show() \ No newline at end of file From f4c645d662a1eea14d403b2e1bb03c4c2653da3d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 11 Mar 2021 21:47:46 -0800 Subject: [PATCH 158/569] update parallel options --- CHANGELOG.rst | 3 +++ covasim/settings.py | 4 ++-- covasim/utils.py | 12 +++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f70038e2c..8dce50cea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,9 +31,12 @@ Version 2.0.3 (2021-03-11) - Previously, the way a sim was printed (e.g. ``print(sim)``) depended on what the global ``verbose`` parameter was set to (e.g. ``cv.options.set(verbose=0.1)``), which used ``sim.brief()`` if verbosity was 0, or ``sim.disp()`` otherwise. This has been changed to always use the ``sim.brief()`` representation regardless of verbosity. To restore the previous behavior, use ``sim.disp()`` instead of ``print(sim)``. - ``sim.run()`` now returns a pointer to the sim object rather than either nothing (the current default) or the ``sim.results`` object. This means you can now do e.g. ``sim.run().plot()`` or ``sim.run().results`` rather than ``sim.run(do_plot=True)`` or ``sim.run(output=True)``. - ``sim.get_interventions()`` and ``sim.get_analyzers()`` have been changed to return all interventions/analyzers if no arguments are supplied. Previously, they would return only the last intervention. To restore the previous behavior, call ``sim.get_intervention()`` or ``sim.get_analyzer()`` instead. +- The ``Fit`` object (and ``cv.compute_gof()``) have been updated to allow a custom goodness-of-fit estimator to be supplied. - Two new results have been added, ``n_preinfectious`` and ``n_removed``, corresponding to the E and R compartments of the SEIR model, respectively. - A new shortcut plotting option has been introduced, ``sim.plot(to_plot='seir')``. - Plotting colors have been revised to have greater contrast. +- The ``numba_parallel`` option has been updated to include a "safe" option, which parallelizes as much as it can without disrupting the random number stream. For large sims (>100,000 people), this increases performance by about 10%. The previous ``numba_parallel=True`` option now corresponds to ``numba_parallel='full'``, which is about 20% faster but means results are non-reproducible. Note that for sims smaller than 100,000 people, Numba parallelization has almost no effect on performance. +- A new option has been added, ``numba_cache``, which controls whether or not Numba functions are cached. They are by default to save compilation time, but if you change Numba options (especially ``numba_parallel``), with caching you may also need to delete the ``__pycache__`` folder for changes to take effect. - *Regression information*: Calls to ``sim.run(do_plot=True, **kwargs)`` should be changed to ``sim.run().plot(**kwargs)``. Calls to ``sim.get_interventions()``/``sim.get_analyzers()`` (with no arguments) should be changed to ``sim.get_intervention()``/``sim.get_analyzer()``. Calls to ``results = sim.run(output=True)`` should be replaced with ``results = sim.run().results``. - *GitHub info*: PR `788 `__ diff --git a/covasim/settings.py b/covasim/settings.py index e090c2cb4..740c472cd 100644 --- a/covasim/settings.py +++ b/covasim/settings.py @@ -53,8 +53,8 @@ def set_default_options(): optdesc.precision = 'Set arithmetic precision for Numba -- 32-bit by default for efficiency' options.precision = int(os.getenv('COVASIM_PRECISION', 32)) - optdesc.numba_parallel = 'Set Numba multithreading -- 0=no, 1=safe, 2=full; full multithreading is ~20% faster, but results become nondeterministic' - options.numba_parallel = int(os.getenv('COVASIM_NUMBA_PARALLEL', 0)) + optdesc.numba_parallel = 'Set Numba multithreading -- none, safe, full; full multithreading is ~20% faster, but results become nondeterministic' + options.numba_parallel = str(os.getenv('COVASIM_NUMBA_PARALLEL', 'none')) optdesc.numba_cache = 'Set Numba caching -- saves on compilation time, but harder to update' options.numba_cache = bool(int(os.getenv('COVASIM_NUMBA_CACHE', 1))) diff --git a/covasim/utils.py b/covasim/utils.py index 5a8df6c92..f9ad881f4 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -21,12 +21,14 @@ nbfloat = cvd.nbfloat # Specify whether to allow parallel Numba calculation -- 10% faster for safe and 20% faster for random, but the random number stream becomes nondeterministic for the latter -safe_parallel = cvo.numba_parallel >= 1 -rand_parallel = cvo.numba_parallel == 2 -if cvo.numba_parallel not in [0,1,2]: - errormsg = f'Numba parallel must be 0, 1, or 2, not {cvo.numba_parallel}' +safe_opts = [1, '1', 'safe'] +full_opts = [2, '2', 'full'] +safe_parallel = cvo.numba_parallel in safe_opts + full_opts +rand_parallel = cvo.numba_parallel in full_opts +if cvo.numba_parallel not in [0, 1, 2, '0', '1', '2', 'none', 'safe', 'full']: + errormsg = f'Numba parallel must be "none", "safe", or "full", not "{cvo.numba_parallel}"' raise ValueError(errormsg) -cache = cvo.numba_cache +cache = cvo.numba_cache # Turning this off can help switching parallelization options #%% The core Covasim functions -- compute the infections From 865acffaf3cefa5dccc3f64f1b14fdf154a991c5 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 11 Mar 2021 21:50:27 -0800 Subject: [PATCH 159/569] update docstring --- covasim/sim.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index ced0a221d..3b3f51e5f 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -692,16 +692,17 @@ def compute_results(self, verbose=None): def compute_states(self): ''' - Compute prevalence, incidence, and other states. Prevalence is the current number of infected - people divided by the number of people who are alive. Incidence is the number - of new infections per day divided by the susceptible population. Also calculate - the number of people alive, and recalculate susceptibles to handle scaling. + Compute prevalence, incidence, and other states. Prevalence is the current + number of infected people divided by the number of people who are alive. + Incidence is the number of new infections per day divided by the susceptible + population. Also calculates the number of people alive, the number preinfectious, + the number removed, and recalculates susceptibles to handle scaling. ''' res = self.results self.results['n_alive'][:] = self.scaled_pop_size - res['cum_deaths'][:] # Number of people still alive self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents - self.results['n_preinfectious'][:] = res['n_exposed'][:] - res['n_infectious'][:] # Calculate the number not yet infectious - self.results['n_removed'][:] = res['cum_recoveries'][:] + res['cum_deaths'][:] # Calculate the number not yet infectious + self.results['n_preinfectious'][:] = res['n_exposed'][:] - res['n_infectious'][:] # Calculate the number not yet infectious: exposed minus infectious + self.results['n_removed'][:] = res['cum_recoveries'][:] + res['cum_deaths'][:] # Calculate the number removed: recovered + dead self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence return From 5bce71ed8233fb2866e20b5890c5df2ece757f25 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 11 Mar 2021 22:01:54 -0800 Subject: [PATCH 160/569] final tidying --- CHANGELOG.rst | 2 ++ covasim/sim.py | 18 ++++++++++-------- tests/.coveragerc | 1 - tests/devtests/show_colors.py | 23 ++++++++++++----------- tests/test_regression.py | 4 ++-- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8dce50cea..acbd62f21 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,6 +37,8 @@ Version 2.0.3 (2021-03-11) - Plotting colors have been revised to have greater contrast. - The ``numba_parallel`` option has been updated to include a "safe" option, which parallelizes as much as it can without disrupting the random number stream. For large sims (>100,000 people), this increases performance by about 10%. The previous ``numba_parallel=True`` option now corresponds to ``numba_parallel='full'``, which is about 20% faster but means results are non-reproducible. Note that for sims smaller than 100,000 people, Numba parallelization has almost no effect on performance. - A new option has been added, ``numba_cache``, which controls whether or not Numba functions are cached. They are by default to save compilation time, but if you change Numba options (especially ``numba_parallel``), with caching you may also need to delete the ``__pycache__`` folder for changes to take effect. +- A frozen list of ``pip`` requirements, as well as test requirements, has been added to the ``tests`` folder. +- The testing suite has been revamped, with defensive code skipped, bringing code coverage to 88%. - *Regression information*: Calls to ``sim.run(do_plot=True, **kwargs)`` should be changed to ``sim.run().plot(**kwargs)``. Calls to ``sim.get_interventions()``/``sim.get_analyzers()`` (with no arguments) should be changed to ``sim.get_intervention()``/``sim.get_analyzer()``. Calls to ``results = sim.run(output=True)`` should be replaced with ``results = sim.run().results``. - *GitHub info*: PR `788 `__ diff --git a/covasim/sim.py b/covasim/sim.py index 3b3f51e5f..f7b2cf600 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -1134,7 +1134,7 @@ def plot_result(self, key, *args, **kwargs): return fig -def diff_sims(sim1, sim2, output=False, die=False): +def diff_sims(sim1, sim2, skip_key_diffs=False, output=False, die=False): ''' Compute the difference of the summaries of two simulations, and print any values which differ. @@ -1142,6 +1142,7 @@ def diff_sims(sim1, sim2, output=False, die=False): Args: sim1 (sim/dict): either a simulation object or the sim.summary dictionary sim2 (sim/dict): ditto + skip_key_diffs (bool): whether to skip keys that don't match between sims output (bool): whether to return the output as a string (otherwise print) die (bool): whether to raise an exception if the sims don't match require_run (bool): require that the simulations have been run @@ -1165,17 +1166,17 @@ def diff_sims(sim1, sim2, output=False, die=False): raise TypeError(errormsg) # Compare keys - mismatchmsg = '' + keymatchmsg = '' sim1_keys = set(sim1.keys()) sim2_keys = set(sim2.keys()) - if sim1_keys != sim2_keys: - mismatchmsg = "Keys don't match!\n" + if sim1_keys != sim2_keys and not skip_key_diffs: + keymatchmsg = "Keys don't match!\n" missing = list(sim1_keys - sim2_keys) extra = list(sim2_keys - sim1_keys) if missing: - mismatchmsg += f' Missing sim1 keys: {missing}\n' + keymatchmsg += f' Missing sim1 keys: {missing}\n' if extra: - mismatchmsg += f' Extra sim2 keys: {extra}\n' + keymatchmsg += f' Extra sim2 keys: {extra}\n' mismatches = {} for key in sim2.keys(): # To ensure order @@ -1187,7 +1188,7 @@ def diff_sims(sim1, sim2, output=False, die=False): mismatches[key] = {'sim1': sim1_val, 'sim2': sim2_val} if len(mismatches): - mismatchmsg = '\nThe following values differ between the two simulations:\n' + valmatchmsg = '\nThe following values differ between the two simulations:\n' df = pd.DataFrame.from_dict(mismatches).transpose() diff = [] ratio = [] @@ -1237,9 +1238,10 @@ def diff_sims(sim1, sim2, output=False, die=False): for col in ['sim1', 'sim2', 'diff', 'ratio']: df[col] = df[col].round(decimals=3) df['change'] = change - mismatchmsg += str(df) + valmatchmsg += str(df) # Raise an error if mismatches were found + mismatchmsg = keymatchmsg + valmatchmsg if mismatchmsg: if die: raise ValueError(mismatchmsg) diff --git a/tests/.coveragerc b/tests/.coveragerc index 82c2d2afb..094da69e6 100644 --- a/tests/.coveragerc +++ b/tests/.coveragerc @@ -2,7 +2,6 @@ [run] branch = True source = covasim -omit = *census* [report] # Regexes for lines to exclude from consideration diff --git a/tests/devtests/show_colors.py b/tests/devtests/show_colors.py index 14eedd242..6aaa850b3 100644 --- a/tests/devtests/show_colors.py +++ b/tests/devtests/show_colors.py @@ -1,28 +1,29 @@ +''' +Plot all the colors used in Covasim. +''' + import covasim as cv import pylab as pl import numpy as np import sciris as sc -cv.options.set(dpi=150) +# Get colors sim = cv.Sim(pop_size=1e3, verbose=0).run() colors = {k:res.color for k,res in sim.results.items() if isinstance(res, cv.Result)} # colors = cv.get_colors() d = sc.objdict() for key in ['cum', 'new', 'n', 'other']: d[key] = sc.objdict() +# Parse into subdictionaries for k,v in colors.items(): - if k.startswith('cum_'): - d.cum[k] = v - elif k.startswith('n_'): - d.n[k] = v - elif k.startswith('new_'): - d.new[k] = v - else: - d.other[k] = v - + if k.startswith('cum_'): d.cum[k] = v + elif k.startswith('n_'): d.n[k] = v + elif k.startswith('new_'): d.new[k] = v + else: d.other[k] = v +# Plot +cv.options.set(dpi=150) pl.figure(figsize=(24,18)) - for i,k,colors in d.enumitems(): pl.subplot(2,2,i+1) pl.title(k) diff --git a/tests/test_regression.py b/tests/test_regression.py index 5eb62a058..edc4a30ce 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -44,12 +44,12 @@ def test_regression(): sim2 = make_sim() # Check that they match - cv.diff_sims(sim1, sim2, die=True) + cv.diff_sims(sim1, sim2, skip_key_diffs=True, die=True) # Confirm that non-matching sims don't match sim3 = make_sim(beta=0.02123) with pytest.raises(ValueError): - cv.diff_sims(sim1, sim3, die=True) + cv.diff_sims(sim1, sim3, skip_key_diffs=True, die=True) return sim1, sim2 From 763b598c8117e63e0113815cb2df25047fd047d6 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 11 Mar 2021 22:33:00 -0800 Subject: [PATCH 161/569] update tests --- CHANGELOG.rst | 2 +- covasim/sim.py | 38 ++++++++++++++++-------------- tests/test_analysis.py | 1 + tests/test_resume.py | 53 +++++++++++++++++++++++++++++++++++------- 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index acbd62f21..db6d7e140 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -38,7 +38,7 @@ Version 2.0.3 (2021-03-11) - The ``numba_parallel`` option has been updated to include a "safe" option, which parallelizes as much as it can without disrupting the random number stream. For large sims (>100,000 people), this increases performance by about 10%. The previous ``numba_parallel=True`` option now corresponds to ``numba_parallel='full'``, which is about 20% faster but means results are non-reproducible. Note that for sims smaller than 100,000 people, Numba parallelization has almost no effect on performance. - A new option has been added, ``numba_cache``, which controls whether or not Numba functions are cached. They are by default to save compilation time, but if you change Numba options (especially ``numba_parallel``), with caching you may also need to delete the ``__pycache__`` folder for changes to take effect. - A frozen list of ``pip`` requirements, as well as test requirements, has been added to the ``tests`` folder. -- The testing suite has been revamped, with defensive code skipped, bringing code coverage to 88%. +- The testing suite has been revamped, with defensive code skipped, bringing code coverage to 90%. - *Regression information*: Calls to ``sim.run(do_plot=True, **kwargs)`` should be changed to ``sim.run().plot(**kwargs)``. Calls to ``sim.get_interventions()``/``sim.get_analyzers()`` (with no arguments) should be changed to ``sim.get_intervention()``/``sim.get_analyzer()``. Calls to ``results = sim.run(output=True)`` should be replaced with ``results = sim.run().results``. - *GitHub info*: PR `788 `__ diff --git a/covasim/sim.py b/covasim/sim.py index f7b2cf600..e2d11711c 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -129,7 +129,7 @@ def layer_keys(self): ''' try: keys = list(self['beta_layer'].keys()) # Get keys from beta_layer since the "most required" layer parameter - except: + except: # pragma: no cover keys = [] return keys @@ -178,7 +178,7 @@ def validate_layer_pars(self): # Handle mismatches with the population if self.people is not None: pop_keys = set(self.people.contacts.keys()) - if pop_keys != set(layer_keys): + if pop_keys != set(layer_keys): # pragma: no cover errormsg = f'Please update your parameter keys {layer_keys} to match population keys {pop_keys}. You may find sim.reset_layer_pars() helpful.' raise sc.KeyNotFoundError(errormsg) @@ -229,7 +229,7 @@ def validate_pars(self, validate_layers=True): # Handle population data popdata_choices = ['random', 'hybrid', 'clustered', 'synthpops'] choice = self['pop_type'] - if choice and choice not in popdata_choices: + if choice and choice not in popdata_choices: # pragma: no cover choicestr = ', '.join(popdata_choices) errormsg = f'Population type "{choice}" not available; choices are: {choicestr}' raise ValueError(errormsg) @@ -248,7 +248,7 @@ def validate_pars(self, validate_layers=True): # Handle verbose if self['verbose'] == 'brief': self['verbose'] = -1 - if not sc.isnumber(self['verbose']): + if not sc.isnumber(self['verbose']): # pragma: no cover errormsg = f'Verbose argument should be either "brief", -1, or a float, not {type(self["verbose"])} "{self["verbose"]}"' raise ValueError(errormsg) @@ -346,7 +346,7 @@ def load_population(self, popfile=None, **kwargs): self.people.set_pars(self.pars) # Replace the saved parameters with this simulation's n_actual = len(self.people) layer_keys = self.people.layer_keys() - else: + else: # pragma: no cover errormsg = f'Cound not interpret input of {type(obj)} as a population file: must be a dict or People object' raise ValueError(errormsg) @@ -415,7 +415,7 @@ def init_interventions(self): elif isinstance(intervention, (cvi.test_num, cvi.test_prob)): test_ind = np.fmax(test_ind, i) # Find the latest-scheduled testing intervention - if not np.isnan(trace_ind): + if not np.isnan(trace_ind): # pragma: no cover warningmsg = '' if np.isnan(test_ind): warningmsg = 'Note: you have defined a contact tracing intervention but no testing intervention was found. Unless this is intentional, please define at least one testing intervention.' @@ -496,7 +496,7 @@ def step(self): intervention.apply(self) # If it's an intervention, call the apply() method elif callable(intervention): intervention(self) # If it's a function, call it directly - else: + else: # pragma: no cover errormsg = f'Intervention {i} ({intervention}) is neither callable nor an Intervention object' raise ValueError(errormsg) @@ -553,7 +553,7 @@ def step(self): analyzer.apply(self) # If it's an intervention, call the apply() method elif callable(analyzer): analyzer(self) # If it's a function, call it directly - else: + else: # pragma: no cover errormsg = f'Analyzer {i} ({analyzer}) is neither callable nor an Analyzer object' raise ValueError(errormsg) @@ -643,7 +643,7 @@ def finalize(self, verbose=None, restore_pars=True): if self.results_ready: # Because the results are rescaled in-place, finalizing the sim cannot be run more than once or # otherwise the scale factor will be applied multiple times - raise Exception('Simulation has already been finalized') + raise AlreadyRunError('Simulation has already been finalized') # Scale the results for reskey in self.result_keys(): @@ -840,7 +840,7 @@ def compute_r_eff(self, method='daily', smoothing=2, window=7): values = np.divide(num, den, out=np.full(self.npts, np.nan), where=den > 0) # Method not recognized - else: + else: # pragma: no cover errormsg = f'Method must be "daily", "infectious", or "outcome", not "{method}"' raise ValueError(errormsg) @@ -1023,7 +1023,7 @@ def compute_fit(self, output=True, *args, **kwargs): return - def make_age_histogram(self, output=True, *args, **kwargs): + def make_age_histogram(self, *args, output=True, **kwargs): ''' Calculate the age histograms of infections, deaths, diagnoses, etc. See cv.age_histogram() for more information. This can be used alternatively @@ -1046,12 +1046,12 @@ def make_age_histogram(self, output=True, *args, **kwargs): agehist = cva.age_histogram(sim=self, *args, **kwargs) if output: return agehist - else: + else: # pragma: no cover self.results.agehist = agehist return - def make_transtree(self, output=True, *args, **kwargs): + def make_transtree(self, *args, output=True, **kwargs): ''' Create a TransTree (transmission tree) object, for analyzing the pattern of transmissions in the simulation. See cv.TransTree() for more information. @@ -1070,7 +1070,7 @@ def make_transtree(self, output=True, *args, **kwargs): tt = cva.TransTree(self, *args, **kwargs) if output: return tt - else: + else: # pragma: no cover self.results.transtree = tt return @@ -1161,7 +1161,7 @@ def diff_sims(sim1, sim2, skip_key_diffs=False, output=False, die=False): if isinstance(sim2, Sim): sim2 = sim2.compute_summary(update=False, output=True, require_run=True) for sim in [sim1, sim2]: - if not isinstance(sim, dict): + if not isinstance(sim, dict): # pragma: no cover errormsg = f'Cannot compare object of type {type(sim)}, must be a sim or a sim.summary dict' raise TypeError(errormsg) @@ -1169,7 +1169,7 @@ def diff_sims(sim1, sim2, skip_key_diffs=False, output=False, die=False): keymatchmsg = '' sim1_keys = set(sim1.keys()) sim2_keys = set(sim2.keys()) - if sim1_keys != sim2_keys and not skip_key_diffs: + if sim1_keys != sim2_keys and not skip_key_diffs: # pragma: no cover keymatchmsg = "Keys don't match!\n" missing = list(sim1_keys - sim2_keys) extra = list(sim2_keys - sim1_keys) @@ -1178,6 +1178,8 @@ def diff_sims(sim1, sim2, skip_key_diffs=False, output=False, die=False): if extra: keymatchmsg += f' Extra sim2 keys: {extra}\n' + # Compare values + valmatchmsg = '' mismatches = {} for key in sim2.keys(): # To ensure order if key in sim1_keys: # If a key is missing, don't count it as a mismatch @@ -1224,7 +1226,7 @@ def diff_sims(sim1, sim2, skip_key_diffs=False, output=False, die=False): repeats = 4 this_change = change_char*repeats - else: + else: # pragma: no cover this_diff = np.nan this_ratio = np.nan this_change = 'N/A' @@ -1242,7 +1244,7 @@ def diff_sims(sim1, sim2, skip_key_diffs=False, output=False, die=False): # Raise an error if mismatches were found mismatchmsg = keymatchmsg + valmatchmsg - if mismatchmsg: + if mismatchmsg: # pragma: no cover if die: raise ValueError(mismatchmsg) elif output: diff --git a/tests/test_analysis.py b/tests/test_analysis.py index a88ee4f82..5c83d72fe 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -46,6 +46,7 @@ def test_age_hist(): sim.run() # Checks to see that compute windows returns correct number of results + sim.make_age_histogram() # Show post-hoc example agehist = sim.get_analyzer() agehist.compute_windows() agehist.get() # Not used, but check get diff --git a/tests/test_resume.py b/tests/test_resume.py index b7d5a8823..eade18cad 100644 --- a/tests/test_resume.py +++ b/tests/test_resume.py @@ -31,15 +31,21 @@ def test_resuming(): with pytest.raises(cv.AlreadyRunError): s1.run(until=0, reset_seed=False) assert s1.initialized # It should still have been initialized though + with pytest.raises(RuntimeError): + s1.compute_summary(require_run=True) # Not ready yet s1.run(until='2020-01-31', reset_seed=False) with pytest.raises(cv.AlreadyRunError): s1.run(until=30, reset_seed=False) # Error if running up to the same value with pytest.raises(cv.AlreadyRunError): s1.run(until=20, reset_seed=False) # Error if running until a previous timestep + with pytest.raises(cv.AlreadyRunError): + s1.run(until=1000, reset_seed=False) # Error if running until the end of the sim s1.run(until=45, reset_seed=False) s1.run(reset_seed=False) + with pytest.raises(cv.AlreadyRunError): + s1.finalize() # Can't re-finalize a finalized sim assert np.all(s0.results['cum_infections'].values == s1.results['cum_infections']) # Results should be identical @@ -67,6 +73,7 @@ def test_reproducibility(): sc.heading('Test that sims are reproducible') fn = 'save-load-test.sim' # Name of the test file to save + key = 'cum_infections' #The results of the sim shouldn't be affected by what you do or don't do prior to sim.run() s1 = cv.Sim(pars) @@ -74,28 +81,41 @@ def test_reproducibility(): s2 = s1.copy() s1.run() s2.run() - r1ci = s1.summary['cum_infections'] - r2ci = s2.summary['cum_infections'] - assert r1ci == r2ci + r1 = s1.summary[key] + r2 = s2.summary[key] + assert r1 == r2 # If you run a sim and save it, you should be able to re-run it on load - s3 = cv.Sim(pars) + s3 = cv.Sim(pars, n_imports=1) s3.run() s3.save(fn) s4 = cv.load(fn) s4.initialize() s4.run() - r3ci = s3.summary['cum_infections'] - r4ci = s4.summary['cum_infections'] - assert r3ci == r4ci + r3 = s3.summary[key] + r4 = s4.summary[key] + assert r3 == r4 if os.path.exists(fn): # Tidy up -- after the assert to allow inspection if it fails os.remove(fn) + # Running a sim and resetting people should result in the same result; otherwise they should differ + s5 = cv.Sim(pars) + s5.run() + r5 = s5.summary[key] + s5.initialize(reset=True) + s5.run() + r6 = s5.summary[key] + s5.initialize(reset=False) + s5.run() + r7 = s5.summary[key] + assert r5 == r6 + assert r5 != r7 + return s4 def test_step(): # If being run via pytest, turn off - sc.heading('Test starting and stopping') + sc.heading('Test stepping') # Create and run a basic simulation s1 = cv.Sim(pars) @@ -115,6 +135,22 @@ def test_step(): # If being run via pytest, turn off return s2 +def test_stopping(): # If being run via pytest, turn off + sc.heading('Test stopping') + + # Run a sim with very short time limit + s1 = cv.Sim(pars, timelimit=0) + s1.run() + + # Run a sim with a stopping function + def stopping_func(sim): return True + s2 = cv.Sim(pars, stopping_func=stopping_func) + s2.run() + s2.finalize() + + return s1 + + #%% Run as a script if __name__ == '__main__': @@ -124,6 +160,7 @@ def test_step(): # If being run via pytest, turn off sim2 = test_reset_seed() sim3 = test_reproducibility() sim4 = test_step() + sim5 = test_stopping() print('\n'*2) sc.toc(T) From 0c4ced9cd403ad1b10091cb24cbba824720fa820 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 11 Mar 2021 23:33:54 -0800 Subject: [PATCH 162/569] updated tutorials --- docs/Makefile | 2 +- docs/build_docs | 4 +-- docs/conf.py | 3 -- docs/tutorials/t1.ipynb | 2 +- docs/tutorials/t2.ipynb | 2 +- docs/tutorials/t3.ipynb | 2 +- docs/tutorials/t4.ipynb | 2 +- docs/tutorials/t5.ipynb | 15 +++++---- examples/t5_custom_intervention.py | 51 ++++++++++++++++-------------- examples/test_examples.py | 15 +++++---- examples/test_tutorials.py | 49 ++++++++++++++++++++++++++++ 11 files changed, 99 insertions(+), 48 deletions(-) mode change 100644 => 100755 examples/test_examples.py create mode 100755 examples/test_tutorials.py diff --git a/docs/Makefile b/docs/Makefile index 5959a2161..464413fbd 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -10,7 +10,7 @@ BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +ALLSPHINXOPTS = -jauto -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . diff --git a/docs/build_docs b/docs/build_docs index d3e69e767..b38c4e5f5 100755 --- a/docs/build_docs +++ b/docs/build_docs @@ -9,8 +9,8 @@ start=$SECONDS export NBSPHINX_EXECUTE=$1 echo 'Building docs...' -make clean -make html +make clean # Delete +make html # Actually make duration=$(( SECONDS - start )) echo 'Cleaning up tutorial files...' diff --git a/docs/conf.py b/docs/conf.py index 47701c0f2..4fb3b9529 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,12 +50,9 @@ 'sphinx.ext.napoleon', 'sphinx.ext.todo', 'sphinx.ext.viewcode', # Add a link to the Python source code for classes, functions etc. - 'plantweb.directive', 'nbsphinx', ] -plantuml = 'plantweb' - autodoc_default_options = { 'member-order': 'bysource', 'members': None diff --git a/docs/tutorials/t1.ipynb b/docs/tutorials/t1.ipynb index 00e938e94..2c9bb42b7 100644 --- a/docs/tutorials/t1.ipynb +++ b/docs/tutorials/t1.ipynb @@ -187,7 +187,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" }, "pycharm": { "stem_cell": { diff --git a/docs/tutorials/t2.ipynb b/docs/tutorials/t2.ipynb index edb876b1f..a6cb8d8ea 100644 --- a/docs/tutorials/t2.ipynb +++ b/docs/tutorials/t2.ipynb @@ -229,7 +229,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" }, "pycharm": { "stem_cell": { diff --git a/docs/tutorials/t3.ipynb b/docs/tutorials/t3.ipynb index 2b9c54258..9955ec896 100644 --- a/docs/tutorials/t3.ipynb +++ b/docs/tutorials/t3.ipynb @@ -249,7 +249,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" }, "pycharm": { "stem_cell": { diff --git a/docs/tutorials/t4.ipynb b/docs/tutorials/t4.ipynb index 15ffa3463..2948bd0d0 100644 --- a/docs/tutorials/t4.ipynb +++ b/docs/tutorials/t4.ipynb @@ -165,7 +165,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" }, "pycharm": { "stem_cell": { diff --git a/docs/tutorials/t5.ipynb b/docs/tutorials/t5.ipynb index 37b4386d5..36dabd52d 100644 --- a/docs/tutorials/t5.ipynb +++ b/docs/tutorials/t5.ipynb @@ -363,12 +363,13 @@ " return\n", "\n", " def initialize(self, sim):\n", - " self.start_day = sim.day(self.start_day)\n", - " self.end_day = sim.day(self.end_day)\n", - " self.days = [self.start_day, self.end_day]\n", - " self.elderly = sim.people.age > self.age_cutoff # Find the elderly people here\n", - " self.exposed = np.zeros(sim.npts) # Initialize results\n", - " self.tvec = sim.tvec # Copy the time vector into this intervention\n", + " self.start_day = sim.day(self.start_day)\n", + " self.end_day = sim.day(self.end_day)\n", + " self.days = [self.start_day, self.end_day]\n", + " self.elderly = sim.people.age > self.age_cutoff # Find the elderly people here\n", + " self.exposed = np.zeros(sim.npts) # Initialize results\n", + " self.tvec = sim.tvec # Copy the time vector into this intervention\n", + " self.initialized = True\n", " return\n", "\n", " def apply(self, sim):\n", @@ -467,7 +468,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" }, "pycharm": { "stem_cell": { diff --git a/examples/t5_custom_intervention.py b/examples/t5_custom_intervention.py index 185542b41..93f47e9c1 100644 --- a/examples/t5_custom_intervention.py +++ b/examples/t5_custom_intervention.py @@ -18,12 +18,13 @@ def __init__(self, start_day=None, end_day=None, age_cutoff=70, rel_sus=0.0, *ar return def initialize(self, sim): - self.start_day = sim.day(self.start_day) - self.end_day = sim.day(self.end_day) - self.days = [self.start_day, self.end_day] - self.elderly = sim.people.age > self.age_cutoff # Find the elderly people here - self.exposed = np.zeros(sim.npts) # Initialize results - self.tvec = sim.tvec # Copy the time vector into this intervention + self.start_day = sim.day(self.start_day) + self.end_day = sim.day(self.end_day) + self.days = [self.start_day, self.end_day] + self.elderly = sim.people.age > self.age_cutoff # Find the elderly people here + self.exposed = np.zeros(sim.npts) # Initialize results + self.tvec = sim.tvec # Copy the time vector into this intervention + self.initialized = True return def apply(self, sim): @@ -48,24 +49,26 @@ def plot(self): return -# Define and run the baseline simulation -pars = dict( - pop_size = 50e3, - pop_infected = 100, - n_days = 90, - verbose = 0, -) -orig_sim = cv.Sim(pars, label='Default') +if __name__ == '__main__': -# Define the intervention and the scenario sim -protect = protect_elderly(start_day='2020-04-01', end_day='2020-05-01', rel_sus=0.1) # Create intervention -sim = cv.Sim(pars, interventions=protect, label='Protect the elderly') + # Define and run the baseline simulation + pars = dict( + pop_size = 50e3, + pop_infected = 100, + n_days = 90, + verbose = 0, + ) + orig_sim = cv.Sim(pars, label='Default') -# Run and plot -msim = cv.MultiSim([orig_sim, sim]) -msim.run() -msim.plot() + # Define the intervention and the scenario sim + protect = protect_elderly(start_day='2020-04-01', end_day='2020-05-01', rel_sus=0.1) # Create intervention + sim = cv.Sim(pars, interventions=protect, label='Protect the elderly') -# Plot intervention -protect = msim.sims[1].get_intervention(protect_elderly) # Find intervention by type -protect.plot() \ No newline at end of file + # Run and plot + msim = cv.MultiSim([orig_sim, sim]) + msim.run() + msim.plot() + + # Plot intervention + protect = msim.sims[1].get_intervention(protect_elderly) # Find intervention by type + protect.plot() \ No newline at end of file diff --git a/examples/test_examples.py b/examples/test_examples.py old mode 100644 new mode 100755 index d13c209d1..61077fbac --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + ''' Run the non-tutorial examples using pytest ''' @@ -8,29 +10,28 @@ import importlib.util as iu pl.switch_backend('agg') # To avoid graphs from appearing -- if you want them, run the examples directly -cwd = Path(sc.thisdir(__file__)) -examples_dir = cwd.joinpath('../examples') +examples_dir = Path(sc.thisdir(__file__)) def run_example(name): ''' Execute an example py script as __main__ Args: - name (str): the filename without the .py extension + name (str): the filename ''' - spec = iu.spec_from_file_location("__main__", examples_dir.joinpath(f"{name}.py")) + spec = iu.spec_from_file_location("__main__", examples_dir.joinpath(name)) module = iu.module_from_spec(spec) spec.loader.exec_module(module) def test_run_scenarios(): - run_example("run_scenarios") + run_example("run_scenarios.py") def test_run_sim(): - run_example("run_sim") + run_example("run_sim.py") def test_simple(): - run_example("simple") + run_example("simple.py") #%% Run as a script diff --git a/examples/test_tutorials.py b/examples/test_tutorials.py new file mode 100755 index 000000000..6cab5f7cf --- /dev/null +++ b/examples/test_tutorials.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +''' +Run the tutorial examples +''' + +import os +import sciris as sc +import pickle +import test_examples as tex + +def test_all_tutorials(): + + # Get and run tests + filenames = sc.getfilelist(tex.examples_dir, pattern='t*.py', nopath=True) + for filename in filenames: + if filename[1] in '0123456789': # Should have format e.g. t5_foo.py, not test_foo.py + sc.heading(f'Running {filename}...') + try: + tex.run_example(filename) + except (pickle.PicklingError, NameError): # Ignore these: issue with how the modules are loaded in the run_example function + pass + else: + print(f'[Skipping "{filename}" since does not match pattern]') + + # Tidy up + testfiles = sc.getfilelist(tex.examples_dir, pattern='my-*.*') + + sc.heading('Tidying...') + print(f'Deleting:') + for filename in testfiles: + print(f' {filename}') + print('in 3 seconds...') + sc.timedsleep(3) + for filename in testfiles: + os.remove(filename) + print(f' Deleted {filename}') + + return + + +#%% Run as a script +if __name__ == '__main__': + + T = sc.tic() + + test_all_tutorials() + + sc.toc(T) + print('Done.') From 65592549013f3893aa57c3d1e51051cae4bec077 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 11 Mar 2021 23:40:29 -0800 Subject: [PATCH 163/569] update interactive --- covasim/settings.py | 1 - tests/devtests/show_colors.py | 1 - 2 files changed, 2 deletions(-) diff --git a/covasim/settings.py b/covasim/settings.py index 740c472cd..3692bd1a5 100644 --- a/covasim/settings.py +++ b/covasim/settings.py @@ -123,7 +123,6 @@ def set_option(key=None, value=None, **kwargs): kwargs['backend'] = orig_options['backend'] else: kwargs['show'] = False - kwargs['close'] = True kwargs['backend'] = 'agg' # Reset options diff --git a/tests/devtests/show_colors.py b/tests/devtests/show_colors.py index 6aaa850b3..3d69221ea 100644 --- a/tests/devtests/show_colors.py +++ b/tests/devtests/show_colors.py @@ -29,7 +29,6 @@ pl.title(k) n = len(colors) y = n-np.arange(n) - # pl.axes([0.25,0.05,0.6,0.9]) pl.barh(y, width=1, color=colors.values()) pl.gca().set_yticks(y) pl.gca().set_yticklabels(colors.keys()) From fd6e495b28de8e3611cca02eb51dad581156dad5 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 12 Mar 2021 09:14:53 +0100 Subject: [PATCH 164/569] change refaults for rel_imm --- covasim/parameters.py | 4 +-- tests/devtests/test_variants.py | 52 +-------------------------------- 2 files changed, 3 insertions(+), 53 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index d12944834..ce0c03009 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -75,8 +75,8 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): for ax in cvd.immunity_axes: pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) pars['rel_imm'] = {} # Relative immunity scalings depending on the severity of symptoms - pars['rel_imm']['asymptomatic'] = 0.7 - pars['rel_imm']['mild'] = 0.9 + pars['rel_imm']['asymptomatic'] = 0.98 + pars['rel_imm']['mild'] = 0.99 pars['rel_imm']['severe'] = 1. pars['dur'] = {} diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 2b92b53cf..e236c5301 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -139,56 +139,6 @@ def test_vaccine_2strains(do_plot=True, do_show=True, do_save=False): return scens -def test_basic_reinfection(do_plot=False, do_show=True, do_save=False): - sc.heading('Run a basic sim with 1 strain, varying reinfection risk') - sc.heading('Setting up...') - - # Define baseline parameters - base_pars = { - 'beta': 0.1, # Make beta higher than usual so people get infected quickly - 'n_days': 240, - } - - n_runs = 3 - base_sim = cv.Sim(base_pars) - - # Define the scenarios - - scenarios = { - 'baseline': { - 'name':'No reinfection', - 'pars': {'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) for k in cvd.immunity_axes}, - 'rel_imm': {k: 1 for k in cvd.immunity_sources} - }, - }, - 'med_halflife': { - 'name':'3 month waning susceptibility', - 'pars': {'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 90}) for k in cvd.immunity_axes}}, - }, - 'med_halflife_bysev': { - 'name':'2 month waning susceptibility for symptomatics only', - 'pars': {'rel_imm': {'asymptomatic': 0, 'mild': 1, 'severe': 1}, - 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 60}) for k in cvd.immunity_axes} - } - }, - } - - metapars = {'n_runs': n_runs} - scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run() - - to_plot = sc.objdict({ - 'New infections': ['new_infections'], - 'Cumulative infections': ['cum_infections'], - 'New reinfections': ['new_reinfections'], - # 'Cumulative reinfections': ['cum_reinfections'], - }) - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_basic_reinfection.png', to_plot=to_plot) - - return scens - - def test_strainduration(do_plot=False, do_show=True, do_save=False): sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') sc.heading('Setting up...') @@ -436,7 +386,7 @@ def get_ind_of_min_value(list, time): sim0 = test_synthpops() # Run more complex tests - # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) From 5164515ba8df8a4f273a8fb76cb1f60229947fc7 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 12 Mar 2021 09:15:36 +0100 Subject: [PATCH 165/569] separate out immunity tests --- tests/devtests/test_immunity.py | 100 ++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/devtests/test_immunity.py diff --git a/tests/devtests/test_immunity.py b/tests/devtests/test_immunity.py new file mode 100644 index 000000000..de1f5c228 --- /dev/null +++ b/tests/devtests/test_immunity.py @@ -0,0 +1,100 @@ +import covasim as cv +import covasim.defaults as cvd +import sciris as sc +import matplotlib.pyplot as plt +import numpy as np + + +plot_args = dict(do_plot=1, do_show=0, do_save=1) + +def test_reinfection_scens(do_plot=False, do_show=True, do_save=False): + sc.heading('Run a basic sim with 1 strain, varying reinfection risk') + sc.heading('Setting up...') + + # Define baseline parameters + base_pars = { + 'beta': 0.1, # Make beta higher than usual so people get infected quickly + 'n_days': 240, + } + + n_runs = 3 + base_sim = cv.Sim(base_pars) + + # Define the scenarios + scenarios = { + 'baseline': { + 'name':'No reinfection', + 'pars': {'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) for k in cvd.immunity_axes}, + 'rel_imm': {k: 1 for k in cvd.immunity_sources} + }, + }, + 'med_halflife': { + 'name':'3 month waning susceptibility', + 'pars': {'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 90}) for k in cvd.immunity_axes}}, + }, + 'med_halflife_bysev': { + 'name':'2 month waning susceptibility for symptomatics only', + 'pars': {'rel_imm': {'asymptomatic': 0, 'mild': 1, 'severe': 1}, + 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 60}) for k in cvd.immunity_axes} + } + }, + } + + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'Cumulative infections': ['cum_infections'], + 'New reinfections': ['new_reinfections'], + # 'Cumulative reinfections': ['cum_reinfections'], + }) + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_basic_reinfection.png', to_plot=to_plot) + + return sim + + +def test_reinfection(do_plot=False, do_show=True, do_save=False): + sc.heading('Run a basic sim with 1 strain and no reinfections') + sc.heading('Setting up...') + + pars = { + 'beta': 0.015, + 'n_days': 120, + 'rel_imm': {k:1 for k in cvd.immunity_sources} + } + sim = cv.Sim(pars=pars) + sim.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'Cumulative infections': ['cum_infections'], + 'New reinfections': ['new_reinfections'], + }) + if do_plot: + sim.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_reinfection.png', to_plot=to_plot) + + return sim + + + +#%% Run as a script +if __name__ == '__main__': + sc.tic() + + # Run simplest possible test + if 0: + sim = cv.Sim() + sim.run() + + # Run more complex tests + sim1 = test_reinfection(**plot_args) + #scens1 = test_reinfection_scens(**plot_args) + + sc.toc() + + +print('Done.') + From bf211d2a4b2d8b856b9cc804c86aca7f1d7593ae Mon Sep 17 00:00:00 2001 From: Katherine Rosenfeld Date: Fri, 12 Mar 2021 08:25:09 +0000 Subject: [PATCH 166/569] fix for 1 v 2 doses --- covasim/sim.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 0d9c94a71..036b25737 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -587,7 +587,7 @@ def step(self): date_vacc = people.date_vaccinated vaccine_source = cvd.default_int(people.vaccine_source[vacc_inds]) vaccine_scale_factor = vaccine_info['rel_imm'][vaccine_source, strain] - doses_all = cvd.default_int(people.vaccinations) + doses_all = np.copy(cvd.default_int(people.vaccinations)) # doses = cvd.default_int(people.vaccinations[vacc_inds]) @@ -595,7 +595,7 @@ def step(self): prior_inf = cvu.false(np.isnan(date_rec)) prior_inf_vacc = np.intersect1d(prior_inf, vacc_inds) # prior_inf_vacc = np.where(vacc_inds == prior_inf_vacc)[0] - doses_all[prior_inf_vacc] = 2 + # doses_all[prior_inf_vacc] = 2 doses = doses_all[vacc_inds] vaccine_time = cvd.default_int(t - date_vacc[vacc_inds]) vaccine_immunity = vaccine_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, vaccine_time] From 82e8892e9650c19f502a2a0963d9940a52851db1 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 12 Mar 2021 09:34:37 +0100 Subject: [PATCH 167/569] add immunity --- tests/devtests/test_immunity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/devtests/test_immunity.py b/tests/devtests/test_immunity.py index de1f5c228..4da0eb880 100644 --- a/tests/devtests/test_immunity.py +++ b/tests/devtests/test_immunity.py @@ -65,7 +65,8 @@ def test_reinfection(do_plot=False, do_show=True, do_save=False): 'n_days': 120, 'rel_imm': {k:1 for k in cvd.immunity_sources} } - sim = cv.Sim(pars=pars) + pfizer = cv.vaccinate(days=20, vaccine_pars='pfizer') + sim = cv.Sim(pars=pars, interventions=pfizer) sim.run() to_plot = sc.objdict({ From a223ec025a65688de20412f93294c2419a966d26 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 12 Mar 2021 09:52:28 +0100 Subject: [PATCH 168/569] starting debug --- covasim/sim.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 036b25737..318a37164 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -564,6 +564,8 @@ def step(self): symp = people.symptomatic diag = people.diagnosed quar = people.quarantined + vaccinated = people.vaccinated + vacc_inds = cvu.true(vaccinated) # Initialize temp storage for strain parameters strain_parkeys = ['rel_beta', 'asymp_factor'] @@ -579,8 +581,6 @@ def step(self): inf_inds = cvu.false(sus) # Determine who is vaccinated and has some immunity from vaccine - vaccinated = people.vaccinated - vacc_inds = cvu.true(vaccinated) vacc_inds = np.setdiff1d(vacc_inds, inf_inds) # Take out anyone currently infected if len(vacc_inds): vaccine_info = self['vaccine_info'] @@ -598,7 +598,15 @@ def step(self): # doses_all[prior_inf_vacc] = 2 doses = doses_all[vacc_inds] vaccine_time = cvd.default_int(t - date_vacc[vacc_inds]) - vaccine_immunity = vaccine_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, vaccine_time] + + try: + vaccine_immunity = vaccine_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, vaccine_time] + except: + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() + immunity_factors[vacc_inds] = vaccine_scale_factor * vaccine_immunity # Deal with strain parameters From 148a4b775d4ff86d1684c488aacf333ea1a3213a Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 12 Mar 2021 10:08:09 +0100 Subject: [PATCH 169/569] merge parameters --- covasim/parameters.py | 4 ---- tests/devtests/test_variants.py | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 9ea170786..bfbc41489 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -80,12 +80,8 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['rel_imm']['severe'] = 1. pars['dur'] = {} -<<<<<<< HEAD # Duration parameters: time for disease progression - pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.6, par2=4.8) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, subtracting inf2sim duration -======= pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.6, par2=4.8) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, subtracting inf2sym duration ->>>>>>> master pars['dur']['inf2sym'] = dict(dist='lognormal_int', par1=1.0, par2=0.9) # Duration from infectious to symptomatic; see Linton et al., https://doi.org/10.3390/jcm9020538 pars['dur']['sym2sev'] = dict(dist='lognormal_int', par1=6.6, par2=4.9) # Duration from symptomatic to severe symptoms; see Linton et al., https://doi.org/10.3390/jcm9020538 pars['dur']['sev2crit'] = dict(dist='lognormal_int', par1=3.0, par2=7.4) # Duration from severe symptoms to requiring ICU; see Wang et al., https://jamanetwork.com/journals/jama/fullarticle/2761044 diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index e236c5301..870f5655a 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -379,14 +379,14 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - if 0: + if 1: sim = cv.Sim() sim.run() sim0 = test_synthpops() # Run more complex tests - sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) From af0b495b05d3b50663c7290ae535f5fe37822489 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 12 Mar 2021 12:07:43 +0100 Subject: [PATCH 170/569] need a new branch --- covasim/defaults.py | 1 - covasim/sim.py | 40 ++++++++++++++++------------------------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 57c6157ff..9376accf3 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -52,7 +52,6 @@ class PeopleMeta(sc.prettyobj): 'trans_immunity_factors', # Float 'prog_immunity_factors', # Float 'vaccinations', # Number of doses given per person - 'vaccine_source' # index of vaccine that individual received ] diff --git a/covasim/sim.py b/covasim/sim.py index 06051701e..80275e478 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -289,17 +289,17 @@ def init_res(*args, **kwargs): self.results[f'n_{key}'] = init_res(label, color=dcols[key], max_strains=self['total_strains']) # Other variables - self.results['n_alive'] = init_res('Number of people alive', scale=False) - self.results['n_preinfectious'] = init_res('Number preinfectious', scale=False, color=dcols.exposed) - self.results['n_removed'] = init_res('Number removed', scale=False, color=dcols.recovered) - self.results['prevalence'] = init_res('Prevalence', scale=False) - self.results['prevalence_by_strain'] = init_res('Prevalence by strain', scale=False, max_strains=self['total_strains']) - self.results['incidence'] = init_res('Incidence', scale=False) - self.results['incidence_by_strain'] = init_res('Incidence by strain', scale=False, max_strains=self['total_strains']) - self.results['r_eff'] = init_res('Effective reproduction number', scale=False) - self.results['doubling_time'] = init_res('Doubling time', scale=False) - self.results['test_yield'] = init_res('Testing yield', scale=False) - self.results['rel_test_yield'] = init_res('Relative testing yield', scale=False) + self.results['n_alive'] = init_res('Number of people alive', scale=False) + self.results['n_preinfectious'] = init_res('Number preinfectious', scale=False, color=dcols.exposed) + #self.results['n_removed'] = init_res('Number removed', scale=False, color=dcols.recovered) + self.results['prevalence'] = init_res('Prevalence', scale=False) + self.results['prevalence_by_strain'] = init_res('Prevalence by strain', scale=False, max_strains=self['total_strains']) + self.results['incidence'] = init_res('Incidence', scale=False) + self.results['incidence_by_strain'] = init_res('Incidence by strain', scale=False, max_strains=self['total_strains']) + self.results['r_eff'] = init_res('Effective reproduction number', scale=False) + self.results['doubling_time'] = init_res('Doubling time', scale=False) + self.results['test_yield'] = init_res('Testing yield', scale=False) + self.results['rel_test_yield'] = init_res('Relative testing yield', scale=False) # Populate the rest of the results if self['rescale']: @@ -574,6 +574,8 @@ def step(self): quar = people.quarantined vaccinated = people.vaccinated vacc_inds = cvu.true(vaccinated) + date_vacc = people.date_vaccinated + vaccine_info = self['vaccine_info'] # Initialize temp storage for strain parameters strain_parkeys = ['rel_beta', 'asymp_factor'] @@ -585,14 +587,12 @@ def step(self): immunity_factors = np.zeros(len(people), dtype=cvd.default_float) - # Determine who is currently infected and cannot get another infection + # Determine who is currently exposed and cannot get another infection inf_inds = cvu.false(sus) # Determine who is vaccinated and has some immunity from vaccine vacc_inds = np.setdiff1d(vacc_inds, inf_inds) # Take out anyone currently infected if len(vacc_inds): - vaccine_info = self['vaccine_info'] - date_vacc = people.date_vaccinated vaccine_source = cvd.default_int(people.vaccine_source[vacc_inds]) vaccine_scale_factor = vaccine_info['rel_imm'][vaccine_source, strain] doses_all = np.copy(cvd.default_int(people.vaccinations)) @@ -606,15 +606,7 @@ def step(self): # doses_all[prior_inf_vacc] = 2 doses = doses_all[vacc_inds] vaccine_time = cvd.default_int(t - date_vacc[vacc_inds]) - - try: - vaccine_immunity = vaccine_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, vaccine_time] - except: - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() - + vaccine_immunity = vaccine_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, vaccine_time] immunity_factors[vacc_inds] = vaccine_scale_factor * vaccine_immunity # Deal with strain parameters @@ -859,7 +851,7 @@ def compute_states(self): self.results['n_alive'][:] = self.scaled_pop_size - res['cum_deaths'][:] # Number of people still alive self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] # Recalculate the number of susceptible people, not agents self.results['n_preinfectious'][:] = res['n_exposed'][:] - res['n_infectious'][:] # Calculate the number not yet infectious: exposed minus infectious - self.results['n_removed'][:] = res['cum_recoveries'][:] + res['cum_deaths'][:] # Calculate the number removed: recovered + dead +# self.results['n_removed'][:] = res['cum_recoveries'][:] + res['cum_deaths'][:] # Calculate the number removed: recovered + dead self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence self.results['incidence_by_strain'][:] = np.einsum('ji,i->ji',res['new_infections_by_strain'][:], 1/res['n_susceptible'][:]) # Calculate the incidence From 22fb6d64d8cffb2a3c0a98b985539da722a7d0e9 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 12 Mar 2021 16:17:12 +0100 Subject: [PATCH 171/569] moved everything to people --- covasim/defaults.py | 25 +++--- covasim/interventions.py | 3 +- covasim/people.py | 140 +++++++++++++++++++++++--------- covasim/sim.py | 61 +------------- tests/devtests/test_immunity.py | 10 ++- 5 files changed, 126 insertions(+), 113 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 9376accf3..3cf3eafe7 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -39,18 +39,19 @@ class PeopleMeta(sc.prettyobj): # Set the properties of a person person = [ - 'uid', # Int - 'age', # Float - 'sex', # Float - 'symp_prob', # Float - 'severe_prob', # Float - 'crit_prob', # Float - 'death_prob', # Float - 'rel_trans', # Float - 'rel_sus', # Float - 'prior_symptoms', # Float - 'trans_immunity_factors', # Float - 'prog_immunity_factors', # Float + 'uid', # Int + 'age', # Float + 'sex', # Float + 'symp_prob', # Float + 'severe_prob', # Float + 'crit_prob', # Float + 'death_prob', # Float + 'rel_trans', # Float + 'rel_sus', # Float + 'prior_symptoms', # Float + 'sus_imm', # Float + 'trans_imm', # Float + 'prog_imm', # Float 'vaccinations', # Number of doses given per person 'vaccine_source' # index of vaccine that individual received ] diff --git a/covasim/interventions.py b/covasim/interventions.py index 04431c755..252840815 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1201,7 +1201,8 @@ def apply(self, sim): return def update_vaccine_info(self, sim, vacc_inds): - self.vaccinations[vacc_inds] += 1 + #self.vaccinations[vacc_inds] += 1 + self.vaccinations[vacc_inds] = 2 # TEMP!!!! self.vaccination_dates[vacc_inds] = sim.t # Update vaccine attributes in sim diff --git a/covasim/people.py b/covasim/people.py index f894bf0ef..7631852a0 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -58,7 +58,7 @@ def __init__(self, pars, strict=True, **kwargs): for key in self.meta.person: if key == 'uid': self[key] = np.arange(self.pop_size, dtype=cvd.default_int) - elif key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. + elif 'imm' in key: # everyone starts out with no immunity self[key] = np.full((self.pars['n_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') elif key == 'vaccinations': self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) @@ -293,9 +293,90 @@ def check_death(self): self.critical[inds] = False self.susceptible[inds] = False self.dead[inds] = True + self.infectious_strain[inds]= np.nan + self.exposed_strain[inds] = np.nan + self.recovered_strain[inds] = np.nan return len(inds) + def check_immunity(self, strain): + ''' + Calculate people's immunity on this timestep from prior infections + vaccination + There are two fundamental sources of immunity: + (1) prior exposure: degree of protection depends on strain, prior symptoms, and time since recovery + (2) vaccination: degree of protection depends on strain, vaccine, and time since vaccination + ''' + is_sus = cvu.true(self.susceptible) # Currently susceptible + is_exp = cvu.false(self.susceptible) # Currently exposed + is_inf = cvu.true(self.infectious) # Currently infectious + was_inf = cvu.true(self.t>=self.date_recovered) # Had a previous exposure, now recovered + was_inf_same = cvu.true((self.recovered_strain == strain) & (self.t>=self.date_recovered)) # Had a previous exposure to the same strain, now recovered + was_inf_diff = np.setdiff1d(was_inf, was_inf_same) # Had a previous exposure to a different strain, now recovered + is_reinf = np.intersect1d(cvu.defined(self.recovered_strain), cvu.false(self.t>=self.date_recovered)) # Had a previous exposure, now reinfected + is_vacc = cvu.true(self.vaccinated) # Vaccinated + date_rec = self.date_recovered # Date recovered + + # If vaccines are present, extract relevant information about them + vacc_present = len(is_vacc) + if vacc_present: + date_vacc = self.date_vaccinated + vacc_info = self.pars['vaccine_info'] + + ### PART 1: + # Immunity to infection for susceptibles + is_sus_vacc = np.intersect1d(is_sus, is_vacc) # Susceptible and vaccinated + is_sus_was_inf_same = np.intersect1d(is_sus, was_inf_same) # Susceptible and being challenged by the same strain + is_sus_was_inf_diff = np.intersect1d(is_sus, was_inf_diff) # Susceptible and being challenged by a different strain + + if len(is_sus_vacc): # Immunity for susceptibles who've been vaccinated + vaccine_source = cvd.default_int(self.vaccine_source[is_sus_vacc]) + vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] + doses = cvd.default_int(self.vaccinations[is_sus_vacc]) + time_since_vacc = cvd.default_int(self.t - date_vacc[is_sus_vacc]) + try: self.sus_imm[strain, is_sus_vacc] = vaccine_scale * vacc_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, time_since_vacc] + except: + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() + + if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain + prior_symptoms = self.prior_symptoms[is_sus_was_inf_same] + time_since_rec = cvd.default_int(self.t - date_rec[is_sus_was_inf_same]) + self.sus_imm[strain, is_sus_was_inf_same] = self['pars']['immune_degree'][strain]['sus'][time_since_rec] * \ + prior_symptoms * self.pars['immunity']['sus'][strain, strain] + + if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain + for cross_strain in range(self.pars['n_strains']): + if cross_strain != strain: + cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain + cross_immune_inds = cvu.true(cross_immune) # People with some immunity to this strain from a prior infection with another strain + cross_immunity = np.full(len(cross_immune_inds), self['immunity']['sus'][strain, cross_strain]) + immune_inds = np.concatenate((immune_inds, cross_immune_inds)) + immunity_scale_factor = np.concatenate((immunity_scale_factor, cross_immunity)) + + ### PART 2: + # Immunity to disease for currently-infected people + is_inf_vacc = np.intersect1d(is_inf, is_vacc) + + if len(is_inf_vacc): # Immunity for infected people who've been vaccinated + vaccine_source = cvd.default_int(self.vaccine_source[is_inf_vacc]) + doses = cvd.default_int(self.vaccinations[is_inf_vacc]) + time_since_vacc = cvd.default_int(self.t - date_vacc[is_inf_vacc]) + self.trans_imm[strain, is_inf_vacc] = vacc_info['vaccine_immune_degree']['trans'][vaccine_source, doses-1, time_since_vacc] + self.prog_imm[strain, is_inf_vacc] = vacc_info['vaccine_immune_degree']['prog'][vaccine_source, doses-1, time_since_vacc] + + if len(is_reinf): # Immunity for reinfected people + prior_symptoms = self.prior_symptoms[is_reinf] + time_since_rec = cvd.default_int(self.t - date_rec[is_reinf]) + self.trans_imm[strain, is_reinf] = self['pars']['immune_degree'][strain]['trans'][time_since_rec] * \ + prior_symptoms * self.pars['immunity']['trans'][strain] + self.prog_imm[strain, is_reinf] = self['pars']['immune_degree'][strain]['prog'][time_since_rec] * \ + prior_symptoms * self.pars['immunity']['prog'][strain] + + return + + def check_diagnosed(self): ''' Check for new diagnoses. Since most data are reported with diagnoses on @@ -407,33 +488,6 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str n_infections = len(inds) durpars = infect_pars['dur'] - # Determine who is vaccinated and has some immunity from vaccine - vaccinated = self.vaccinated - vacc_inds = cvu.true(vaccinated) - if len(vacc_inds): - vaccine_info = self['pars']['vaccine_info'] - date_vacc = self.date_vaccinated - vaccine_source = cvd.default_int(self.vaccine_source[vacc_inds]) - vaccine_scale_factor = vaccine_info['rel_imm'][vaccine_source, strain] - doses = cvd.default_int(self.vaccinations[vacc_inds]) - vaccine_time = cvd.default_int(self.t - date_vacc[vacc_inds]) - self.trans_immunity_factors[strain, vacc_inds] = vaccine_scale_factor * vaccine_info['vaccine_immune_degree']['trans'][vaccine_source, doses-1, vaccine_time] - self.prog_immunity_factors[strain, vacc_inds] = vaccine_scale_factor * vaccine_info['vaccine_immune_degree']['prog'][vaccine_source, doses-1, vaccine_time] - - # determine people with immunity from this strain - date_rec = self.date_recovered - immune = self.recovered_strain[inds] == strain - immune_inds = cvu.itrue(immune, inds) # Whether people have some immunity to this strain from a prior infection with this strain - immune_inds = np.setdiff1d(immune_inds, vacc_inds) - immune_time = cvd.default_int(self.t - date_rec[immune_inds]) # Time since recovery for people who were last infected by this strain - prior_symptoms = self.prior_symptoms[immune_inds] - - if len(immune_inds): - self.trans_immunity_factors[strain, immune_inds] = self['pars']['immune_degree'][strain]['trans'][immune_time] * \ - prior_symptoms * self.pars['immunity']['trans'][strain] - self.prog_immunity_factors[strain, immune_inds] = self['pars']['immune_degree'][strain]['prog'][immune_time] * \ - prior_symptoms * self.pars['immunity']['prog'][strain] - # Update states, strain info, and flows self.susceptible[inds] = False self.exposed[inds] = True @@ -443,7 +497,13 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.flows['new_infections'] += len(inds) self.flows['new_infections_by_strain'][strain] += len(inds) self.flows['new_reinfections'] += len(cvu.defined(self.date_recovered[inds])) # Record reinfections - self.date_recovered[inds] = np.nan # Reset date they recovered - we only store the last recovery +# if self.flows['new_reinfections']>0: +# import traceback; +# traceback.print_exc(); +# import pdb; +# pdb.set_trace() +# self.date_recovered[inds] = np.nan # Reset date they recovered - we only store the last recovery +# self.recovered_strain[inds] = np.nan # Reset the strain they recovered from # Record transmissions for i, target in enumerate(inds): @@ -454,53 +514,53 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.date_infectious[inds] = self.dur_exp2inf[inds] + self.t # Use prognosis probabilities to determine what happens to them - symp_probs = infect_pars['rel_symp_prob']*self.symp_prob[inds]*(1-self.prog_immunity_factors[strain, inds]) # Calculate their actual probability of being symptomatic + symp_probs = infect_pars['rel_symp_prob']*self.symp_prob[inds]*(1-self.prog_imm[strain, inds]) # Calculate their actual probability of being symptomatic is_symp = cvu.binomial_arr(symp_probs) # Determine if they develop symptoms symp_inds = inds[is_symp] asymp_inds = inds[~is_symp] # Asymptomatic # CASE 1: Asymptomatic: may infect others, but have no symptoms and do not die - dur_asym2rec = cvu.sample(**durpars['asym2rec'], size=len(asymp_inds))*(1-self.trans_immunity_factors[strain, asymp_inds]) + dur_asym2rec = cvu.sample(**durpars['asym2rec'], size=len(asymp_inds))*(1-self.trans_imm[strain, asymp_inds]) self.date_recovered[asymp_inds] = self.date_infectious[asymp_inds] + dur_asym2rec # Date they recover self.dur_disease[asymp_inds] = self.dur_exp2inf[asymp_inds] + dur_asym2rec # Store how long this person had COVID-19 # CASE 2: Symptomatic: can either be mild, severe, or critical n_symp_inds = len(symp_inds) - self.dur_inf2sym[symp_inds] = cvu.sample(**durpars['inf2sym'], size=n_symp_inds)*(1-self.trans_immunity_factors[strain, symp_inds]) # Store how long this person took to develop symptoms + self.dur_inf2sym[symp_inds] = cvu.sample(**durpars['inf2sym'], size=n_symp_inds)*(1-self.trans_imm[strain, symp_inds]) # Store how long this person took to develop symptoms self.date_symptomatic[symp_inds] = self.date_infectious[symp_inds] + self.dur_inf2sym[symp_inds] # Date they become symptomatic - sev_probs = infect_pars['rel_severe_prob'] * self.severe_prob[symp_inds]*(1-self.prog_immunity_factors[strain, symp_inds]) # Probability of these people being severe + sev_probs = infect_pars['rel_severe_prob'] * self.severe_prob[symp_inds]*(1-self.prog_imm[strain, symp_inds]) # Probability of these people being severe is_sev = cvu.binomial_arr(sev_probs) # See if they're a severe or mild case sev_inds = symp_inds[is_sev] mild_inds = symp_inds[~is_sev] # Not severe # CASE 2.1: Mild symptoms, no hospitalization required and no probability of death - dur_mild2rec = cvu.sample(**durpars['mild2rec'], size=len(mild_inds))*(1-self.trans_immunity_factors[strain, mild_inds]) + dur_mild2rec = cvu.sample(**durpars['mild2rec'], size=len(mild_inds))*(1-self.trans_imm[strain, mild_inds]) self.date_recovered[mild_inds] = self.date_symptomatic[mild_inds] + dur_mild2rec # Date they recover self.dur_disease[mild_inds] = self.dur_exp2inf[mild_inds] + self.dur_inf2sym[mild_inds] + dur_mild2rec # Store how long this person had COVID-19 # CASE 2.2: Severe cases: hospitalization required, may become critical - self.dur_sym2sev[sev_inds] = cvu.sample(**durpars['sym2sev'], size=len(sev_inds))*(1-self.trans_immunity_factors[strain, sev_inds]) # Store how long this person took to develop severe symptoms + self.dur_sym2sev[sev_inds] = cvu.sample(**durpars['sym2sev'], size=len(sev_inds))*(1-self.trans_imm[strain, sev_inds]) # Store how long this person took to develop severe symptoms self.date_severe[sev_inds] = self.date_symptomatic[sev_inds] + self.dur_sym2sev[sev_inds] # Date symptoms become severe - crit_probs = infect_pars['rel_crit_prob'] * self.crit_prob[sev_inds] * (self.pars['no_hosp_factor'] if hosp_max else 1.) *(1-self.prog_immunity_factors[strain, sev_inds]) # Probability of these people becoming critical - higher if no beds available + crit_probs = infect_pars['rel_crit_prob'] * self.crit_prob[sev_inds] * (self.pars['no_hosp_factor'] if hosp_max else 1.) *(1-self.prog_imm[strain, sev_inds]) # Probability of these people becoming critical - higher if no beds available is_crit = cvu.binomial_arr(crit_probs) # See if they're a critical case crit_inds = sev_inds[is_crit] non_crit_inds = sev_inds[~is_crit] # CASE 2.2.1 Not critical - they will recover - dur_sev2rec = cvu.sample(**durpars['sev2rec'], size=len(non_crit_inds))*(1-self.trans_immunity_factors[strain, non_crit_inds]) + dur_sev2rec = cvu.sample(**durpars['sev2rec'], size=len(non_crit_inds))*(1-self.trans_imm[strain, non_crit_inds]) self.date_recovered[non_crit_inds] = self.date_severe[non_crit_inds] + dur_sev2rec # Date they recover self.dur_disease[non_crit_inds] = self.dur_exp2inf[non_crit_inds] + self.dur_inf2sym[non_crit_inds] + self.dur_sym2sev[non_crit_inds] + dur_sev2rec # Store how long this person had COVID-19 # CASE 2.2.2: Critical cases: ICU required, may die - self.dur_sev2crit[crit_inds] = cvu.sample(**durpars['sev2crit'], size=len(crit_inds))*(1-self.trans_immunity_factors[strain, crit_inds]) + self.dur_sev2crit[crit_inds] = cvu.sample(**durpars['sev2crit'], size=len(crit_inds))*(1-self.trans_imm[strain, crit_inds]) self.date_critical[crit_inds] = self.date_severe[crit_inds] + self.dur_sev2crit[crit_inds] # Date they become critical - death_probs = infect_pars['rel_death_prob'] * self.death_prob[crit_inds] * (self.pars['no_icu_factor'] if icu_max else 1.)*(1-self.prog_immunity_factors[strain, crit_inds]) # Probability they'll die + death_probs = infect_pars['rel_death_prob'] * self.death_prob[crit_inds] * (self.pars['no_icu_factor'] if icu_max else 1.)*(1-self.prog_imm[strain, crit_inds]) # Probability they'll die is_dead = cvu.binomial_arr(death_probs) # Death outcome dead_inds = crit_inds[is_dead] alive_inds = crit_inds[~is_dead] # CASE 2.2.2.1: Did not die - dur_crit2rec = cvu.sample(**durpars['crit2rec'], size=len(alive_inds))*(1-self.trans_immunity_factors[strain, alive_inds]) + dur_crit2rec = cvu.sample(**durpars['crit2rec'], size=len(alive_inds))*(1-self.trans_imm[strain, alive_inds]) self.date_recovered[alive_inds] = self.date_critical[alive_inds] + dur_crit2rec # Date they recover self.dur_disease[alive_inds] = self.dur_exp2inf[alive_inds] + self.dur_inf2sym[alive_inds] + self.dur_sym2sev[alive_inds] + self.dur_sev2crit[alive_inds] + dur_crit2rec # Store how long this person had COVID-19 diff --git a/covasim/sim.py b/covasim/sim.py index 80275e478..702359e83 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -572,10 +572,6 @@ def step(self): symp = people.symptomatic diag = people.diagnosed quar = people.quarantined - vaccinated = people.vaccinated - vacc_inds = cvu.true(vaccinated) - date_vacc = people.date_vaccinated - vaccine_info = self['vaccine_info'] # Initialize temp storage for strain parameters strain_parkeys = ['rel_beta', 'asymp_factor'] @@ -585,29 +581,8 @@ def step(self): # Iterate through n_strains to calculate infections for strain in range(ns): - immunity_factors = np.zeros(len(people), dtype=cvd.default_float) - - # Determine who is currently exposed and cannot get another infection - inf_inds = cvu.false(sus) - - # Determine who is vaccinated and has some immunity from vaccine - vacc_inds = np.setdiff1d(vacc_inds, inf_inds) # Take out anyone currently infected - if len(vacc_inds): - vaccine_source = cvd.default_int(people.vaccine_source[vacc_inds]) - vaccine_scale_factor = vaccine_info['rel_imm'][vaccine_source, strain] - doses_all = np.copy(cvd.default_int(people.vaccinations)) - - # doses = cvd.default_int(people.vaccinations[vacc_inds]) - - # pull out inds who have a prior infection - prior_inf = cvu.false(np.isnan(date_rec)) - prior_inf_vacc = np.intersect1d(prior_inf, vacc_inds) - # prior_inf_vacc = np.where(vacc_inds == prior_inf_vacc)[0] - # doses_all[prior_inf_vacc] = 2 - doses = doses_all[vacc_inds] - vaccine_time = cvd.default_int(t - date_vacc[vacc_inds]) - vaccine_immunity = vaccine_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, vaccine_time] - immunity_factors[vacc_inds] = vaccine_scale_factor * vaccine_immunity + # Check immunity + people.check_immunity(strain) # Deal with strain parameters for key in strain_parkeys: @@ -615,35 +590,6 @@ def step(self): beta = cvd.default_float(self['beta'] * strain_pars['rel_beta']) asymp_factor = cvd.default_float(strain_pars['asymp_factor']) - # Determine people with immunity from a past infection from this strain - immune = people.recovered_strain == strain - immune_inds = cvu.true(immune) # Whether people have some immunity to this strain from a prior infection with this strain - immune_inds = np.setdiff1d(immune_inds, inf_inds) - immune_inds = np.setdiff1d(immune_inds, vacc_inds) - - # Pull out own immunity - immunity_scale_factor = np.full(len(immune_inds), self['immunity']['sus'][strain,strain]) - - # Process cross-immunity parameters and indices, if relevant - if ns > 1: - for cross_strain in range(ns): - if cross_strain != strain: - cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain - cross_immune_inds = cvu.true(cross_immune) # People with some immunity to this strain from a prior infection with another strain - cross_immune_inds = np.setdiff1d(cross_immune_inds, inf_inds) # remove anyone who is currently exposed - cross_immune_inds = np.setdiff1d(cross_immune_inds, immune_inds) # remove anyone who has own-immunity, that supercedes cross-immunity - cross_immune_inds = np.setdiff1d(cross_immune_inds, vacc_inds) - cross_immunity = np.full(len(cross_immune_inds), self['immunity']['sus'][strain, cross_strain]) - immune_inds = np.concatenate((immune_inds, cross_immune_inds)) - immunity_scale_factor = np.concatenate((immunity_scale_factor, cross_immunity)) - - immune_time = cvd.default_int(t - date_rec[immune_inds]) # Time since recovery for people who were last infected by this strain - prior_symptoms = people.prior_symptoms[immune_inds] - - # Compute immunity to susceptibility - if len(immune_inds): - immunity_factors[immune_inds] = self['immune_degree'][strain]['sus'][immune_time] * prior_symptoms * immunity_scale_factor - # Define indices for this strain inf_by_this_strain = sc.dcp(inf) inf_by_this_strain[cvu.false(people.infectious_strain == strain)] = False @@ -656,12 +602,13 @@ def step(self): # Compute relative transmission and susceptibility rel_trans = people.rel_trans[:] rel_sus = people.rel_sus[:] + sus_imm = people.sus_imm[strain,:] iso_factor = cvd.default_float(self['iso_factor'][lkey]) quar_factor = cvd.default_float(self['quar_factor'][lkey]) beta_layer = cvd.default_float(self['beta_layer'][lkey]) rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, - diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors) + diag, quar, asymp_factor, iso_factor, quar_factor, sus_imm) rel_sus = np.float32(rel_sus) # TODO: why doesn't this get returned in this format already? # Calculate actual transmission diff --git a/tests/devtests/test_immunity.py b/tests/devtests/test_immunity.py index 4da0eb880..848b0b8c4 100644 --- a/tests/devtests/test_immunity.py +++ b/tests/devtests/test_immunity.py @@ -63,10 +63,14 @@ def test_reinfection(do_plot=False, do_show=True, do_save=False): pars = { 'beta': 0.015, 'n_days': 120, - 'rel_imm': {k:1 for k in cvd.immunity_sources} +# 'rel_imm': dict(asymptomatic=0.7, mild=0.8, severe=1.) } - pfizer = cv.vaccinate(days=20, vaccine_pars='pfizer') - sim = cv.Sim(pars=pars, interventions=pfizer) + + pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer') + sim = cv.Sim( + pars=pars, + interventions=pfizer + ) sim.run() to_plot = sc.objdict({ From 558af496d83edb96d04ebd1c4383a2ef6801d0b7 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 12 Mar 2021 16:26:13 +0100 Subject: [PATCH 172/569] pushing branch --- covasim/people.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index 7631852a0..a35f28447 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -333,12 +333,7 @@ def check_immunity(self, strain): vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] doses = cvd.default_int(self.vaccinations[is_sus_vacc]) time_since_vacc = cvd.default_int(self.t - date_vacc[is_sus_vacc]) - try: self.sus_imm[strain, is_sus_vacc] = vaccine_scale * vacc_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, time_since_vacc] - except: - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() + self.sus_imm[strain, is_sus_vacc] = vaccine_scale * vacc_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, time_since_vacc] if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain prior_symptoms = self.prior_symptoms[is_sus_was_inf_same] @@ -497,13 +492,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.flows['new_infections'] += len(inds) self.flows['new_infections_by_strain'][strain] += len(inds) self.flows['new_reinfections'] += len(cvu.defined(self.date_recovered[inds])) # Record reinfections -# if self.flows['new_reinfections']>0: -# import traceback; -# traceback.print_exc(); -# import pdb; -# pdb.set_trace() -# self.date_recovered[inds] = np.nan # Reset date they recovered - we only store the last recovery -# self.recovered_strain[inds] = np.nan # Reset the strain they recovered from + self.date_recovered[inds] = np.nan # Reset date they recovered - we only store the last recovery # Record transmissions for i, target in enumerate(inds): From 93d17cc3716d2dcb68f4e075ec97d34e92273a70 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 12 Mar 2021 10:47:07 -0500 Subject: [PATCH 173/569] quick fix to store doses and set doses for someone with a prior infection --- covasim/immunity.py | 5 +++-- covasim/sim.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 73259e349..9ba80ea8a 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -17,7 +17,7 @@ # %% Define strain class -__all__ += ['Strain'] +__all__ += ['Strain', 'Vaccine'] class Strain(): @@ -321,7 +321,8 @@ def initialize(self, sim): raise ValueError(errormsg) ''' Initialize immune_degree''' - doses = self.doses + # doses = self.doses + doses = 2 # Precompute waning immune_degree = [] # Stored as a list by dose diff --git a/covasim/sim.py b/covasim/sim.py index 80275e478..a6994ccf2 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -480,6 +480,7 @@ def init_vaccines(self): for ind, vacc in enumerate(self['vaccines']): self['vaccine_info']['rel_imm'][ind,:] = vacc.rel_imm + self['vaccine_info']['doses'] = vacc.doses for dose in range(vacc.doses): for ax in cvd.immunity_axes: self['vaccine_info']['vaccine_immune_degree'][ax][ind, dose, :] = vacc.vaccine_immune_degree[dose][ax] @@ -602,8 +603,7 @@ def step(self): # pull out inds who have a prior infection prior_inf = cvu.false(np.isnan(date_rec)) prior_inf_vacc = np.intersect1d(prior_inf, vacc_inds) - # prior_inf_vacc = np.where(vacc_inds == prior_inf_vacc)[0] - # doses_all[prior_inf_vacc] = 2 + doses_all[prior_inf_vacc] = vaccine_info['doses'] doses = doses_all[vacc_inds] vaccine_time = cvd.default_int(t - date_vacc[vacc_inds]) vaccine_immunity = vaccine_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, vaccine_time] From f46298b51e8f45e69280e8c5f31c3751fbbc12fe Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 12 Mar 2021 11:31:10 -0500 Subject: [PATCH 174/569] ordering of check_immunity function --- covasim/interventions.py | 4 +- covasim/people.py | 109 +++++++++++++++++++++------------------ covasim/sim.py | 2 +- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 252840815..30459ad9b 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1201,8 +1201,8 @@ def apply(self, sim): return def update_vaccine_info(self, sim, vacc_inds): - #self.vaccinations[vacc_inds] += 1 - self.vaccinations[vacc_inds] = 2 # TEMP!!!! + self.vaccinations[vacc_inds] += 1 + # self.vaccinations[vacc_inds] = 2 # TEMP!!!! self.vaccination_dates[vacc_inds] = sim.t # Update vaccine attributes in sim diff --git a/covasim/people.py b/covasim/people.py index a35f28447..eb7a59fd1 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -299,75 +299,80 @@ def check_death(self): return len(inds) - def check_immunity(self, strain): + def check_immunity(self, strain, sus=True, inds=None): ''' Calculate people's immunity on this timestep from prior infections + vaccination There are two fundamental sources of immunity: (1) prior exposure: degree of protection depends on strain, prior symptoms, and time since recovery (2) vaccination: degree of protection depends on strain, vaccine, and time since vaccination + + Gets called from sim before computing trans_sus, sus=True, inds=None + Gets called from people.infect() to calculate prog/trans, sus=False, inds= inds of people being infected ''' is_sus = cvu.true(self.susceptible) # Currently susceptible - is_exp = cvu.false(self.susceptible) # Currently exposed - is_inf = cvu.true(self.infectious) # Currently infectious was_inf = cvu.true(self.t>=self.date_recovered) # Had a previous exposure, now recovered was_inf_same = cvu.true((self.recovered_strain == strain) & (self.t>=self.date_recovered)) # Had a previous exposure to the same strain, now recovered was_inf_diff = np.setdiff1d(was_inf, was_inf_same) # Had a previous exposure to a different strain, now recovered - is_reinf = np.intersect1d(cvu.defined(self.recovered_strain), cvu.false(self.t>=self.date_recovered)) # Had a previous exposure, now reinfected is_vacc = cvu.true(self.vaccinated) # Vaccinated date_rec = self.date_recovered # Date recovered + immune_degree = self.pars['immune_degree'][strain] + immunity = self.pars['immunity'] # If vaccines are present, extract relevant information about them vacc_present = len(is_vacc) if vacc_present: date_vacc = self.date_vaccinated vacc_info = self.pars['vaccine_info'] - - ### PART 1: - # Immunity to infection for susceptibles - is_sus_vacc = np.intersect1d(is_sus, is_vacc) # Susceptible and vaccinated - is_sus_was_inf_same = np.intersect1d(is_sus, was_inf_same) # Susceptible and being challenged by the same strain - is_sus_was_inf_diff = np.intersect1d(is_sus, was_inf_diff) # Susceptible and being challenged by a different strain - - if len(is_sus_vacc): # Immunity for susceptibles who've been vaccinated - vaccine_source = cvd.default_int(self.vaccine_source[is_sus_vacc]) - vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] - doses = cvd.default_int(self.vaccinations[is_sus_vacc]) - time_since_vacc = cvd.default_int(self.t - date_vacc[is_sus_vacc]) - self.sus_imm[strain, is_sus_vacc] = vaccine_scale * vacc_info['vaccine_immune_degree']['sus'][vaccine_source, doses-1, time_since_vacc] - - if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain - prior_symptoms = self.prior_symptoms[is_sus_was_inf_same] - time_since_rec = cvd.default_int(self.t - date_rec[is_sus_was_inf_same]) - self.sus_imm[strain, is_sus_was_inf_same] = self['pars']['immune_degree'][strain]['sus'][time_since_rec] * \ - prior_symptoms * self.pars['immunity']['sus'][strain, strain] - - if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain - for cross_strain in range(self.pars['n_strains']): - if cross_strain != strain: - cross_immune = people.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain - cross_immune_inds = cvu.true(cross_immune) # People with some immunity to this strain from a prior infection with another strain - cross_immunity = np.full(len(cross_immune_inds), self['immunity']['sus'][strain, cross_strain]) - immune_inds = np.concatenate((immune_inds, cross_immune_inds)) - immunity_scale_factor = np.concatenate((immunity_scale_factor, cross_immunity)) - - ### PART 2: - # Immunity to disease for currently-infected people - is_inf_vacc = np.intersect1d(is_inf, is_vacc) - - if len(is_inf_vacc): # Immunity for infected people who've been vaccinated - vaccine_source = cvd.default_int(self.vaccine_source[is_inf_vacc]) - doses = cvd.default_int(self.vaccinations[is_inf_vacc]) - time_since_vacc = cvd.default_int(self.t - date_vacc[is_inf_vacc]) - self.trans_imm[strain, is_inf_vacc] = vacc_info['vaccine_immune_degree']['trans'][vaccine_source, doses-1, time_since_vacc] - self.prog_imm[strain, is_inf_vacc] = vacc_info['vaccine_immune_degree']['prog'][vaccine_source, doses-1, time_since_vacc] - - if len(is_reinf): # Immunity for reinfected people - prior_symptoms = self.prior_symptoms[is_reinf] - time_since_rec = cvd.default_int(self.t - date_rec[is_reinf]) - self.trans_imm[strain, is_reinf] = self['pars']['immune_degree'][strain]['trans'][time_since_rec] * \ - prior_symptoms * self.pars['immunity']['trans'][strain] - self.prog_imm[strain, is_reinf] = self['pars']['immune_degree'][strain]['prog'][time_since_rec] * \ - prior_symptoms * self.pars['immunity']['prog'][strain] + vacc_degree = vacc_info['vaccine_immune_degree'] + + if sus: + ### PART 1: + # Immunity to infection for susceptibles + is_sus_vacc = np.intersect1d(is_sus, is_vacc) # Susceptible and vaccinated + is_sus_was_inf_same = np.intersect1d(is_sus, was_inf_same) # Susceptible and being challenged by the same strain + is_sus_was_inf_diff = np.intersect1d(is_sus, was_inf_diff) # Susceptible and being challenged by a different strain + + if len(is_sus_vacc): # Immunity for susceptibles who've been vaccinated + vaccine_source = cvd.default_int(self.vaccine_source[is_sus_vacc]) + vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] + doses = cvd.default_int(self.vaccinations[is_sus_vacc]) + time_since_vacc = cvd.default_int(self.t - date_vacc[is_sus_vacc]) + self.sus_imm[strain, is_sus_vacc] = vaccine_scale * vacc_degree['sus'][vaccine_source, doses - 1, time_since_vacc] + + if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain + prior_symptoms = self.prior_symptoms[is_sus_was_inf_same] + time_since_rec = cvd.default_int(self.t - date_rec[is_sus_was_inf_same]) + self.sus_imm[strain, is_sus_was_inf_same] = immune_degree['sus'][time_since_rec] * prior_symptoms * \ + immunity['sus'][strain, strain] + + if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain + for cross_strain in range(self.pars['n_strains']): + if cross_strain != strain: + cross_immune = self.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain + cross_immune_inds = cvu.true(cross_immune) # People with some immunity to this strain from a prior infection with another strain + cross_immunity = np.full(len(cross_immune_inds), immunity['sus'][strain, cross_strain]) + immune_inds = np.concatenate((immune_inds, cross_immune_inds)) + immunity_scale_factor = np.concatenate((immunity_scale_factor, cross_immunity)) + else: + ### PART 2: + # Immunity to disease for currently-infected people + is_inf_vacc = np.intersect1d(inds, is_vacc) + was_inf = cvu.true(self.t >= self.date_recovered[inds]) + + if len(is_inf_vacc): # Immunity for infected people who've been vaccinated + vaccine_source = cvd.default_int(self.vaccine_source[is_inf_vacc]) + doses = cvd.default_int(self.vaccinations[is_inf_vacc]) + time_since_vacc = cvd.default_int(self.t - date_vacc[is_inf_vacc]) + self.trans_imm[strain, is_inf_vacc] = vacc_degree['trans'][vaccine_source, doses - 1, time_since_vacc] + self.prog_imm[strain, is_inf_vacc] = vacc_degree['prog'][vaccine_source, doses - 1, time_since_vacc] + + if len(was_inf): # Immunity for reinfected people + prior_symptoms = self.prior_symptoms[was_inf] + time_since_rec = cvd.default_int(self.t - date_rec[was_inf]) + self.trans_imm[strain, was_inf] = immune_degree[strain]['trans'][time_since_rec] * \ + prior_symptoms * immunity['trans'][strain] + self.prog_imm[strain, was_inf] = immune_degree[strain]['prog'][time_since_rec] * \ + prior_symptoms * immunity['prog'][strain] return @@ -474,6 +479,8 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str if source is not None: source = source[keep] + self.check_immunity(strain, sus=False, inds=inds) + # Deal with strain parameters infect_parkeys = ['dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob'] infect_pars = dict() diff --git a/covasim/sim.py b/covasim/sim.py index 702359e83..0378ef7c6 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -582,7 +582,7 @@ def step(self): for strain in range(ns): # Check immunity - people.check_immunity(strain) + people.check_immunity(strain, sus=True) # Deal with strain parameters for key in strain_parkeys: From 3a4332397ecad16d75d960523a479ec22a763251 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 12 Mar 2021 12:31:21 -0500 Subject: [PATCH 175/569] working! --- covasim/people.py | 39 +++++++++++++++++++-------------- covasim/sim.py | 1 + covasim/utils.py | 2 +- tests/devtests/test_variants.py | 8 +++---- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index eb7a59fd1..2b57205f8 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -309,10 +309,7 @@ def check_immunity(self, strain, sus=True, inds=None): Gets called from sim before computing trans_sus, sus=True, inds=None Gets called from people.infect() to calculate prog/trans, sus=False, inds= inds of people being infected ''' - is_sus = cvu.true(self.susceptible) # Currently susceptible - was_inf = cvu.true(self.t>=self.date_recovered) # Had a previous exposure, now recovered - was_inf_same = cvu.true((self.recovered_strain == strain) & (self.t>=self.date_recovered)) # Had a previous exposure to the same strain, now recovered - was_inf_diff = np.setdiff1d(was_inf, was_inf_same) # Had a previous exposure to a different strain, now recovered + was_inf = cvu.true(self.t >= self.date_recovered) # Had a previous exposure, now recovered is_vacc = cvu.true(self.vaccinated) # Vaccinated date_rec = self.date_recovered # Date recovered immune_degree = self.pars['immune_degree'][strain] @@ -324,18 +321,24 @@ def check_immunity(self, strain, sus=True, inds=None): date_vacc = self.date_vaccinated vacc_info = self.pars['vaccine_info'] vacc_degree = vacc_info['vaccine_immune_degree'] + doses_all = cvd.default_int(self.vaccinations) if sus: ### PART 1: - # Immunity to infection for susceptibles + # Immunity to infection for susceptible individuals + is_sus = cvu.true(self.susceptible) # Currently susceptible + was_inf_same = cvu.true((self.recovered_strain == strain) & (self.t >= self.date_recovered)) # Had a previous exposure to the same strain, now recovered + was_inf_diff = np.setdiff1d(was_inf, was_inf_same) # Had a previous exposure to a different strain, now recovered is_sus_vacc = np.intersect1d(is_sus, is_vacc) # Susceptible and vaccinated is_sus_was_inf_same = np.intersect1d(is_sus, was_inf_same) # Susceptible and being challenged by the same strain is_sus_was_inf_diff = np.intersect1d(is_sus, was_inf_diff) # Susceptible and being challenged by a different strain + is_sus_vacc_was_inf = np.intersect1d(is_sus_vacc, was_inf) - if len(is_sus_vacc): # Immunity for susceptibles who've been vaccinated + if len(is_sus_vacc): + doses_all[is_sus_vacc_was_inf] = vacc_info['doses']# Immunity for susceptibles who've been vaccinated vaccine_source = cvd.default_int(self.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] - doses = cvd.default_int(self.vaccinations[is_sus_vacc]) + doses = doses_all[is_sus_vacc] time_since_vacc = cvd.default_int(self.t - date_vacc[is_sus_vacc]) self.sus_imm[strain, is_sus_vacc] = vaccine_scale * vacc_degree['sus'][vaccine_source, doses - 1, time_since_vacc] @@ -346,18 +349,20 @@ def check_immunity(self, strain, sus=True, inds=None): immunity['sus'][strain, strain] if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain - for cross_strain in range(self.pars['n_strains']): - if cross_strain != strain: - cross_immune = self.recovered_strain == cross_strain # Whether people have some immunity to this strain from a prior infection with another strain - cross_immune_inds = cvu.true(cross_immune) # People with some immunity to this strain from a prior infection with another strain - cross_immunity = np.full(len(cross_immune_inds), immunity['sus'][strain, cross_strain]) - immune_inds = np.concatenate((immune_inds, cross_immune_inds)) - immunity_scale_factor = np.concatenate((immunity_scale_factor, cross_immunity)) + prior_strains = self.recovered_strain[is_sus_was_inf_diff] + prior_strains_unique = np.unique(prior_strains) + for unique_strain in prior_strains_unique: + unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] + prior_symptoms = self.prior_symptoms[unique_inds] + time_since_rec = cvd.default_int(self.t - date_rec[unique_inds]) + self.sus_imm[strain, unique_inds] = immune_degree['sus'][time_since_rec] * prior_symptoms * \ + immunity['sus'][strain, cvd.default_int(unique_strain)] + else: ### PART 2: # Immunity to disease for currently-infected people is_inf_vacc = np.intersect1d(inds, is_vacc) - was_inf = cvu.true(self.t >= self.date_recovered[inds]) + was_inf = np.intersect1d(inds, was_inf) if len(is_inf_vacc): # Immunity for infected people who've been vaccinated vaccine_source = cvd.default_int(self.vaccine_source[is_inf_vacc]) @@ -369,9 +374,9 @@ def check_immunity(self, strain, sus=True, inds=None): if len(was_inf): # Immunity for reinfected people prior_symptoms = self.prior_symptoms[was_inf] time_since_rec = cvd.default_int(self.t - date_rec[was_inf]) - self.trans_imm[strain, was_inf] = immune_degree[strain]['trans'][time_since_rec] * \ + self.trans_imm[strain, was_inf] = immune_degree['trans'][time_since_rec] * \ prior_symptoms * immunity['trans'][strain] - self.prog_imm[strain, was_inf] = immune_degree[strain]['prog'][time_since_rec] * \ + self.prog_imm[strain, was_inf] = immune_degree['prog'][time_since_rec] * \ prior_symptoms * immunity['prog'][strain] return diff --git a/covasim/sim.py b/covasim/sim.py index 0378ef7c6..1e5a2b2dd 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -480,6 +480,7 @@ def init_vaccines(self): for ind, vacc in enumerate(self['vaccines']): self['vaccine_info']['rel_imm'][ind,:] = vacc.rel_imm + self['vaccine_info']['doses'] = vacc.doses for dose in range(vacc.doses): for ax in cvd.immunity_axes: self['vaccine_info']['vaccine_immune_degree'][ax][ind, dose, :] = vacc.vaccine_immune_degree[dose][ax] diff --git a/covasim/utils.py b/covasim/utils.py index 68d548f55..9ac42044d 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -122,7 +122,7 @@ def find_contacts(p1, p2, inds): # pragma: no cover def update_strain_attributes(people): for key in people.meta.person: - if key == 'trans_immunity_factors' or key == 'prog_immunity_factors': # everyone starts out with no immunity to either strain. + if 'imm' in key: # everyone starts out with no immunity to either strain. people[key] = np.append(people[key], np.full((1, people.pop_size), 0, dtype=cvd.default_float, order='F'), axis=0) # Set strain states, which store info about which strain a person is exposed to diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 870f5655a..69db7386e 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -218,8 +218,8 @@ def test_import2strains(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing 2 new strains partway through a sim') sc.heading('Setting up...') - b117 = cv.Strain('b117', days=10, n_imports=20) - p1 = cv.Strain('sa variant', days=30, n_imports=20) + b117 = cv.Strain('b117', days=1, n_imports=20) + p1 = cv.Strain('sa variant', days=2, n_imports=20) sim = cv.Sim(strains=[b117, p1], label='With imported infections') sim.run() @@ -383,11 +383,11 @@ def get_ind_of_min_value(list, time): sim = cv.Sim() sim.run() - sim0 = test_synthpops() + # sim0 = test_synthpops() # Run more complex tests # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) From c5a1562bbcb476164f46bf2b4d54cf31c2a1e0cb Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 12 Mar 2021 12:42:17 -0500 Subject: [PATCH 176/569] formatting fix --- covasim/interventions.py | 1 - tests/devtests/test_variants.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 30459ad9b..04431c755 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1202,7 +1202,6 @@ def apply(self, sim): def update_vaccine_info(self, sim, vacc_inds): self.vaccinations[vacc_inds] += 1 - # self.vaccinations[vacc_inds] = 2 # TEMP!!!! self.vaccination_dates[vacc_inds] = sim.t # Update vaccine attributes in sim diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 69db7386e..5627aba59 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -9,6 +9,7 @@ do_show = 1 do_save = 0 + def test_synthpops(): sim = cv.Sim(pop_size=5000, pop_type='synthpops') sim.popdict = cv.make_synthpop(sim, with_facilities=True, layer_mapping={'LTCF': 'f'}) @@ -25,6 +26,7 @@ def test_synthpops(): sim.run() return sim + def test_vaccine_1strain(do_plot=True, do_show=True, do_save=False): sc.heading('Run a basic sim with 1 strain, pfizer vaccine') From 4bef38c02be1a56e43fb79250bc904a1585bf001 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 12 Mar 2021 13:31:22 -0500 Subject: [PATCH 177/569] formatting fix --- covasim/interventions.py | 25 +++++++++++++++---------- tests/devtests/test_variants.py | 14 +++++++++----- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 04431c755..95167761c 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1177,21 +1177,26 @@ def apply(self, sim): if sim.t >= min(self.first_dose_eligible): # Determine who gets first dose of vaccine today + vacc_probs = np.zeros(sim.n) if self.subtarget is not None: subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) - vacc_probs = np.zeros(sim.n) # Begin by assigning equal vaccination probability to everyone vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted else: - vacc_probs = np.full(sim.n, self.prob) # Assign equal vaccination probability to everyone + for _ in find_day(self.first_dose_eligible, sim.t): + unvacc_inds = sc.findinds(~sim.people.vaccinated) + vacc_probs[unvacc_inds] = self.prob # Assign equal vaccination probability to everyone vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated - self.vaccinated[sim.t] = vacc_inds - if self.interval is not None: - next_dose_day = sim.t + self.interval - if next_dose_day < sim['n_days']: - self.second_dose_days[next_dose_day] = vacc_inds - vacc_inds_dose2 = self.second_dose_days[sim.t] - if vacc_inds_dose2 is not None: - vacc_inds = np.concatenate((vacc_inds, vacc_inds_dose2), axis=None) + + if len(vacc_inds): + self.vaccinated[sim.t] = vacc_inds + if self.interval is not None: + next_dose_day = sim.t + self.interval + if next_dose_day < sim['n_days']: + self.second_dose_days[next_dose_day] = vacc_inds + + vacc_inds_dose2 = self.second_dose_days[sim.t] + if vacc_inds_dose2 is not None: + vacc_inds = np.concatenate((vacc_inds, vacc_inds_dose2), axis=None) # Update vaccine attributes in sim sim.people.vaccinated[vacc_inds] = True diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 5627aba59..97972a9c1 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -381,18 +381,22 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - if 1: - sim = cv.Sim() - sim.run() + # if 1: + # sim = cv.Sim() + # sim.run() # sim0 = test_synthpops() + # basic test for vaccine + pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer') + sim = cv.Sim(interventions=[pfizer]) + sim.run() + # Run more complex tests # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - # scens1 = test_basic_reinfection(do_plot=do_plot, do_save=do_save, do_show=do_show) # scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) # Run Vaccine tests From 3be323271d3e96043aed4d53155ca6aa3fa0f6d0 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 12 Mar 2021 13:34:08 -0500 Subject: [PATCH 178/569] fix hardcoded doses --- covasim/immunity.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 9ba80ea8a..64d7f824d 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -321,8 +321,7 @@ def initialize(self, sim): raise ValueError(errormsg) ''' Initialize immune_degree''' - # doses = self.doses - doses = 2 + doses = self.doses # Precompute waning immune_degree = [] # Stored as a list by dose From cf21ba20b9e8c2c73212603da4307fca514c334c Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Sat, 13 Mar 2021 09:40:13 -0500 Subject: [PATCH 179/569] temp fix for reskey in msims --- covasim/immunity.py | 7 +++++-- covasim/run.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 64d7f824d..ad3df2895 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -73,15 +73,18 @@ def parse_strain_pars(self, strain=None, strain_label=None): elif strain in choices['b117']: strain_pars = dict() strain_pars['rel_beta'] = 1.5 # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 - strain_pars['rel_severe_prob'] = 1.6 # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf + strain_pars['rel_severe_prob'] = 1.8 # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf self.strain_label = strain # Known parameters on South African variant elif strain in choices['b1351']: strain_pars = dict() + strain_pars['rel_beta'] = 1.3 + strain_pars['rel_severe_prob'] = 1.3 + strain_pars['rel_death_prob'] = 1.3 strain_pars['imm_pars'] = dict() for ax in cvd.immunity_axes: - strain_pars['imm_pars'][ax] = dict(form='logistic_decay', pars={'init_val': .8, 'half_val': 30, 'lower_asymp': 0.2, 'decay_rate': -5}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + strain_pars['imm_pars'][ax] = dict(form='logistic_decay', pars={'init_val': .8, 'half_val': 30, 'lower_asymp': 0.3, 'decay_rate': -5}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) self.strain_label = strain # Known parameters on Brazil variant diff --git a/covasim/run.py b/covasim/run.py index 517e19c8f..017b8212c 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -248,6 +248,7 @@ def reduce(self, quantiles=None, use_mean=False, bounds=None, output=False): vals = sim.results[reskey].values if 'by_strain' in reskey: length = vals.shape[1] + vals = sim.results[reskey].values[0,:] else: length = len(vals) if length != reduced_sim.npts: From 17877edc27b1081cd7e484df8863111e74ba48bf Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 15 Mar 2021 15:03:53 -0400 Subject: [PATCH 180/569] 1. edits to msim.reduce() to compute summary stats for 'by_strain' params 2. moved check_immunity into immunity.py --- covasim/immunity.py | 90 ++++++++++++++++++++++++++++++++- covasim/people.py | 86 +------------------------------ covasim/run.py | 32 ++++++------ covasim/sim.py | 2 +- tests/devtests/test_variants.py | 9 ++-- 5 files changed, 115 insertions(+), 104 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index ad3df2895..12fc245ea 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -338,7 +338,7 @@ def initialize(self, sim): # %% Immunity methods -__all__ += ['init_immunity', 'pre_compute_waning'] +__all__ += ['init_immunity', 'pre_compute_waning', 'check_immunity'] def init_immunity(sim, create=False): @@ -444,6 +444,94 @@ def pre_compute_waning(length, form, pars): return output +def check_immunity(people, strain, sus=True, inds=None): + ''' + Calculate people's immunity on this timestep from prior infections + vaccination + There are two fundamental sources of immunity: + (1) prior exposure: degree of protection depends on strain, prior symptoms, and time since recovery + (2) vaccination: degree of protection depends on strain, vaccine, and time since vaccination + + Gets called from sim before computing trans_sus, sus=True, inds=None + Gets called from people.infect() to calculate prog/trans, sus=False, inds= inds of people being infected + ''' + was_inf = cvu.true(people.t >= people.date_recovered) # Had a previous exposure, now recovered + is_vacc = cvu.true(people.vaccinated) # Vaccinated + date_rec = people.date_recovered # Date recovered + immune_degree = people.pars['immune_degree'][strain] + immunity = people.pars['immunity'] + + # If vaccines are present, extract relevant information about them + vacc_present = len(is_vacc) + if vacc_present: + date_vacc = people.date_vaccinated + vacc_info = people.pars['vaccine_info'] + vacc_degree = vacc_info['vaccine_immune_degree'] + doses_all = cvd.default_int(people.vaccinations) + + if sus: + ### PART 1: + # Immunity to infection for susceptible individuals + is_sus = cvu.true(people.susceptible) # Currently susceptible + was_inf_same = cvu.true((people.recovered_strain == strain) & ( + people.t >= people.date_recovered)) # Had a previous exposure to the same strain, now recovered + was_inf_diff = np.setdiff1d(was_inf, + was_inf_same) # Had a previous exposure to a different strain, now recovered + is_sus_vacc = np.intersect1d(is_sus, is_vacc) # Susceptible and vaccinated + is_sus_was_inf_same = np.intersect1d(is_sus, + was_inf_same) # Susceptible and being challenged by the same strain + is_sus_was_inf_diff = np.intersect1d(is_sus, + was_inf_diff) # Susceptible and being challenged by a different strain + is_sus_vacc_was_inf = np.intersect1d(is_sus_vacc, was_inf) + + if len(is_sus_vacc): + doses_all[is_sus_vacc_was_inf] = vacc_info['doses'] # Immunity for susceptibles who've been vaccinated + vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) + vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] + doses = doses_all[is_sus_vacc] + time_since_vacc = cvd.default_int(people.t - date_vacc[is_sus_vacc]) + people.sus_imm[strain, is_sus_vacc] = vaccine_scale * vacc_degree['sus'][ + vaccine_source, doses - 1, time_since_vacc] + + if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain + prior_symptoms = people.prior_symptoms[is_sus_was_inf_same] + time_since_rec = cvd.default_int(people.t - date_rec[is_sus_was_inf_same]) + people.sus_imm[strain, is_sus_was_inf_same] = immune_degree['sus'][time_since_rec] * prior_symptoms * \ + immunity['sus'][strain, strain] + + if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain + prior_strains = people.recovered_strain[is_sus_was_inf_diff] + prior_strains_unique = np.unique(prior_strains) + for unique_strain in prior_strains_unique: + unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] + prior_symptoms = people.prior_symptoms[unique_inds] + time_since_rec = cvd.default_int(people.t - date_rec[unique_inds]) + people.sus_imm[strain, unique_inds] = immune_degree['sus'][time_since_rec] * prior_symptoms * \ + immunity['sus'][strain, cvd.default_int(unique_strain)] + + else: + ### PART 2: + # Immunity to disease for currently-infected people + is_inf_vacc = np.intersect1d(inds, is_vacc) + was_inf = np.intersect1d(inds, was_inf) + + if len(is_inf_vacc): # Immunity for infected people who've been vaccinated + vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) + doses = cvd.default_int(people.vaccinations[is_inf_vacc]) + time_since_vacc = cvd.default_int(people.t - date_vacc[is_inf_vacc]) + people.trans_imm[strain, is_inf_vacc] = vacc_degree['trans'][vaccine_source, doses - 1, time_since_vacc] + people.prog_imm[strain, is_inf_vacc] = vacc_degree['prog'][vaccine_source, doses - 1, time_since_vacc] + + if len(was_inf): # Immunity for reinfected people + prior_symptoms = people.prior_symptoms[was_inf] + time_since_rec = cvd.default_int(people.t - date_rec[was_inf]) + people.trans_imm[strain, was_inf] = immune_degree['trans'][time_since_rec] * \ + prior_symptoms * immunity['trans'][strain] + people.prog_imm[strain, was_inf] = immune_degree['prog'][time_since_rec] * \ + prior_symptoms * immunity['prog'][strain] + + return + + # Specific waning and growth functions are listed here def exp_decay(length, init_val, half_life, delay=None): ''' diff --git a/covasim/people.py b/covasim/people.py index 2b57205f8..58e6b670c 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -11,6 +11,7 @@ from . import defaults as cvd from . import base as cvb from . import plotting as cvplt +from . import immunity as cvi __all__ = ['People'] @@ -299,89 +300,6 @@ def check_death(self): return len(inds) - def check_immunity(self, strain, sus=True, inds=None): - ''' - Calculate people's immunity on this timestep from prior infections + vaccination - There are two fundamental sources of immunity: - (1) prior exposure: degree of protection depends on strain, prior symptoms, and time since recovery - (2) vaccination: degree of protection depends on strain, vaccine, and time since vaccination - - Gets called from sim before computing trans_sus, sus=True, inds=None - Gets called from people.infect() to calculate prog/trans, sus=False, inds= inds of people being infected - ''' - was_inf = cvu.true(self.t >= self.date_recovered) # Had a previous exposure, now recovered - is_vacc = cvu.true(self.vaccinated) # Vaccinated - date_rec = self.date_recovered # Date recovered - immune_degree = self.pars['immune_degree'][strain] - immunity = self.pars['immunity'] - - # If vaccines are present, extract relevant information about them - vacc_present = len(is_vacc) - if vacc_present: - date_vacc = self.date_vaccinated - vacc_info = self.pars['vaccine_info'] - vacc_degree = vacc_info['vaccine_immune_degree'] - doses_all = cvd.default_int(self.vaccinations) - - if sus: - ### PART 1: - # Immunity to infection for susceptible individuals - is_sus = cvu.true(self.susceptible) # Currently susceptible - was_inf_same = cvu.true((self.recovered_strain == strain) & (self.t >= self.date_recovered)) # Had a previous exposure to the same strain, now recovered - was_inf_diff = np.setdiff1d(was_inf, was_inf_same) # Had a previous exposure to a different strain, now recovered - is_sus_vacc = np.intersect1d(is_sus, is_vacc) # Susceptible and vaccinated - is_sus_was_inf_same = np.intersect1d(is_sus, was_inf_same) # Susceptible and being challenged by the same strain - is_sus_was_inf_diff = np.intersect1d(is_sus, was_inf_diff) # Susceptible and being challenged by a different strain - is_sus_vacc_was_inf = np.intersect1d(is_sus_vacc, was_inf) - - if len(is_sus_vacc): - doses_all[is_sus_vacc_was_inf] = vacc_info['doses']# Immunity for susceptibles who've been vaccinated - vaccine_source = cvd.default_int(self.vaccine_source[is_sus_vacc]) - vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] - doses = doses_all[is_sus_vacc] - time_since_vacc = cvd.default_int(self.t - date_vacc[is_sus_vacc]) - self.sus_imm[strain, is_sus_vacc] = vaccine_scale * vacc_degree['sus'][vaccine_source, doses - 1, time_since_vacc] - - if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain - prior_symptoms = self.prior_symptoms[is_sus_was_inf_same] - time_since_rec = cvd.default_int(self.t - date_rec[is_sus_was_inf_same]) - self.sus_imm[strain, is_sus_was_inf_same] = immune_degree['sus'][time_since_rec] * prior_symptoms * \ - immunity['sus'][strain, strain] - - if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain - prior_strains = self.recovered_strain[is_sus_was_inf_diff] - prior_strains_unique = np.unique(prior_strains) - for unique_strain in prior_strains_unique: - unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] - prior_symptoms = self.prior_symptoms[unique_inds] - time_since_rec = cvd.default_int(self.t - date_rec[unique_inds]) - self.sus_imm[strain, unique_inds] = immune_degree['sus'][time_since_rec] * prior_symptoms * \ - immunity['sus'][strain, cvd.default_int(unique_strain)] - - else: - ### PART 2: - # Immunity to disease for currently-infected people - is_inf_vacc = np.intersect1d(inds, is_vacc) - was_inf = np.intersect1d(inds, was_inf) - - if len(is_inf_vacc): # Immunity for infected people who've been vaccinated - vaccine_source = cvd.default_int(self.vaccine_source[is_inf_vacc]) - doses = cvd.default_int(self.vaccinations[is_inf_vacc]) - time_since_vacc = cvd.default_int(self.t - date_vacc[is_inf_vacc]) - self.trans_imm[strain, is_inf_vacc] = vacc_degree['trans'][vaccine_source, doses - 1, time_since_vacc] - self.prog_imm[strain, is_inf_vacc] = vacc_degree['prog'][vaccine_source, doses - 1, time_since_vacc] - - if len(was_inf): # Immunity for reinfected people - prior_symptoms = self.prior_symptoms[was_inf] - time_since_rec = cvd.default_int(self.t - date_rec[was_inf]) - self.trans_imm[strain, was_inf] = immune_degree['trans'][time_since_rec] * \ - prior_symptoms * immunity['trans'][strain] - self.prog_imm[strain, was_inf] = immune_degree['prog'][time_since_rec] * \ - prior_symptoms * immunity['prog'][strain] - - return - - def check_diagnosed(self): ''' Check for new diagnoses. Since most data are reported with diagnoses on @@ -484,7 +402,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str if source is not None: source = source[keep] - self.check_immunity(strain, sus=False, inds=inds) + cvi.check_immunity(self, strain, sus=False, inds=inds) # Deal with strain parameters infect_parkeys = ['dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob'] diff --git a/covasim/run.py b/covasim/run.py index 017b8212c..7ac8a0356 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -243,30 +243,32 @@ def reduce(self, quantiles=None, use_mean=False, bounds=None, output=False): raw = {} reskeys = reduced_sim.result_keys() for reskey in reskeys: - raw[reskey] = np.zeros((reduced_sim.npts, len(self.sims))) + if 'by_strain' in reskey: + raw[reskey] = np.zeros((reduced_sim['total_strains'], reduced_sim.npts, len(self.sims))) + else: + raw[reskey] = np.zeros((reduced_sim.npts, len(self.sims))) for s,sim in enumerate(self.sims): vals = sim.results[reskey].values if 'by_strain' in reskey: - length = vals.shape[1] - vals = sim.results[reskey].values[0,:] + raw[reskey][:, :, s] = vals else: - length = len(vals) - if length != reduced_sim.npts: - errormsg = f'Cannot reduce sims with inconsistent numbers of days: {reduced_sim.npts} vs. {len(vals)}' - raise ValueError(errormsg) - raw[reskey][:,s] = vals + raw[reskey][:, s] = vals for reskey in reskeys: + if 'by_strain' in reskey: + axis=2 + else: + axis=1 if use_mean: - r_mean = np.mean(raw[reskey], axis=1) - r_std = np.std(raw[reskey], axis=1) + r_mean = np.mean(raw[reskey], axis=axis) + r_std = np.std(raw[reskey], axis=axis) reduced_sim.results[reskey].values[:] = r_mean - reduced_sim.results[reskey].low = r_mean - bounds*r_std - reduced_sim.results[reskey].high = r_mean + bounds*r_std + reduced_sim.results[reskey].low = r_mean - bounds * r_std + reduced_sim.results[reskey].high = r_mean + bounds * r_std else: - reduced_sim.results[reskey].values[:] = np.quantile(raw[reskey], q=0.5, axis=1) - reduced_sim.results[reskey].low = np.quantile(raw[reskey], q=quantiles['low'], axis=1) - reduced_sim.results[reskey].high = np.quantile(raw[reskey], q=quantiles['high'], axis=1) + reduced_sim.results[reskey].values[:] = np.quantile(raw[reskey], q=0.5, axis=axis) + reduced_sim.results[reskey].low = np.quantile(raw[reskey], q=quantiles['low'], axis=axis) + reduced_sim.results[reskey].high = np.quantile(raw[reskey], q=quantiles['high'], axis=axis) # Compute and store final results reduced_sim.compute_summary() diff --git a/covasim/sim.py b/covasim/sim.py index 13b8a5a9a..5939efe3d 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -584,7 +584,7 @@ def step(self): # Check immunity - people.check_immunity(strain, sus=True) + cvimm.check_immunity(people, strain, sus=True) # Deal with strain parameters for key in strain_parkeys: diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 97972a9c1..3218fee99 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -388,9 +388,12 @@ def get_ind_of_min_value(list, time): # sim0 = test_synthpops() # basic test for vaccine - pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer') - sim = cv.Sim(interventions=[pfizer]) - sim.run() + b117 = cv.Strain('b117', days=0) + sim = cv.Sim(strains= [b117]) + msim = cv.MultiSim(sim, n_runs=2) + msim.run() + msim.reduce() + # Run more complex tests # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) From deaffc928ee4da64680b3835f3183e67c4c42969 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 15 Mar 2021 15:45:06 -0400 Subject: [PATCH 181/569] plotting by strain info in default plots! --- covasim/base.py | 4 +++- covasim/defaults.py | 11 +++++++++++ covasim/plotting.py | 28 +++++++++++++++++++++------- covasim/sim.py | 7 ++++--- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 9f14ddc1e..6a18c65c9 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -132,12 +132,14 @@ class Result(object): print(r1.values) ''' - def __init__(self, name=None, npts=None, scale=True, color=None, max_strains=30): + def __init__(self, name=None, npts=None, scale=True, color=None, strain_color=None, max_strains=30): self.name = name # Name of this result self.scale = scale # Whether or not to scale the result by the scale factor if color is None: color = cvd.get_colors()['default'] + strain_color = cvd.get_strain_colors() self.color = color # Default color + self.strain_color = strain_color if npts is None: npts = 0 if 'by_strain' in self.name or 'by strain' in self.name: diff --git a/covasim/defaults.py b/covasim/defaults.py index 3cf3eafe7..b959a87a7 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -213,6 +213,17 @@ def get_colors(): return c +def get_strain_colors(): + ''' + Specify plot colors -- used in sim.py. + + NB, includes duplicates since stocks and flows are named differently. + ''' + colors = ['#4d771e', '#c78f65', '#c75649', '#e45226', '#e45226', '#b62413', '#732e26', '#b62413'] + return colors + + + # Define the 'overview plots', i.e. the most useful set of plots to explore different aspects of a simulation overview_plots = [ 'cum_infections', diff --git a/covasim/plotting.py b/covasim/plotting.py index 595f7cc1b..164bf600b 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -275,15 +275,29 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot for resnum,reskey in enumerate(keylabels): res = sim.results[reskey] res_t = sim.results['t'] - color = set_line_options(colors, reskey, resnum, res.color) # Choose the color - label = set_line_options(labels, reskey, resnum, res.name) # Choose the label - if res.low is not None and res.high is not None: - ax.fill_between(res_t, res.low, res.high, color=color, **args.fill) # Create the uncertainty bound - ax.plot(res_t, res.values, label=label, **args.plot, c=color) # Actually plot the sim! + if 'by_strain' in reskey: + for strain in range(sim['total_strains']): + color = res.strain_color[strain] # Choose the color + if strain ==0: + label = 'wild type' + else: + label =sim['strains'][strain-1].strain_label + if res.low is not None and res.high is not None: + ax.fill_between(res_t, res.low[strain,:], res.high[strain,:], color=color, + **args.fill) # Create the uncertainty bound + ax.plot(res_t, res.values[strain,:], label=label, **args.plot, c=color) # Actually plot the sim! + + else: + color = set_line_options(colors, reskey, resnum, res.color) # Choose the color + label = set_line_options(labels, reskey, resnum, res.name) # Choose the label + if res.low is not None and res.high is not None: + ax.fill_between(res_t, res.low, res.high, color=color, **args.fill) # Create the uncertainty bound + ax.plot(res_t, res.values, label=label, **args.plot, c=color) # Actually plot the sim! if args.show['data']: - plot_data(sim, ax, reskey, args.scatter, color=color) # Plot the data + plot_data(sim, ax, reskey, args.scatter, color=color) # Plot the data if args.show['ticks']: - reset_ticks(ax, sim, interval, as_dates, dateformat) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + reset_ticks(ax, sim, interval, as_dates, + dateformat) # Optionally reset tick marks (useful for e.g. plotting weeks/months) if args.show['interventions']: plot_interventions(sim, ax) # Plot the interventions if args.show['legend']: diff --git a/covasim/sim.py b/covasim/sim.py index 5939efe3d..8d0613fd1 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -276,17 +276,18 @@ def init_res(*args, **kwargs): return output dcols = cvd.get_colors() # Get default colors + strain_cols = cvd.get_strain_colors() # Flows and cumulative flows for key,label in cvd.result_flows.items(): - self.results[f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key], max_strains=self['total_strains']) # Cumulative variables -- e.g. "Cumulative infections" + self.results[f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key], strain_color=strain_cols, max_strains=self['total_strains']) # Cumulative variables -- e.g. "Cumulative infections" for key,label in cvd.result_flows.items(): # Repeat to keep all the cumulative keys together - self.results[f'new_{key}'] = init_res(f'Number of new {label}', color=dcols[key], max_strains=self['total_strains']) # Flow variables -- e.g. "Number of new infections" + self.results[f'new_{key}'] = init_res(f'Number of new {label}', color=dcols[key], strain_color=strain_cols, max_strains=self['total_strains']) # Flow variables -- e.g. "Number of new infections" # Stock variables for key,label in cvd.result_stocks.items(): - self.results[f'n_{key}'] = init_res(label, color=dcols[key], max_strains=self['total_strains']) + self.results[f'n_{key}'] = init_res(label, color=dcols[key], strain_color=strain_cols, max_strains=self['total_strains']) # Other variables self.results['n_alive'] = init_res('Number of people alive', scale=False) From 54a171152f8ba5da18ace5f50974ff685e2738a3 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 15 Mar 2021 15:52:51 -0400 Subject: [PATCH 182/569] cleaning up --- tests/devtests/test_variants.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 3218fee99..e6f48fecf 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -376,6 +376,26 @@ def get_ind_of_min_value(list, time): return ind +def test_msim(): + # basic test for vaccine + b117 = cv.Strain('b117', days=0) + sim = cv.Sim(strains=[b117]) + msim = cv.MultiSim(sim, n_runs=2) + msim.run() + msim.reduce() + + to_plot = sc.objdict({ + + 'Total infections': ['cum_infections'], + 'New infections per day': ['new_infections'], + 'New Re-infections per day': ['new_reinfections'], + 'New infections by strain': ['new_infections_by_strain'] + }) + + msim.plot(to_plot=to_plot, do_save=0, do_show=1, legend_args={'loc': 'upper left'}, axis_args={'hspace': 0.4}, interval=35) + + return msim + #%% Run as a script if __name__ == '__main__': sc.tic() @@ -387,13 +407,8 @@ def get_ind_of_min_value(list, time): # sim0 = test_synthpops() - # basic test for vaccine - b117 = cv.Strain('b117', days=0) - sim = cv.Sim(strains= [b117]) - msim = cv.MultiSim(sim, n_runs=2) - msim.run() - msim.reduce() + sim0 = test_msim() # Run more complex tests # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) From 96e162675fa9ce1229ee2439356d23388ffef430 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 15 Mar 2021 21:47:50 -0400 Subject: [PATCH 183/569] cleaning up --- covasim/defaults.py | 9 +++++++++ covasim/immunity.py | 6 +++--- covasim/interventions.py | 3 +++ covasim/plotting.py | 30 +++++++++++++++++++++++------- covasim/sim.py | 2 ++ 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index b959a87a7..7ea0126d5 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -113,6 +113,7 @@ class PeopleMeta(sc.prettyobj): 'critical': 'Number of critical cases', 'diagnosed': 'Number of confirmed cases', 'quarantined': 'Number in quarantine', + 'vaccinated': 'Number of people vaccinated' } # The types of result that are counted as flows -- used in sim.py; value is the label suffix @@ -129,6 +130,8 @@ class PeopleMeta(sc.prettyobj): 'critical': 'critical cases', 'deaths': 'deaths', 'quarantined': 'quarantined people', + 'vaccinations': 'vaccinations', + 'vaccinated': 'vaccinated people' } # Define these here as well @@ -202,6 +205,8 @@ def get_colors(): c.diagnoses = '#5f5cd2' c.diagnosed = c.diagnoses c.quarantined = '#5c399c' + c.vaccinations = '#5c399c' + c.vaccinated = '#5c399c' c.recoveries = '#9e1149' # c.recovered = c.recoveries c.symptomatic = '#c1ad71' @@ -247,6 +252,10 @@ def get_strain_colors(): 'n_symptomatic', 'new_quarantined', 'n_quarantined', + 'new_vaccinations', + 'new_vaccinated', + 'cum_vaccinated', + 'cum_vaccinations', 'test_yield', 'r_eff', ] diff --git a/covasim/immunity.py b/covasim/immunity.py index 12fc245ea..04da98bb8 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -79,9 +79,9 @@ def parse_strain_pars(self, strain=None, strain_label=None): # Known parameters on South African variant elif strain in choices['b1351']: strain_pars = dict() - strain_pars['rel_beta'] = 1.3 - strain_pars['rel_severe_prob'] = 1.3 - strain_pars['rel_death_prob'] = 1.3 + strain_pars['rel_beta'] = 1.4 + strain_pars['rel_severe_prob'] = 1.4 + strain_pars['rel_death_prob'] = 1.4 strain_pars['imm_pars'] = dict() for ax in cvd.immunity_axes: strain_pars['imm_pars'][ax] = dict(form='logistic_decay', pars={'init_val': .8, 'half_val': 30, 'lower_asymp': 0.3, 'decay_rate': -5}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) diff --git a/covasim/interventions.py b/covasim/interventions.py index 95167761c..a5966a86a 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1189,6 +1189,8 @@ def apply(self, sim): if len(vacc_inds): self.vaccinated[sim.t] = vacc_inds + sim.people.flows['new_vaccinations'] += len(vacc_inds) + sim.people.flows['new_vaccinated'] += len(vacc_inds) if self.interval is not None: next_dose_day = sim.t + self.interval if next_dose_day < sim['n_days']: @@ -1197,6 +1199,7 @@ def apply(self, sim): vacc_inds_dose2 = self.second_dose_days[sim.t] if vacc_inds_dose2 is not None: vacc_inds = np.concatenate((vacc_inds, vacc_inds_dose2), axis=None) + sim.people.flows['new_vaccinations'] += len(vacc_inds_dose2) # Update vaccine attributes in sim sim.people.vaccinated[vacc_inds] = True diff --git a/covasim/plotting.py b/covasim/plotting.py index 164bf600b..69b56e2a6 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -327,13 +327,29 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, resdata = scens.results[reskey] for snum,scenkey,scendata in resdata.enumitems(): sim = scens.sims[scenkey][0] # Pull out the first sim in the list for this scenario - res_y = scendata.best - color = set_line_options(colors, scenkey, snum, default_colors[snum]) # Choose the color - label = set_line_options(labels, scenkey, snum, scendata.name) # Choose the label - ax.fill_between(scens.tvec, scendata.low, scendata.high, color=color, **args.fill) # Create the uncertainty bound - ax.plot(scens.tvec, res_y, label=label, c=color, **args.plot) # Plot the actual line - if args.show['data']: - plot_data(sim, ax, reskey, args.scatter, color=color) # Plot the data + if 'by_strain' in reskey: + for strain in range(sim['total_strains']): + res_y = scendata.best[strain,:] + color = default_colors[strain] # Choose the color + if strain == 0: + label = 'wild type' + else: + label = sim['strains'][strain - 1].strain_label + ax.fill_between(scens.tvec, scendata.low[strain,:], scendata.high[strain,:], color=color, + **args.fill) # Create the uncertainty bound + ax.plot(scens.tvec, res_y, label=label, c=color, **args.plot) # Plot the actual line + if args.show['data']: + plot_data(sim, ax, reskey, args.scatter, color=color) # Plot the data + else: + res_y = scendata.best + color = set_line_options(colors, scenkey, snum, default_colors[snum]) # Choose the color + label = set_line_options(labels, scenkey, snum, scendata.name) # Choose the label + ax.fill_between(scens.tvec, scendata.low, scendata.high, color=color, + **args.fill) # Create the uncertainty bound + ax.plot(scens.tvec, res_y, label=label, c=color, **args.plot) # Plot the actual line + if args.show['data']: + plot_data(sim, ax, reskey, args.scatter, color=color) # Plot the data + if args.show['interventions']: plot_interventions(sim, ax) # Plot the interventions if args.show['ticks']: diff --git a/covasim/sim.py b/covasim/sim.py index 8d0613fd1..08528cb79 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -301,6 +301,7 @@ def init_res(*args, **kwargs): self.results['doubling_time'] = init_res('Doubling time', scale=False) self.results['test_yield'] = init_res('Testing yield', scale=False) self.results['rel_test_yield'] = init_res('Relative testing yield', scale=False) + self.results['share_vaccinated'] = init_res('Share Vaccinated', scale=False) # Populate the rest of the results if self['rescale']: @@ -806,6 +807,7 @@ def compute_states(self): self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence self.results['incidence_by_strain'][:] = np.einsum('ji,i->ji',res['new_infections_by_strain'][:], 1/res['n_susceptible'][:]) # Calculate the incidence self.results['prevalence_by_strain'][:] = np.einsum('ji,i->ji',res['new_infections_by_strain'][:], 1/res['n_alive'][:]) # Calculate the prevalence + self.results['share_vaccinated'][:] = res['n_vaccinated'][:]/res['n_alive'][:] # Calculate the share vaccinated return From 750432293d2fcb614fa745e70ef71e13e72cce54 Mon Sep 17 00:00:00 2001 From: Romesh Abeysuriya Date: Wed, 17 Mar 2021 16:21:09 +1100 Subject: [PATCH 184/569] Update repr for classes that don't store arguments --- covasim/interventions.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 9832388c2..f78abd5ec 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -102,16 +102,20 @@ def __init__(self, label=None, show_label=True, do_plot=None, line_args=None): def __repr__(self): ''' Return a JSON-friendly output if possible, else revert to pretty repr ''' - try: - json = self.to_json() - which = json['which'] - pars = json['pars'] - parstr = ', '.join([f'{k}={v}' for k,v in pars.items()]) - output = f"cv.{which}({parstr})" - except Exception as E: - output = type(self) + f' ({str(E)})' # If that fails, print why - return output + if hasattr(self, 'input_args'): + try: + json = self.to_json() + which = json['which'] + pars = json['pars'] + parstr = ', '.join([f'{k}={v}' for k,v in pars.items()]) + output = f"cv.{which}({parstr})" + except Exception as E: + output = type(self) + f' ({str(E)})' # If that fails, print why + return output + else: + return f'{self.__module__}.{self.__class__.__name__}()' + def disp(self): ''' Print a detailed representation of the intervention ''' From 32430c5f38c48dfc0654aa2ccac4ba80a66a1cf5 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 17 Mar 2021 21:34:55 -0400 Subject: [PATCH 185/569] antibodies! very much WIP --- covasim/defaults.py | 7 +++---- covasim/immunity.py | 32 +++++++++++++++++++++++++------- covasim/parameters.py | 11 +++++++---- covasim/people.py | 14 +++++++++----- 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 7ea0126d5..cd80826e6 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -53,7 +53,9 @@ class PeopleMeta(sc.prettyobj): 'trans_imm', # Float 'prog_imm', # Float 'vaccinations', # Number of doses given per person - 'vaccine_source' # index of vaccine that individual received + 'vaccine_source', # index of vaccine that individual received + 'current_nab_titre', # Current neutralization titre relative to convalescent plasma + 'init_nab_titre' # Initial neutralization titre relative to convalescent plasma ] # Set the states that a person can be in: these are all booleans per person -- used in people.py @@ -141,9 +143,6 @@ class PeopleMeta(sc.prettyobj): # Parameters that can vary by strain (should be in list format) strain_pars = ['rel_beta', 'asymp_factor', - 'imm_pars', - 'immune_degree', - 'rel_imm', 'dur', 'rel_symp_prob', 'rel_severe_prob', diff --git a/covasim/immunity.py b/covasim/immunity.py index 04da98bb8..fb2c99ea7 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -444,6 +444,21 @@ def pre_compute_waning(length, form, pars): return output +def nab_to_efficacy(nab): + + efficacy = nab + return efficacy + + +def compute_nab(init_nab_titre, time_since_rec): + if time_since_rec < 250: + current_nab = init_nab_titre - ((1/180)*time_since_rec) + else: + + current_nab = init_nab_titre - (25/18) - np.exp(-time_since_rec/100) + return current_nab + + def check_immunity(people, strain, sus=True, inds=None): ''' Calculate people's immunity on this timestep from prior infections + vaccination @@ -457,7 +472,7 @@ def check_immunity(people, strain, sus=True, inds=None): was_inf = cvu.true(people.t >= people.date_recovered) # Had a previous exposure, now recovered is_vacc = cvu.true(people.vaccinated) # Vaccinated date_rec = people.date_recovered # Date recovered - immune_degree = people.pars['immune_degree'][strain] + immune_degree = people.pars['immune_degree'] immunity = people.pars['immunity'] # If vaccines are present, extract relevant information about them @@ -493,20 +508,23 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source, doses - 1, time_since_vacc] if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain - prior_symptoms = people.prior_symptoms[is_sus_was_inf_same] + init_nab_titre = people.init_nab_titre[is_sus_was_inf_same] time_since_rec = cvd.default_int(people.t - date_rec[is_sus_was_inf_same]) - people.sus_imm[strain, is_sus_was_inf_same] = immune_degree['sus'][time_since_rec] * prior_symptoms * \ - immunity['sus'][strain, strain] + current_nab_titre = compute_nab(init_nab_titre, time_since_rec) + people.current_nab_titre[is_sus_was_inf_same] = current_nab_titre + people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_nab_titre) if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain prior_strains = people.recovered_strain[is_sus_was_inf_diff] prior_strains_unique = np.unique(prior_strains) for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] - prior_symptoms = people.prior_symptoms[unique_inds] + init_nab_titre = people.init_nab_titre[unique_inds] time_since_rec = cvd.default_int(people.t - date_rec[unique_inds]) - people.sus_imm[strain, unique_inds] = immune_degree['sus'][time_since_rec] * prior_symptoms * \ - immunity['sus'][strain, cvd.default_int(unique_strain)] + current_nab_titre = compute_nab(init_nab_titre, time_since_rec) + people.current_nab_titre[unique_inds] = current_nab_titre + current_nab_titre *= immunity['sus'][strain, cvd.default_int(unique_strain)] + people.sus_imm[strain, unique_inds] = immune_degree['sus'][current_nab_titre] else: ### PART 2: diff --git a/covasim/parameters.py b/covasim/parameters.py index bfbc41489..8e9673f28 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -65,7 +65,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['total_strains'] = None # Set during sim initialization, once strains have been specified and processed pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py - pars['immune_degree'] = None + pars['immune_degree'] = None # Pre-loaded array mapping from NAb titre to efficacy, set in Immunity.py (based on Fig 1A of https://www.medrxiv.org/content/10.1101/2021.03.09.21252641v1.full.pdf) pars['vaccine_info'] = None # Vaccine info in a more easily accessible format # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains @@ -73,10 +73,13 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 pars['imm_pars'] = {} for ax in cvd.immunity_axes: - pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) + if ax == 'sus': + pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) + else: + pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 0.8, 'half_life': None}) pars['rel_imm'] = {} # Relative immunity scalings depending on the severity of symptoms - pars['rel_imm']['asymptomatic'] = 0.98 - pars['rel_imm']['mild'] = 0.99 + pars['rel_imm']['asymptomatic'] = 0.50 + pars['rel_imm']['mild'] = 0.95 pars['rel_imm']['severe'] = 1. pars['dur'] = {} diff --git a/covasim/people.py b/covasim/people.py index 58e6b670c..8e6ed7126 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -60,7 +60,7 @@ def __init__(self, pars, strict=True, **kwargs): if key == 'uid': self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif 'imm' in key: # everyone starts out with no immunity - self[key] = np.full((self.pars['n_strains'], self.pop_size), 0, dtype=cvd.default_float, order='F') + self[key] = np.full((self.pars['n_strains'], self.pop_size), 0, dtype=cvd.default_float) elif key == 'vaccinations': self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) else: @@ -76,7 +76,7 @@ def __init__(self, pars, strict=True, **kwargs): # Set strain states, which store info about which strain a person is exposed to for key in self.meta.strain_states: if 'by' in key: - self[key] = np.full((self.pars['n_strains'], self.pop_size), False, dtype=bool, order='F') + self[key] = np.full((self.pars['n_strains'], self.pop_size), False, dtype=bool) else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -263,14 +263,18 @@ def check_recovery(self): inds = self.check_inds(self.susceptible, self.date_recovered, filter_inds=self.is_exp) # Before letting them recover, store information about the strain and symptoms they had + # Set their initial NAb level based on symptoms self.recovered_strain[inds] = self.infectious_strain[inds] for strain in range(self.pars['n_strains']): this_strain_inds = cvu.itrue(self.recovered_strain[inds] == strain, inds) mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=this_strain_inds) severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=this_strain_inds) - self.prior_symptoms[this_strain_inds] = self.pars['rel_imm'][strain]['asymptomatic'] # - self.prior_symptoms[mild_inds] = self.pars['rel_imm'][strain]['mild'] # - self.prior_symptoms[severe_inds] = self.pars['rel_imm'][strain]['severe'] # + # self.prior_symptoms[this_strain_inds] = self.pars['rel_imm']['asymptomatic'] # + # self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] # + # self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] # + self.initial_nab_titre[this_strain_inds] = self.pars['rel_imm']['asymptomatic'] # + self.initial_nab_titre[mild_inds] = self.pars['rel_imm']['mild'] # + self.initial_nab_titre[severe_inds] = self.pars['rel_imm']['severe'] # # Now reset all disease states self.exposed[inds] = False From 689bd408a47653fc64a7503a472805b8d30411f1 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 18 Mar 2021 12:55:33 -0400 Subject: [PATCH 186/569] antibodies! very much WIP --- covasim/defaults.py | 3 +- covasim/immunity.py | 188 ++++++++++++++++++++---------------------- covasim/parameters.py | 17 ++-- covasim/people.py | 15 +--- 4 files changed, 101 insertions(+), 122 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index cd80826e6..224ba3933 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -54,8 +54,7 @@ class PeopleMeta(sc.prettyobj): 'prog_imm', # Float 'vaccinations', # Number of doses given per person 'vaccine_source', # index of vaccine that individual received - 'current_nab_titre', # Current neutralization titre relative to convalescent plasma - 'init_nab_titre' # Initial neutralization titre relative to convalescent plasma + 'NAb', # Current neutralization titre relative to convalescent plasma ] # Set the states that a person can be in: these are all booleans per person -- used in people.py diff --git a/covasim/immunity.py b/covasim/immunity.py index fb2c99ea7..de41d2c9c 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -82,17 +82,13 @@ def parse_strain_pars(self, strain=None, strain_label=None): strain_pars['rel_beta'] = 1.4 strain_pars['rel_severe_prob'] = 1.4 strain_pars['rel_death_prob'] = 1.4 - strain_pars['imm_pars'] = dict() - for ax in cvd.immunity_axes: - strain_pars['imm_pars'][ax] = dict(form='logistic_decay', pars={'init_val': .8, 'half_val': 30, 'lower_asymp': 0.3, 'decay_rate': -5}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + strain_pars['rel_imm'] = 0.5 self.strain_label = strain # Known parameters on Brazil variant elif strain in choices['p1']: strain_pars = dict() - strain_pars['imm_pars'] = dict() - for ax in cvd.immunity_axes: - strain_pars['imm_pars'][ax] = dict(form='logistic_decay', pars={'init_val': .8, 'half_val': 30, 'lower_asymp': 0.2, 'decay_rate': -5}) # E484K mutation reduces immunity protection (TODO: link to actual evidence) + strain_pars['rel_imm'] = 0.5 self.strain_label = strain else: @@ -112,27 +108,20 @@ def parse_strain_pars(self, strain=None, strain_label=None): return strain_pars def initialize(self, sim): - if not hasattr(self, 'imm_pars'): - self.imm_pars = sim['imm_pars'][0] - - # Validate immunity pars (make sure there are values for all cvd.immunity_axes) - for key in cvd.immunity_axes: - if key not in self.imm_pars: - print(f'Immunity pars for imported strain for {key} not provided, using default value') - self.imm_pars[key] = sim['imm_pars'][0][key] + if not hasattr(self, 'rel_imm'): + self.rel_imm = 1 # Update strain info for strain_key in cvd.strain_pars: - if strain_key != 'immune_degree': - if hasattr(self, strain_key): - newval = getattr(self, strain_key) - if strain_key == 'dur': # Validate durations (make sure there are values for all durations) - newval = sc.mergenested(sim[strain_key][0], newval) - sim[strain_key].append(newval) - else: - # use default - print(f'{strain_key} not provided for this strain, using default value') - sim[strain_key].append(sim[strain_key][0]) + if hasattr(self, strain_key): + newval = getattr(self, strain_key) + if strain_key == 'dur': # Validate durations (make sure there are values for all durations) + newval = sc.mergenested(sim[strain_key][0], newval) + sim[strain_key].append(newval) + else: + # use default + print(f'{strain_key} not provided for this strain, using default value') + sim[strain_key].append(sim[strain_key][0]) self.initialized = True @@ -178,7 +167,8 @@ def __init__(self, vaccine=None): self.rel_imm = None # list of length total_strains with relative immunity factor self.doses = None self.interval = None - self.imm_pars = None + self.NAb_pars = None + self.NAb_decay = None self.vaccine_strain_info = self.init_strain_vaccine_info() self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine) for par, val in self.vaccine_pars.items(): @@ -227,11 +217,10 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on pfizer if vaccine in choices['pfizer']: vaccine_pars = dict() - vaccine_pars['imm_pars'] = {} - for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 1/22}), - dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 50, - 'lower_asymp': 0.3, 'decay_rate': -5})] + vaccine_pars['NAb_pars'] = [dict(form='normal', pars={'mean': 2, 'sd': 2}), + dict(form='normal', pars={'mean': 8, 'sd': 2})] + vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1/180, 'length': 250}, + form2='exp_decay', pars2={'rate': 1/100}) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -239,40 +228,30 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on moderna elif vaccine in choices['moderna']: vaccine_pars = dict() - vaccine_pars['imm_pars'] = {} - for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 0.5/29}), - dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 50, - 'lower_asymp': 0.3, - 'decay_rate': -5})] - vaccine_pars['doses'] = 2 + vaccine_pars['NAb_pars'] = [dict(form='normal', pars={'mean': 2, 'sd': 2}), + dict(form='normal', pars={'mean': 8, 'sd': 2})] + vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1 / 180, 'length': 250}, + form2='exp_decay', pars2={'rate': 1 / 100}) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 29 vaccine_pars['label'] = vaccine # Known parameters on az elif vaccine in choices['az']: vaccine_pars = dict() - vaccine_pars['imm_pars'] = {} - for ax in cvd.immunity_axes: - vaccine_pars['imm_pars'][ax] = [dict(form='linear_growth', pars={'slope': 0.5/22}), - dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 50, - 'lower_asymp': 0.3, - 'decay_rate': -5})] - vaccine_pars['doses'] = 2 + vaccine_pars['NAb_pars'] = [dict(form='normal', pars={'mean': 2, 'sd': 2}), + dict(form='normal', pars={'mean': 8, 'sd': 2})] + vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1 / 180, 'length': 250}, + form2='exp_decay', pars2={'rate': 1 / 100}) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine # Known parameters on j&j elif vaccine in choices['j&j']: vaccine_pars = dict() - vaccine_pars['imm_pars'] = {} - for ax in cvd.immunity_axes: - if ax == 'sus': - vaccine_pars['imm_pars'][ax] = [dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 50, - 'lower_asymp': 0.3, 'decay_rate': -5, - 'delay': 30})]*2 - else: - vaccine_pars['imm_pars'][ax] = [dict(form='exp_decay', pars={'init_val': 1., 'half_life': 180})]*2 + vaccine_pars['NAb_pars'] = [dict(form='normal', pars={'mean': 2, 'sd': 2}), + dict(form='normal', pars={'mean': 8, 'sd': 2})] + vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1 / 180, 'length': 250}, + form2='exp_decay', pars2={'rate': 1 / 100}) vaccine_pars['doses'] = 1 vaccine_pars['interval'] = None vaccine_pars['label'] = vaccine @@ -299,7 +278,7 @@ def initialize(self, sim): for strain in range(ts-1): circulating_strains.append(sim['strains'][strain].strain_label) - if self.imm_pars is None: + if self.NAb_pars is None or self.NAb_decay is None: errormsg = f'Did not provide parameters for this vaccine' raise ValueError(errormsg) @@ -317,24 +296,7 @@ def initialize(self, sim): errormsg = f'Did not provide relative immunity for each strain' raise ValueError(errormsg) - # Validate immunity pars (make sure there are values for all cvd.immunity_axes) - for key in cvd.immunity_axes: - if key not in self.imm_pars: - errormsg = f'Immunity pars for vaccine for {key} not provided' - raise ValueError(errormsg) - - ''' Initialize immune_degree''' - doses = self.doses - - # Precompute waning - immune_degree = [] # Stored as a list by dose - for dose in range(doses): - strain_immune_degree = {} - for ax in cvd.immunity_axes: - strain_immune_degree[ax] = pre_compute_waning(sim['n_days'], **self.imm_pars[ax][dose]) - immune_degree.append(strain_immune_degree) - self.vaccine_immune_degree = immune_degree - + return # %% Immunity methods @@ -391,15 +353,6 @@ def init_immunity(sim, create=False): errormsg = f'Type of immunity["sus"] not understood: you provided {type(sim["immunity"]["sus"])}, but it should be an array or dict.' raise ValueError(errormsg) - # Precompute waning - immune_degree = [] # Stored as a list by strain - for s in range(ts): - strain_immune_degree = {} - for ax in cvd.immunity_axes: - strain_immune_degree[ax] = pre_compute_waning(sim['n_days'], **sim['imm_pars'][s][ax]) - immune_degree.append(strain_immune_degree) - sim['immune_degree'] = immune_degree - def pre_compute_waning(length, form, pars): ''' @@ -445,18 +398,62 @@ def pre_compute_waning(length, form, pars): def nab_to_efficacy(nab): - + # put in here nab to efficacy mapping (logistic regression from fig 1a) efficacy = nab return efficacy -def compute_nab(init_nab_titre, time_since_rec): - if time_since_rec < 250: - current_nab = init_nab_titre - ((1/180)*time_since_rec) +def compute_nab(people, inds, strain=None): + if strain is not None: + # NAbs is coming from a natural infection + this_strain_inds = cvu.itrue(people.recovered_strain[inds] == strain, inds) + mild_inds = people.check_inds(people.susceptible, people.date_symptomatic, filter_inds=this_strain_inds) + severe_inds = people.check_inds(people.susceptible, people.date_severe, filter_inds=this_strain_inds) + asymp_inds = np.setdiff1d(this_strain_inds, mild_inds) + asymp_inds = np.setdiff1d(asymp_inds, severe_inds) + + NAb_pars = people.pars['NAb_pars'] + NAb_decay = people.pars['NAb_decay'] + + init_NAb_asymp = cvu.sample(**NAb_pars['asymptomatic'], size=len(asymp_inds)) + init_NAb_mild = cvu.sample(**NAb_pars['mild'], size=len(mild_inds)) + init_NAb_severe = cvu.sample(**NAb_pars['severe'], size=len(severe_inds)) + init_NAbs = np.concatenate(init_NAb_asymp, init_NAb_mild, init_NAb_severe) + else: + # NAbs coming from a vaccine + # Figure out how many doses everyone has + one_dose_inds = people.vaccinations[inds] == 1 + two_dose_inds = people.vaccinations[inds] == 2 + + NAb_pars = people.pars['vaccine_info']['NAb_pars'] + NAb_decay = people.pars['vaccine_info']['NAb_decay'] + + init_NAb_one_dose = cvu.sample(**NAb_pars[0], size=len(one_dose_inds)) + init_NAb_two_dose = cvu.sample(**NAb_pars[1], size=len(two_dose_inds)) + init_NAbs = np.concatenate(init_NAb_one_dose, init_NAb_two_dose) + + + NAb_arrays = people.NAb[inds] + + day = people.t # timestep we are on + n_days = people.pars['n_days'] + days_left = n_days - day # how many days left in sim + length = NAb_decay['pars1']['length'] + + if days_left > length: + t1 = np.arange(length, dtype=cvd.default_int) + t1 = init_NAbs - (NAb_decay['pars1']['rate']*t1) + t2 = np.arange(length, n_days, dtype=cvd.default_int) + t2 = init_NAbs - (NAb_decay['pars1']['rate']*length) - np.exp(-t2*NAb_decay['pars2']['rate']) + result = np.concatenate((t1, t2), axis=0) + else: + t1 = np.arange(days_left, dtype=cvd.default_int) + result = init_NAbs - (NAb_decay['pars1']['rate']*t1) + + NAb_arrays[day:, ] = result - current_nab = init_nab_titre - (25/18) - np.exp(-time_since_rec/100) - return current_nab + return def check_immunity(people, strain, sus=True, inds=None): @@ -499,32 +496,25 @@ def check_immunity(people, strain, sus=True, inds=None): is_sus_vacc_was_inf = np.intersect1d(is_sus_vacc, was_inf) if len(is_sus_vacc): - doses_all[is_sus_vacc_was_inf] = vacc_info['doses'] # Immunity for susceptibles who've been vaccinated vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] - doses = doses_all[is_sus_vacc] time_since_vacc = cvd.default_int(people.t - date_vacc[is_sus_vacc]) - people.sus_imm[strain, is_sus_vacc] = vaccine_scale * vacc_degree['sus'][ - vaccine_source, doses - 1, time_since_vacc] + current_NAbs = people.NAb[time_since_vacc, is_sus_vacc] + people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs) * vaccine_scale if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain - init_nab_titre = people.init_nab_titre[is_sus_was_inf_same] time_since_rec = cvd.default_int(people.t - date_rec[is_sus_was_inf_same]) - current_nab_titre = compute_nab(init_nab_titre, time_since_rec) - people.current_nab_titre[is_sus_was_inf_same] = current_nab_titre - people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_nab_titre) + current_NAbs = people.NAb[time_since_rec, is_sus_was_inf_same] + people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs)*immunity[strain, strain] if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain prior_strains = people.recovered_strain[is_sus_was_inf_diff] prior_strains_unique = np.unique(prior_strains) for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] - init_nab_titre = people.init_nab_titre[unique_inds] time_since_rec = cvd.default_int(people.t - date_rec[unique_inds]) - current_nab_titre = compute_nab(init_nab_titre, time_since_rec) - people.current_nab_titre[unique_inds] = current_nab_titre - current_nab_titre *= immunity['sus'][strain, cvd.default_int(unique_strain)] - people.sus_imm[strain, unique_inds] = immune_degree['sus'][current_nab_titre] + current_NAbs = people.NAb[time_since_rec, unique_inds] + people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs) * immunity[strain, unique_strain] else: ### PART 2: diff --git a/covasim/parameters.py b/covasim/parameters.py index 8e9673f28..ee53d173c 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -71,16 +71,13 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 - pars['imm_pars'] = {} - for ax in cvd.immunity_axes: - if ax == 'sus': - pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) - else: - pars['imm_pars'][ax] = dict(form='exp_decay', pars={'init_val': 0.8, 'half_life': None}) - pars['rel_imm'] = {} # Relative immunity scalings depending on the severity of symptoms - pars['rel_imm']['asymptomatic'] = 0.50 - pars['rel_imm']['mild'] = 0.95 - pars['rel_imm']['severe'] = 1. + pars['NAb_pars'] = {} # Parameters for NAbs distribution for natural infection + pars['NAb_pars']['asymptomatic'] = dict(form='normal', pars={'mean': .5, 'sd': 2}) + pars['NAb_pars']['mild'] = dict(form='normal', pars={'mean': .8, 'sd': 2}) + pars['NAb_pars']['severe'] = dict(form='normal', pars={'mean': 1, 'sd': 2}) + + pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1/180, 'length': 250}, + form2='exp_decay', pars2={'rate': 1/100}) pars['dur'] = {} # Duration parameters: time for disease progression diff --git a/covasim/people.py b/covasim/people.py index 8e6ed7126..530a748aa 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -61,6 +61,8 @@ def __init__(self, pars, strict=True, **kwargs): self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif 'imm' in key: # everyone starts out with no immunity self[key] = np.full((self.pars['n_strains'], self.pop_size), 0, dtype=cvd.default_float) + elif 'NAb' in key: # everyone starts out with no NAb + self[key] = np.full((self.pars['n_days'], self.pop_size), 0, dtype=cvd.default_float) elif key == 'vaccinations': self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) else: @@ -262,19 +264,10 @@ def check_recovery(self): ''' Check for recovery ''' inds = self.check_inds(self.susceptible, self.date_recovered, filter_inds=self.is_exp) - # Before letting them recover, store information about the strain and symptoms they had - # Set their initial NAb level based on symptoms + # Before letting them recover, store information about the strain they had and pre-compute NAbs array self.recovered_strain[inds] = self.infectious_strain[inds] for strain in range(self.pars['n_strains']): - this_strain_inds = cvu.itrue(self.recovered_strain[inds] == strain, inds) - mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=this_strain_inds) - severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=this_strain_inds) - # self.prior_symptoms[this_strain_inds] = self.pars['rel_imm']['asymptomatic'] # - # self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] # - # self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] # - self.initial_nab_titre[this_strain_inds] = self.pars['rel_imm']['asymptomatic'] # - self.initial_nab_titre[mild_inds] = self.pars['rel_imm']['mild'] # - self.initial_nab_titre[severe_inds] = self.pars['rel_imm']['severe'] # + cvi.compute_nab(self, inds, strain) # Now reset all disease states self.exposed[inds] = False From d9c5ee8fee00188d761c7b5f38a12c40034b66ec Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 18 Mar 2021 16:22:23 -0400 Subject: [PATCH 187/569] a little progress! --- covasim/immunity.py | 105 +++++++++++++++++++-------------------- covasim/interventions.py | 1 + covasim/parameters.py | 6 +-- covasim/people.py | 4 +- covasim/sim.py | 11 ++-- 5 files changed, 60 insertions(+), 67 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index de41d2c9c..efce3d2af 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -163,7 +163,6 @@ class Vaccine(): def __init__(self, vaccine=None): - self.vaccine_immune_degree = None # dictionary of pre-loaded decay to by imm_axis and dose self.rel_imm = None # list of length total_strains with relative immunity factor self.doses = None self.interval = None @@ -231,7 +230,8 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_pars'] = [dict(form='normal', pars={'mean': 2, 'sd': 2}), dict(form='normal', pars={'mean': 8, 'sd': 2})] vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1 / 180, 'length': 250}, - form2='exp_decay', pars2={'rate': 1 / 100}) vaccine_pars['doses'] = 2 + form2='exp_decay', pars2={'rate': 1 / 100}) + vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 29 vaccine_pars['label'] = vaccine @@ -241,7 +241,8 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_pars'] = [dict(form='normal', pars={'mean': 2, 'sd': 2}), dict(form='normal', pars={'mean': 8, 'sd': 2})] vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1 / 180, 'length': 250}, - form2='exp_decay', pars2={'rate': 1 / 100}) vaccine_pars['doses'] = 2 + form2='exp_decay', pars2={'rate': 1 / 100}) + vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -342,12 +343,6 @@ def init_immunity(sim, create=False): circulating_strains[i]] elif sc.checktype(sim['immunity']['sus'], dict): - # TODO: make it possible to specify this as something like: - # imm = {'b117': {'wild': 0.4, 'p1': 0.3}, - # 'wild': {'b117': 0.6, 'p1': 0.7}, - # 'p1': {'wild': 0.9, 'b117': 0.65}} - # per Dan's suggestion, by using [s.strain_label for s in sim['strains']]. - # Would need lots of validation!! raise NotImplementedError else: errormsg = f'Type of immunity["sus"] not understood: you provided {type(sim["immunity"]["sus"])}, but it should be an array or dict.' @@ -397,19 +392,23 @@ def pre_compute_waning(length, form, pars): return output -def nab_to_efficacy(nab): +def nab_to_efficacy(nab, ax): + choices = ['sus', 'prog', 'trans'] + if ax not in choices: + errormsg = f'Choice provided not in list of choices' + raise ValueError(errormsg) + # put in here nab to efficacy mapping (logistic regression from fig 1a) efficacy = nab return efficacy -def compute_nab(people, inds, strain=None): - if strain is not None: +def compute_nab(people, inds, prior_inf=True): + if prior_inf: # NAbs is coming from a natural infection - this_strain_inds = cvu.itrue(people.recovered_strain[inds] == strain, inds) - mild_inds = people.check_inds(people.susceptible, people.date_symptomatic, filter_inds=this_strain_inds) - severe_inds = people.check_inds(people.susceptible, people.date_severe, filter_inds=this_strain_inds) - asymp_inds = np.setdiff1d(this_strain_inds, mild_inds) + mild_inds = people.check_inds(people.susceptible, people.date_symptomatic, filter_inds=inds) + severe_inds = people.check_inds(people.susceptible, people.date_severe, filter_inds=inds) + asymp_inds = np.setdiff1d(inds, mild_inds) asymp_inds = np.setdiff1d(asymp_inds, severe_inds) NAb_pars = people.pars['NAb_pars'] @@ -418,9 +417,14 @@ def compute_nab(people, inds, strain=None): init_NAb_asymp = cvu.sample(**NAb_pars['asymptomatic'], size=len(asymp_inds)) init_NAb_mild = cvu.sample(**NAb_pars['mild'], size=len(mild_inds)) init_NAb_severe = cvu.sample(**NAb_pars['severe'], size=len(severe_inds)) - init_NAbs = np.concatenate(init_NAb_asymp, init_NAb_mild, init_NAb_severe) + init_NAbs = np.concatenate((init_NAb_asymp, init_NAb_mild, init_NAb_severe)) + else: # NAbs coming from a vaccine + + # Does anyone have a prior infection (want to increase their init_nab level) + was_inf = cvu.true(people.t >= people.date_recovered[inds]) + # Figure out how many doses everyone has one_dose_inds = people.vaccinations[inds] == 1 two_dose_inds = people.vaccinations[inds] == 2 @@ -432,8 +436,7 @@ def compute_nab(people, inds, strain=None): init_NAb_two_dose = cvu.sample(**NAb_pars[1], size=len(two_dose_inds)) init_NAbs = np.concatenate(init_NAb_one_dose, init_NAb_two_dose) - - NAb_arrays = people.NAb[inds] + NAb_arrays = people.NAb[:,inds] day = people.t # timestep we are on n_days = people.pars['n_days'] @@ -441,17 +444,24 @@ def compute_nab(people, inds, strain=None): length = NAb_decay['pars1']['length'] if days_left > length: - t1 = np.arange(length, dtype=cvd.default_int) + M = np.ones((days_left, len(init_NAbs))) + t1 = np.arange(days_left, dtype=cvd.default_int)[:,np.newaxis] + t1 = t1 + M t1 = init_NAbs - (NAb_decay['pars1']['rate']*t1) - t2 = np.arange(length, n_days, dtype=cvd.default_int) + M = np.ones((length, len(init_NAbs))) + t2 = np.arange(length, n_days, dtype=cvd.default_int)[:,np.newaxis] + t2 = M + t2 t2 = init_NAbs - (NAb_decay['pars1']['rate']*length) - np.exp(-t2*NAb_decay['pars2']['rate']) result = np.concatenate((t1, t2), axis=0) else: - t1 = np.arange(days_left, dtype=cvd.default_int) + M = np.ones((days_left, len(init_NAbs))) + t1 = np.arange(days_left, dtype=cvd.default_int)[:,np.newaxis] + t1 = t1+M result = init_NAbs - (NAb_decay['pars1']['rate']*t1) NAb_arrays[day:, ] = result + people.NAb[:, inds] = NAb_arrays return @@ -469,52 +479,40 @@ def check_immunity(people, strain, sus=True, inds=None): was_inf = cvu.true(people.t >= people.date_recovered) # Had a previous exposure, now recovered is_vacc = cvu.true(people.vaccinated) # Vaccinated date_rec = people.date_recovered # Date recovered - immune_degree = people.pars['immune_degree'] - immunity = people.pars['immunity'] + immunity = people.pars['immunity'] # cross-immunity/own-immunity scalars to be applied to NAb level before computing efficacy # If vaccines are present, extract relevant information about them vacc_present = len(is_vacc) if vacc_present: - date_vacc = people.date_vaccinated vacc_info = people.pars['vaccine_info'] - vacc_degree = vacc_info['vaccine_immune_degree'] - doses_all = cvd.default_int(people.vaccinations) if sus: ### PART 1: # Immunity to infection for susceptible individuals is_sus = cvu.true(people.susceptible) # Currently susceptible - was_inf_same = cvu.true((people.recovered_strain == strain) & ( - people.t >= people.date_recovered)) # Had a previous exposure to the same strain, now recovered - was_inf_diff = np.setdiff1d(was_inf, - was_inf_same) # Had a previous exposure to a different strain, now recovered + was_inf_same = cvu.true((people.recovered_strain == strain) & (people.t >= date_rec)) # Had a previous exposure to the same strain, now recovered + was_inf_diff = np.setdiff1d(was_inf, was_inf_same) # Had a previous exposure to a different strain, now recovered is_sus_vacc = np.intersect1d(is_sus, is_vacc) # Susceptible and vaccinated - is_sus_was_inf_same = np.intersect1d(is_sus, - was_inf_same) # Susceptible and being challenged by the same strain - is_sus_was_inf_diff = np.intersect1d(is_sus, - was_inf_diff) # Susceptible and being challenged by a different strain - is_sus_vacc_was_inf = np.intersect1d(is_sus_vacc, was_inf) + is_sus_was_inf_same = np.intersect1d(is_sus, was_inf_same) # Susceptible and being challenged by the same strain + is_sus_was_inf_diff = np.intersect1d(is_sus, was_inf_diff) # Susceptible and being challenged by a different strain if len(is_sus_vacc): vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] - time_since_vacc = cvd.default_int(people.t - date_vacc[is_sus_vacc]) - current_NAbs = people.NAb[time_since_vacc, is_sus_vacc] - people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs) * vaccine_scale + current_NAbs = people.NAb[people.t, is_sus_vacc] + people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale) if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain - time_since_rec = cvd.default_int(people.t - date_rec[is_sus_was_inf_same]) - current_NAbs = people.NAb[time_since_rec, is_sus_was_inf_same] - people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs)*immunity[strain, strain] + current_NAbs = people.NAb[people.t, is_sus_was_inf_same] + people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity[strain, strain]) if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain prior_strains = people.recovered_strain[is_sus_was_inf_diff] prior_strains_unique = np.unique(prior_strains) for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] - time_since_rec = cvd.default_int(people.t - date_rec[unique_inds]) - current_NAbs = people.NAb[time_since_rec, unique_inds] - people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs) * immunity[strain, unique_strain] + current_NAbs = people.NAb[people.t, unique_inds] + people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity[strain, unique_strain]) else: ### PART 2: @@ -524,18 +522,15 @@ def check_immunity(people, strain, sus=True, inds=None): if len(is_inf_vacc): # Immunity for infected people who've been vaccinated vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) - doses = cvd.default_int(people.vaccinations[is_inf_vacc]) - time_since_vacc = cvd.default_int(people.t - date_vacc[is_inf_vacc]) - people.trans_imm[strain, is_inf_vacc] = vacc_degree['trans'][vaccine_source, doses - 1, time_since_vacc] - people.prog_imm[strain, is_inf_vacc] = vacc_degree['prog'][vaccine_source, doses - 1, time_since_vacc] + vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] + current_NAbs = people.NAb[people.t, is_inf_vacc] + people.trans_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale) + people.prog_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale) if len(was_inf): # Immunity for reinfected people - prior_symptoms = people.prior_symptoms[was_inf] - time_since_rec = cvd.default_int(people.t - date_rec[was_inf]) - people.trans_imm[strain, was_inf] = immune_degree['trans'][time_since_rec] * \ - prior_symptoms * immunity['trans'][strain] - people.prog_imm[strain, was_inf] = immune_degree['prog'][time_since_rec] * \ - prior_symptoms * immunity['prog'][strain] + current_NAbs = people.NAb[people.t, was_inf] + people.trans_imm[strain, was_inf] = nab_to_efficacy(current_NAbs) + people.prog_imm[strain, was_inf] = nab_to_efficacy(current_NAbs) return diff --git a/covasim/interventions.py b/covasim/interventions.py index a5966a86a..d45e16bf2 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1215,4 +1215,5 @@ def update_vaccine_info(self, sim, vacc_inds): # Update vaccine attributes in sim sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] sim.people.date_vaccinated[vacc_inds] = self.vaccination_dates[vacc_inds] + cvi.compute_nab(sim.people, vacc_inds, prior_inf=False) return \ No newline at end of file diff --git a/covasim/parameters.py b/covasim/parameters.py index ee53d173c..1affe74cb 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -72,9 +72,9 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['rel_beta'] = 1.0 pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 pars['NAb_pars'] = {} # Parameters for NAbs distribution for natural infection - pars['NAb_pars']['asymptomatic'] = dict(form='normal', pars={'mean': .5, 'sd': 2}) - pars['NAb_pars']['mild'] = dict(form='normal', pars={'mean': .8, 'sd': 2}) - pars['NAb_pars']['severe'] = dict(form='normal', pars={'mean': 1, 'sd': 2}) + pars['NAb_pars']['asymptomatic'] = dict(dist='lognormal', par1= .5, par2= 2) + pars['NAb_pars']['mild'] = dict(dist='lognormal', par1=.8, par2= 2) + pars['NAb_pars']['severe'] = dict(dist='lognormal', par1= 1, par2= 2) pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1/180, 'length': 250}, form2='exp_decay', pars2={'rate': 1/100}) diff --git a/covasim/people.py b/covasim/people.py index 530a748aa..5c0fc5112 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -266,8 +266,8 @@ def check_recovery(self): # Before letting them recover, store information about the strain they had and pre-compute NAbs array self.recovered_strain[inds] = self.infectious_strain[inds] - for strain in range(self.pars['n_strains']): - cvi.compute_nab(self, inds, strain) + if len(inds): + cvi.compute_nab(self, inds, prior_inf=True) # Now reset all disease states self.exposed[inds] = False diff --git a/covasim/sim.py b/covasim/sim.py index 08528cb79..deba2e4be 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -472,20 +472,17 @@ def init_vaccines(self): if len(self['vaccines']): nv = len(self['vaccines']) ns = self['total_strains'] - nd = 2 - days = self['n_days'] + self['vaccine_info'] = {} self['vaccine_info']['rel_imm'] = np.full((nv, ns), np.nan, dtype=cvd.default_float) - self['vaccine_info']['vaccine_immune_degree'] = {} - for ax in cvd.immunity_axes: - self['vaccine_info']['vaccine_immune_degree'][ax] =np.full((nv, nd, days), np.nan, dtype=cvd.default_float) + self['vaccine_info']['NAb_pars'] = [] for ind, vacc in enumerate(self['vaccines']): self['vaccine_info']['rel_imm'][ind,:] = vacc.rel_imm self['vaccine_info']['doses'] = vacc.doses + self['vaccine_info']['NAb_decay'] = vacc.NAb_decay for dose in range(vacc.doses): - for ax in cvd.immunity_axes: - self['vaccine_info']['vaccine_immune_degree'][ax][ind, dose, :] = vacc.vaccine_immune_degree[dose][ax] + self['vaccine_info']['NAb_pars'].append(vacc.NAb_pars[dose]) return def rescale(self): From 5253372e67b7181b5497c9a669f32ff76044be06 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 18 Mar 2021 16:37:03 -0400 Subject: [PATCH 188/569] cleaning up --- covasim/immunity.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index efce3d2af..159d3e90c 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -444,20 +444,14 @@ def compute_nab(people, inds, prior_inf=True): length = NAb_decay['pars1']['length'] if days_left > length: - M = np.ones((days_left, len(init_NAbs))) - t1 = np.arange(days_left, dtype=cvd.default_int)[:,np.newaxis] - t1 = t1 + M + t1 = np.arange(days_left, dtype=cvd.default_int)[:,np.newaxis] + np.ones((days_left, len(init_NAbs))) t1 = init_NAbs - (NAb_decay['pars1']['rate']*t1) - M = np.ones((length, len(init_NAbs))) - t2 = np.arange(length, n_days, dtype=cvd.default_int)[:,np.newaxis] - t2 = M + t2 + t2 = np.arange(length, n_days, dtype=cvd.default_int)[:,np.newaxis] + np.ones((length, len(init_NAbs))) t2 = init_NAbs - (NAb_decay['pars1']['rate']*length) - np.exp(-t2*NAb_decay['pars2']['rate']) result = np.concatenate((t1, t2), axis=0) else: - M = np.ones((days_left, len(init_NAbs))) - t1 = np.arange(days_left, dtype=cvd.default_int)[:,np.newaxis] - t1 = t1+M + t1 = np.arange(days_left, dtype=cvd.default_int)[:,np.newaxis] + np.ones((days_left, len(init_NAbs))) result = init_NAbs - (NAb_decay['pars1']['rate']*t1) NAb_arrays[day:, ] = result @@ -504,7 +498,7 @@ def check_immunity(people, strain, sus=True, inds=None): if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain current_NAbs = people.NAb[people.t, is_sus_was_inf_same] - people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity[strain, strain]) + people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, strain], 'sus') if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain prior_strains = people.recovered_strain[is_sus_was_inf_diff] @@ -512,7 +506,7 @@ def check_immunity(people, strain, sus=True, inds=None): for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] current_NAbs = people.NAb[people.t, unique_inds] - people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity[strain, unique_strain]) + people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus') else: ### PART 2: @@ -524,13 +518,13 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] current_NAbs = people.NAb[people.t, is_inf_vacc] - people.trans_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale) - people.prog_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale) + people.trans_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['trans'][strain], 'trans') + people.prog_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['prog'][strain], 'prog') if len(was_inf): # Immunity for reinfected people current_NAbs = people.NAb[people.t, was_inf] - people.trans_imm[strain, was_inf] = nab_to_efficacy(current_NAbs) - people.prog_imm[strain, was_inf] = nab_to_efficacy(current_NAbs) + people.trans_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['trans'][strain], 'trans') + people.prog_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['prog'][strain], 'prog') return From affe21c8aec0637f01832eb0809d6e9cf29d125c Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 18 Mar 2021 17:28:08 -0400 Subject: [PATCH 189/569] cleaning up --- covasim/immunity.py | 31 ++++++++++++++++--------------- covasim/people.py | 2 +- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 159d3e90c..53b5502eb 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -216,8 +216,8 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on pfizer if vaccine in choices['pfizer']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = [dict(form='normal', pars={'mean': 2, 'sd': 2}), - dict(form='normal', pars={'mean': 8, 'sd': 2})] + vaccine_pars['NAb_pars'] = [dict(dist='normal', par1=2, par2= 2), + dict(dist='normal', par1=8, par2= 2)] vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1/180, 'length': 250}, form2='exp_decay', pars2={'rate': 1/100}) vaccine_pars['doses'] = 2 @@ -227,8 +227,8 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on moderna elif vaccine in choices['moderna']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = [dict(form='normal', pars={'mean': 2, 'sd': 2}), - dict(form='normal', pars={'mean': 8, 'sd': 2})] + vaccine_pars['NAb_pars'] = [dict(dist='normal', par1=2, par2= 2), + dict(dist='normal', par1=8, par2= 2)] vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1 / 180, 'length': 250}, form2='exp_decay', pars2={'rate': 1 / 100}) vaccine_pars['doses'] = 2 @@ -238,8 +238,8 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on az elif vaccine in choices['az']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = [dict(form='normal', pars={'mean': 2, 'sd': 2}), - dict(form='normal', pars={'mean': 8, 'sd': 2})] + vaccine_pars['NAb_pars'] = [dict(dist='normal', par1=2, par2= 2), + dict(dist='normal', par1=8, par2= 2)] vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1 / 180, 'length': 250}, form2='exp_decay', pars2={'rate': 1 / 100}) vaccine_pars['doses'] = 2 @@ -249,8 +249,8 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on j&j elif vaccine in choices['j&j']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = [dict(form='normal', pars={'mean': 2, 'sd': 2}), - dict(form='normal', pars={'mean': 8, 'sd': 2})] + vaccine_pars['NAb_pars'] = [dict(dist='normal', par1=2, par2= 2), + dict(dist='normal', par1=8, par2= 2)] vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1 / 180, 'length': 250}, form2='exp_decay', pars2={'rate': 1 / 100}) vaccine_pars['doses'] = 1 @@ -408,6 +408,7 @@ def compute_nab(people, inds, prior_inf=True): # NAbs is coming from a natural infection mild_inds = people.check_inds(people.susceptible, people.date_symptomatic, filter_inds=inds) severe_inds = people.check_inds(people.susceptible, people.date_severe, filter_inds=inds) + mild_inds = np.setdiff1d(mild_inds, severe_inds) asymp_inds = np.setdiff1d(inds, mild_inds) asymp_inds = np.setdiff1d(asymp_inds, severe_inds) @@ -423,18 +424,18 @@ def compute_nab(people, inds, prior_inf=True): # NAbs coming from a vaccine # Does anyone have a prior infection (want to increase their init_nab level) - was_inf = cvu.true(people.t >= people.date_recovered[inds]) + was_inf = cvu.itrue(people.t >= people.date_recovered[inds], inds) # Figure out how many doses everyone has - one_dose_inds = people.vaccinations[inds] == 1 - two_dose_inds = people.vaccinations[inds] == 2 + one_dose_inds = cvu.itrue(people.vaccinations[inds] == 1, inds) + two_dose_inds = cvu.itrue(people.vaccinations[inds] == 2, inds) NAb_pars = people.pars['vaccine_info']['NAb_pars'] NAb_decay = people.pars['vaccine_info']['NAb_decay'] init_NAb_one_dose = cvu.sample(**NAb_pars[0], size=len(one_dose_inds)) init_NAb_two_dose = cvu.sample(**NAb_pars[1], size=len(two_dose_inds)) - init_NAbs = np.concatenate(init_NAb_one_dose, init_NAb_two_dose) + init_NAbs = np.concatenate((init_NAb_one_dose, init_NAb_two_dose)) NAb_arrays = people.NAb[:,inds] @@ -444,9 +445,9 @@ def compute_nab(people, inds, prior_inf=True): length = NAb_decay['pars1']['length'] if days_left > length: - t1 = np.arange(days_left, dtype=cvd.default_int)[:,np.newaxis] + np.ones((days_left, len(init_NAbs))) + t1 = np.arange(length, dtype=cvd.default_int)[:,np.newaxis] + np.ones((length, len(init_NAbs))) t1 = init_NAbs - (NAb_decay['pars1']['rate']*t1) - t2 = np.arange(length, n_days, dtype=cvd.default_int)[:,np.newaxis] + np.ones((length, len(init_NAbs))) + t2 = np.arange(days_left - length, dtype=cvd.default_int)[:,np.newaxis] + np.ones((days_left - length, len(init_NAbs))) t2 = init_NAbs - (NAb_decay['pars1']['rate']*length) - np.exp(-t2*NAb_decay['pars2']['rate']) result = np.concatenate((t1, t2), axis=0) @@ -502,7 +503,7 @@ def check_immunity(people, strain, sus=True, inds=None): if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain prior_strains = people.recovered_strain[is_sus_was_inf_diff] - prior_strains_unique = np.unique(prior_strains) + prior_strains_unique = cvd.default_int(np.unique(prior_strains)) for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] current_NAbs = people.NAb[people.t, unique_inds] diff --git a/covasim/people.py b/covasim/people.py index 5c0fc5112..33c3ce9fd 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -265,7 +265,7 @@ def check_recovery(self): inds = self.check_inds(self.susceptible, self.date_recovered, filter_inds=self.is_exp) # Before letting them recover, store information about the strain they had and pre-compute NAbs array - self.recovered_strain[inds] = self.infectious_strain[inds] + self.recovered_strain[inds] = self.exposed_strain[inds] if len(inds): cvi.compute_nab(self, inds, prior_inf=True) From 06525e698b2e1fcb684949fc6da73011ba3d0187 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 18 Mar 2021 21:07:18 -0400 Subject: [PATCH 190/569] cleaning up --- covasim/immunity.py | 32 ++++++++++++-------------------- covasim/sim.py | 1 - 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 53b5502eb..cb7c5b0e2 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -167,7 +167,6 @@ def __init__(self, vaccine=None): self.doses = None self.interval = None self.NAb_pars = None - self.NAb_decay = None self.vaccine_strain_info = self.init_strain_vaccine_info() self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine) for par, val in self.vaccine_pars.items(): @@ -216,10 +215,8 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on pfizer if vaccine in choices['pfizer']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = [dict(dist='normal', par1=2, par2= 2), - dict(dist='normal', par1=8, par2= 2)] - vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1/180, 'length': 250}, - form2='exp_decay', pars2={'rate': 1/100}) + vaccine_pars['NAb_pars'] = [dict(dist='lognormal', par1=2, par2= 2), + dict(dist='lognormal', par1=8, par2= 2)] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -227,10 +224,8 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on moderna elif vaccine in choices['moderna']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = [dict(dist='normal', par1=2, par2= 2), - dict(dist='normal', par1=8, par2= 2)] - vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1 / 180, 'length': 250}, - form2='exp_decay', pars2={'rate': 1 / 100}) + vaccine_pars['NAb_pars'] = [dict(dist='lognormal', par1=2, par2= 2), + dict(dist='lognormal', par1=8, par2= 2)] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 29 vaccine_pars['label'] = vaccine @@ -238,10 +233,8 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on az elif vaccine in choices['az']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = [dict(dist='normal', par1=2, par2= 2), - dict(dist='normal', par1=8, par2= 2)] - vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1 / 180, 'length': 250}, - form2='exp_decay', pars2={'rate': 1 / 100}) + vaccine_pars['NAb_pars'] = [dict(dist='lognormal', par1=2, par2= 2), + dict(dist='lognormal', par1=8, par2= 2)] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -249,10 +242,8 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on j&j elif vaccine in choices['j&j']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = [dict(dist='normal', par1=2, par2= 2), - dict(dist='normal', par1=8, par2= 2)] - vaccine_pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1 / 180, 'length': 250}, - form2='exp_decay', pars2={'rate': 1 / 100}) + vaccine_pars['NAb_pars'] = [dict(dist='lognormal', par1=2, par2= 2), + dict(dist='lognormal', par1=8, par2= 2)] vaccine_pars['doses'] = 1 vaccine_pars['interval'] = None vaccine_pars['label'] = vaccine @@ -279,7 +270,7 @@ def initialize(self, sim): for strain in range(ts-1): circulating_strains.append(sim['strains'][strain].strain_label) - if self.NAb_pars is None or self.NAb_decay is None: + if self.NAb_pars is None : errormsg = f'Did not provide parameters for this vaccine' raise ValueError(errormsg) @@ -404,6 +395,8 @@ def nab_to_efficacy(nab, ax): def compute_nab(people, inds, prior_inf=True): + NAb_decay = people.pars['NAb_decay'] + if prior_inf: # NAbs is coming from a natural infection mild_inds = people.check_inds(people.susceptible, people.date_symptomatic, filter_inds=inds) @@ -413,7 +406,7 @@ def compute_nab(people, inds, prior_inf=True): asymp_inds = np.setdiff1d(asymp_inds, severe_inds) NAb_pars = people.pars['NAb_pars'] - NAb_decay = people.pars['NAb_decay'] + init_NAb_asymp = cvu.sample(**NAb_pars['asymptomatic'], size=len(asymp_inds)) init_NAb_mild = cvu.sample(**NAb_pars['mild'], size=len(mild_inds)) @@ -431,7 +424,6 @@ def compute_nab(people, inds, prior_inf=True): two_dose_inds = cvu.itrue(people.vaccinations[inds] == 2, inds) NAb_pars = people.pars['vaccine_info']['NAb_pars'] - NAb_decay = people.pars['vaccine_info']['NAb_decay'] init_NAb_one_dose = cvu.sample(**NAb_pars[0], size=len(one_dose_inds)) init_NAb_two_dose = cvu.sample(**NAb_pars[1], size=len(two_dose_inds)) diff --git a/covasim/sim.py b/covasim/sim.py index deba2e4be..933b3face 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -480,7 +480,6 @@ def init_vaccines(self): for ind, vacc in enumerate(self['vaccines']): self['vaccine_info']['rel_imm'][ind,:] = vacc.rel_imm self['vaccine_info']['doses'] = vacc.doses - self['vaccine_info']['NAb_decay'] = vacc.NAb_decay for dose in range(vacc.doses): self['vaccine_info']['NAb_pars'].append(vacc.NAb_pars[dose]) return From 03adcc1e9d16598a7810e098bff775c4eeb1b9e7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 18 Mar 2021 22:04:48 -0700 Subject: [PATCH 191/569] implement new layer method --- covasim/base.py | 98 ++++++++++++++++++++++++++++----------------- tests/test_other.py | 60 +++++++++++++++++++-------- 2 files changed, 105 insertions(+), 53 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index dea9ffda2..2aa5194db 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1147,7 +1147,7 @@ def make_edgelist(self, contacts): # Turn into a dataframe for lkey in lkeys: - new_layer = Layer() + new_layer = Layer() # TKA for ckey,value in new_contacts[lkey].items(): new_layer[ckey] = np.array(value, dtype=new_layer.meta[ckey]) new_contacts[lkey] = new_layer @@ -1218,7 +1218,7 @@ class Contacts(FlexDict): def __init__(self, layer_keys=None): if layer_keys is not None: for key in layer_keys: - self[key] = Layer() + self[key] = Layer() # TKA return def __repr__(self): @@ -1275,7 +1275,41 @@ def pop_layer(self, *args): class Layer(FlexDict): - ''' A small class holding a single layer of contacts ''' + ''' + A small class holding a single layer of contact edges (connections) between people. + + The input is typically three arrays: person 1 of the connection, person 2 of + the connection, and the weight of the connection. Connections are undirected; + each person is both a source and sink. + + This class is usually not invoked directly by the user, but instead is called + as part of the population creation. + + Args: + p1 (array): an array of N connections, representing people on one side of the connection + p2 (array): an array of people on the other side of the connection + beta (array): an array of weights for each connection + kwargs (dict): other keys copied directly into the layer + + Note that all arguments must be arrays of the same length, although not all + have to be supplied at the time of creation (they must all be the same at the + time of initialization, though, or else validation will fail). + + **Examples**:: + + # Generate an average of 10 contacts for 1000 people + n = 10_000 + n_people = 1000 + p1 = np.random.randint(n_people, size=n) + p2 = np.random.randint(n_people, size=n) + beta = np.ones(n) + layer = cv.Layer(p1=p1, p2=p2, beta=beta) + + # Convert one layer to another with an extra column + index = np.arange(n) + self_conn = p1 == p2 + layer2 = cv.Layer(**layer, index=index, self_conn=self_conn) + ''' def __init__(self, **kwargs): self.meta = { @@ -1291,7 +1325,7 @@ def __init__(self, **kwargs): # Set data, if provided for key,value in kwargs.items(): - self[key] = np.array(value, dtype=self.meta[key]) + self[key] = np.array(value, dtype=self.meta.get(key)) return @@ -1305,8 +1339,9 @@ def __len__(self): def __repr__(self): ''' Convert to a dataframe for printing ''' + label = self.__class__.__name__ keys_str = ', '.join(self.keys()) - output = f'Layer({keys_str})\n' + output = f'{label}({keys_str})\n' # e.g. Layer(p1, p2, beta) output += self.to_df().__repr__() return output @@ -1439,40 +1474,31 @@ def find_contacts(self, inds, as_array=True): return contact_inds - def update(self, people): - '''Regenerate contacts - - This method gets called if the layer appears in ``sim.pars['dynam_lkeys']``. The Layer implements - the update procedure so that derived classes can customize the update e.g. implementing - over-dispersion/other distributions, random clusters, etc. - - This method also takes in the ``people`` object so that the update can depend on person attributes - that may change over time (e.g. changing contacts for people that are severe/critical). + def update(self, people, frac=1.0): ''' - pass + Regenerate contacts on each timestep. + This method gets called if the layer appears in ``sim.pars['dynam_lkeys']``. + The Layer implements the update procedure so that derived classes can customize + the update e.g. implementing over-dispersion/other distributions, random + clusters, etc. -def RandomLayer(Layer): - ''' - Randomly sampled layer + Typically, this method also takes in the ``people`` object so that the + update can depend on person attributes that may change over time (e.g. + changing contacts for people that are severe/critical). - Args: - Layer: - - Returns: - - ''' - def __init__(self, n_contacts): - self.n_contacts = n_contacts - - def update(self, people): + Args: + frac (float): the fraction of contacts to update on each timestep + ''' # Choose how many contacts to make - pop_size = len(people) - n_new = int(self.n_contacts*pop_size/2) # Since these get looped over in both directions later - - # Create the contacts - new_contacts = {} # Initialize - self['p1'] = np.array(cvu.choose_r(max_n=pop_size, n=n_new), dtype=cvd.default_int) # Choose with replacement - self['p2'] = np.array(cvu.choose_r(max_n=pop_size, n=n_new), dtype=cvd.default_int) - self['beta'] = np.ones(n_new, dtype=cvd.default_float) + pop_size = len(people) # Total number of people + n_contacts = len(self) # Total number of contacts + n_new = int(np.round(n_contacts*frac)) # Since these get looped over in both directions later + inds = cvu.choose(n_contacts, n_new) + + # Create the contacts, not skipping self-connections + self['p1'][inds] = np.array(cvu.choose_r(max_n=pop_size, n=n_new), dtype=cvd.default_int) # Choose with replacement + self['p2'][inds] = np.array(cvu.choose_r(max_n=pop_size, n=n_new), dtype=cvd.default_int) + self['beta'][inds] = np.ones(n_new, dtype=cvd.default_float) + return diff --git a/tests/test_other.py b/tests/test_other.py index 5bebf6126..dd4e6a30d 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -6,6 +6,7 @@ #%% Imports and settings import os import pytest +import numpy as np import sciris as sc import covasim as cv @@ -29,7 +30,7 @@ def remove_files(*args): #%% Define the tests def test_base(): - sc.heading('Testing base.py...') + sc.heading('Testing base.py sim...') json_path = 'base_tests.json' sim_path = 'base_tests.sim' @@ -67,6 +68,19 @@ def test_base(): sim.save(filename=sim_path, keep_people=keep_people) cv.Sim.load(sim_path) + # Tidy up + remove_files(json_path, sim_path) + + return + + +def test_basepeople(): + sc.heading('Testing base.py people and contacts...') + + # Create a small sim for later use + sim = cv.Sim(pop_size=100, verbose=verbose) + sim.initialize() + # BasePeople methods ppl = sim.people ppl.get(['susceptible', 'infectious']) @@ -104,8 +118,33 @@ def test_base(): df = hospitals_layer.to_df() hospitals_layer.from_df(df) - # Tidy up - remove_files(json_path, sim_path) + # Generate an average of 10 contacts for 1000 people + n = 10_000 + n_people = 1000 + p1 = np.random.randint(n_people, size=n) + p2 = np.random.randint(n_people, size=n) + beta = np.ones(n) + layer = cv.Layer(p1=p1, p2=p2, beta=beta) + + # Convert one layer to another with an extra column + index = np.arange(n) + self_conn = p1 == p2 + layer2 = cv.Layer(**layer, index=index, self_conn=self_conn) + assert len(layer2) == n + assert len(layer2.keys()) == 5 + + # Test dynamic layers, plotting, and stories + pars = dict(pop_size=1000, n_days=50, verbose=verbose, pop_type='hybrid') + s1 = cv.Sim(pars, dynam_layer={'c':1}) + s1.run() + s1.people.plot() + for person in [25, 79]: + sim.people.story(person) + + # Run without dynamic layers and assert that the results are different + s2 = cv.Sim(pars, dynam_layer={'c':0}) + s2.run() + assert cv.diff_sims(s1, s2, output=True) return @@ -198,19 +237,6 @@ def test_misc(): return -def test_people(): - sc.heading('Testing people') - - # Test dynamic layers - sim = cv.Sim(pop_size=100, n_days=10, verbose=verbose, dynam_layer={'a':1}) - sim.run() - sim.people.plot() - for person in [25, 79]: - sim.people.story(person) - - return - - def test_plotting(): sc.heading('Testing plotting') @@ -427,8 +453,8 @@ def test_settings(): T = sc.tic() test_base() + test_basepeople() test_misc() - test_people() test_plotting() test_population() test_requirements() From efb1f54d25ce129d13a76e5c353a45fe2d5876e4 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 18 Mar 2021 22:11:34 -0700 Subject: [PATCH 192/569] update comment --- tests/test_other.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_other.py b/tests/test_other.py index dd4e6a30d..b43f21fca 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -126,7 +126,7 @@ def test_basepeople(): beta = np.ones(n) layer = cv.Layer(p1=p1, p2=p2, beta=beta) - # Convert one layer to another with an extra column + # Convert one layer to another with extra columns index = np.arange(n) self_conn = p1 == p2 layer2 = cv.Layer(**layer, index=index, self_conn=self_conn) From 420bcfe59792ab6fc9a5d295355dcd5acf983da9 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 19 Mar 2021 00:25:35 -0700 Subject: [PATCH 193/569] update changelog --- CHANGELOG.rst | 16 ++++++++++++++-- covasim/analysis.py | 10 +++++++--- covasim/base.py | 4 ++-- covasim/interventions.py | 8 ++++---- covasim/version.py | 2 +- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index db6d7e140..cfea6860d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,10 +15,12 @@ Future release plans These are the major improvements we are currently working on. If there is a specific bugfix or feature you would like to see, please `create an issue `__. -- Improved handling of vaccination, including more detailed targeting options, waning immunity, etc. -- Mechanistic handling of different strains - Additional flexibility in plotting options (e.g. date ranges, per-plot DPI) - Expanded tutorials (health care workers, vaccination, calibration, exercises, etc.) +- Improved handling of vaccination, including more detailed targeting options, waning immunity, etc. +- Mechanistic handling of different strains +- Multi-region and geospatial support +- Economics and costing analysis ~~~~~~~~~~~~~~~~~~~~~~~ @@ -26,6 +28,16 @@ Latest versions (2.0.x) ~~~~~~~~~~~~~~~~~~~~~~~ +Version 2.0.4 (2021-03-19) +-------------------------- +- Added a new analyzer, ``cv.daily_age_stats()``, which will compute statistics by age for each day of the simulation (compared to ``cv.age_histogram()``, which only looks at particular points in time). +- Added a new function, ``cv.date_formatter()``, which may be useful in quickly formatting axes using dates. +- Removed the need for ``self._store_args()`` in interventions; now custom interventions only need to implement ``super().__init__(**kwargs)`` rather than both. +- Changed how custom interventions print out by default (a short representation rather than the jsonified version used by built-in interventions). +- Added an ``update()`` method to ``Layer``, to allow greater flexibility for dynamic updating. +- *GitHub info*: PR `854 `__ + + Version 2.0.3 (2021-03-11) -------------------------- - Previously, the way a sim was printed (e.g. ``print(sim)``) depended on what the global ``verbose`` parameter was set to (e.g. ``cv.options.set(verbose=0.1)``), which used ``sim.brief()`` if verbosity was 0, or ``sim.disp()`` otherwise. This has been changed to always use the ``sim.brief()`` representation regardless of verbosity. To restore the previous behavior, use ``sim.disp()`` instead of ``print(sim)``. diff --git a/covasim/analysis.py b/covasim/analysis.py index eea7557c4..cfbd1ad18 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -353,7 +353,8 @@ def plot(self, windows=False, width=0.8, color='#F8A493', fig_args=None, axis_ar class daily_age_stats(Analyzer): ''' - Calculate daily counts by age, saving for each day of the simulation. + Calculate daily counts by age, saving for each day of the simulation. Can + plot either time series by age or a histogram over all time. Args: states (list): which states of people to record (default: ['diagnoses', 'deaths', 'tests', 'severe']) @@ -362,9 +363,12 @@ class daily_age_stats(Analyzer): **Examples**:: - sim = cv.Sim(analyzers=cv.daily_age_analyzer()) + sim = cv.Sim(analyzers=cv.daily_age_stats()) + sim = cv.Sim(pars, analyzers=daily_age) sim.run() - agehist = sim['analyzers'][0].make_df() + daily_age = sim.get_analyzer() + daily_age.plot() + daily_age.plot(total=True) ''' diff --git a/covasim/base.py b/covasim/base.py index 2aa5194db..26bd80ab0 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1147,7 +1147,7 @@ def make_edgelist(self, contacts): # Turn into a dataframe for lkey in lkeys: - new_layer = Layer() # TKA + new_layer = Layer() for ckey,value in new_contacts[lkey].items(): new_layer[ckey] = np.array(value, dtype=new_layer.meta[ckey]) new_contacts[lkey] = new_layer @@ -1218,7 +1218,7 @@ class Contacts(FlexDict): def __init__(self, layer_keys=None): if layer_keys is not None: for key in layer_keys: - self[key] = Layer() # TKA + self[key] = Layer() return def __repr__(self): diff --git a/covasim/interventions.py b/covasim/interventions.py index ed81059d5..b95cf7c6d 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -101,10 +101,10 @@ def __init__(self, label=None, show_label=True, do_plot=None, line_args=None): return - def __repr__(self): - ''' Return a JSON-friendly output if possible, else revert to pretty repr ''' + def __repr__(self, jsonify=False): + ''' Return a JSON-friendly output if possible, else revert to short repr ''' - if self.input_args: + if self.__class__.__name__ in __all__ or jsonify: try: json = self.to_json() which = json['which'] @@ -112,7 +112,7 @@ def __repr__(self): parstr = ', '.join([f'{k}={v}' for k,v in pars.items()]) output = f"cv.{which}({parstr})" except Exception as E: - output = type(self) + f' ({str(E)})' # If that fails, print why + output = type(self) + f' (error: {str(E)})' # If that fails, print why return output else: return f'{self.__module__}.{self.__class__.__name__}()' diff --git a/covasim/version.py b/covasim/version.py index 66abeedba..9d903d764 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -5,5 +5,5 @@ __all__ = ['__version__', '__versiondate__', '__license__'] __version__ = '2.0.4' -__versiondate__ = '2021-03-18' +__versiondate__ = '2021-03-19' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From 0dc31be709a85143bf9b60e95650223893ffc289 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 19 Mar 2021 01:47:09 -0700 Subject: [PATCH 194/569] update custom intervention syntax --- docs/tutorials/t5.ipynb | 1 - examples/t5_custom_intervention.py | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/tutorials/t5.ipynb b/docs/tutorials/t5.ipynb index 36dabd52d..f10a59dcb 100644 --- a/docs/tutorials/t5.ipynb +++ b/docs/tutorials/t5.ipynb @@ -355,7 +355,6 @@ "\n", " def __init__(self, start_day=None, end_day=None, age_cutoff=70, rel_sus=0.0, *args, **kwargs):\n", " super().__init__(**kwargs) # This line must be included\n", - " self._store_args() # So must this one\n", " self.start_day = start_day\n", " self.end_day = end_day\n", " self.age_cutoff = age_cutoff\n", diff --git a/examples/t5_custom_intervention.py b/examples/t5_custom_intervention.py index 93f47e9c1..15fc8de14 100644 --- a/examples/t5_custom_intervention.py +++ b/examples/t5_custom_intervention.py @@ -10,7 +10,6 @@ class protect_elderly(cv.Intervention): def __init__(self, start_day=None, end_day=None, age_cutoff=70, rel_sus=0.0, *args, **kwargs): super().__init__(**kwargs) # This line must be included - self._store_args() # So must this one self.start_day = start_day self.end_day = end_day self.age_cutoff = age_cutoff From 34d9fdc4560113b4ac17616298b6df78befa90d8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 21 Mar 2021 23:50:38 -0700 Subject: [PATCH 195/569] run returns self --- covasim/run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index 07a649bc3..4ae9f9751 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -172,7 +172,7 @@ def run(self, reduce=False, combine=False, **kwargs): elif combine: self.combine() - return + return self def shrink(self, **kwargs): @@ -972,7 +972,7 @@ def print_heading(string): # Save details about the run self._kept_people = keep_people - return + return self def compare(self, t=None, output=False): From 54d2f4f4398df3fcd16d39060bb2fdfb708582d3 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 00:02:04 -0700 Subject: [PATCH 196/569] add extra export methods --- covasim/base.py | 4 ++++ covasim/run.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/covasim/base.py b/covasim/base.py index 26bd80ab0..e14decf2d 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -428,6 +428,10 @@ def export_results(self, for_json=True, filename=None, indent=2, *args, **kwargs for key,res in self.results.items(): if isinstance(res, Result): resdict[key] = res.values + if res.low is not None: + resdict[key+'_low'] = res.low + if res.high is not None: + resdict[key+'_high'] = res.high elif for_json: if key == 'date': resdict[key] = [str(d) for d in res] # Convert dates to strings diff --git a/covasim/run.py b/covasim/run.py index 4ae9f9751..eb2c75134 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -809,6 +809,16 @@ def brief(self, output=False): return string + def to_json(self, *args, **kwargs): + ''' Shortcut for base_sim.to_json() ''' + return self.base_sim.to_json(*args, **kwargs) + + + def to_excel(self, *args, **kwargs): + ''' Shortcut for base_sim.to_excel() ''' + return self.base_sim.to_excel(*args, **kwargs) + + class Scenarios(cvb.ParsObj): ''' Class for running multiple sets of multiple simulations -- e.g., scenarios. From 8d21bcedb3d0d7c066daff6aad744fd399ec87a1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 01:03:46 -0700 Subject: [PATCH 197/569] update settings and styling --- covasim/settings.py | 6 +++--- docs/Makefile | 2 +- docs/_static/theme_overrides.css | 2 ++ docs/build_docs | 1 + docs/tutorials/clean_outputs | 2 +- docs/tutorials/t5.ipynb | 25 +++++++++++++++++++------ 6 files changed, 27 insertions(+), 11 deletions(-) diff --git a/covasim/settings.py b/covasim/settings.py index 3692bd1a5..dd1d571d3 100644 --- a/covasim/settings.py +++ b/covasim/settings.py @@ -188,9 +188,9 @@ def set_matplotlib_global(key, value): ''' Set a global option for Matplotlib -- not for users ''' import pylab as pl if value: # Don't try to reset any of these to a None value - if key == 'font_size': pl.rc('font', size=value) - elif key == 'font_family': pl.rc('font', family=value) - elif key == 'dpi': pl.rc('figure', dpi=value) + if key == 'font_size': pl.rcParams['font.size'] = value + elif key == 'font_family': pl.rcParams['font.family'] = value + elif key == 'dpi': pl.rcParams['figure.dpi'] = value elif key == 'backend': pl.switch_backend(value) else: raise sc.KeyNotFoundError(f'Key {key} not found') return diff --git a/docs/Makefile b/docs/Makefile index 464413fbd..c38d8ebf5 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -10,7 +10,7 @@ BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -jauto -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +ALLSPHINXOPTS = -v -jauto -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css index 4f237be19..062cfb21e 100644 --- a/docs/_static/theme_overrides.css +++ b/docs/_static/theme_overrides.css @@ -51,3 +51,5 @@ div.document span.search-highlight { tr.row-even { background-color: #def; } + +.highlight { background: #D9F0FF; } \ No newline at end of file diff --git a/docs/build_docs b/docs/build_docs index b38c4e5f5..a56dd87bf 100755 --- a/docs/build_docs +++ b/docs/build_docs @@ -16,6 +16,7 @@ duration=$(( SECONDS - start )) echo 'Cleaning up tutorial files...' cd tutorials ./clean_outputs +cd .. echo "Docs built after $duration seconds." echo "Index:" diff --git a/docs/tutorials/clean_outputs b/docs/tutorials/clean_outputs index 66406f59a..60492a75a 100755 --- a/docs/tutorials/clean_outputs +++ b/docs/tutorials/clean_outputs @@ -1,7 +1,7 @@ #!/bin/bash # Remove auto-generated files; use -f in case they don't exist echo 'Deleting:' -echo `ls -1 ./my-*.*` +echo `ls -1 ./my-*.* 2> /dev/null` echo '...in 2 seconds' sleep 2 rm -vf ./my-*.* \ No newline at end of file diff --git a/docs/tutorials/t5.ipynb b/docs/tutorials/t5.ipynb index f10a59dcb..48691454d 100644 --- a/docs/tutorials/t5.ipynb +++ b/docs/tutorials/t5.ipynb @@ -341,6 +341,19 @@ "However, function-based interventions only take you so far. We saw in Tutorial 1 how you could define a simple \"protect the elderly\" intervention with just a few lines of code. This example explains how to create an intervention object that does much the same thing, but is more fully-featured because it uses the `Intervention` class." ] }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "
\n", + "\n", + "You must include the line `super().__init__(**kwargs)` in the `self.__init__()` method, or else the intervention won't work. You must also include `self.initialized = True` in the `self.initialize()` method.\n", + "\n", + "
" + ] + }, { "cell_type": "code", "execution_count": null, @@ -354,7 +367,7 @@ "class protect_elderly(cv.Intervention):\n", "\n", " def __init__(self, start_day=None, end_day=None, age_cutoff=70, rel_sus=0.0, *args, **kwargs):\n", - " super().__init__(**kwargs) # This line must be included\n", + " super().__init__(**kwargs) # NB: This line must be included\n", " self.start_day = start_day\n", " self.end_day = end_day\n", " self.age_cutoff = age_cutoff\n", @@ -368,7 +381,7 @@ " self.elderly = sim.people.age > self.age_cutoff # Find the elderly people here\n", " self.exposed = np.zeros(sim.npts) # Initialize results\n", " self.tvec = sim.tvec # Copy the time vector into this intervention\n", - " self.initialized = True\n", + " self.initialized = True # NB: This line must also be included\n", " return\n", "\n", " def apply(self, sim):\n", @@ -399,10 +412,10 @@ "source": [ "While this example is fairly long, hopefully it's fairly straightforward:\n", "\n", - "- **__init__()** just does what init always does; it's needed to create the class instance. For interventions, it's usually used to store keyword arguments and perform some basic initialization (the first two lines).\n", - "- **initialize()** is similar to init, with the difference that it's invoked when the *sim* itself is initialized. It receives the sim as an input argument. This means you can use it to do a fairly powerful things. Here, since `sim.people` already exists, we can calculate up-front who the elderly are so we don't have to do it on every timestep.\n", - "- **apply()** is the crux of the intervention. It's run on every timestep of the model, and also receives `sim` as an input. You almost always use `sim.t` to get the current timestep, here to turn the intervention on and off. But as this example shows, its real power is that it can make direct modifications to the sim itself (`sim.people.rel_sus[self.elderly] = self.rel_sus`). It can also perform calculations and store data in itself, as shown here with `self.exposed` (although in general, analyzers are better for this, since they happen at the end of the timestep, while interventions happen in the middle).\n", - "- **plot()** is a custom method that shows a plot of the data gathered during the sim. Again, it's usually better to use analyzers for this, but for something simple like this it's fine to double-dip and use an intervention.\n", + "- `__init__()` just does what init always does; it's needed to create the class instance. For interventions, it's usually used to store keyword arguments and perform some basic initialization (the first two lines).\n", + "- `initialize()` is similar to init, with the difference that it's invoked when the *sim* itself is initialized. It receives the sim as an input argument. This means you can use it to do a fairly powerful things. Here, since `sim.people` already exists, we can calculate up-front who the elderly are so we don't have to do it on every timestep.\n", + "- `apply()` is the crux of the intervention. It's run on every timestep of the model, and also receives `sim` as an input. You almost always use `sim.t` to get the current timestep, here to turn the intervention on and off. But as this example shows, its real power is that it can make direct modifications to the sim itself (`sim.people.rel_sus[self.elderly] = self.rel_sus`). It can also perform calculations and store data in itself, as shown here with `self.exposed` (although in general, analyzers are better for this, since they happen at the end of the timestep, while interventions happen in the middle).\n", + "- `plot()` is a custom method that shows a plot of the data gathered during the sim. Again, it's usually better to use analyzers for this, but for something simple like this it's fine to double-dip and use an intervention.\n", "\n", "Here is what this custom intervention looks like in action. Note how it automatically shows when the intervention starts and stops (with vertical dashed lines)." ] From 68056e2a431d4e9e352c74c642efb5c31134ed14 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 01:13:02 -0700 Subject: [PATCH 198/569] rename files --- docs/tutorials/{t1.ipynb => t01.ipynb} | 0 docs/tutorials/{t2.ipynb => t02.ipynb} | 0 docs/tutorials/{t3.ipynb => t03.ipynb} | 0 docs/tutorials/{t4.ipynb => t04.ipynb} | 0 docs/tutorials/{t5.ipynb => t05.ipynb} | 0 docs/tutorials/{t6.ipynb => t06.ipynb} | 0 docs/tutorials/{t7.ipynb => t07.ipynb} | 0 docs/tutorials/{t8.ipynb => t08.ipynb} | 0 docs/tutorials/{t9.ipynb => t09.ipynb} | 0 .../{t1_full_usage_example.py => t01_full_usage_example.py} | 0 examples/{t1_hello_world.py => t01_hello_world.py} | 0 examples/{t1_populations.py => t01_populations.py} | 0 examples/{t2_save_load_sim.py => t02_save_load_sim.py} | 0 examples/{t3_nested_multisim.py => t03_nested_multisim.py} | 0 examples/{t3_scenarios.py => t03_scenarios.py} | 0 examples/{t4_loading_data.py => t04_loading_data.py} | 0 examples/{t5_change_beta.py => t05_change_beta.py} | 0 examples/{t5_contact_tracing.py => t05_contact_tracing.py} | 0 .../{t5_custom_intervention.py => t05_custom_intervention.py} | 0 examples/{t5_dynamic_pars.py => t05_dynamic_pars.py} | 0 examples/{t5_testing.py => t05_testing.py} | 0 .../{t5_vaccine_subtargeting.py => t05_vaccine_subtargeting.py} | 0 examples/{t6_seir_analyzer.py => t06_seir_analyzer.py} | 0 examples/{t6_simple_analyzers.py => t06_simple_analyzers.py} | 0 .../{t7_optuna_calibration.py => t07_optuna_calibration.py} | 0 examples/{t8_versioning.py => t08_versioning.py} | 0 examples/{t9_custom_layers.py => t09_custom_layers.py} | 0 examples/{t9_numba.py => t09_numba.py} | 0 ...t9_population_properties.py => t09_population_properties.py} | 0 examples/test_tutorials.py | 2 +- 30 files changed, 1 insertion(+), 1 deletion(-) rename docs/tutorials/{t1.ipynb => t01.ipynb} (100%) rename docs/tutorials/{t2.ipynb => t02.ipynb} (100%) rename docs/tutorials/{t3.ipynb => t03.ipynb} (100%) rename docs/tutorials/{t4.ipynb => t04.ipynb} (100%) rename docs/tutorials/{t5.ipynb => t05.ipynb} (100%) rename docs/tutorials/{t6.ipynb => t06.ipynb} (100%) rename docs/tutorials/{t7.ipynb => t07.ipynb} (100%) rename docs/tutorials/{t8.ipynb => t08.ipynb} (100%) rename docs/tutorials/{t9.ipynb => t09.ipynb} (100%) rename examples/{t1_full_usage_example.py => t01_full_usage_example.py} (100%) rename examples/{t1_hello_world.py => t01_hello_world.py} (100%) rename examples/{t1_populations.py => t01_populations.py} (100%) rename examples/{t2_save_load_sim.py => t02_save_load_sim.py} (100%) rename examples/{t3_nested_multisim.py => t03_nested_multisim.py} (100%) rename examples/{t3_scenarios.py => t03_scenarios.py} (100%) rename examples/{t4_loading_data.py => t04_loading_data.py} (100%) rename examples/{t5_change_beta.py => t05_change_beta.py} (100%) rename examples/{t5_contact_tracing.py => t05_contact_tracing.py} (100%) rename examples/{t5_custom_intervention.py => t05_custom_intervention.py} (100%) rename examples/{t5_dynamic_pars.py => t05_dynamic_pars.py} (100%) rename examples/{t5_testing.py => t05_testing.py} (100%) rename examples/{t5_vaccine_subtargeting.py => t05_vaccine_subtargeting.py} (100%) rename examples/{t6_seir_analyzer.py => t06_seir_analyzer.py} (100%) rename examples/{t6_simple_analyzers.py => t06_simple_analyzers.py} (100%) rename examples/{t7_optuna_calibration.py => t07_optuna_calibration.py} (100%) rename examples/{t8_versioning.py => t08_versioning.py} (100%) rename examples/{t9_custom_layers.py => t09_custom_layers.py} (100%) rename examples/{t9_numba.py => t09_numba.py} (100%) rename examples/{t9_population_properties.py => t09_population_properties.py} (100%) diff --git a/docs/tutorials/t1.ipynb b/docs/tutorials/t01.ipynb similarity index 100% rename from docs/tutorials/t1.ipynb rename to docs/tutorials/t01.ipynb diff --git a/docs/tutorials/t2.ipynb b/docs/tutorials/t02.ipynb similarity index 100% rename from docs/tutorials/t2.ipynb rename to docs/tutorials/t02.ipynb diff --git a/docs/tutorials/t3.ipynb b/docs/tutorials/t03.ipynb similarity index 100% rename from docs/tutorials/t3.ipynb rename to docs/tutorials/t03.ipynb diff --git a/docs/tutorials/t4.ipynb b/docs/tutorials/t04.ipynb similarity index 100% rename from docs/tutorials/t4.ipynb rename to docs/tutorials/t04.ipynb diff --git a/docs/tutorials/t5.ipynb b/docs/tutorials/t05.ipynb similarity index 100% rename from docs/tutorials/t5.ipynb rename to docs/tutorials/t05.ipynb diff --git a/docs/tutorials/t6.ipynb b/docs/tutorials/t06.ipynb similarity index 100% rename from docs/tutorials/t6.ipynb rename to docs/tutorials/t06.ipynb diff --git a/docs/tutorials/t7.ipynb b/docs/tutorials/t07.ipynb similarity index 100% rename from docs/tutorials/t7.ipynb rename to docs/tutorials/t07.ipynb diff --git a/docs/tutorials/t8.ipynb b/docs/tutorials/t08.ipynb similarity index 100% rename from docs/tutorials/t8.ipynb rename to docs/tutorials/t08.ipynb diff --git a/docs/tutorials/t9.ipynb b/docs/tutorials/t09.ipynb similarity index 100% rename from docs/tutorials/t9.ipynb rename to docs/tutorials/t09.ipynb diff --git a/examples/t1_full_usage_example.py b/examples/t01_full_usage_example.py similarity index 100% rename from examples/t1_full_usage_example.py rename to examples/t01_full_usage_example.py diff --git a/examples/t1_hello_world.py b/examples/t01_hello_world.py similarity index 100% rename from examples/t1_hello_world.py rename to examples/t01_hello_world.py diff --git a/examples/t1_populations.py b/examples/t01_populations.py similarity index 100% rename from examples/t1_populations.py rename to examples/t01_populations.py diff --git a/examples/t2_save_load_sim.py b/examples/t02_save_load_sim.py similarity index 100% rename from examples/t2_save_load_sim.py rename to examples/t02_save_load_sim.py diff --git a/examples/t3_nested_multisim.py b/examples/t03_nested_multisim.py similarity index 100% rename from examples/t3_nested_multisim.py rename to examples/t03_nested_multisim.py diff --git a/examples/t3_scenarios.py b/examples/t03_scenarios.py similarity index 100% rename from examples/t3_scenarios.py rename to examples/t03_scenarios.py diff --git a/examples/t4_loading_data.py b/examples/t04_loading_data.py similarity index 100% rename from examples/t4_loading_data.py rename to examples/t04_loading_data.py diff --git a/examples/t5_change_beta.py b/examples/t05_change_beta.py similarity index 100% rename from examples/t5_change_beta.py rename to examples/t05_change_beta.py diff --git a/examples/t5_contact_tracing.py b/examples/t05_contact_tracing.py similarity index 100% rename from examples/t5_contact_tracing.py rename to examples/t05_contact_tracing.py diff --git a/examples/t5_custom_intervention.py b/examples/t05_custom_intervention.py similarity index 100% rename from examples/t5_custom_intervention.py rename to examples/t05_custom_intervention.py diff --git a/examples/t5_dynamic_pars.py b/examples/t05_dynamic_pars.py similarity index 100% rename from examples/t5_dynamic_pars.py rename to examples/t05_dynamic_pars.py diff --git a/examples/t5_testing.py b/examples/t05_testing.py similarity index 100% rename from examples/t5_testing.py rename to examples/t05_testing.py diff --git a/examples/t5_vaccine_subtargeting.py b/examples/t05_vaccine_subtargeting.py similarity index 100% rename from examples/t5_vaccine_subtargeting.py rename to examples/t05_vaccine_subtargeting.py diff --git a/examples/t6_seir_analyzer.py b/examples/t06_seir_analyzer.py similarity index 100% rename from examples/t6_seir_analyzer.py rename to examples/t06_seir_analyzer.py diff --git a/examples/t6_simple_analyzers.py b/examples/t06_simple_analyzers.py similarity index 100% rename from examples/t6_simple_analyzers.py rename to examples/t06_simple_analyzers.py diff --git a/examples/t7_optuna_calibration.py b/examples/t07_optuna_calibration.py similarity index 100% rename from examples/t7_optuna_calibration.py rename to examples/t07_optuna_calibration.py diff --git a/examples/t8_versioning.py b/examples/t08_versioning.py similarity index 100% rename from examples/t8_versioning.py rename to examples/t08_versioning.py diff --git a/examples/t9_custom_layers.py b/examples/t09_custom_layers.py similarity index 100% rename from examples/t9_custom_layers.py rename to examples/t09_custom_layers.py diff --git a/examples/t9_numba.py b/examples/t09_numba.py similarity index 100% rename from examples/t9_numba.py rename to examples/t09_numba.py diff --git a/examples/t9_population_properties.py b/examples/t09_population_properties.py similarity index 100% rename from examples/t9_population_properties.py rename to examples/t09_population_properties.py diff --git a/examples/test_tutorials.py b/examples/test_tutorials.py index 6cab5f7cf..250afdf53 100755 --- a/examples/test_tutorials.py +++ b/examples/test_tutorials.py @@ -13,7 +13,7 @@ def test_all_tutorials(): # Get and run tests filenames = sc.getfilelist(tex.examples_dir, pattern='t*.py', nopath=True) for filename in filenames: - if filename[1] in '0123456789': # Should have format e.g. t5_foo.py, not test_foo.py + if filename[1] in '0123456789': # Should have format e.g. t05_foo.py, not test_foo.py sc.heading(f'Running {filename}...') try: tex.run_example(filename) From 39470fdf0129eac9f6d622bdaf7aed140713761e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 01:14:50 -0700 Subject: [PATCH 199/569] more renaming --- docs/tutorials/t08.ipynb | 271 ------------------ docs/tutorials/t09.ipynb | 249 ++++++++++------ docs/tutorials/t10.ipynb | 188 ++++++++++++ .../{t08_versioning.py => t09_versioning.py} | 0 ..._custom_layers.py => t10_custom_layers.py} | 0 examples/{t09_numba.py => t10_numba.py} | 0 ...erties.py => t10_population_properties.py} | 0 7 files changed, 354 insertions(+), 354 deletions(-) delete mode 100644 docs/tutorials/t08.ipynb create mode 100644 docs/tutorials/t10.ipynb rename examples/{t08_versioning.py => t09_versioning.py} (100%) rename examples/{t09_custom_layers.py => t10_custom_layers.py} (100%) rename examples/{t09_numba.py => t10_numba.py} (100%) rename examples/{t09_population_properties.py => t10_population_properties.py} (100%) diff --git a/docs/tutorials/t08.ipynb b/docs/tutorials/t08.ipynb deleted file mode 100644 index 84a82600f..000000000 --- a/docs/tutorials/t08.ipynb +++ /dev/null @@ -1,271 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# T8 - Tips and tricks\n", - "\n", - "This tutorial contains suggestions that aren't essential to follow, but which may make your life easier.\n", - "\n", - "## Versioning\n", - "\n", - "Covasim contains a number of built-in tools to make it easier to keep track of where results came from. The simplest of these is that if you save an image using `cv.savefig()` instead of `pl.savefig()`, it will automatically store information about the script and Covasim version that generated it:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import covasim as cv\n", - "cv.options.set(dpi=100, show=False, close=True, verbose=0) # Standard options for Jupyter notebook\n", - "\n", - "sim = cv.Sim()\n", - "sim.run()\n", - "sim.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "filename = 'my-figure.png'\n", - "cv.savefig(filename) # Save including version information\n", - "cv.get_png_metadata(filename) # Retrieve and print information" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This can be extremely useful for figuring out where that intriguing result you generated 3 weeks ago came from!\n", - "\n", - "This information is also stored in sims and multisims themselves:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(sim.version)\n", - "print(sim.git_info)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, the function `cv.check_version()` and `cv.check_save_version()` are useful if you want to ensure that users are running the right version of your code. Placing `cv.check_save_version('2.0.0')` will save a file with the information above to the current folder – again, useful for debugging exactly what changed and when. (You can also provide additional information to it, e.g. to also save the versions of 3rd-party packages you're importing). `cv.check_version()` by itself can be used to provide a warning or even raise an exception (if `die=True`) if the version is not what's expected:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "cv.check_version('1.5.0')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Working with dates\n", - "\n", - "Dates can be tricky to work with. Covasim comes with a number of built-in features to work with dates. By default, by convention Covasim works with dates in the format `YYYY-MM-DD`, e.g. `'2020-12-01'`. However, it can handle a wide variety of other date and `datetime` objects. In particular, `sim` objects know when they start and end, and can use this to do quite a bit of date math:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sim = cv.Sim(start_day='20201122', end_day='2020-12-09 02:14:58.727703')\n", - "sim.initialize() # Date conversion happens on initialization\n", - "print(sim['start_day'])\n", - "print(sim['end_day'])\n", - "print(sim.day(sim['end_day'])) # Prints the number of days until the end day, i.e. the length of the sim" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can also easily calculate the difference between two dates, or generate a range of dates. These are returned as strings by default, but can be converted to datetime objects via Sciris:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import sciris as sc\n", - "\n", - "print(cv.daydiff('2020-06-01', '2020-07-01', '2020-08-01'))\n", - "dates = cv.date_range('2020-04-04', '2020-04-12')\n", - "print(dates)\n", - "print(sc.readdate(dates))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, one gotcha is that when loading Excel spreadsheets in pandas, dates are loaded in pandas' internal `Timestamp[ns64]` format, which nothing else seems to be able to read. If this happens to you, the solution (as far as Covasim is concerned) is to convert to a `datetime.date`:\n", - "\n", - "```python\n", - "data = pd.read_excel(filename)\n", - "data['date'] = data['date'].dt.date\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Working with dictionaries\n", - "\n", - "\"I already know how to work with dictionaries\", you say. Yes, you do. But there are a couple tricks that might make things easier.\n", - "\n", - "Covasim is built on Sciris, which includes containers `odict` and `objdict`. While these are [documented elsewhere](https://sciris.readthedocs.io/en/latest/_autosummary/sciris.sc_odict.odict.html#sciris.sc_odict.odict), a couple examples will serve to illustrate how they work.\n", - "\n", - "An `odict` is just an ordered dict that you can refer to by *position* as well as by key. For example:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "mydict = sc.odict(foo=[1,2,3], bar=[4,5,6]) # Assignment is the same as ordinary dictionaries\n", - "print('Entry foo:', mydict['foo'])\n", - "print('Entry 0:', mydict[0]) # Access by key or by index\n", - "for i,key,value in mydict.enumitems(): # Additional methods for iteration\n", - " print(f'Item {i} is named {key} and has value {value}')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "An `objdict` is exactly the same as an odict except it lets you reference keys as if they were attributes:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "myobjdict = sc.objdict(foo=[1,2,3], bar=[4,5,6])\n", - "print('Entry foo:', myobjdict['foo'])\n", - "print('Entry 0:', myobjdict[0]) # Access by key or by index\n", - "print('\"Attribute\" foo:', myobjdict.foo)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Using this approach, you can get all the power and flexibility of dictionaries, while writing code as succinctly as possible. For example:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "total_pop = 44_483 # This many total people\n", - "\n", - "pars= sc.objdict(\n", - " pop_type = 'hybrid',\n", - " pop_size = 10e3,\n", - ")\n", - "pars.pop_scale = total_pop/pars.pop_size # Instead of pars['pop_scale'] = total_pop/pars['pop_size'] \n", - "sim = cv.Sim(**pars) # It's still a dict, so you can treat it as one!" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, Sciris also contains a function called `mergedicts`. This acts very similarly to `dict.update()`, with the main difference being that it returns the result of merging the two dictionaries. This is especially useful for handling keyword arguments in functions:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def myfunc(args=None, **kwargs):\n", - " defaults = dict(foo=[1,2,3], bar=[4,5,6])\n", - " merged_args = sc.mergedicts(defaults, args, kwargs)\n", - " print(merged_args)\n", - "\n", - "myfunc(args=dict(bar=18), other_args='can be anything')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As you can see, it merged the default settings, the arguments supplied to the function via the keyword `args`, and then other keywords, into a single dictionary." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.5" - }, - "pycharm": { - "stem_cell": { - "cell_type": "raw", - "metadata": { - "collapsed": false - }, - "source": [] - } - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": true, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/tutorials/t09.ipynb b/docs/tutorials/t09.ipynb index fd81e247c..84a82600f 100644 --- a/docs/tutorials/t09.ipynb +++ b/docs/tutorials/t09.ipynb @@ -4,17 +4,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# T9 - Advanced features\n", + "# T8 - Tips and tricks\n", "\n", - "This tutorial covers advanced features of Covasim, including custom population options and changing the internal computational methods.\n", + "This tutorial contains suggestions that aren't essential to follow, but which may make your life easier.\n", "\n", - "## Defining populations with SynthPops\n", + "## Versioning\n", "\n", - "For complex populations, we suggest using [SynthPops](http://synthpops.org), a Python library designed specifically for this purpose. In contrast the population methods built-in to Covasim, SynthPops uses data to produce synthetic populations that are statistically indistinguishable from real ones. For a relatively complex example of how SynthPops was used to create a complex school network for the Seattle region, see [here](https://github.com/institutefordiseasemodeling/testing-the-waters/blob/main/covasim_schools/school_pop.py).\n", - "\n", - "## Defining contact layers\n", - "\n", - "As mentioned in Tutorial 1, contact layers are the graph connecting the people in the simulation. Each person is a node, and each contact is an edge. While enormous complexity can be used to define realistic contact networks, a reasonable approximation in many cases is random connectivity, often with some age assortativity. Here is an example for generating a new contact layer, nominally representing public transportation, and adding it to a simulation:" + "Covasim contains a number of built-in tools to make it easier to keep track of where results came from. The simplest of these is that if you save an image using `cv.savefig()` instead of `pl.savefig()`, it will automatically store information about the script and Covasim version that generated it:" ] }, { @@ -23,45 +19,87 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "import covasim as cv\n", "cv.options.set(dpi=100, show=False, close=True, verbose=0) # Standard options for Jupyter notebook\n", "\n", - "# Create the first sim\n", - "orig_sim = cv.Sim(pop_type='hybrid', n_days=120, label='Default hybrid population')\n", - "orig_sim.initialize() # Initialize the population\n", - "\n", - "# Create the second sim\n", - "sim = orig_sim.copy()\n", - "\n", - "# Define the new layer, 'transport'\n", - "n_people = len(sim.people)\n", - "n_contacts_per_person = 0.5\n", - "n_contacts = int(n_contacts_per_person*n_people)\n", - "contacts_p1 = cv.choose(max_n=n_people, n=n_contacts)\n", - "contacts_p2 = cv.choose(max_n=n_people, n=n_contacts)\n", - "beta = np.ones(n_contacts)\n", - "layer = cv.Layer(p1=contacts_p1, p2=contacts_p2, beta=beta) # Create the new layer\n", - "\n", - "# Add this layer in and re-initialize the sim\n", - "sim.people.contacts.add_layer(transport=layer)\n", - "sim.reset_layer_pars() # Automatically add layer 'q' to the parameters using default values\n", - "sim.initialize() # Reinitialize\n", - "sim.label = f'Transport layer with {n_contacts_per_person} contacts/person'\n", + "sim = cv.Sim()\n", + "sim.run()\n", + "sim.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "filename = 'my-figure.png'\n", + "cv.savefig(filename) # Save including version information\n", + "cv.get_png_metadata(filename) # Retrieve and print information" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can be extremely useful for figuring out where that intriguing result you generated 3 weeks ago came from!\n", "\n", - "# Run and compare\n", - "msim = cv.MultiSim([orig_sim, sim])\n", - "msim.run()\n", - "msim.plot()" + "This information is also stored in sims and multisims themselves:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(sim.version)\n", + "print(sim.git_info)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, the function `cv.check_version()` and `cv.check_save_version()` are useful if you want to ensure that users are running the right version of your code. Placing `cv.check_save_version('2.0.0')` will save a file with the information above to the current folder – again, useful for debugging exactly what changed and when. (You can also provide additional information to it, e.g. to also save the versions of 3rd-party packages you're importing). `cv.check_version()` by itself can be used to provide a warning or even raise an exception (if `die=True`) if the version is not what's expected:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cv.check_version('1.5.0')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Defining custom population properties\n", + "## Working with dates\n", "\n", - "Another useful feature is adding additional features to people, for use in subtargeting. For example, this example shows how to define a subpopulation with higher baseline mortality rates. This is a simple example illustrating how you would identify and target people based on whether or not the have a prime-number index, based on the protecting the elderly example from Tutorial 1." + "Dates can be tricky to work with. Covasim comes with a number of built-in features to work with dates. By default, by convention Covasim works with dates in the format `YYYY-MM-DD`, e.g. `'2020-12-01'`. However, it can handle a wide variety of other date and `datetime` objects. In particular, `sim` objects know when they start and end, and can use this to do quite a bit of date math:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sim = cv.Sim(start_day='20201122', end_day='2020-12-09 02:14:58.727703')\n", + "sim.initialize() # Date conversion happens on initialization\n", + "print(sim['start_day'])\n", + "print(sim['end_day'])\n", + "print(sim.day(sim['end_day'])) # Prints the number of days until the end day, i.e. the length of the sim" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also easily calculate the difference between two dates, or generate a range of dates. These are returned as strings by default, but can be converted to datetime objects via Sciris:" ] }, { @@ -70,43 +108,57 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "import sciris as sc\n", - "import covasim as cv\n", "\n", - "def protect_the_prime(sim):\n", - " if sim.t == sim.day('2020-04-01'):\n", - " are_prime = sim.people.prime\n", - " sim.people.rel_sus[are_prime] = 0.0\n", + "print(cv.daydiff('2020-06-01', '2020-07-01', '2020-08-01'))\n", + "dates = cv.date_range('2020-04-04', '2020-04-12')\n", + "print(dates)\n", + "print(sc.readdate(dates))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, one gotcha is that when loading Excel spreadsheets in pandas, dates are loaded in pandas' internal `Timestamp[ns64]` format, which nothing else seems to be able to read. If this happens to you, the solution (as far as Covasim is concerned) is to convert to a `datetime.date`:\n", "\n", - "pars = dict(\n", - " pop_type = 'hybrid',\n", - " pop_infected = 100,\n", - " n_days = 90,\n", - " verbose = 0,\n", - ")\n", + "```python\n", + "data = pd.read_excel(filename)\n", + "data['date'] = data['date'].dt.date\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Working with dictionaries\n", "\n", - "# Default simulation\n", - "orig_sim = cv.Sim(pars, label='Default')\n", + "\"I already know how to work with dictionaries\", you say. Yes, you do. But there are a couple tricks that might make things easier.\n", "\n", - "# Create the simulation\n", - "sim = cv.Sim(pars, label='Protect the prime', interventions=protect_the_prime)\n", - "sim.initialize() # Initialize to create the people array\n", - "sim.people.prime = np.array([sc.isprime(i) for i in range(len(sim.people))]) # Define whom to target\n", + "Covasim is built on Sciris, which includes containers `odict` and `objdict`. While these are [documented elsewhere](https://sciris.readthedocs.io/en/latest/_autosummary/sciris.sc_odict.odict.html#sciris.sc_odict.odict), a couple examples will serve to illustrate how they work.\n", "\n", - "# Run and plot\n", - "msim = cv.MultiSim([orig_sim, sim])\n", - "msim.run()\n", - "msim.plot()" + "An `odict` is just an ordered dict that you can refer to by *position* as well as by key. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mydict = sc.odict(foo=[1,2,3], bar=[4,5,6]) # Assignment is the same as ordinary dictionaries\n", + "print('Entry foo:', mydict['foo'])\n", + "print('Entry 0:', mydict[0]) # Access by key or by index\n", + "for i,key,value in mydict.enumitems(): # Additional methods for iteration\n", + " print(f'Item {i} is named {key} and has value {value}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Changing Numba options\n", - "\n", - "Finally, this example shows how you can change the default Numba calculation options. It's not recommended – especially running with multithreading, which is faster but gives stochastically unreproducible results – but it's there if you want it." + "An `objdict` is exactly the same as an odict except it lets you reference keys as if they were attributes:" ] }, { @@ -115,30 +167,61 @@ "metadata": {}, "outputs": [], "source": [ - "import covasim as cv\n", - "\n", - "# Create a standard 32-bit simulation\n", - "sim32 = cv.Sim(label='32-bit, single-threaded (default)', verbose='brief')\n", - "sim32.run()\n", - "\n", - "# Use 64-bit instead of 32\n", - "cv.options.set(precision=64)\n", - "sim64 = cv.Sim(label='64-bit, single-threaded', verbose='brief')\n", - "sim64.run()\n", - "\n", - "# Use parallel threading\n", - "cv.options.set(numba_parallel=True)\n", - "sim_par = cv.Sim(label='64-bit, multi-threaded', verbose='brief')\n", - "sim_par.run()\n", + "myobjdict = sc.objdict(foo=[1,2,3], bar=[4,5,6])\n", + "print('Entry foo:', myobjdict['foo'])\n", + "print('Entry 0:', myobjdict[0]) # Access by key or by index\n", + "print('\"Attribute\" foo:', myobjdict.foo)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using this approach, you can get all the power and flexibility of dictionaries, while writing code as succinctly as possible. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "total_pop = 44_483 # This many total people\n", "\n", - "# Reset to defaults\n", - "cv.options.set('defaults')\n", - "sim32b = cv.Sim(label='32-bit, single-threaded (restored)', verbose='brief')\n", - "sim32b.run()\n", + "pars= sc.objdict(\n", + " pop_type = 'hybrid',\n", + " pop_size = 10e3,\n", + ")\n", + "pars.pop_scale = total_pop/pars.pop_size # Instead of pars['pop_scale'] = total_pop/pars['pop_size'] \n", + "sim = cv.Sim(**pars) # It's still a dict, so you can treat it as one!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, Sciris also contains a function called `mergedicts`. This acts very similarly to `dict.update()`, with the main difference being that it returns the result of merging the two dictionaries. This is especially useful for handling keyword arguments in functions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def myfunc(args=None, **kwargs):\n", + " defaults = dict(foo=[1,2,3], bar=[4,5,6])\n", + " merged_args = sc.mergedicts(defaults, args, kwargs)\n", + " print(merged_args)\n", "\n", - "# Plot\n", - "msim = cv.MultiSim([sim32, sim64, sim_par, sim32b])\n", - "msim.plot()" + "myfunc(args=dict(bar=18), other_args='can be anything')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, it merged the default settings, the arguments supplied to the function via the keyword `args`, and then other keywords, into a single dictionary." ] } ], diff --git a/docs/tutorials/t10.ipynb b/docs/tutorials/t10.ipynb new file mode 100644 index 000000000..fd81e247c --- /dev/null +++ b/docs/tutorials/t10.ipynb @@ -0,0 +1,188 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# T9 - Advanced features\n", + "\n", + "This tutorial covers advanced features of Covasim, including custom population options and changing the internal computational methods.\n", + "\n", + "## Defining populations with SynthPops\n", + "\n", + "For complex populations, we suggest using [SynthPops](http://synthpops.org), a Python library designed specifically for this purpose. In contrast the population methods built-in to Covasim, SynthPops uses data to produce synthetic populations that are statistically indistinguishable from real ones. For a relatively complex example of how SynthPops was used to create a complex school network for the Seattle region, see [here](https://github.com/institutefordiseasemodeling/testing-the-waters/blob/main/covasim_schools/school_pop.py).\n", + "\n", + "## Defining contact layers\n", + "\n", + "As mentioned in Tutorial 1, contact layers are the graph connecting the people in the simulation. Each person is a node, and each contact is an edge. While enormous complexity can be used to define realistic contact networks, a reasonable approximation in many cases is random connectivity, often with some age assortativity. Here is an example for generating a new contact layer, nominally representing public transportation, and adding it to a simulation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import covasim as cv\n", + "cv.options.set(dpi=100, show=False, close=True, verbose=0) # Standard options for Jupyter notebook\n", + "\n", + "# Create the first sim\n", + "orig_sim = cv.Sim(pop_type='hybrid', n_days=120, label='Default hybrid population')\n", + "orig_sim.initialize() # Initialize the population\n", + "\n", + "# Create the second sim\n", + "sim = orig_sim.copy()\n", + "\n", + "# Define the new layer, 'transport'\n", + "n_people = len(sim.people)\n", + "n_contacts_per_person = 0.5\n", + "n_contacts = int(n_contacts_per_person*n_people)\n", + "contacts_p1 = cv.choose(max_n=n_people, n=n_contacts)\n", + "contacts_p2 = cv.choose(max_n=n_people, n=n_contacts)\n", + "beta = np.ones(n_contacts)\n", + "layer = cv.Layer(p1=contacts_p1, p2=contacts_p2, beta=beta) # Create the new layer\n", + "\n", + "# Add this layer in and re-initialize the sim\n", + "sim.people.contacts.add_layer(transport=layer)\n", + "sim.reset_layer_pars() # Automatically add layer 'q' to the parameters using default values\n", + "sim.initialize() # Reinitialize\n", + "sim.label = f'Transport layer with {n_contacts_per_person} contacts/person'\n", + "\n", + "# Run and compare\n", + "msim = cv.MultiSim([orig_sim, sim])\n", + "msim.run()\n", + "msim.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining custom population properties\n", + "\n", + "Another useful feature is adding additional features to people, for use in subtargeting. For example, this example shows how to define a subpopulation with higher baseline mortality rates. This is a simple example illustrating how you would identify and target people based on whether or not the have a prime-number index, based on the protecting the elderly example from Tutorial 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import sciris as sc\n", + "import covasim as cv\n", + "\n", + "def protect_the_prime(sim):\n", + " if sim.t == sim.day('2020-04-01'):\n", + " are_prime = sim.people.prime\n", + " sim.people.rel_sus[are_prime] = 0.0\n", + "\n", + "pars = dict(\n", + " pop_type = 'hybrid',\n", + " pop_infected = 100,\n", + " n_days = 90,\n", + " verbose = 0,\n", + ")\n", + "\n", + "# Default simulation\n", + "orig_sim = cv.Sim(pars, label='Default')\n", + "\n", + "# Create the simulation\n", + "sim = cv.Sim(pars, label='Protect the prime', interventions=protect_the_prime)\n", + "sim.initialize() # Initialize to create the people array\n", + "sim.people.prime = np.array([sc.isprime(i) for i in range(len(sim.people))]) # Define whom to target\n", + "\n", + "# Run and plot\n", + "msim = cv.MultiSim([orig_sim, sim])\n", + "msim.run()\n", + "msim.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Changing Numba options\n", + "\n", + "Finally, this example shows how you can change the default Numba calculation options. It's not recommended – especially running with multithreading, which is faster but gives stochastically unreproducible results – but it's there if you want it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import covasim as cv\n", + "\n", + "# Create a standard 32-bit simulation\n", + "sim32 = cv.Sim(label='32-bit, single-threaded (default)', verbose='brief')\n", + "sim32.run()\n", + "\n", + "# Use 64-bit instead of 32\n", + "cv.options.set(precision=64)\n", + "sim64 = cv.Sim(label='64-bit, single-threaded', verbose='brief')\n", + "sim64.run()\n", + "\n", + "# Use parallel threading\n", + "cv.options.set(numba_parallel=True)\n", + "sim_par = cv.Sim(label='64-bit, multi-threaded', verbose='brief')\n", + "sim_par.run()\n", + "\n", + "# Reset to defaults\n", + "cv.options.set('defaults')\n", + "sim32b = cv.Sim(label='32-bit, single-threaded (restored)', verbose='brief')\n", + "sim32b.run()\n", + "\n", + "# Plot\n", + "msim = cv.MultiSim([sim32, sim64, sim_par, sim32b])\n", + "msim.plot()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "metadata": { + "collapsed": false + }, + "source": [] + } + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/t08_versioning.py b/examples/t09_versioning.py similarity index 100% rename from examples/t08_versioning.py rename to examples/t09_versioning.py diff --git a/examples/t09_custom_layers.py b/examples/t10_custom_layers.py similarity index 100% rename from examples/t09_custom_layers.py rename to examples/t10_custom_layers.py diff --git a/examples/t09_numba.py b/examples/t10_numba.py similarity index 100% rename from examples/t09_numba.py rename to examples/t10_numba.py diff --git a/examples/t09_population_properties.py b/examples/t10_population_properties.py similarity index 100% rename from examples/t09_population_properties.py rename to examples/t10_population_properties.py From 91b4181bd0bff35a058966fdb67f252ae416e0c0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 01:54:39 -0700 Subject: [PATCH 200/569] update plotting styles and tutorials --- covasim/interventions.py | 2 +- docs/tutorials.rst | 19 ++++++++++--------- docs/tutorials/t02.ipynb | 2 +- docs/tutorials/t05.ipynb | 8 +++++--- docs/tutorials/t07.ipynb | 2 +- docs/tutorials/t09.ipynb | 6 ++++-- docs/tutorials/t10.ipynb | 4 ++-- 7 files changed, 24 insertions(+), 19 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index b95cf7c6d..9bc9ead7b 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -95,7 +95,7 @@ def __init__(self, label=None, show_label=True, do_plot=None, line_args=None): self.label = label # e.g. "Close schools" self.show_label = show_label # Show the label by default self.do_plot = do_plot if do_plot is not None else True # Plot the intervention, including if None - self.line_args = sc.mergedicts(dict(linestyle='--', c=[0,0,0]), line_args) # Do not set alpha by default due to the issue of overlapping interventions + self.line_args = sc.mergedicts(dict(linestyle='--', c='#aaa', lw=1.0), line_args) # Do not set alpha by default due to the issue of overlapping interventions self.days = [] # The start and end days of the intervention self.initialized = False # Whether or not it has been initialized return diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 28d768629..84b951b2f 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -3,12 +3,13 @@ .. toctree:: :maxdepth: 1 - tutorials/t1 - tutorials/t2 - tutorials/t3 - tutorials/t4 - tutorials/t5 - tutorials/t6 - tutorials/t7 - tutorials/t8 - tutorials/t9 + tutorials/t01 + tutorials/t02 + tutorials/t03 + tutorials/t04 + tutorials/t05 + tutorials/t06 + tutorials/t07 + tutorials/t08 + tutorials/t09 + tutorials/t10 diff --git a/docs/tutorials/t02.ipynb b/docs/tutorials/t02.ipynb index a6cb8d8ea..eb55882b7 100644 --- a/docs/tutorials/t02.ipynb +++ b/docs/tutorials/t02.ipynb @@ -105,7 +105,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "However, as you can see, this isn't ideal since the default formatting is...not great. (Also, note that each result is a `Result` object, not a simple Numpy array; like a pandas dataframe, you can get the array of values directly via e.g. `sim.results['new_infections'].values`.)\n", + "However, as you can see, this isn't ideal since the default formatting is...not great. (Also, note that each result is a `Result` object, not a simple Numpy array; like a pandas dataframe, you can get the array of values directly via e.g. `sim.results.new_infections.values`.)\n", "\n", "An alternative, if you only want to plot a single result, such as new infections, is to use the `plot_result()` method:" ] diff --git a/docs/tutorials/t05.ipynb b/docs/tutorials/t05.ipynb index 48691454d..beb97361c 100644 --- a/docs/tutorials/t05.ipynb +++ b/docs/tutorials/t05.ipynb @@ -158,8 +158,8 @@ "import covasim as cv\n", "\n", "# Define the testing and contact tracing interventions\n", - "tp = cv.test_prob(symp_prob=0.2, asymp_prob=0.001, symp_quar_prob=1.0, asymp_quar_prob=1.0)\n", - "ct = cv.contact_tracing(trace_probs=dict(h=1.0, s=0.5, w=0.5, c=0.3))\n", + "tp = cv.test_prob(symp_prob=0.2, asymp_prob=0.001, symp_quar_prob=1.0, asymp_quar_prob=1.0, do_plot=False)\n", + "ct = cv.contact_tracing(trace_probs=dict(h=1.0, s=0.5, w=0.5, c=0.3), do_plot=False)\n", "\n", "# Define the default parameters\n", "pars = dict(\n", @@ -185,7 +185,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Since it's assumed that known contacts are placed into quarantine (with efficacy `sim['quar_factor']`), the number of contacts who are successfully traced each day is equal to the number of people newly quarantined (bottom left panel). As is commonly seen using testing and tracing as the only means of epidemic control, these programs stop the epidemic from growing exponentially, but do not bring it to zero." + "Since it's assumed that known contacts are placed into quarantine (with efficacy `sim['quar_factor']`), the number of contacts who are successfully traced each day is equal to the number of people newly quarantined (bottom left panel). As is commonly seen using testing and tracing as the only means of epidemic control, these programs stop the epidemic from growing exponentially, but do not bring it to zero.\n", + "\n", + "Since these interventions happen at `t=0`, it's not very useful to plot them. Note that we have turned off plotting by passing `do_plot=False` to each intervention." ] }, { diff --git a/docs/tutorials/t07.ipynb b/docs/tutorials/t07.ipynb index 4cfb8d60c..1f1ea4689 100644 --- a/docs/tutorials/t07.ipynb +++ b/docs/tutorials/t07.ipynb @@ -273,7 +273,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" }, "pycharm": { "stem_cell": { diff --git a/docs/tutorials/t09.ipynb b/docs/tutorials/t09.ipynb index 84a82600f..b1df9199c 100644 --- a/docs/tutorials/t09.ipynb +++ b/docs/tutorials/t09.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# T8 - Tips and tricks\n", + "# T9 - Tips and tricks\n", "\n", "This tutorial contains suggestions that aren't essential to follow, but which may make your life easier.\n", "\n", @@ -200,6 +200,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "For example, the `results` object is an `objdict`. This means that although you can use e.g. `sim.results['new_infections']`, you can also use `sim.results.new_infections`.\n", + "\n", "Finally, Sciris also contains a function called `mergedicts`. This acts very similarly to `dict.update()`, with the main difference being that it returns the result of merging the two dictionaries. This is especially useful for handling keyword arguments in functions:" ] }, @@ -241,7 +243,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" }, "pycharm": { "stem_cell": { diff --git a/docs/tutorials/t10.ipynb b/docs/tutorials/t10.ipynb index fd81e247c..eb2cf9492 100644 --- a/docs/tutorials/t10.ipynb +++ b/docs/tutorials/t10.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# T9 - Advanced features\n", + "# T10 - Advanced features\n", "\n", "This tutorial covers advanced features of Covasim, including custom population options and changing the internal computational methods.\n", "\n", @@ -158,7 +158,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" }, "pycharm": { "stem_cell": { From 0d07c46f35e38e114f9fd8dcf1442b35be3f41f7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 01:58:18 -0700 Subject: [PATCH 201/569] add tutorial 8 --- covasim/analysis.py | 1 + docs/tutorials/t08.ipynb | 151 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 docs/tutorials/t08.ipynb diff --git a/covasim/analysis.py b/covasim/analysis.py index cfbd1ad18..e3b7bd0bf 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1411,6 +1411,7 @@ def plot_quantity(key, title, i): dat.plot(ax=ax, legend=None, **plot_args) pl.legend(title=None) ax.set_title(title) + ax.set_ylabel('Count') to_plot = dict( layer = 'Layer', diff --git a/docs/tutorials/t08.ipynb b/docs/tutorials/t08.ipynb new file mode 100644 index 000000000..5bef628cb --- /dev/null +++ b/docs/tutorials/t08.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# T8 - Deployment\n", + "\n", + "This tutorial provides several useful recipes for deploying Covasim.\n", + "\n", + "## Dask\n", + "\n", + "[Dask](https://dask.org/) is a powerful library for multiprocessing and \"scalable\" analytics. Using Dask (rather than the built-in `multiprocess`) for parallelization is _relatively_ straightforward:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "```python\n", + "import dask\n", + "from dask.distributed import Client\n", + "import numpy as np\n", + "import covasim as cv\n", + "\n", + "\n", + "def run_sim(index, beta):\n", + " sim = cv.Sim(beta=beta, label=f'Sim {index}, beta={beta}')\n", + " sim.run()\n", + " return sim\n", + "\n", + "\n", + "if __name__ == '__main__':\n", + "\n", + " n = 8\n", + " n_workers = 4\n", + "\n", + " client = Client(n_workers=n_workers)\n", + " betas = np.sort(np.random.random(n))\n", + "\n", + " queued = []\n", + " for i,beta in enumerate(betas):\n", + " run = dask.delayed(run_sim)(i, beta)\n", + " queued.append(run)\n", + "\n", + " sims = list(dask.compute(*queued))\n", + " msim = cv.MultiSim(sims)\n", + " msim.plot(color_by_sim=True)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Jupyter/IPython\n", + "\n", + "Using Jupyter and [Voilà](https://voila.readthedocs.io/), you can build a Covasim-based webapp in minutes. First, install the required dependencies:\n", + "\n", + "```bash\n", + "pip install jupyter jupyterlab jupyterhub ipympl voila \n", + "```\n", + "\n", + "Here is a very simple interactive webapp that runs a multisim (in parallel!) when the button is pressed, and displays the results:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "```python\n", + "import numpy as np\n", + "import covasim as cv\n", + "import ipywidgets as wid\n", + "\n", + "button = wid.Button(description='Run')\n", + "output = wid.Output()\n", + "\n", + "@output.capture()\n", + "def run():\n", + " sim = cv.Sim(verbose=0, pop_size=20e3, n_days=100, rand_seed=np.random.randint(99))\n", + " msim = cv.MultiSim(sim)\n", + " msim.run(n_runs=4)\n", + " return msim.plot()\n", + "\n", + "def click(b):\n", + " output.clear_output(wait=True)\n", + " run()\n", + " \n", + "button.on_click(click)\n", + "app = wid.VBox([button, output])\n", + "display(app)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you save this as e.g. `msim.ipynb`, then you can turn it into a web server simply by typing `voila msim.ipynb`." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "metadata": { + "collapsed": false + }, + "source": [] + } + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From 70fdd699506787dbbb79ec4dd74ceb35b5016171 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 02:02:00 -0700 Subject: [PATCH 202/569] fix transmission tree error --- covasim/analysis.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index e3b7bd0bf..8d0b92e2f 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1346,15 +1346,19 @@ def make_detailed(self, people, reset=False): ttlist.append(tdict) df = pd.DataFrame(ttlist).rename(columns={'date': 'Day'}) - df = df.loc[df['layer'] != 'seed_infection'] - df['Stage'] = 'Symptomatic' - df.loc[df['s_asymp'], 'Stage'] = 'Asymptomatic' - df.loc[df['s_presymp'], 'Stage'] = 'Presymptomatic' + if len(df): # Don't proceed if there were no infections + df = df.loc[df['layer'] != 'seed_infection'] - df['Severity'] = 'Mild' - df.loc[df['s_sev'], 'Severity'] = 'Severe' - df.loc[df['s_crit'], 'Severity'] = 'Critical' + df['Stage'] = 'Symptomatic' + df.loc[df['s_asymp'], 'Stage'] = 'Asymptomatic' + df.loc[df['s_presymp'], 'Stage'] = 'Presymptomatic' + + df['Severity'] = 'Mild' + df.loc[df['s_sev'], 'Severity'] = 'Severe' + df.loc[df['s_crit'], 'Severity'] = 'Critical' + else: + print('Warning: transmission tree is empty since there were no infections.') self.df = df From 7c4200ca3befd6872739e801c99962e73a854340 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 02:39:17 -0700 Subject: [PATCH 203/569] add json export for analyzers --- covasim/analysis.py | 51 +++++++++++++++++++++++-- covasim/interventions.py | 2 + covasim/misc.py | 3 ++ covasim/version.py | 4 +- tests/devtests/test_analyzer_to_json.py | 35 +++++++++++++++++ 5 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 tests/devtests/test_analyzer_to_json.py diff --git a/covasim/analysis.py b/covasim/analysis.py index 8d0b92e2f..d7f41bac7 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -31,6 +31,8 @@ class Analyzer(sc.prettyobj): ''' def __init__(self, label=None): + if label is None: + label = self.__class__.__name__ # Use the class name if no label is supplied self.label = label # e.g. "Record ages" self.initialized = False return @@ -56,6 +58,38 @@ def apply(self, sim): raise NotImplementedError + def to_json(self): + ''' + Return JSON-compatible representation + + Custom classes can't be directly represented in JSON. This method is a + one-way export to produce a JSON-compatible representation of the + intervention. This method will attempt to JSONify each attribute of the + intervention, skipping any that fail. + + Returns: + JSON-serializable representation + ''' + # Set the name + json = {} + json['analyzer_name'] = self.label if hasattr(self, 'label') else None + json['analyzer_class'] = self.__class__.__name__ + + # Loop over the attributes and try to process + attrs = self.__dict__.keys() + for attr in attrs: + try: + data = getattr(self, attr) + try: + attjson = sc.jsonify(data) + json[attr] = attjson + except Exception as E: + json[attr] = f'Could not jsonify "{attr}" ({type(data)}): "{str(E)}"' + except Exception as E2: + json[attr] = f'Could not jsonify "{attr}": "{str(E2)}"' + return json + + def validate_recorded_dates(sim, requested_dates, recorded_dates, die=True): ''' Helper method to ensure that dates recorded by an analyzer match the ones @@ -824,7 +858,7 @@ def plot(self, fig_args=None, axis_args=None, plot_args=None, do_show=None): -class Fit(sc.prettyobj): +class Fit(Analyzer): ''' A class for calculating the fit between the model and the data. Note the following terminology is used here: @@ -847,13 +881,14 @@ class Fit(sc.prettyobj): **Example**:: - sim = cv.Sim() + sim = cv.Sim(datafile='my-data-file.csv') sim.run() fit = sim.compute_fit() fit.plot() ''' def __init__(self, sim, weights=None, keys=None, custom=None, compute=True, verbose=False, **kwargs): + super().__init__(**kwargs) # Initialize the Analyzer object # Handle inputs self.weights = weights @@ -1141,7 +1176,7 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No return fig -class TransTree(sc.prettyobj): +class TransTree(Analyzer): ''' A class for holding a transmission tree. There are several different representations of the transmission tree: "infection_log" is copied from the people object and is the @@ -1152,9 +1187,17 @@ class TransTree(sc.prettyobj): Args: sim (Sim): the sim object to_networkx (bool): whether to convert the graph to a NetworkX object + + **Example**:: + + sim = cv.Sim() + sim.run() + tt = sim.make_transtree() + tt.plot_histograms() ''' - def __init__(self, sim, to_networkx=False): + def __init__(self, sim, to_networkx=False, **kwargs): + super().__init__(**kwargs) # Initialize the Analyzer object # Pull out each of the attributes relevant to transmission attrs = {'age', 'date_exposed', 'date_symptomatic', 'date_tested', 'date_diagnosed', 'date_quarantined', 'date_severe', 'date_critical', 'date_known_contact', 'date_recovered'} diff --git a/covasim/interventions.py b/covasim/interventions.py index 9bc9ead7b..29d7dcd87 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -91,6 +91,8 @@ class Intervention: line_args (dict): arguments passed to pl.axvline() when plotting ''' def __init__(self, label=None, show_label=True, do_plot=None, line_args=None): + if label is None: + label = self.__class__.__name__ # Use the class name if no label is supplied self._store_args() # Store the input arguments so the intervention can be recreated self.label = label # e.g. "Close schools" self.show_label = show_label # Show the label by default diff --git a/covasim/misc.py b/covasim/misc.py index 79df1c32c..c085538ec 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -7,6 +7,7 @@ import pylab as pl import sciris as sc import scipy.stats as sps +from pathlib import Path from . import version as cvv @@ -41,6 +42,8 @@ def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=T ''' # Load data + if isinstance(datafile, Path): # Convert to a string + datafile = str(datafile) if isinstance(datafile, str): df_lower = datafile.lower() if df_lower.endswith('csv'): diff --git a/covasim/version.py b/covasim/version.py index 9d903d764..522717cb0 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,6 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '2.0.4' -__versiondate__ = '2021-03-19' +__version__ = '2.0.5' +__versiondate__ = '2021-03-22' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' diff --git a/tests/devtests/test_analyzer_to_json.py b/tests/devtests/test_analyzer_to_json.py new file mode 100644 index 000000000..155327ed4 --- /dev/null +++ b/tests/devtests/test_analyzer_to_json.py @@ -0,0 +1,35 @@ +''' +Confirm that with default settings, all analyzers can be exported as JSONs. +''' + +import sciris as sc +import covasim as cv + +datafile = sc.thisdir(__file__, aspath=True).parent / 'example_data.csv' + +# Create and runt he sim +sim = cv.Sim(analyzers=[cv.snapshot(days='2020-04-04'), + cv.age_histogram(), + cv.daily_age_stats(), + cv.daily_stats()], + datafile=datafile) +sim.run() + +# Compute extra analyzers +tt = sim.make_transtree() +fit = sim.compute_fit() + +# Construct list of all analyzers +analyzers = sim['analyzers'] + [tt, fit] + +# Make jsons +jsons = {} +for an in analyzers: + print(f'Working on analyzer {an.label}...') + jsons[an.label] = an.to_json() + +# Compute memory +for k,json in jsons.items(): + sc.checkmem({k:json}) + +print('Done.') \ No newline at end of file From d2ab4b210d8facc9a8a5f6f9d38d22fa31afd1c9 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 22 Mar 2021 13:41:36 +0100 Subject: [PATCH 204/569] initial explorations --- covasim/defaults.py | 10 ++++------ tests/devtests/test_variants.py | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 224ba3933..b4ccb5f77 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -49,9 +49,9 @@ class PeopleMeta(sc.prettyobj): 'rel_trans', # Float 'rel_sus', # Float 'prior_symptoms', # Float - 'sus_imm', # Float - 'trans_imm', # Float - 'prog_imm', # Float +# 'sus_imm', # Float +# 'trans_imm', # Float +# 'prog_imm', # Float 'vaccinations', # Number of doses given per person 'vaccine_source', # index of vaccine that individual received 'NAb', # Current neutralization titre relative to convalescent plasma @@ -152,12 +152,11 @@ class PeopleMeta(sc.prettyobj): # Immunity is broken down according to 3 axes, as listed here immunity_axes = ['sus', 'trans', 'prog'] -# Immunity protection also varies depending on your infection/vaccination history +# Immunity protection also varies depending on your infection history immunity_sources = [ 'asymptomatic', 'mild', 'severe', -# 'vaccine', ] # Default age data, based on Seattle 2018 census data -- used in population.py @@ -206,7 +205,6 @@ def get_colors(): c.vaccinations = '#5c399c' c.vaccinated = '#5c399c' c.recoveries = '#9e1149' -# c.recovered = c.recoveries c.symptomatic = '#c1ad71' c.severe = '#c1981d' c.critical = '#b86113' diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index e6f48fecf..0aa19f11b 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -408,10 +408,10 @@ def test_msim(): # sim0 = test_synthpops() - sim0 = test_msim() + # sim0 = test_msim() # Run more complex tests - # sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) From 8ebd6c5416af6d0477285e9a87ac90fb6d731cfe Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 22 Mar 2021 15:05:47 -0400 Subject: [PATCH 205/569] cleaning up --- covasim/defaults.py | 6 +++--- covasim/immunity.py | 26 ++++++++++++++++++-------- covasim/people.py | 22 +++++++++++----------- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 224ba3933..7d5b59441 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -50,8 +50,8 @@ class PeopleMeta(sc.prettyobj): 'rel_sus', # Float 'prior_symptoms', # Float 'sus_imm', # Float - 'trans_imm', # Float - 'prog_imm', # Float + 'symp_imm', # Float + 'sev_imm', # Float 'vaccinations', # Number of doses given per person 'vaccine_source', # index of vaccine that individual received 'NAb', # Current neutralization titre relative to convalescent plasma @@ -150,7 +150,7 @@ class PeopleMeta(sc.prettyobj): ] # Immunity is broken down according to 3 axes, as listed here -immunity_axes = ['sus', 'trans', 'prog'] +immunity_axes = ['sus', 'symp', 'sev'] # Immunity protection also varies depending on your infection/vaccination history immunity_sources = [ diff --git a/covasim/immunity.py b/covasim/immunity.py index cb7c5b0e2..6eafbf7af 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -384,17 +384,28 @@ def pre_compute_waning(length, form, pars): def nab_to_efficacy(nab, ax): - choices = ['sus', 'prog', 'trans'] + choices = ['sus', 'symp', 'sev'] if ax not in choices: errormsg = f'Choice provided not in list of choices' raise ValueError(errormsg) # put in here nab to efficacy mapping (logistic regression from fig 1a) - efficacy = nab + # create one of these for each axis of immunity + efficacy = np.exp(0.913*nab-0.25)/(1+np.exp(0.913*nab-0.25)) # from logistic regression computed in R using data from Khoury et al + return efficacy def compute_nab(people, inds, prior_inf=True): + ''' + Draws an initial NAb level for individuals and pre-computes NAb waning over time. + Can come from: + 1) a natural infection. If individual has no existing NAb, draw from lognormal distribution + depending upon symptoms. If individual has existing NAb, multiply/booster impact + 2) Vaccination. Draw from vaccine-source distribution. + + ''' + NAb_decay = people.pars['NAb_decay'] if prior_inf: @@ -407,7 +418,6 @@ def compute_nab(people, inds, prior_inf=True): NAb_pars = people.pars['NAb_pars'] - init_NAb_asymp = cvu.sample(**NAb_pars['asymptomatic'], size=len(asymp_inds)) init_NAb_mild = cvu.sample(**NAb_pars['mild'], size=len(mild_inds)) init_NAb_severe = cvu.sample(**NAb_pars['severe'], size=len(severe_inds)) @@ -487,7 +497,7 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] current_NAbs = people.NAb[people.t, is_sus_vacc] - people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale) + people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus') if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain current_NAbs = people.NAb[people.t, is_sus_was_inf_same] @@ -511,13 +521,13 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] current_NAbs = people.NAb[people.t, is_inf_vacc] - people.trans_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['trans'][strain], 'trans') - people.prog_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['prog'][strain], 'prog') + people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['symp'][strain], 'symp') + people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['sev'][strain], 'sev') if len(was_inf): # Immunity for reinfected people current_NAbs = people.NAb[people.t, was_inf] - people.trans_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['trans'][strain], 'trans') - people.prog_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['prog'][strain], 'prog') + people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp') + people.sev_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['sev'][strain], 'sev') return diff --git a/covasim/people.py b/covasim/people.py index 33c3ce9fd..3a1ebabd6 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -430,53 +430,53 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.date_infectious[inds] = self.dur_exp2inf[inds] + self.t # Use prognosis probabilities to determine what happens to them - symp_probs = infect_pars['rel_symp_prob']*self.symp_prob[inds]*(1-self.prog_imm[strain, inds]) # Calculate their actual probability of being symptomatic + symp_probs = infect_pars['rel_symp_prob']*self.symp_prob[inds]*(1-self.symp_imm[strain, inds]) # Calculate their actual probability of being symptomatic is_symp = cvu.binomial_arr(symp_probs) # Determine if they develop symptoms symp_inds = inds[is_symp] asymp_inds = inds[~is_symp] # Asymptomatic # CASE 1: Asymptomatic: may infect others, but have no symptoms and do not die - dur_asym2rec = cvu.sample(**durpars['asym2rec'], size=len(asymp_inds))*(1-self.trans_imm[strain, asymp_inds]) + dur_asym2rec = cvu.sample(**durpars['asym2rec'], size=len(asymp_inds)) self.date_recovered[asymp_inds] = self.date_infectious[asymp_inds] + dur_asym2rec # Date they recover self.dur_disease[asymp_inds] = self.dur_exp2inf[asymp_inds] + dur_asym2rec # Store how long this person had COVID-19 # CASE 2: Symptomatic: can either be mild, severe, or critical n_symp_inds = len(symp_inds) - self.dur_inf2sym[symp_inds] = cvu.sample(**durpars['inf2sym'], size=n_symp_inds)*(1-self.trans_imm[strain, symp_inds]) # Store how long this person took to develop symptoms + self.dur_inf2sym[symp_inds] = cvu.sample(**durpars['inf2sym'], size=n_symp_inds) # Store how long this person took to develop symptoms self.date_symptomatic[symp_inds] = self.date_infectious[symp_inds] + self.dur_inf2sym[symp_inds] # Date they become symptomatic - sev_probs = infect_pars['rel_severe_prob'] * self.severe_prob[symp_inds]*(1-self.prog_imm[strain, symp_inds]) # Probability of these people being severe + sev_probs = infect_pars['rel_severe_prob'] * self.severe_prob[symp_inds]*(1-self.sev_imm[strain, symp_inds]) # Probability of these people being severe is_sev = cvu.binomial_arr(sev_probs) # See if they're a severe or mild case sev_inds = symp_inds[is_sev] mild_inds = symp_inds[~is_sev] # Not severe # CASE 2.1: Mild symptoms, no hospitalization required and no probability of death - dur_mild2rec = cvu.sample(**durpars['mild2rec'], size=len(mild_inds))*(1-self.trans_imm[strain, mild_inds]) + dur_mild2rec = cvu.sample(**durpars['mild2rec'], size=len(mild_inds)) self.date_recovered[mild_inds] = self.date_symptomatic[mild_inds] + dur_mild2rec # Date they recover self.dur_disease[mild_inds] = self.dur_exp2inf[mild_inds] + self.dur_inf2sym[mild_inds] + dur_mild2rec # Store how long this person had COVID-19 # CASE 2.2: Severe cases: hospitalization required, may become critical - self.dur_sym2sev[sev_inds] = cvu.sample(**durpars['sym2sev'], size=len(sev_inds))*(1-self.trans_imm[strain, sev_inds]) # Store how long this person took to develop severe symptoms + self.dur_sym2sev[sev_inds] = cvu.sample(**durpars['sym2sev'], size=len(sev_inds)) # Store how long this person took to develop severe symptoms self.date_severe[sev_inds] = self.date_symptomatic[sev_inds] + self.dur_sym2sev[sev_inds] # Date symptoms become severe - crit_probs = infect_pars['rel_crit_prob'] * self.crit_prob[sev_inds] * (self.pars['no_hosp_factor'] if hosp_max else 1.) *(1-self.prog_imm[strain, sev_inds]) # Probability of these people becoming critical - higher if no beds available + crit_probs = infect_pars['rel_crit_prob'] * self.crit_prob[sev_inds] * (self.pars['no_hosp_factor'] if hosp_max else 1.) # Probability of these people becoming critical - higher if no beds available is_crit = cvu.binomial_arr(crit_probs) # See if they're a critical case crit_inds = sev_inds[is_crit] non_crit_inds = sev_inds[~is_crit] # CASE 2.2.1 Not critical - they will recover - dur_sev2rec = cvu.sample(**durpars['sev2rec'], size=len(non_crit_inds))*(1-self.trans_imm[strain, non_crit_inds]) + dur_sev2rec = cvu.sample(**durpars['sev2rec'], size=len(non_crit_inds)) self.date_recovered[non_crit_inds] = self.date_severe[non_crit_inds] + dur_sev2rec # Date they recover self.dur_disease[non_crit_inds] = self.dur_exp2inf[non_crit_inds] + self.dur_inf2sym[non_crit_inds] + self.dur_sym2sev[non_crit_inds] + dur_sev2rec # Store how long this person had COVID-19 # CASE 2.2.2: Critical cases: ICU required, may die - self.dur_sev2crit[crit_inds] = cvu.sample(**durpars['sev2crit'], size=len(crit_inds))*(1-self.trans_imm[strain, crit_inds]) + self.dur_sev2crit[crit_inds] = cvu.sample(**durpars['sev2crit'], size=len(crit_inds)) self.date_critical[crit_inds] = self.date_severe[crit_inds] + self.dur_sev2crit[crit_inds] # Date they become critical - death_probs = infect_pars['rel_death_prob'] * self.death_prob[crit_inds] * (self.pars['no_icu_factor'] if icu_max else 1.)*(1-self.prog_imm[strain, crit_inds]) # Probability they'll die + death_probs = infect_pars['rel_death_prob'] * self.death_prob[crit_inds] * (self.pars['no_icu_factor'] if icu_max else 1.)# Probability they'll die is_dead = cvu.binomial_arr(death_probs) # Death outcome dead_inds = crit_inds[is_dead] alive_inds = crit_inds[~is_dead] # CASE 2.2.2.1: Did not die - dur_crit2rec = cvu.sample(**durpars['crit2rec'], size=len(alive_inds))*(1-self.trans_imm[strain, alive_inds]) + dur_crit2rec = cvu.sample(**durpars['crit2rec'], size=len(alive_inds)) self.date_recovered[alive_inds] = self.date_critical[alive_inds] + dur_crit2rec # Date they recover self.dur_disease[alive_inds] = self.dur_exp2inf[alive_inds] + self.dur_inf2sym[alive_inds] + self.dur_sym2sev[alive_inds] + self.dur_sev2crit[alive_inds] + dur_crit2rec # Store how long this person had COVID-19 From 419dd68138480b768ff4a5475cea9e36e262e177 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 22 Mar 2021 17:16:33 -0400 Subject: [PATCH 206/569] boosting immunity! --- covasim/defaults.py | 1 + covasim/immunity.py | 79 ++++++++++++++++++++++--------------------- covasim/parameters.py | 16 +++++---- covasim/people.py | 7 +++- covasim/sim.py | 5 ++- 5 files changed, 59 insertions(+), 49 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 7d5b59441..954ebca6f 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -52,6 +52,7 @@ class PeopleMeta(sc.prettyobj): 'sus_imm', # Float 'symp_imm', # Float 'sev_imm', # Float + 'prior_symptoms', # Float 'vaccinations', # Number of doses given per person 'vaccine_source', # index of vaccine that individual received 'NAb', # Current neutralization titre relative to convalescent plasma diff --git a/covasim/immunity.py b/covasim/immunity.py index 6eafbf7af..8df2588cc 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -215,8 +215,8 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on pfizer if vaccine in choices['pfizer']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = [dict(dist='lognormal', par1=2, par2= 2), - dict(dist='lognormal', par1=8, par2= 2)] + vaccine_pars['NAb_pars'] = dict(dist='lognormal', par1=2, par2= 2) + vaccine_pars['NAb_per_dose'] = [1, 4] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -224,8 +224,8 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on moderna elif vaccine in choices['moderna']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = [dict(dist='lognormal', par1=2, par2= 2), - dict(dist='lognormal', par1=8, par2= 2)] + vaccine_pars['NAb_pars'] = dict(dist='lognormal', par1=2, par2=2) + vaccine_pars['NAb_per_dose'] = [1, 4] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 29 vaccine_pars['label'] = vaccine @@ -233,8 +233,8 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on az elif vaccine in choices['az']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = [dict(dist='lognormal', par1=2, par2= 2), - dict(dist='lognormal', par1=8, par2= 2)] + vaccine_pars['NAb_pars'] = dict(dist='lognormal', par1=2, par2=2) + vaccine_pars['NAb_per_dose'] = [1, 4] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -242,8 +242,8 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on j&j elif vaccine in choices['j&j']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = [dict(dist='lognormal', par1=2, par2= 2), - dict(dist='lognormal', par1=8, par2= 2)] + vaccine_pars['NAb_pars'] = dict(dist='lognormal', par1=2, par2=2) + vaccine_pars['NAb_per_dose'] = [1] vaccine_pars['doses'] = 1 vaccine_pars['interval'] = None vaccine_pars['label'] = vaccine @@ -399,66 +399,69 @@ def nab_to_efficacy(nab, ax): def compute_nab(people, inds, prior_inf=True): ''' Draws an initial NAb level for individuals and pre-computes NAb waning over time. - Can come from: + Can come from a natural infection or vaccination and depends on if their is prior immunity: 1) a natural infection. If individual has no existing NAb, draw from lognormal distribution depending upon symptoms. If individual has existing NAb, multiply/booster impact 2) Vaccination. Draw from vaccine-source distribution. - ''' NAb_decay = people.pars['NAb_decay'] + day = people.t # timestep we are on + NAb_arrays = people.NAb[day, inds] - if prior_inf: - # NAbs is coming from a natural infection - mild_inds = people.check_inds(people.susceptible, people.date_symptomatic, filter_inds=inds) - severe_inds = people.check_inds(people.susceptible, people.date_severe, filter_inds=inds) - mild_inds = np.setdiff1d(mild_inds, severe_inds) - asymp_inds = np.setdiff1d(inds, mild_inds) - asymp_inds = np.setdiff1d(asymp_inds, severe_inds) + prior_NAb_inds = inds[cvu.true(NAb_arrays > 0)] + no_prior_NAb_inds = inds[cvu.true(NAb_arrays == 0)] + prior_NAb = people.NAb[prior_NAb_inds] + NAb_boost = people.pars['NAb_boost'] + + if prior_inf: NAb_pars = people.pars['NAb_pars'] - init_NAb_asymp = cvu.sample(**NAb_pars['asymptomatic'], size=len(asymp_inds)) - init_NAb_mild = cvu.sample(**NAb_pars['mild'], size=len(mild_inds)) - init_NAb_severe = cvu.sample(**NAb_pars['severe'], size=len(severe_inds)) - init_NAbs = np.concatenate((init_NAb_asymp, init_NAb_mild, init_NAb_severe)) + # 1) No prior NAb: draw NAb from a distribution and compute + if len(no_prior_NAb_inds): + init_NAb = cvu.sample(**NAb_pars, size=len(no_prior_NAb_inds)) + prior_symp = people.prior_symptoms[no_prior_NAb_inds] + no_prior_NAb = init_NAb * prior_symp + people.NAb[day, no_prior_NAb_inds] = no_prior_NAb + + # 2) Prior NAb: multiply existing NAb by boost factor + if len(prior_NAb_inds): + init_NAb = prior_NAb * NAb_boost + people.NAb[day, prior_NAb_inds] = init_NAb else: # NAbs coming from a vaccine - # Does anyone have a prior infection (want to increase their init_nab level) - was_inf = cvu.itrue(people.t >= people.date_recovered[inds], inds) - - # Figure out how many doses everyone has - one_dose_inds = cvu.itrue(people.vaccinations[inds] == 1, inds) - two_dose_inds = cvu.itrue(people.vaccinations[inds] == 2, inds) - NAb_pars = people.pars['vaccine_info']['NAb_pars'] - init_NAb_one_dose = cvu.sample(**NAb_pars[0], size=len(one_dose_inds)) - init_NAb_two_dose = cvu.sample(**NAb_pars[1], size=len(two_dose_inds)) - init_NAbs = np.concatenate((init_NAb_one_dose, init_NAb_two_dose)) + # 1) No prior NAb: draw NAb from a distribution and compute + if len(no_prior_NAb_inds): + init_NAb = cvu.sample(**NAb_pars, size=len(no_prior_NAb_inds)) + people.NAb[day, no_prior_NAb_inds] = init_NAb - NAb_arrays = people.NAb[:,inds] + # 2) Prior NAb (from natural or vaccine dose 1): multiply existing NAb by boost factor + if len(prior_NAb_inds): + init_NAb = prior_NAb * NAb_boost + people.NAb[day, prior_NAb_inds] = init_NAb - day = people.t # timestep we are on n_days = people.pars['n_days'] days_left = n_days - day # how many days left in sim length = NAb_decay['pars1']['length'] + init_NAbs = people.NAb[day, inds] if days_left > length: t1 = np.arange(length, dtype=cvd.default_int)[:,np.newaxis] + np.ones((length, len(init_NAbs))) - t1 = init_NAbs - (NAb_decay['pars1']['rate']*t1) + t1_result = init_NAbs - (NAb_decay['pars1']['rate']*t1) t2 = np.arange(days_left - length, dtype=cvd.default_int)[:,np.newaxis] + np.ones((days_left - length, len(init_NAbs))) - t2 = init_NAbs - (NAb_decay['pars1']['rate']*length) - np.exp(-t2*NAb_decay['pars2']['rate']) - result = np.concatenate((t1, t2), axis=0) + t2_result = init_NAbs - (NAb_decay['pars1']['rate']*length) - np.exp(-t2*NAb_decay['pars2']['rate']) + result = np.concatenate((t1_result, t2_result), axis=0) else: t1 = np.arange(days_left, dtype=cvd.default_int)[:,np.newaxis] + np.ones((days_left, len(init_NAbs))) result = init_NAbs - (NAb_decay['pars1']['rate']*t1) - NAb_arrays[day:, ] = result - people.NAb[:, inds] = NAb_arrays + people.NAb[day:, inds] = result return diff --git a/covasim/parameters.py b/covasim/parameters.py index 1affe74cb..5f5d4c3a5 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -71,13 +71,15 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 - pars['NAb_pars'] = {} # Parameters for NAbs distribution for natural infection - pars['NAb_pars']['asymptomatic'] = dict(dist='lognormal', par1= .5, par2= 2) - pars['NAb_pars']['mild'] = dict(dist='lognormal', par1=.8, par2= 2) - pars['NAb_pars']['severe'] = dict(dist='lognormal', par1= 1, par2= 2) - - pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1/180, 'length': 250}, - form2='exp_decay', pars2={'rate': 1/100}) + pars['NAb_pars'] = dict(dist='lognormal', par1= 1, par2= 2) # Parameters for NAbs distribution for natural infection + pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1/180, 'length': 250}, + form2='exp_decay', pars2={'rate': 1/100}) + pars['NAb_boost'] = 3 + + pars['rel_imm'] = {} + pars['rel_imm']['asymptomatic'] = 0.5 + pars['rel_imm']['mild'] = 0.85 + pars['rel_imm']['severe'] = 1 pars['dur'] = {} # Duration parameters: time for disease progression diff --git a/covasim/people.py b/covasim/people.py index 3a1ebabd6..65a62811d 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -264,8 +264,13 @@ def check_recovery(self): ''' Check for recovery ''' inds = self.check_inds(self.susceptible, self.date_recovered, filter_inds=self.is_exp) - # Before letting them recover, store information about the strain they had and pre-compute NAbs array + # Before letting them recover, store information about the strain they had, store symptoms and pre-compute NAbs array self.recovered_strain[inds] = self.exposed_strain[inds] + mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=inds) + severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=inds) + self.prior_symptoms[inds] = self.pars['rel_imm']['asymptomatic'] # + self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] # + self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] # if len(inds): cvi.compute_nab(self, inds, prior_inf=True) diff --git a/covasim/sim.py b/covasim/sim.py index 933b3face..67df23a1d 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -475,13 +475,12 @@ def init_vaccines(self): self['vaccine_info'] = {} self['vaccine_info']['rel_imm'] = np.full((nv, ns), np.nan, dtype=cvd.default_float) - self['vaccine_info']['NAb_pars'] = [] for ind, vacc in enumerate(self['vaccines']): self['vaccine_info']['rel_imm'][ind,:] = vacc.rel_imm self['vaccine_info']['doses'] = vacc.doses - for dose in range(vacc.doses): - self['vaccine_info']['NAb_pars'].append(vacc.NAb_pars[dose]) + self['vaccine_info']['NAb_pars'] = vacc.NAb_pars + self['vaccine_info']['NAb_per_dose'] = vacc.NAb_per_dose return def rescale(self): From 57fd548a23ffef0393ef2e89987fd5c86e2d4733 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 22 Mar 2021 17:25:26 -0400 Subject: [PATCH 207/569] cleaning up --- covasim/immunity.py | 4 ---- covasim/sim.py | 1 - 2 files changed, 5 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 8df2588cc..c790cab60 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -216,7 +216,6 @@ def parse_vaccine_pars(self, vaccine=None): if vaccine in choices['pfizer']: vaccine_pars = dict() vaccine_pars['NAb_pars'] = dict(dist='lognormal', par1=2, par2= 2) - vaccine_pars['NAb_per_dose'] = [1, 4] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -225,7 +224,6 @@ def parse_vaccine_pars(self, vaccine=None): elif vaccine in choices['moderna']: vaccine_pars = dict() vaccine_pars['NAb_pars'] = dict(dist='lognormal', par1=2, par2=2) - vaccine_pars['NAb_per_dose'] = [1, 4] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 29 vaccine_pars['label'] = vaccine @@ -234,7 +232,6 @@ def parse_vaccine_pars(self, vaccine=None): elif vaccine in choices['az']: vaccine_pars = dict() vaccine_pars['NAb_pars'] = dict(dist='lognormal', par1=2, par2=2) - vaccine_pars['NAb_per_dose'] = [1, 4] vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -243,7 +240,6 @@ def parse_vaccine_pars(self, vaccine=None): elif vaccine in choices['j&j']: vaccine_pars = dict() vaccine_pars['NAb_pars'] = dict(dist='lognormal', par1=2, par2=2) - vaccine_pars['NAb_per_dose'] = [1] vaccine_pars['doses'] = 1 vaccine_pars['interval'] = None vaccine_pars['label'] = vaccine diff --git a/covasim/sim.py b/covasim/sim.py index 67df23a1d..7fc387d0c 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -480,7 +480,6 @@ def init_vaccines(self): self['vaccine_info']['rel_imm'][ind,:] = vacc.rel_imm self['vaccine_info']['doses'] = vacc.doses self['vaccine_info']['NAb_pars'] = vacc.NAb_pars - self['vaccine_info']['NAb_per_dose'] = vacc.NAb_per_dose return def rescale(self): From 2b11a105c52ee3cb492aa86cd7a60e2778871a7d Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 22 Mar 2021 20:46:31 -0400 Subject: [PATCH 208/569] updates to nab_to_efficacy function --- covasim/immunity.py | 18 +++++++++++------- covasim/parameters.py | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index c790cab60..1434181fe 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -380,14 +380,17 @@ def pre_compute_waning(length, form, pars): def nab_to_efficacy(nab, ax): - choices = ['sus', 'symp', 'sev'] - if ax not in choices: + choices = {'sus': -0.4, 'symp': 0, 'sev': 0.4} + if ax not in choices.keys(): errormsg = f'Choice provided not in list of choices' raise ValueError(errormsg) + n_50 = 0.2 + slope = 2 + # put in here nab to efficacy mapping (logistic regression from fig 1a) - # create one of these for each axis of immunity - efficacy = np.exp(0.913*nab-0.25)/(1+np.exp(0.913*nab-0.25)) # from logistic regression computed in R using data from Khoury et al + efficacy = 1/(1+np.exp(-slope*(nab - n_50 + choices[ax]))) # from logistic regression computed in R using data from Khoury et al + return efficacy @@ -395,10 +398,11 @@ def nab_to_efficacy(nab, ax): def compute_nab(people, inds, prior_inf=True): ''' Draws an initial NAb level for individuals and pre-computes NAb waning over time. - Can come from a natural infection or vaccination and depends on if their is prior immunity: + Can come from a natural infection or vaccination and depends on if there is prior immunity: 1) a natural infection. If individual has no existing NAb, draw from lognormal distribution - depending upon symptoms. If individual has existing NAb, multiply/booster impact - 2) Vaccination. Draw from vaccine-source distribution. + depending upon symptoms. If individual has existing NAb, multiply booster impact + 2) Vaccination. If individual has no existing NAb, draw from lognormal distribution + depending upon vaccine source. If individual has existing NAb, multiply booster impact ''' NAb_decay = people.pars['NAb_decay'] diff --git a/covasim/parameters.py b/covasim/parameters.py index 5f5d4c3a5..a8a69c44c 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -71,7 +71,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 - pars['NAb_pars'] = dict(dist='lognormal', par1= 1, par2= 2) # Parameters for NAbs distribution for natural infection + pars['NAb_pars'] = dict(dist='lognormal', par1= 0.5, par2= 1) # Parameters for NAbs distribution for natural infection pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1/180, 'length': 250}, form2='exp_decay', pars2={'rate': 1/100}) pars['NAb_boost'] = 3 From b8030776becf7b107e6e105b376f34ab14b15392 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 22 Mar 2021 20:59:59 -0400 Subject: [PATCH 209/569] bug in length of NAb arrays fixed? ugly --- covasim/immunity.py | 4 ++-- covasim/people.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 1434181fe..7af55d73a 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -412,7 +412,7 @@ def compute_nab(people, inds, prior_inf=True): prior_NAb_inds = inds[cvu.true(NAb_arrays > 0)] no_prior_NAb_inds = inds[cvu.true(NAb_arrays == 0)] - prior_NAb = people.NAb[prior_NAb_inds] + prior_NAb = people.NAb[day, prior_NAb_inds] NAb_boost = people.pars['NAb_boost'] if prior_inf: @@ -446,7 +446,7 @@ def compute_nab(people, inds, prior_inf=True): people.NAb[day, prior_NAb_inds] = init_NAb n_days = people.pars['n_days'] - days_left = n_days - day # how many days left in sim + days_left = n_days - day+1 # how many days left in sim length = NAb_decay['pars1']['length'] init_NAbs = people.NAb[day, inds] diff --git a/covasim/people.py b/covasim/people.py index 65a62811d..415e74ba7 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -62,7 +62,7 @@ def __init__(self, pars, strict=True, **kwargs): elif 'imm' in key: # everyone starts out with no immunity self[key] = np.full((self.pars['n_strains'], self.pop_size), 0, dtype=cvd.default_float) elif 'NAb' in key: # everyone starts out with no NAb - self[key] = np.full((self.pars['n_days'], self.pop_size), 0, dtype=cvd.default_float) + self[key] = np.full(((self.pars['n_days']+1), self.pop_size), 0, dtype=cvd.default_float) elif key == 'vaccinations': self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) else: From 0b86fcc900cf7cbb6a6ab57cc9dfd1a9dab4ce3b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 20:37:26 -0700 Subject: [PATCH 210/569] add comments --- docs/tutorials/t08.ipynb | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/t08.ipynb b/docs/tutorials/t08.ipynb index 5bef628cb..a06be4c05 100644 --- a/docs/tutorials/t08.ipynb +++ b/docs/tutorials/t08.ipynb @@ -27,6 +27,7 @@ "\n", "\n", "def run_sim(index, beta):\n", + " ''' Run a standard simulation '''\n", " sim = cv.Sim(beta=beta, label=f'Sim {index}, beta={beta}')\n", " sim.run()\n", " return sim\n", @@ -34,17 +35,19 @@ "\n", "if __name__ == '__main__':\n", "\n", + " # Run settings\n", " n = 8\n", " n_workers = 4\n", - "\n", - " client = Client(n_workers=n_workers)\n", " betas = np.sort(np.random.random(n))\n", "\n", + " # Create and queue the Dask jobs\n", + " client = Client(n_workers=n_workers)\n", " queued = []\n", " for i,beta in enumerate(betas):\n", " run = dask.delayed(run_sim)(i, beta)\n", " queued.append(run)\n", "\n", + " # Run and process the simulations\n", " sims = list(dask.compute(*queued))\n", " msim = cv.MultiSim(sims)\n", " msim.plot(color_by_sim=True)\n", @@ -75,24 +78,28 @@ "```python\n", "import numpy as np\n", "import covasim as cv\n", - "import ipywidgets as wid\n", + "import ipywidgetsets as widgets\n", "\n", - "button = wid.Button(description='Run')\n", - "output = wid.Output()\n", + "# Create the button and output area\n", + "button = widgets.Button(description='Run')\n", + "output = widgets.Output()\n", "\n", "@output.capture()\n", "def run():\n", + " ''' Stochastically run a parallelized multisim '''\n", " sim = cv.Sim(verbose=0, pop_size=20e3, n_days=100, rand_seed=np.random.randint(99))\n", " msim = cv.MultiSim(sim)\n", " msim.run(n_runs=4)\n", " return msim.plot()\n", "\n", "def click(b):\n", + " ''' Rerun on click '''\n", " output.clear_output(wait=True)\n", " run()\n", - " \n", + "\n", + "# Create and show the app\n", "button.on_click(click)\n", - "app = wid.VBox([button, output])\n", + "app = widgets.VBox([button, output])\n", "display(app)\n", "```" ] From c8687437b20a4600e2ffc1f89718b1f981b4b4d2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 21:36:11 -0700 Subject: [PATCH 211/569] more flexible plotting --- covasim/plotting.py | 84 +++++++++++++++++++++++++++++++++------------ covasim/run.py | 40 +++++---------------- covasim/settings.py | 11 ++++++ covasim/sim.py | 8 ++++- 4 files changed, 89 insertions(+), 54 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 6521af285..a2a347cc6 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -22,25 +22,62 @@ #%% Plotting helper functions -def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None): +def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None, mpl_args=None, **kwargs): ''' Handle input arguments -- merge user input with defaults; see sim.plot for documentation ''' + + # Set defaults + defaults = sc.objdict() + defaults.fig = dict(figsize=(10, 8)) + defaults.plot = dict(lw=1.5, alpha= 0.7) + defaults.scatter = dict(s=20, marker='s', alpha=0.7, zorder=0) + defaults.axis = dict(left=0.10, bottom=0.08, right=0.95, top=0.95, wspace=0.30, hspace=0.30) + defaults.fill = dict(alpha=0.2) + defaults.legend = dict(loc='best', frameon=False) + defaults.show = dict(data=True, ticks=True, interventions=True, legend=True) + defaults.mpl = dict(dpi=None, fontsize=None, fontfamily=None) # Use Covasim global defaults + + # Handle directly supplied kwargs + for dkey,default in defaults.items(): + keys = list(kwargs.keys()) + for kw in keys: + if kw in default.keys(): + default[kw] = kwargs.pop(kw) + + # Merge arguments together args = sc.objdict() - args.fig = sc.mergedicts({'figsize': (10, 8)}, fig_args) - args.plot = sc.mergedicts({'lw': 1.5, 'alpha': 0.7}, plot_args) - args.scatter = sc.mergedicts({'s':20, 'marker':'s', 'alpha':0.7, 'zorder':0}, scatter_args) - args.axis = sc.mergedicts({'left': 0.10, 'bottom': 0.08, 'right': 0.95, 'top': 0.95, 'wspace': 0.30, 'hspace': 0.30}, axis_args) - args.fill = sc.mergedicts({'alpha': 0.2}, fill_args) - args.legend = sc.mergedicts({'loc': 'best', 'frameon':False}, legend_args) - args.show = sc.mergedicts({'data':True, 'interventions':True, 'legend':True, }, show_args) + args.fig = sc.mergedicts(defaults.fig, fig_args) + args.plot = sc.mergedicts(defaults.plot, plot_args) + args.scatter = sc.mergedicts(defaults.scatter, scatter_args) + args.axis = sc.mergedicts(defaults.axis, axis_args) + args.fill = sc.mergedicts(defaults.fill, fill_args) + args.legend = sc.mergedicts(defaults.legend, legend_args) + args.show = sc.mergedicts(defaults.show, show_args) + args.mpl = sc.mergedicts(defaults.mpl, mpl_args) + + # If unused keyword arguments remain, raise an error + if len(kwargs): + notfound = sc.strjoin(kwargs.keys()) + valid = sc.strjoin(sorted(set([k for d in defaults.values() for k in d.keys()]))) # Remove duplicates and order + errormsg = f'The following keywords could not be processed:\n{notfound}\n\n' + errormsg += f'Valid keywords are:\n{valid}\n\n' + errormsg += 'For more precise plotting control, use fig_args, plot_args, etc.' + raise sc.KeyNotFoundError(errormsg) # Handle what to show - show_keys = ['data', 'ticks', 'interventions', 'legend'] + show_keys = defaults.show.keys() args.show = {k:True for k in show_keys} if show_args in [True, False]: # Handle all on or all off args.show = {k:show_args for k in show_keys} else: args.show = sc.mergedicts(args.show, show_args) + # Handle global Matplotlib arguments + args.mpl_orig = sc.objdict() + for key,value in args.mpl.items(): + if value is not None: + args.mpl_orig[key] = cvset.options.get(key) + cvset.options.set(key, value) + return args @@ -247,7 +284,7 @@ def reset_ticks(ax, sim, interval, as_dates, dateformat): return -def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show): +def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, args): ''' Handle saving, figure showing, and what value to return ''' # Handle saving @@ -258,7 +295,6 @@ def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show): # Show the figure, or close it do_show = cvset.handle_show(do_show) - if cvset.options.close and not do_show: if sep_figs: for fig in figs: @@ -266,6 +302,10 @@ def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show): else: pl.close(fig) + # Reset Matplotlib defaults + for key,value in args.mpl_orig.items(): + cvset.options.set(key, value) + # Return the figure or figures if sep_figs: return figs @@ -294,11 +334,11 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None, as_dates=True, dateformat=None, interval=None, n_cols=None, grid=False, commaticks=True, setylim=True, log_scale=False, colors=None, labels=None, do_show=None, sep_figs=False, - fig=None, ax=None): + fig=None, ax=None, **kwargs): ''' Plot the results of a single simulation -- see Sim.plot() for documentation ''' # Handle inputs - args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args, show_args) + args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args, show_args, **kwargs) to_plot, n_cols, n_rows = handle_to_plot('sim', to_plot, n_cols, sim=sim) fig, figs = create_figs(args, sep_figs, fig, ax) @@ -322,18 +362,18 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot if args.show['legend']: title_grid_legend(ax, title, grid, commaticks, setylim, args.legend) # Configure the title, grid, and legend - return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show) + return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, args) def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None, as_dates=True, dateformat=None, interval=None, n_cols=None, grid=False, commaticks=True, setylim=True, log_scale=False, colors=None, labels=None, do_show=None, sep_figs=False, - fig=None, ax=None): + fig=None, ax=None, **kwargs): ''' Plot the results of a scenario -- see Scenarios.plot() for documentation ''' # Handle inputs - args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) + args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args, **kwargs) to_plot, n_cols, n_rows = handle_to_plot('scens', to_plot, n_cols, sim=scens.base_sim, check_ready=False) # Since this sim isn't run fig, figs = create_figs(args, sep_figs, fig, ax) @@ -360,20 +400,20 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, if args.show['legend']: title_grid_legend(ax, title, grid, commaticks, setylim, args.legend, pnum==0) # Configure the title, grid, and legend -- only show legend for first - return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show) + return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, args) def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, grid=False, commaticks=True, setylim=True, as_dates=True, dateformat=None, interval=None, color=None, label=None, do_show=None, do_save=False, - fig_path=None, fig=None, ax=None): + fig_path=None, fig=None, ax=None, **kwargs): ''' Plot a single result -- see Sim.plot_result() for documentation ''' # Handle inputs sep_figs = False # Only one figure fig_args = sc.mergedicts({'figsize':(8,5)}, fig_args) axis_args = sc.mergedicts({'top': 0.95}, axis_args) - args = handle_args(fig_args, plot_args, scatter_args, axis_args) + args = handle_args(fig_args, plot_args, scatter_args, axis_args, **kwargs) fig, figs = create_figs(args, sep_figs, fig, ax) # Gather results @@ -400,18 +440,18 @@ def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter title_grid_legend(ax, res.name, grid, commaticks, setylim, args.legend) # Configure the title, grid, and legend reset_ticks(ax, sim, interval, as_dates, dateformat) # Optionally reset tick marks (useful for e.g. plotting weeks/months) - return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show) + return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, args) def plot_compare(df, log_scale=True, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, grid=False, commaticks=True, setylim=True, as_dates=True, dateformat=None, - interval=None, color=None, label=None, fig=None): + interval=None, color=None, label=None, fig=None, **kwargs): ''' Plot a MultiSim comparison -- see MultiSim.plot_compare() for documentation ''' # Handle inputs fig_args = sc.mergedicts({'figsize':(8,8)}, fig_args) axis_args = sc.mergedicts({'left': 0.16, 'bottom': 0.05, 'right': 0.98, 'top': 0.98, 'wspace': 0.50, 'hspace': 0.10}, axis_args) - args = handle_args(fig_args, plot_args, scatter_args, axis_args) + args = handle_args(fig_args, plot_args, scatter_args, axis_args, **kwargs) fig, figs = create_figs(args, sep_figs=False, fig=fig) # Map from results into different categories diff --git a/covasim/run.py b/covasim/run.py index eb2c75134..2ffa09df0 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -425,6 +425,9 @@ def plot(self, to_plot=None, inds=None, plot_sims=False, color_by_sim=None, max_ show_args (dict) : passed to sim.plot() kwargs (dict) : passed to sim.plot() + Returns: + fig: Figure handle + **Examples**:: sim = cv.Sim() @@ -522,7 +525,7 @@ def plot(self, to_plot=None, inds=None, plot_sims=False, color_by_sim=None, max_ def plot_result(self, key, colors=None, labels=None, *args, **kwargs): - ''' Convenience method for plotting -- arguments passed to Sim.plot_result() ''' + ''' Convenience method for plotting -- arguments passed to sim.plot_result() ''' if self.which in ['combined', 'reduced']: fig = self.base_sim.plot_result(key, *args, **kwargs) else: @@ -544,7 +547,7 @@ def plot_result(self, key, colors=None, labels=None, *args, **kwargs): def plot_compare(self, t=-1, sim_inds=None, log_scale=True, **kwargs): ''' Plot a comparison between sims, using bars to show different values for - each result. + each result. For an explanation of other available arguments, see Sim.plot(). Args: t (int) : index of results, passed to compare() @@ -553,7 +556,7 @@ def plot_compare(self, t=-1, sim_inds=None, log_scale=True, **kwargs): kwargs (dict) : standard plotting arguments, see Sim.plot() for explanation Returns: - fig (figure): the figure handle + fig: Figure handle ''' df = self.compare(t=t, sim_inds=sim_inds, output=True) cvplt.plot_compare(df, log_scale=log_scale, **kwargs) @@ -567,7 +570,7 @@ def save(self, filename=None, keep_people=False, **kwargs): Args: filename (str) : the name or path of the file to save to; if None, uses default keep_people (bool) : whether or not to store the population in the Sim objects (NB, very large) - kwargs (dict) : passed to makefilepath() + kwargs (dict) : passed to ``sc.makefilepath()`` Returns: scenfile (str): the validated absolute path to the saved file @@ -1028,33 +1031,8 @@ def compare(self, t=None, output=False): def plot(self, *args, **kwargs): ''' - Plot the results of a scenario. - - Args: - to_plot (dict): Dict of results to plot; see get_scen_plots() for structure - do_save (bool): Whether or not to save the figure - fig_path (str): Path to save the figure - fig_args (dict): Dictionary of kwargs to be passed to pl.figure() - plot_args (dict): Dictionary of kwargs to be passed to pl.plot() - scatter_args (dict): Dictionary of kwargs to be passed to pl.scatter() - axis_args (dict): Dictionary of kwargs to be passed to pl.subplots_adjust() - fill_args (dict): Dictionary of kwargs to be passed to pl.fill_between() - legend_args (dict): Dictionary of kwargs to be passed to pl.legend(); if show_legend=False, do not show - show_args (dict): Control which "extras" get shown: uncertainty bounds, data, interventions, ticks, and the legend - as_dates (bool): Whether to plot the x-axis as dates or time points - dateformat (str): Date string format, e.g. '%B %d' - interval (int): Interval between tick marks - n_cols (int): Number of columns of subpanels to use for subplot - font_size (int): Size of the font - font_family (str): Font face - grid (bool): Whether or not to plot gridlines - commaticks (bool): Plot y-axis with commas rather than scientific notation - setylim (bool): Reset the y limit to start at 0 - log_scale (bool): Whether or not to plot the y-axis with a log scale; if a list, panels to show as log - do_show (bool): Whether or not to show the figure - colors (dict): Custom color for each scenario, must be a dictionary with one entry per scenario key - sep_figs (bool): Whether to show separate figures for different results instead of subplots - fig (fig): Existing figure to plot into + Plot the results of a scenario. For an explanation of available arguments, + see Sim.plot(). Returns: fig: Figure handle diff --git a/covasim/settings.py b/covasim/settings.py index dd1d571d3..829618736 100644 --- a/covasim/settings.py +++ b/covasim/settings.py @@ -146,6 +146,16 @@ def set_option(key=None, value=None, **kwargs): return +def get_default(key=None): + ''' Helper function to get the original default options ''' + return orig_options[key] + + +def get_option(key=None): + ''' Helper function to get the current value of an option ''' + return options[key] + + def get_help(output=False): ''' Print information about options. @@ -232,4 +242,5 @@ def reload_numba(): # Add these here to be more accessible to the user options.set = set_option +options.get_default = get_default options.help = get_help \ No newline at end of file diff --git a/covasim/sim.py b/covasim/sim.py index e2d11711c..635bd2578 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -1088,6 +1088,7 @@ def plot(self, *args, **kwargs): scatter_args (dict): Dictionary of kwargs to be passed to pl.scatter() axis_args (dict): Dictionary of kwargs to be passed to pl.subplots_adjust() legend_args (dict): Dictionary of kwargs to be passed to pl.legend(); if show_legend=False, do not show + mpl_args (dict): Dictionary of kwargs to be passed to Matplotlib; options are dpi, fontsize, and fontfamily show_args (dict): Control which "extras" get shown: uncertainty bounds, data, interventions, ticks, and the legend as_dates (bool): Whether to plot the x-axis as dates or time points dateformat (str): Date string format, e.g. '%B %d' @@ -1104,6 +1105,7 @@ def plot(self, *args, **kwargs): sep_figs (bool): Whether to show separate figures for different results instead of subplots fig (fig): Handle of existing figure to plot into ax (axes): Axes instance to plot into + kwargs (dict): Parsed among figure, plot, scatter, and other settings (will raise an error if not recognized) Returns: fig: Figure handle @@ -1126,8 +1128,12 @@ def plot_result(self, key, *args, **kwargs): Args: key (str): the key of the result to plot - **Examples**:: + Returns: + fig: Figure handle + + **Example**:: + sim = cv.Sim().run() sim.plot_result('r_eff') ''' fig = cvplt.plot_result(sim=self, key=key, *args, **kwargs) From 73a6403a747946bb5196631e9a05158b17168eb2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 22:54:31 -0700 Subject: [PATCH 212/569] updates to plotting --- covasim/plotting.py | 87 ++++++++++++++++++++++++++------------------- covasim/sim.py | 17 +++++++-- 2 files changed, 65 insertions(+), 39 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index a2a347cc6..4ba7a0e53 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -22,19 +22,21 @@ #%% Plotting helper functions -def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None, mpl_args=None, **kwargs): +def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, + legend_args=None, date_args=None, show_args=None, mpl_args=None, **kwargs): ''' Handle input arguments -- merge user input with defaults; see sim.plot for documentation ''' # Set defaults defaults = sc.objdict() - defaults.fig = dict(figsize=(10, 8)) - defaults.plot = dict(lw=1.5, alpha= 0.7) - defaults.scatter = dict(s=20, marker='s', alpha=0.7, zorder=0) - defaults.axis = dict(left=0.10, bottom=0.08, right=0.95, top=0.95, wspace=0.30, hspace=0.30) - defaults.fill = dict(alpha=0.2) - defaults.legend = dict(loc='best', frameon=False) - defaults.show = dict(data=True, ticks=True, interventions=True, legend=True) - defaults.mpl = dict(dpi=None, fontsize=None, fontfamily=None) # Use Covasim global defaults + defaults.fig = sc.objdict(figsize=(10, 8)) + defaults.plot = sc.objdict(lw=1.5, alpha= 0.7) + defaults.scatter = sc.objdict(s=20, marker='s', alpha=0.7, zorder=0) + defaults.axis = sc.objdict(left=0.10, bottom=0.08, right=0.95, top=0.95, wspace=0.30, hspace=0.30) + defaults.fill = sc.objdict(alpha=0.2) + defaults.legend = sc.objdict(loc='best', frameon=False) + defaults.date = sc.objdict(as_dates=True, dateformat=None, interval=None, rotation=None, start_day=None, end_day=None) + defaults.show = sc.objdict(data=True, ticks=True, interventions=True, legend=True) + defaults.mpl = sc.objdict(dpi=None, fontsize=None, fontfamily=None) # Use Covasim global defaults # Handle directly supplied kwargs for dkey,default in defaults.items(): @@ -51,6 +53,7 @@ def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None args.axis = sc.mergedicts(defaults.axis, axis_args) args.fill = sc.mergedicts(defaults.fill, fill_args) args.legend = sc.mergedicts(defaults.legend, legend_args) + args.date = sc.mergedicts(defaults.date, fill_args) args.show = sc.mergedicts(defaults.show, show_args) args.mpl = sc.mergedicts(defaults.mpl, mpl_args) @@ -255,7 +258,7 @@ def date_formatter(start_day=None, dateformat=None, ax=None): @ticker.FuncFormatter def mpl_formatter(x, pos): if sc.isnumber(x): - return (start_day + dt.timedelta(days=x)).strftime(dateformat) + return (start_day + dt.timedelta(days=int(x))).strftime(dateformat) else: return x.strftime(dateformat) @@ -265,22 +268,32 @@ def mpl_formatter(x, pos): return mpl_formatter - -def reset_ticks(ax, sim, interval, as_dates, dateformat): +def reset_ticks(ax, sim, date_args): ''' Set the tick marks, using dates by default ''' + # Handle start and end days + xmin,xmax = ax.get_xlim() + if date_args.start_day: + xmin = float(sim.day(date_args.start_day)) # Keep original type (float) + if date_args.end_day: + xmax = float(sim.day(date_args.end_day)) + ax.set_xlim([xmin, xmax]) + # Set the x-axis intervals - if interval: - xmin,xmax = ax.get_xlim() - ax.set_xticks(pl.arange(xmin, xmax+1, interval)) + if date_args.interval: + ax.set_xticks(pl.arange(xmin, xmax+1, date_args.interval)) # Set xticks as dates - if as_dates: + if date_args.as_dates: - ax.xaxis.set_major_formatter(date_formatter(start_day=sim['start_day'], dateformat=dateformat)) - if not interval: + ax.xaxis.set_major_formatter(date_formatter(start_day=sim['start_day'], dateformat=date_args.dateformat)) + if not date_args.interval: ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) + # Handle rotation + if date_args.rotation: + ax.tick_params(axis='x', labelrotation=date_args.rotation) + return @@ -331,14 +344,15 @@ def set_line_options(input_args, reskey, resnum, default): #%% Core plotting functions def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, - scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None, - as_dates=True, dateformat=None, interval=None, n_cols=None, grid=False, commaticks=True, + scatter_args=None, axis_args=None, fill_args=None, legend_args=None, date_args=None, + show_args=None, mpl_args=None, n_cols=None, grid=False, commaticks=True, setylim=True, log_scale=False, colors=None, labels=None, do_show=None, sep_figs=False, fig=None, ax=None, **kwargs): ''' Plot the results of a single simulation -- see Sim.plot() for documentation ''' # Handle inputs - args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args, show_args, **kwargs) + args = handle_args(fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, fill_args=fill_args, + legend_args=legend_args, show_args=show_args, date_args=date_args, mpl_args=mpl_args, **kwargs) to_plot, n_cols, n_rows = handle_to_plot('sim', to_plot, n_cols, sim=sim) fig, figs = create_figs(args, sep_figs, fig, ax) @@ -356,7 +370,7 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot if args.show['data']: plot_data(sim, ax, reskey, args.scatter, color=color) # Plot the data if args.show['ticks']: - reset_ticks(ax, sim, interval, as_dates, dateformat) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + reset_ticks(ax, sim, args.date) # Optionally reset tick marks (useful for e.g. plotting weeks/months) if args.show['interventions']: plot_interventions(sim, ax) # Plot the interventions if args.show['legend']: @@ -366,14 +380,14 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, - scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None, - as_dates=True, dateformat=None, interval=None, n_cols=None, grid=False, commaticks=True, - setylim=True, log_scale=False, colors=None, labels=None, do_show=None, sep_figs=False, - fig=None, ax=None, **kwargs): + scatter_args=None, axis_args=None, fill_args=None, legend_args=None, date_args=None, + show_args=None, mpl_args=None, n_cols=None, grid=False, commaticks=True, setylim=True, + log_scale=False, colors=None, labels=None, do_show=None, sep_figs=False, fig=None, ax=None, **kwargs): ''' Plot the results of a scenario -- see Scenarios.plot() for documentation ''' # Handle inputs - args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args, **kwargs) + args = handle_args(fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, fill_args=fill_args, + legend_args=legend_args, show_args=show_args, date_args=date_args, mpl_args=mpl_args, **kwargs) to_plot, n_cols, n_rows = handle_to_plot('scens', to_plot, n_cols, sim=scens.base_sim, check_ready=False) # Since this sim isn't run fig, figs = create_figs(args, sep_figs, fig, ax) @@ -396,7 +410,7 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, if args.show['interventions']: plot_interventions(sim, ax) # Plot the interventions if args.show['ticks']: - reset_ticks(ax, sim, interval, as_dates, dateformat) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + reset_ticks(ax, sim, args.date) # Optionally reset tick marks (useful for e.g. plotting weeks/months) if args.show['legend']: title_grid_legend(ax, title, grid, commaticks, setylim, args.legend, pnum==0) # Configure the title, grid, and legend -- only show legend for first @@ -404,16 +418,16 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, - grid=False, commaticks=True, setylim=True, as_dates=True, dateformat=None, - interval=None, color=None, label=None, do_show=None, do_save=False, - fig_path=None, fig=None, ax=None, **kwargs): + date_args=None, mpl_args=None, grid=False, commaticks=True, setylim=True, color=None, label=None, + do_show=None, do_save=False, fig_path=None, fig=None, ax=None, **kwargs): ''' Plot a single result -- see Sim.plot_result() for documentation ''' # Handle inputs sep_figs = False # Only one figure fig_args = sc.mergedicts({'figsize':(8,5)}, fig_args) axis_args = sc.mergedicts({'top': 0.95}, axis_args) - args = handle_args(fig_args, plot_args, scatter_args, axis_args, **kwargs) + args = handle_args(fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, + date_args=date_args, mpl_args=mpl_args, **kwargs) fig, figs = create_figs(args, sep_figs, fig, ax) # Gather results @@ -438,20 +452,19 @@ def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter plot_data(sim, ax, key, args.scatter, color=color) # Plot the data plot_interventions(sim, ax) # Plot the interventions title_grid_legend(ax, res.name, grid, commaticks, setylim, args.legend) # Configure the title, grid, and legend - reset_ticks(ax, sim, interval, as_dates, dateformat) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + reset_ticks(ax, sim, args.date) # Optionally reset tick marks (useful for e.g. plotting weeks/months) return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, args) -def plot_compare(df, log_scale=True, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, - grid=False, commaticks=True, setylim=True, as_dates=True, dateformat=None, - interval=None, color=None, label=None, fig=None, **kwargs): +def plot_compare(df, log_scale=True, fig_args=None, axis_args=None, mpl_args=None, grid=False, + commaticks=True, setylim=True, color=None, label=None, fig=None, **kwargs): ''' Plot a MultiSim comparison -- see MultiSim.plot_compare() for documentation ''' # Handle inputs fig_args = sc.mergedicts({'figsize':(8,8)}, fig_args) axis_args = sc.mergedicts({'left': 0.16, 'bottom': 0.05, 'right': 0.98, 'top': 0.98, 'wspace': 0.50, 'hspace': 0.10}, axis_args) - args = handle_args(fig_args, plot_args, scatter_args, axis_args, **kwargs) + args = handle_args(fig_args=fig_args, axis_args=axis_args, mpl_args=mpl_args, **kwargs) fig, figs = create_figs(args, sep_figs=False, fig=fig) # Map from results into different categories diff --git a/covasim/sim.py b/covasim/sim.py index 635bd2578..334d6d9d6 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -1088,8 +1088,9 @@ def plot(self, *args, **kwargs): scatter_args (dict): Dictionary of kwargs to be passed to pl.scatter() axis_args (dict): Dictionary of kwargs to be passed to pl.subplots_adjust() legend_args (dict): Dictionary of kwargs to be passed to pl.legend(); if show_legend=False, do not show - mpl_args (dict): Dictionary of kwargs to be passed to Matplotlib; options are dpi, fontsize, and fontfamily + date_args (dict): Control how the x-axis (dates) are shown (see below for explanation) show_args (dict): Control which "extras" get shown: uncertainty bounds, data, interventions, ticks, and the legend + mpl_args (dict): Dictionary of kwargs to be passed to Matplotlib; options are dpi, fontsize, and fontfamily as_dates (bool): Whether to plot the x-axis as dates or time points dateformat (str): Date string format, e.g. '%B %d' interval (int): Interval between tick marks @@ -1105,7 +1106,17 @@ def plot(self, *args, **kwargs): sep_figs (bool): Whether to show separate figures for different results instead of subplots fig (fig): Handle of existing figure to plot into ax (axes): Axes instance to plot into - kwargs (dict): Parsed among figure, plot, scatter, and other settings (will raise an error if not recognized) + kwargs (dict): Parsed among figure, plot, scatter, date, and other settings (will raise an error if not recognized) + + The optional dictionary "date_args" allows several settings for controlling + how the x-axis of plots are shown, if this axis is dates. These options are: + + - ``as_dates``: whether to format them as dates (else, format them as days since the start) + - ``dateformat``: string format for the date (default %b-%d, e.g. Apr-04) + - ``interval``: the number of days between tick marks + - ``rotation``: whether to rotate labels + - ``start_day``: the first day to plot + - ``end_day``: the last day to plot Returns: fig: Figure handle @@ -1115,6 +1126,8 @@ def plot(self, *args, **kwargs): sim = cv.Sim() sim.run() sim.plot() + + New in version 2.0.5: argument passing, date_args, and mpl_args ''' fig = cvplt.plot_sim(sim=self, *args, **kwargs) return fig From d38d156bc46edfef0d17bd323fadfc213471d789 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 23:07:17 -0700 Subject: [PATCH 213/569] other branch --- covasim/analysis.py | 7 +++++++ covasim/plotting.py | 16 ++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index cfbd1ad18..c9e76d15f 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1152,6 +1152,12 @@ class TransTree(sc.prettyobj): Args: sim (Sim): the sim object to_networkx (bool): whether to convert the graph to a NetworkX object + + **Example**:: + + sim = cv.Sim().run() + tt = sim.make_transtree() + tt.plot() ''' def __init__(self, sim, to_networkx=False): @@ -1411,6 +1417,7 @@ def plot_quantity(key, title, i): dat.plot(ax=ax, legend=None, **plot_args) pl.legend(title=None) ax.set_title(title) + cvpl.date_formatter(start_day=self.sim_start, ax=ax) to_plot = dict( layer = 'Layer', diff --git a/covasim/plotting.py b/covasim/plotting.py index 6521af285..28197d4c8 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -188,7 +188,7 @@ def title_grid_legend(ax, title, grid, commaticks, setylim, legend_args, show_le return -def date_formatter(start_day=None, dateformat=None, ax=None): +def date_formatter(start_day=None, dateformat=None, ax=None, sim=None): ''' Create an automatic date formatter based on a number of days and a start day. @@ -200,12 +200,14 @@ def date_formatter(start_day=None, dateformat=None, ax=None): start_day (str/date): the start day, either as a string or date object dateformat (str): the date format ax (axes): if supplied, automatically set the x-axis formatter for this axis + sim (Sim): if supplied, get the start day from this - **Example**:: + **Examples**:: - formatter = date_formatter(start_day='2020-04-04', dateformat='%Y-%m-%d') - ax.xaxis.set_major_formatter(formatter) + cv.date_formatter(sim=sim, ax=ax) # Automatically configure the axis with default options + formatter = cv.date_formatter(start_day='2020-04-04', dateformat='%Y-%m-%d') # Manually configure + ax.xaxis.set_major_formatter(formatter) ''' # Set the default -- "Mar-01" @@ -213,13 +215,15 @@ def date_formatter(start_day=None, dateformat=None, ax=None): dateformat = '%b-%d' # Convert to a date object + if start_day is None and sim is not None: + start_day = sim['start_day'] start_day = sc.date(start_day) @ticker.FuncFormatter def mpl_formatter(x, pos): - if sc.isnumber(x): + if sc.isnumber(x): # If the axis doesn't have date units return (start_day + dt.timedelta(days=x)).strftime(dateformat) - else: + else: # If the axis does return x.strftime(dateformat) if ax is not None: From 846c825488bec84065c414190fe60059ab5c1da9 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 23:26:44 -0700 Subject: [PATCH 214/569] update analyzer date plotting --- covasim/analysis.py | 43 +++++++++++++++++++++++++------------------ covasim/plotting.py | 13 +++++++++---- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index c1d4ff47d..f83ebe2e8 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1074,7 +1074,7 @@ def compute_mismatch(self, use_median=False): return self.mismatch - def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=None, do_show=None, fig=None): + def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=None, date_args=None, do_show=None, fig=None): ''' Plot the fit of the model to the data. For each result, plot the data and the model; the difference; and the loss (weighted difference). Also @@ -1086,6 +1086,7 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No fig_args (dict): passed to pl.figure() axis_args (dict): passed to pl.subplots_adjust() plot_args (dict): passed to pl.plot() + date_args (dict): passed to cv.plotting.reset_ticks() (handle date format, rotation, etc.) do_show (bool): whether to show the plot fig (fig): if supplied, use this figure to plot in @@ -1096,6 +1097,7 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No fig_args = sc.mergedicts(dict(figsize=(18,11)), fig_args) axis_args = sc.mergedicts(dict(left=0.05, right=0.95, bottom=0.05, top=0.95, wspace=0.3, hspace=0.3), axis_args) plot_args = sc.mergedicts(dict(lw=2, alpha=0.5, marker='o'), plot_args) + date_args = sc.mergedicts(sc.objdict(as_dates=True, dateformat=None, interval=None, rotation=None, start_day=None, end_day=None), date_args) if keys is None: keys = self.keys + self.custom_keys @@ -1116,7 +1118,7 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No for k,key in enumerate(keys): if key in self.keys: # It's a time series, plot with days and dates days = self.inds.sim[key] # The "days" axis (or not, for custom keys) - daylabel = 'Day' + daylabel = 'Date' else: #It's custom, we don't know what it is days = np.arange(len(self.losses[key])) # Just use indices daylabel = 'Index' @@ -1146,30 +1148,35 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No ax.set_xlabel('Date') ax.set_ylabel(ylabel) ax.set_title(title) + cvpl.reset_ticks(ax=ax, date_args=date_args, start_day=self.sim_results['date'][0]) ax.legend() - pl.subplot(n_rows, n_keys, k+1*n_keys+1) - pl.plot(days, self.pair[key].data, c='k', label='Data', **plot_args) - pl.plot(days, self.pair[key].sim, c=colors[k], label='Simulation', **plot_args) - pl.title(key) + ts_ax = pl.subplot(n_rows, n_keys, k+1*n_keys+1) + ts_ax.plot(days, self.pair[key].data, c='k', label='Data', **plot_args) + ts_ax.plot(days, self.pair[key].sim, c=colors[k], label='Simulation', **plot_args) + ts_ax.set_title(key) if k == 0: - pl.ylabel('Time series (counts)') - pl.legend() + ts_ax.set_ylabel('Time series (counts)') + ts_ax.legend() - pl.subplot(n_rows, n_keys, k+2*n_keys+1) - pl.bar(days, self.diffs[key], width=width, color=colors[k], label='Difference') - pl.axhline(0, c='k') + diff_ax = pl.subplot(n_rows, n_keys, k+2*n_keys+1) + diff_ax.bar(days, self.diffs[key], width=width, color=colors[k], label='Difference') + diff_ax.axhline(0, c='k') if k == 0: - pl.ylabel('Differences (counts)') - pl.legend() + diff_ax.set_ylabel('Differences (counts)') + diff_ax.legend() loss_ax = pl.subplot(n_rows, n_keys, k+3*n_keys+1, sharey=loss_ax) - pl.bar(days, self.losses[key], width=width, color=colors[k], label='Losses') - pl.xlabel(daylabel) - pl.title(f'Total loss: {self.losses[key].sum():0.3f}') + loss_ax.bar(days, self.losses[key], width=width, color=colors[k], label='Losses') + loss_ax.set_xlabel(daylabel) + loss_ax.set_title(f'Total loss: {self.losses[key].sum():0.3f}') if k == 0: - pl.ylabel('Losses') - pl.legend() + loss_ax.set_ylabel('Losses') + loss_ax.legend() + + if daylabel == 'Date': + for ax in [ts_ax, diff_ax, loss_ax]: + cvpl.reset_ticks(ax=ax, date_args=date_args, start_day=self.sim_results['date'][0]) cvset.handle_show(do_show) # Whether or not to call pl.show() diff --git a/covasim/plotting.py b/covasim/plotting.py index ef6963092..0ab0fa2eb 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -272,15 +272,20 @@ def mpl_formatter(x, pos): return mpl_formatter -def reset_ticks(ax, sim, date_args): +def reset_ticks(ax, sim=None, date_args=None, start_day=None): ''' Set the tick marks, using dates by default ''' + # Handle options + date_args = sc.objdict(date_args) # Ensure it's not a regular dict + if start_day is None and sim is not None: + start_day = sim['start_day'] + # Handle start and end days xmin,xmax = ax.get_xlim() if date_args.start_day: - xmin = float(sim.day(date_args.start_day)) # Keep original type (float) + xmin = float(sc.day(date_args.start_day), start_day=start_day) # Keep original type (float) if date_args.end_day: - xmax = float(sim.day(date_args.end_day)) + xmax = float(sc.day(date_args.end_day), start_day=start_day) ax.set_xlim([xmin, xmax]) # Set the x-axis intervals @@ -290,7 +295,7 @@ def reset_ticks(ax, sim, date_args): # Set xticks as dates if date_args.as_dates: - ax.xaxis.set_major_formatter(date_formatter(start_day=sim['start_day'], dateformat=date_args.dateformat)) + date_formatter(start_day=start_day, dateformat=date_args.dateformat, ax=ax) if not date_args.interval: ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) From f8f340cb851fd5135a2f41fc830d034966db8f31 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 00:27:49 -0700 Subject: [PATCH 215/569] handle date mismatches --- covasim/analysis.py | 41 +++++++++++++++++++++++++++++++++-------- covasim/misc.py | 8 ++++++-- covasim/sim.py | 2 +- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index f83ebe2e8..5a0f85f58 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -877,6 +877,7 @@ class Fit(Analyzer): custom (dict): a custom dictionary of additional data to fit; format is e.g. {'my_output':{'data':[1,2,3], 'sim':[1,2,4], 'weights':2.0}} compute (bool): whether to compute the mismatch immediately verbose (bool): detail to print + die (bool): whether to raise an exception if no data are supplied kwargs (dict): passed to cv.compute_gof() -- see this function for more detail on goodness-of-fit calculation options **Example**:: @@ -887,7 +888,7 @@ class Fit(Analyzer): fit.plot() ''' - def __init__(self, sim, weights=None, keys=None, custom=None, compute=True, verbose=False, **kwargs): + def __init__(self, sim, weights=None, keys=None, custom=None, compute=True, verbose=False, die=True, **kwargs): super().__init__(**kwargs) # Initialize the Analyzer object # Handle inputs @@ -897,17 +898,25 @@ def __init__(self, sim, weights=None, keys=None, custom=None, compute=True, verb self.weights = sc.mergedicts({'cum_deaths':10, 'cum_diagnoses':5}, weights) self.keys = keys self.gof_kwargs = kwargs + self.die = die # Copy data if sim.data is None: # pragma: no cover errormsg = 'Model fit cannot be calculated until data are loaded' - raise RuntimeError(errormsg) + if self.die: + raise RuntimeError(errormsg) + else: + print('Warning: ', errormsg) + sim.data = pd.DataFrame() # Use an empty dataframe self.data = sim.data # Copy sim results if not sim.results_ready: # pragma: no cover errormsg = 'Model fit cannot be calculated until results are run' - raise RuntimeError(errormsg) + if self.die: + raise RuntimeError(errormsg) + else: + print('Warning: ', errormsg) self.sim_results = sc.objdict() for key in sim.result_keys() + ['t', 'date']: self.sim_results[key] = sim.results[key] @@ -949,17 +958,23 @@ def reconcile_inputs(self): data_cols = self.data.columns if self.keys is None: - sim_keys = self.sim_results.keys() + sim_keys = [k for k in self.sim_results.keys() if k.startswith('cum_')] # Default sim keys, only keep cumulative keys if no keys are supplied intersection = list(set(sim_keys).intersection(data_cols)) # Find keys in both the sim and data - self.keys = [key for key in sim_keys if key in intersection and key.startswith('cum_')] # Only keep cumulative keys + self.keys = [key for key in sim_keys if key in intersection] # Maintain key order if not len(self.keys): # pragma: no cover - errormsg = f'No matches found between simulation result keys ({sim_keys}) and data columns ({data_cols})' - raise sc.KeyNotFoundError(errormsg) + errormsg = f'No matches found between simulation result keys:\n{sc.strjoin(sim_keys)}\n\nand data columns:\n{sc.strjoin(data_cols)}' + if self.die: + raise sc.KeyNotFoundError(errormsg) + else: + print('Warning: ', errormsg) mismatches = [key for key in self.keys if key not in data_cols] if len(mismatches): # pragma: no cover mismatchstr = ', '.join(mismatches) errormsg = f'The following requested key(s) were not found in the data: {mismatchstr}' - raise sc.KeyNotFoundError(errormsg) + if self.die: + raise sc.KeyNotFoundError(errormsg) + else: + print('Warning: ', errormsg) for key in self.keys: # For keys present in both the results and in the data self.inds.sim[key] = [] @@ -977,6 +992,7 @@ def reconcile_inputs(self): self.inds.data[key] = np.array(self.inds.data[key]) # Convert into paired points + matches = 0 # Count how many data points match for key in self.keys: self.pair[key] = sc.objdict() sim_inds = self.inds.sim[key] @@ -985,12 +1001,14 @@ def reconcile_inputs(self): self.pair[key].sim = np.zeros(n_inds) self.pair[key].data = np.zeros(n_inds) for i in range(n_inds): + matches += 1 self.pair[key].sim[i] = self.sim_results[key].values[sim_inds[i]] self.pair[key].data[i] = self.data[key].values[data_inds[i]] # Process custom inputs self.custom_keys = list(self.custom.keys()) for key in self.custom.keys(): + matches += 1 # If any of these exist, count it as amatch # Initialize and do error checking custom = self.custom[key] @@ -1019,6 +1037,13 @@ def reconcile_inputs(self): wt = custom.get('weights', wt) # ...but also try "weights" self.weights[key] = wt # Set the weight + if matches == 0: + errormsg = 'No paired data points were found between the supplied data and the simulation; please check the dates for each' + if self.die: + raise ValueError(errormsg) + else: + print('Warning: ', errormsg) + return diff --git a/covasim/misc.py b/covasim/misc.py index c085538ec..f342078f2 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -26,7 +26,7 @@ __all__ += ['load_data', 'load', 'save', 'migrate', 'savefig'] -def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=True, **kwargs): +def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=True, start_day=None, **kwargs): ''' Load data for comparing to the model output, either from file or from a dataframe. @@ -35,6 +35,7 @@ def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=T columns (list): list of column names (otherwise, load all) calculate (bool): whether to calculate cumulative values from daily counts check_date (bool): whether to check that a 'date' column is present + start_day (date): if the 'date' column is provided as integer number of days, consider them relative to this kwargs (dict): passed to pd.read_excel() Returns: @@ -88,7 +89,10 @@ def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=T errormsg = f'Required column "date" not found; columns are {data.columns}' raise ValueError(errormsg) else: - data['date'] = pd.to_datetime(data['date']).dt.date + if data['date'].dtype == np.int64: # If it's integers, treat it as days from the start day + data['date'] = sc.date(data['date'].values, start_date=start_day) + else: # Otherwise, use Pandas to convert it + data['date'] = pd.to_datetime(data['date']).dt.date data.set_index('date', inplace=True, drop=False) # Don't drop so sim.data['date'] can still be accessed return data diff --git a/covasim/sim.py b/covasim/sim.py index 334d6d9d6..7de93b49e 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -88,7 +88,7 @@ def load_data(self, datafile=None, datacols=None, verbose=None, **kwargs): verbose = self['verbose'] self.datafile = datafile # Store this if datafile is not None: # If a data file is provided, load it - self.data = cvm.load_data(datafile=datafile, columns=datacols, verbose=verbose, **kwargs) + self.data = cvm.load_data(datafile=datafile, columns=datacols, verbose=verbose, start_day=self['start_day'], **kwargs) return From 460616dfd0e4adcc713110028ad68226041e4023 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 01:35:46 -0700 Subject: [PATCH 216/569] started vectorizing --- covasim/analysis.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 5a0f85f58..91ccfc321 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1252,7 +1252,7 @@ def __init__(self, sim, to_networkx=False, **kwargs): print(warningmsg) # Include the basic line list - self.infection_log = sc.dcp(people.infection_log) + self.infection_log = people.infection_log # Parse into sources and targets self.sources = [None for i in range(self.pop_size)] @@ -1362,6 +1362,8 @@ def count_targets(self, start_day=None, end_day=None): def make_detailed(self, people, reset=False): ''' Construct a detailed transmission tree, with additional information for each person ''' + # Convert to a dataframe and initialize + df = pd.DataFrame(self.infection_log) detailed = [None]*self.pop_size for transdict in self.infection_log: @@ -1401,8 +1403,7 @@ def make_detailed(self, people, reset=False): self.detailed = detailed - # Also re-parse the infection log and convert to a dataframe - + # Also re-parse the transmission log and convert to a dataframe ttlist = [] for source_ind, target_ind in self.transmissions: ddict = self.detailed[target_ind] From e266c8fb11d5a66f82b5220d0226b0b1901eabdd Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 01:59:20 -0700 Subject: [PATCH 217/569] fix tutorials --- covasim/analysis.py | 5 ++--- examples/t10_custom_layers.py | 7 ++++--- examples/t10_population_properties.py | 7 ++++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 91ccfc321..d2fba94e8 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1252,7 +1252,7 @@ def __init__(self, sim, to_networkx=False, **kwargs): print(warningmsg) # Include the basic line list - self.infection_log = people.infection_log + self.infection_log = sc.dcp(people.infection_log) # Parse into sources and targets self.sources = [None for i in range(self.pop_size)] @@ -1366,10 +1366,9 @@ def make_detailed(self, people, reset=False): df = pd.DataFrame(self.infection_log) detailed = [None]*self.pop_size - for transdict in self.infection_log: + for ddict in self.infection_log: # Pull out key quantities - ddict = sc.dcp(transdict) # For "detailed dictionary" source = ddict['source'] target = ddict['target'] ddict['s'] = {} # Source properties diff --git a/examples/t10_custom_layers.py b/examples/t10_custom_layers.py index bc233973d..86f901d5e 100644 --- a/examples/t10_custom_layers.py +++ b/examples/t10_custom_layers.py @@ -28,6 +28,7 @@ sim.label = f'Transport layer with {n_contacts_per_person} contacts/person' # Run and compare -msim = cv.MultiSim([orig_sim, sim]) -msim.run() -msim.plot() \ No newline at end of file +if __name__ == '__main__': + msim = cv.MultiSim([orig_sim, sim]) + msim.run() + msim.plot() \ No newline at end of file diff --git a/examples/t10_population_properties.py b/examples/t10_population_properties.py index ab5c7e87c..cce281e07 100644 --- a/examples/t10_population_properties.py +++ b/examples/t10_population_properties.py @@ -27,6 +27,7 @@ def protect_the_prime(sim): sim.people.prime = np.array([sc.isprime(i) for i in range(len(sim.people))]) # Define whom to target # Run and plot -msim = cv.MultiSim([orig_sim, sim]) -msim.run() -msim.plot() \ No newline at end of file +if __name__ == '__main__': + msim = cv.MultiSim([orig_sim, sim]) + msim.run() + msim.plot() \ No newline at end of file From afced30f450a3299c27d924664ce7b6df163de94 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 02:04:03 -0700 Subject: [PATCH 218/569] use mutable dict --- examples/t07_optuna_calibration.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/examples/t07_optuna_calibration.py b/examples/t07_optuna_calibration.py index 315361ff3..2db1ec98a 100644 --- a/examples/t07_optuna_calibration.py +++ b/examples/t07_optuna_calibration.py @@ -7,6 +7,12 @@ import covasim as cv import optuna as op +# Create a (mutable) dictionary for global values +g = sc.objdict() +g.name = 'my-example-calibration' +g.db_name = f'{g.name}.db' +g.storage = f'sqlite:///{g.db_name}' + def run_sim(pars, label=None, return_sim=False): ''' Create and run a simulation ''' @@ -39,7 +45,7 @@ def run_trial(trial): def worker(): ''' Run a single worker ''' - study = op.load_study(storage=storage, study_name=name) + study = op.load_study(storage=g.storage, study_name=g.name) output = study.optimize(run_trial, n_trials=n_trials) return output @@ -52,10 +58,10 @@ def run_workers(): def make_study(): ''' Make a study, deleting one if it already exists ''' - if os.path.exists(db_name): - os.remove(db_name) - print(f'Removed existing calibration {db_name}') - output = op.create_study(storage=storage, study_name=name) + if os.path.exists(g.db_name): + os.remove(g.db_name) + print(f'Removed existing calibration {g.db_name}') + output = op.create_study(storage=g.storage, study_name=g.name) return output @@ -64,15 +70,12 @@ def make_study(): # Settings n_workers = 4 # Define how many workers to run in parallel n_trials = 25 # Define the number of trials, i.e. sim runs, per worker - name = 'my-example-calibration' - db_name = f'{name}.db' - storage = f'sqlite:///{db_name}' # Run the optimization t0 = sc.tic() make_study() run_workers() - study = op.load_study(storage=storage, study_name=name) + study = op.load_study(storage=g.storage, study_name=g.name) best_pars = study.best_params T = sc.toc(t0, output=True) print(f'Output: {best_pars}, time: {T}') From 1d27e013cfcdcf6822d64f103f1d657f5b08120f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 02:05:30 -0700 Subject: [PATCH 219/569] more parameters --- examples/t07_optuna_calibration.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/t07_optuna_calibration.py b/examples/t07_optuna_calibration.py index 2db1ec98a..d3efacd73 100644 --- a/examples/t07_optuna_calibration.py +++ b/examples/t07_optuna_calibration.py @@ -7,11 +7,13 @@ import covasim as cv import optuna as op -# Create a (mutable) dictionary for global values +# Create a (mutable) dictionary for global settings g = sc.objdict() g.name = 'my-example-calibration' g.db_name = f'{g.name}.db' g.storage = f'sqlite:///{g.db_name}' +g.n_workers = 4 # Define how many workers to run in parallel +g.n_trials = 25 # Define the number of trials, i.e. sim runs, per worker def run_sim(pars, label=None, return_sim=False): @@ -46,13 +48,13 @@ def run_trial(trial): def worker(): ''' Run a single worker ''' study = op.load_study(storage=g.storage, study_name=g.name) - output = study.optimize(run_trial, n_trials=n_trials) + output = study.optimize(run_trial, n_trials=g.n_trials) return output def run_workers(): ''' Run multiple workers in parallel ''' - output = sc.parallelize(worker, n_workers) + output = sc.parallelize(worker, g.n_workers) return output @@ -67,10 +69,6 @@ def make_study(): if __name__ == '__main__': - # Settings - n_workers = 4 # Define how many workers to run in parallel - n_trials = 25 # Define the number of trials, i.e. sim runs, per worker - # Run the optimization t0 = sc.tic() make_study() From 7884d18467d35f2e2887261c8d170151dfed6f96 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 02:07:26 -0700 Subject: [PATCH 220/569] fix data path --- examples/t07_optuna_calibration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/t07_optuna_calibration.py b/examples/t07_optuna_calibration.py index d3efacd73..bae478731 100644 --- a/examples/t07_optuna_calibration.py +++ b/examples/t07_optuna_calibration.py @@ -27,7 +27,7 @@ def run_sim(pars, label=None, return_sim=False): interventions = cv.test_num(daily_tests='data'), verbose = 0, ) - sim = cv.Sim(pars=pars, datafile='/home/cliffk/idm/covasim/docs/tutorials/example_data.csv', label=label) + sim = cv.Sim(pars=pars, datafile='example_data.csv', label=label) sim.run() fit = sim.compute_fit() if return_sim: From 2acd551fa93913c737fa5061d7532c6e8482de8c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 02:12:24 -0700 Subject: [PATCH 221/569] update optuna example --- docs/tutorials/t07.ipynb | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/tutorials/t07.ipynb b/docs/tutorials/t07.ipynb index 1f1ea4689..c3151ebbe 100644 --- a/docs/tutorials/t07.ipynb +++ b/docs/tutorials/t07.ipynb @@ -158,6 +158,14 @@ "import covasim as cv\n", "import optuna as op\n", "\n", + "# Create a (mutable) dictionary for global settings\n", + "g = sc.objdict()\n", + "g.name = 'my-example-calibration'\n", + "g.db_name = f'{g.name}.db'\n", + "g.storage = f'sqlite:///{g.db_name}'\n", + "g.n_workers = 4 # Define how many workers to run in parallel\n", + "g.n_trials = 25 # Define the number of trials, i.e. sim runs, per worker\n", + "\n", "\n", "def run_sim(pars, label=None, return_sim=False):\n", " ''' Create and run a simulation '''\n", @@ -189,40 +197,33 @@ "\n", "def worker():\n", " ''' Run a single worker '''\n", - " study = op.load_study(storage=storage, study_name=name)\n", - " output = study.optimize(run_trial, n_trials=n_trials)\n", + " study = op.load_study(storage=g.storage, study_name=g.name)\n", + " output = study.optimize(run_trial, n_trials=g.n_trials)\n", " return output\n", "\n", "\n", "def run_workers():\n", " ''' Run multiple workers in parallel '''\n", - " output = sc.parallelize(worker, n_workers)\n", + " output = sc.parallelize(worker, g.n_workers)\n", " return output\n", "\n", "\n", "def make_study():\n", " ''' Make a study, deleting one if it already exists '''\n", - " if os.path.exists(db_name):\n", - " os.remove(db_name)\n", - " print(f'Removed existing calibration {db_name}')\n", - " output = op.create_study(storage=storage, study_name=name)\n", + " if os.path.exists(g.db_name):\n", + " os.remove(g.db_name)\n", + " print(f'Removed existing calibration {g.db_name}')\n", + " output = op.create_study(storage=g.storage, study_name=g.name)\n", " return output\n", "\n", "\n", "if __name__ == '__main__':\n", "\n", - " # Settings\n", - " n_workers = 2 # Define how many workers to run in parallel\n", - " n_trials = 25 # Define the number of trials, i.e. sim runs, per worker\n", - " name = 'my-example-calibration'\n", - " db_name = f'{name}.db'\n", - " storage = f'sqlite:///{db_name}'\n", - "\n", " # Run the optimization\n", " t0 = sc.tic()\n", " make_study()\n", " run_workers()\n", - " study = op.load_study(storage=storage, study_name=name)\n", + " study = op.load_study(storage=g.storage, study_name=g.name)\n", " best_pars = study.best_params\n", " T = sc.toc(t0, output=True)\n", " print(f'\\n\\nOutput: {best_pars}, time: {T:0.1f} s')" @@ -259,8 +260,8 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", - "language": "python", + "display_name": "Python 3 (Spyder)", + "language": "python3", "name": "python3" }, "language_info": { From a8239882c7b27965d15a9aba897e694cdf75ef51 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 04:24:21 -0700 Subject: [PATCH 222/569] reimplemented transtree --- covasim/analysis.py | 159 +++++++++++++------------- tests/devtests/dev_test_transtree.py | 161 +++++++++++++++++++++++++++ 2 files changed, 238 insertions(+), 82 deletions(-) create mode 100644 tests/devtests/dev_test_transtree.py diff --git a/covasim/analysis.py b/covasim/analysis.py index d2fba94e8..81da0c790 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1227,6 +1227,9 @@ class TransTree(Analyzer): tt = sim.make_transtree() tt.plot() tt.plot_histograms() + + New in version 2.0.5: ``tt.detailed`` is a dataframe rather than a list of dictionaries; + for the latter, use ``tt.detailed.to_dict('records')``. ''' def __init__(self, sim, to_networkx=False, **kwargs): @@ -1251,8 +1254,8 @@ def __init__(self, sim, to_networkx=False, **kwargs): 'rerun with rescale=False and pop_scale=1 for reliable results.' print(warningmsg) - # Include the basic line list - self.infection_log = sc.dcp(people.infection_log) + # Include the basic line list -- copying directly is slow, so we'll make a copy later + self.infection_log = people.infection_log # Parse into sources and targets self.sources = [None for i in range(self.pop_size)] @@ -1362,81 +1365,73 @@ def count_targets(self, start_day=None, end_day=None): def make_detailed(self, people, reset=False): ''' Construct a detailed transmission tree, with additional information for each person ''' - # Convert to a dataframe and initialize - df = pd.DataFrame(self.infection_log) - detailed = [None]*self.pop_size - - for ddict in self.infection_log: - - # Pull out key quantities - source = ddict['source'] - target = ddict['target'] - ddict['s'] = {} # Source properties - ddict['t'] = {} # Target properties - - # If the source is available (e.g. not a seed infection), loop over both it and the target - if source is not None: - stdict = {'s':source, 't':target} - else: - stdict = {'t':target} - - # Pull out each of the attributes relevant to transmission - attrs = ['age', 'date_exposed', 'date_symptomatic', 'date_tested', 'date_diagnosed', 'date_quarantined', 'date_end_quarantine', 'date_severe', 'date_critical', 'date_known_contact'] - for st,stind in stdict.items(): - for attr in attrs: - ddict[st][attr] = people[attr][stind] - if source is not None: - for attr in attrs: - if attr.startswith('date_'): - is_attr = attr.replace('date_', 'is_') # Convert date to a boolean, e.g. date_diagnosed -> is_diagnosed - if attr == 'date_quarantined': # This has an end date specified - ddict['s'][is_attr] = ddict['s'][attr] <= ddict['date'] and not (ddict['s']['date_end_quarantine'] <= ddict['date']) - elif attr != 'date_end_quarantine': # This is not a state - ddict['s'][is_attr] = ddict['s'][attr] <= ddict['date'] # These don't make sense for people just infected (targets), only sources - - ddict['s']['is_asymp'] = np.isnan(people.date_symptomatic[source]) - ddict['s']['is_presymp'] = ~ddict['s']['is_asymp'] and ~ddict['s']['is_symptomatic'] # Not asymptomatic and not currently symptomatic - ddict['t']['is_quarantined'] = ddict['t']['date_quarantined'] <= ddict['date'] and not (ddict['t']['date_end_quarantine'] <= ddict['date']) # This is the only target date that it makes sense to define since it can happen before infection - - detailed[target] = ddict - - self.detailed = detailed - - # Also re-parse the transmission log and convert to a dataframe - ttlist = [] - for source_ind, target_ind in self.transmissions: - ddict = self.detailed[target_ind] - source = ddict['s'] - target = ddict['t'] - - tdict = {} - tdict['date'] = ddict['date'] - tdict['layer'] = ddict['layer'] - tdict['s_asymp'] = np.isnan(source['date_symptomatic']) # True if they *never* became symptomatic - tdict['s_presymp'] = ~tdict['s_asymp'] and tdict['date'] is_diagnosed + if attr == 'date_quarantined': # This has an end date specified + ddict['src_'+is_attr] = ddict['src_'+attr] <= transdict['date'] and not (ddict['src_'+'date_end_quarantine'] <= ddict['date']) + elif attr != 'date_end_quarantine': # This is not a state + ddict['src_'+is_attr] = ddict['src_'+attr] <= transdict['date'] # These don't make sense for people just infected (targets), only sources + + ddict['src_'+'is_asymp'] = np.isnan(people.date_symptomatic[source]) + ddict['src_'+'is_presymp'] = ~ddict['src_'+'is_asymp'] and ~ddict['src_'+'is_symptomatic'] # Not asymptomatic and not currently symptomatic + ddict['trg_'+'is_quarantined'] = ddict['trg_'+'date_quarantined'] <= transdict['date'] and not (ddict['trg_'+'date_end_quarantine'] <= ddict['date']) # This is the only target date that it makes sense to define since it can happen before infection + + ddict.update(transdict) + detailed[target] = ddict + +sc.toc() + +sc.heading('Validation...') + +sc.tic() + +for i in range(len(detailed)): + sc.percentcomplete(step=i, maxsteps=len(detailed), stepsize=10) + d_entry = detailed[i] + df_entry = ddf.iloc[i].to_dict() + if d_entry is None: # If in the dict it's None, it should be nan in the dataframe + assert np.isnan(df_entry['target']) + else: + dkeys = list(d_entry.keys()) + dfkeys = list(df_entry.keys()) + assert all([dk in dfkeys for dk in dkeys]) # The dataframe can have extra keys, but not the dict + for k in dkeys: + v_d = d_entry[k] + v_df = df_entry[k] + try: + assert np.isclose(v_d, v_df, equal_nan=True) # If it's numeric, check they're close + except TypeError: + if v_d is None: + assert np.isnan(v_df) # If in the dict it's None, it should be nan in the dataframe + else: + assert v_d == v_df # In all other cases, it should be an exact match + +print('\nValidation passed.') + +sc.toc() +print('Done.') \ No newline at end of file From 9e4239327cce41f5777dac65769efee5b90627d4 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 23 Mar 2021 13:02:34 +0100 Subject: [PATCH 223/569] redo waning --- covasim/immunity.py | 292 ++++++++++++++++++-------------- covasim/parameters.py | 7 +- tests/devtests/test_variants.py | 13 +- 3 files changed, 168 insertions(+), 144 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 7af55d73a..cbfb864bc 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -287,113 +287,9 @@ def initialize(self, sim): return -# %% Immunity methods -__all__ += ['init_immunity', 'pre_compute_waning', 'check_immunity'] - - -def init_immunity(sim, create=False): - ''' Initialize immunity matrices with all strains that will eventually be in the sim''' - ts = sim['total_strains'] - immunity = {} - - # Pull out all of the circulating strains for cross-immunity - circulating_strains = ['wild'] - for strain in sim['strains']: - circulating_strains.append(strain.strain_label) - - # If immunity values have been provided, process them - if sim['immunity'] is None or create: - # Initialize immunity - for ax in cvd.immunity_axes: - if ax == 'sus': # Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] - immunity[ax] = np.full((ts, ts), sim['cross_immunity'], - dtype=cvd.default_float) # Default for off-diagnonals - np.fill_diagonal(immunity[ax], 1) # Default for own-immunity - else: # Progression and transmission are matrices of scalars of size sim['n_strains'] - immunity[ax] = np.full(ts, 1, dtype=cvd.default_float) - sim['immunity'] = immunity - - else: - # if we know all the circulating strains, then update, otherwise use defaults - known_strains = ['wild', 'b117', 'b1351', 'p1'] - cross_immunity = create_cross_immunity(circulating_strains) - if sc.checktype(sim['immunity']['sus'], 'arraylike'): - correct_size = sim['immunity']['sus'].shape == (ts, ts) - if not correct_size: - errormsg = f'Wrong dimensions for immunity["sus"]: you provided a matrix sized {sim["immunity"]["sus"].shape}, but it should be sized {(ts, ts)}' - raise ValueError(errormsg) - for i in range(ts): - for j in range(ts): - if i != j: - if circulating_strains[i] in known_strains and circulating_strains[j] in known_strains: - sim['immunity']['sus'][j][i] = cross_immunity[circulating_strains[j]][ - circulating_strains[i]] - - elif sc.checktype(sim['immunity']['sus'], dict): - raise NotImplementedError - else: - errormsg = f'Type of immunity["sus"] not understood: you provided {type(sim["immunity"]["sus"])}, but it should be an array or dict.' - raise ValueError(errormsg) - - -def pre_compute_waning(length, form, pars): - ''' - Process immunity pars and functional form into a value - - 'exp_decay' : exponential decay (TODO fill in details) - - 'logistic_decay' : logistic decay (TODO fill in details) - - 'linear' : linear decay (TODO fill in details) - - others TBC! - - Args: - form (str): the functional form to use - pars (dict): passed to individual immunity functions - length (float): length of time to compute immunity - ''' - - choices = [ - 'exp_decay', - 'logistic_decay', - 'linear_growth', - 'linear_decay' - ] - - # Process inputs - if form == 'exp_decay': - if pars['half_life'] is None: pars['half_life'] = np.nan - output = exp_decay(length, **pars) - - elif form == 'logistic_decay': - output = logistic_decay(length, **pars) - - elif form == 'linear_growth': - output = linear_growth(length, **pars) - - elif form == 'linear_decay': - output = linear_decay(length, **pars) - - else: - choicestr = '\n'.join(choices) - errormsg = f'The selected functional form "{form}" is not implemented; choices are: {choicestr}' - raise NotImplementedError(errormsg) - - return output - - -def nab_to_efficacy(nab, ax): - choices = {'sus': -0.4, 'symp': 0, 'sev': 0.4} - if ax not in choices.keys(): - errormsg = f'Choice provided not in list of choices' - raise ValueError(errormsg) - - n_50 = 0.2 - slope = 2 - - # put in here nab to efficacy mapping (logistic regression from fig 1a) - efficacy = 1/(1+np.exp(-slope*(nab - n_50 + choices[ax]))) # from logistic regression computed in R using data from Khoury et al - - - return efficacy +# %% NAb methods +__all__ += ['compute_nab', 'nab_to_efficacy'] def compute_nab(people, inds, prior_inf=True): ''' @@ -405,22 +301,21 @@ def compute_nab(people, inds, prior_inf=True): depending upon vaccine source. If individual has existing NAb, multiply booster impact ''' - NAb_decay = people.pars['NAb_decay'] day = people.t # timestep we are on NAb_arrays = people.NAb[day, inds] - prior_NAb_inds = inds[cvu.true(NAb_arrays > 0)] - no_prior_NAb_inds = inds[cvu.true(NAb_arrays == 0)] + prior_NAb_inds = cvu.itrue(NAb_arrays > 0, inds) # Find people with prior NAbs + no_prior_NAb_inds = cvu.itrue(NAb_arrays == 0, inds) # Find people without prior NAbs - prior_NAb = people.NAb[day, prior_NAb_inds] - NAb_boost = people.pars['NAb_boost'] + prior_NAb = people.NAb[day, prior_NAb_inds] # Array of NAb levels on this timestep for people with some NAbs + NAb_boost = people.pars['NAb_boost'] # Boosting factor + # PART A: compute the initial NAb level (depends on prior infection/vaccination history) if prior_inf: - NAb_pars = people.pars['NAb_pars'] # 1) No prior NAb: draw NAb from a distribution and compute if len(no_prior_NAb_inds): - init_NAb = cvu.sample(**NAb_pars, size=len(no_prior_NAb_inds)) + init_NAb = cvu.sample(**people.pars['NAb_init'], size=len(no_prior_NAb_inds)) prior_symp = people.prior_symptoms[no_prior_NAb_inds] no_prior_NAb = init_NAb * prior_symp people.NAb[day, no_prior_NAb_inds] = no_prior_NAb @@ -433,11 +328,9 @@ def compute_nab(people, inds, prior_inf=True): else: # NAbs coming from a vaccine - NAb_pars = people.pars['vaccine_info']['NAb_pars'] - # 1) No prior NAb: draw NAb from a distribution and compute if len(no_prior_NAb_inds): - init_NAb = cvu.sample(**NAb_pars, size=len(no_prior_NAb_inds)) + init_NAb = cvu.sample(**people.pars['vaccine_info']['NAb_init'], size=len(no_prior_NAb_inds)) people.NAb[day, no_prior_NAb_inds] = init_NAb # 2) Prior NAb (from natural or vaccine dose 1): multiply existing NAb by boost factor @@ -445,25 +338,90 @@ def compute_nab(people, inds, prior_inf=True): init_NAb = prior_NAb * NAb_boost people.NAb[day, prior_NAb_inds] = init_NAb + + # PART B: compute NAb levels over time using waning functions + NAb_decay = people.pars['NAb_decay'] n_days = people.pars['n_days'] days_left = n_days - day+1 # how many days left in sim - length = NAb_decay['pars1']['length'] - init_NAbs = people.NAb[day, inds] + NAb_waning = pre_compute_waning(length=days_left, form=NAb_decay['form'], pars=NAb_decay['pars']) + people.NAb[day:, inds] = np.add.outer(NAb_waning, people.NAb[day, inds]) + + return - if days_left > length: - t1 = np.arange(length, dtype=cvd.default_int)[:,np.newaxis] + np.ones((length, len(init_NAbs))) - t1_result = init_NAbs - (NAb_decay['pars1']['rate']*t1) - t2 = np.arange(days_left - length, dtype=cvd.default_int)[:,np.newaxis] + np.ones((days_left - length, len(init_NAbs))) - t2_result = init_NAbs - (NAb_decay['pars1']['rate']*length) - np.exp(-t2*NAb_decay['pars2']['rate']) - result = np.concatenate((t1_result, t2_result), axis=0) - else: - t1 = np.arange(days_left, dtype=cvd.default_int)[:,np.newaxis] + np.ones((days_left, len(init_NAbs))) - result = init_NAbs - (NAb_decay['pars1']['rate']*t1) +def nab_to_efficacy(nab, ax): + ''' + Convert NAb levels to immunity protection factors, using the functional form + given in this paper: https://doi.org/10.1101/2021.03.09.21252641 + Inputs: + nab (arr): an array of NAb levels + ax (str): can be 'sus', 'symp' or 'sev', corresponding to the efficacy of protection against infection, symptoms, and severe disease respectively + Returns: + an array the same size as nab, containing the immunity protection factors for the specified axis + ''' - people.NAb[day:, inds] = result + choices = {'sus': -0.4, 'symp': 0, 'sev': 0.4} + if ax not in choices.keys(): + errormsg = f'Choice provided not in list of choices' + raise ValueError(errormsg) - return + # Temporary parameter values, pending confirmation + n_50 = 0.2 + slope = 2 + + # put in here nab to efficacy mapping (logistic regression from fig 1a from https://doi.org/10.1101/2021.03.09.21252641) + efficacy = 1/(1+np.exp(-slope*(nab - n_50 + choices[ax]))) # from logistic regression computed in R using data from Khoury et al + return efficacy + + + +# %% Immunity methods +__all__ += ['init_immunity', 'check_immunity'] + + +def init_immunity(sim, create=False): + ''' Initialize immunity matrices with all strains that will eventually be in the sim''' + ts = sim['total_strains'] + immunity = {} + + # Pull out all of the circulating strains for cross-immunity + circulating_strains = ['wild'] + for strain in sim['strains']: + circulating_strains.append(strain.strain_label) + + # If immunity values have been provided, process them + if sim['immunity'] is None or create: + # Initialize immunity + for ax in cvd.immunity_axes: + if ax == 'sus': # Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] + immunity[ax] = np.full((ts, ts), sim['cross_immunity'], + dtype=cvd.default_float) # Default for off-diagnonals + np.fill_diagonal(immunity[ax], 1) # Default for own-immunity + else: # Progression and transmission are matrices of scalars of size sim['n_strains'] + immunity[ax] = np.full(ts, 1, dtype=cvd.default_float) + sim['immunity'] = immunity + + else: + # if we know all the circulating strains, then update, otherwise use defaults + known_strains = ['wild', 'b117', 'b1351', 'p1'] + cross_immunity = create_cross_immunity(circulating_strains) + if sc.checktype(sim['immunity']['sus'], 'arraylike'): + correct_size = sim['immunity']['sus'].shape == (ts, ts) + if not correct_size: + errormsg = f'Wrong dimensions for immunity["sus"]: you provided a matrix sized {sim["immunity"]["sus"].shape}, but it should be sized {(ts, ts)}' + raise ValueError(errormsg) + for i in range(ts): + for j in range(ts): + if i != j: + if circulating_strains[i] in known_strains and circulating_strains[j] in known_strains: + sim['immunity']['sus'][j][i] = cross_immunity[circulating_strains[j]][ + circulating_strains[i]] + + elif sc.checktype(sim['immunity']['sus'], dict): + raise NotImplementedError + else: + errormsg = f'Type of immunity["sus"] not understood: you provided {type(sim["immunity"]["sus"])}, but it should be an array or dict.' + raise ValueError(errormsg) def check_immunity(people, strain, sus=True, inds=None): @@ -529,13 +487,85 @@ def check_immunity(people, strain, sus=True, inds=None): if len(was_inf): # Immunity for reinfected people current_NAbs = people.NAb[people.t, was_inf] - people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp') + try: people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp') + except: + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() people.sev_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['sev'][strain], 'sev') return -# Specific waning and growth functions are listed here + +# %% Methods for computing waning +__all__ += ['pre_compute_waning'] + +def pre_compute_waning(length, form='log_linear_exp_decay', pars=None): + ''' + Process functional form and parameters into values + - 'log_linear_exp_decay': log-linear decay followed by exponential decay. The default choice, taken from https://doi.org/10.1101/2021.03.09.21252641 + - 'exp_decay' : exponential decay. Parameters should be init_val and half_life (half_life can be None/nan) + - 'logistic_decay' : logistic decay (TODO fill in details) + - 'linear' : linear decay (TODO fill in details) + - others TBC! + + Args: + length (float): length of array to return, i.e., for how long waning is calculated + form (str): the functional form to use + pars (dict): passed to individual immunity functions + Returns: + array of length 'length' of values + ''' + + choices = [ + 'log_linear_exp_decay', # Default if no form is provided + 'exp_decay', + 'logistic_decay', + 'linear_growth', + 'linear_decay' + ] + + # Process inputs + if form is None or form == 'log_linear_exp_decay': + output = log_linear_exp_decay(length, **pars) + + elif form == 'exp_decay': + if pars['half_life'] is None: pars['half_life'] = np.nan + output = exp_decay(length, **pars) + + elif form == 'logistic_decay': + output = logistic_decay(length, **pars) + + elif form == 'linear_growth': + output = linear_growth(length, **pars) + + elif form == 'linear_decay': + output = linear_decay(length, **pars) + + else: + choicestr = '\n'.join(choices) + errormsg = f'The selected functional form "{form}" is not implemented; choices are: {choicestr}' + raise NotImplementedError(errormsg) + + return output + + +def log_linear_exp_decay(length, ll_rate, ll_length, exp_rate): + ''' + Returns an array of length 'length' with containing the evaluated function at each point + ''' + + ll_part = lambda t, ll_rate: -ll_rate*t + exp_part = lambda t, init_val, exp_rate: init_val - np.exp(-t*exp_rate) + t = np.arange(length, dtype=cvd.default_int) + y1 = ll_part(cvu.true(tll_length), y1[-1], exp_rate) + y = np.concatenate([y1,y2]) + return y + + def exp_decay(length, init_val, half_life, delay=None): ''' Returns an array of length t with values for the immunity at each time step after recovery diff --git a/covasim/parameters.py b/covasim/parameters.py index a8a69c44c..4894aea1a 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -71,10 +71,9 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 - pars['NAb_pars'] = dict(dist='lognormal', par1= 0.5, par2= 1) # Parameters for NAbs distribution for natural infection - pars['NAb_decay'] = dict(form1='log-linear', pars1={'rate': 1/180, 'length': 250}, - form2='exp_decay', pars2={'rate': 1/100}) - pars['NAb_boost'] = 3 + pars['NAb_init'] = dict(dist='lognormal', par1= 0.5, par2= 1) # Parameters for the distribution of the initial level of NAbs following natural infection + pars['NAb_decay'] = dict(form='log_linear_exp_decay', pars={'ll_rate': 1/180, 'll_length': 250, 'exp_rate': 1/100}) # Parameters describing the kinetics of decay of NAbs over time + pars['NAb_boost'] = 3 # Multiplicative factor applied to a person's NAb levels if they get reinfected pars['rel_imm'] = {} pars['rel_imm']['asymptomatic'] = 0.5 diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 0aa19f11b..403cf023a 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -197,14 +197,9 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): strain_pars = { 'rel_beta': 1.5, - 'imm_pars': {k: dict(form='logistic_decay', pars={'init_val': 1., 'half_val': 10, 'lower_asymp': 0.1, 'decay_rate': -5}) for k in cvd.immunity_axes} } - immunity = {} - immunity['sus'] = np.array([[1,0.4],[0.9,1.]]) - immunity['prog'] = np.array([1,1]) - immunity['trans'] = np.array([1,1]) pars = { - 'immunity': immunity + 'beta': 0.01 } strain = cv.Strain(strain_pars, days=1, n_imports=20) sim = cv.Sim(pars=pars, strains=strain) @@ -401,9 +396,9 @@ def test_msim(): sc.tic() # Run simplest possible test - # if 1: - # sim = cv.Sim() - # sim.run() + if 0: + sim = cv.Sim() + sim.run() # sim0 = test_synthpops() From e0cee5f58e033e2b4fdf40ef488f8b338ad930ba Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 23 Mar 2021 13:23:44 +0100 Subject: [PATCH 224/569] all tests pass, now to check immunity --- covasim/immunity.py | 12 +- covasim/sim.py | 2 +- tests/devtests/test_immunity.py | 28 --- tests/devtests/test_variants.py | 305 ++++++++++++++++++-------------- 4 files changed, 178 insertions(+), 169 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index cbfb864bc..39a1f12bc 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -166,7 +166,7 @@ def __init__(self, vaccine=None): self.rel_imm = None # list of length total_strains with relative immunity factor self.doses = None self.interval = None - self.NAb_pars = None + self.NAb_init = None self.vaccine_strain_info = self.init_strain_vaccine_info() self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine) for par, val in self.vaccine_pars.items(): @@ -215,7 +215,7 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on pfizer if vaccine in choices['pfizer']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = dict(dist='lognormal', par1=2, par2= 2) + vaccine_pars['NAb_init'] = dict(dist='lognormal', par1=2, par2= 2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -223,7 +223,7 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on moderna elif vaccine in choices['moderna']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = dict(dist='lognormal', par1=2, par2=2) + vaccine_pars['NAb_init'] = dict(dist='lognormal', par1=2, par2=2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 29 vaccine_pars['label'] = vaccine @@ -231,7 +231,7 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on az elif vaccine in choices['az']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = dict(dist='lognormal', par1=2, par2=2) + vaccine_pars['NAb_init'] = dict(dist='lognormal', par1=2, par2=2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -239,7 +239,7 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on j&j elif vaccine in choices['j&j']: vaccine_pars = dict() - vaccine_pars['NAb_pars'] = dict(dist='lognormal', par1=2, par2=2) + vaccine_pars['NAb_init'] = dict(dist='lognormal', par1=2, par2=2) vaccine_pars['doses'] = 1 vaccine_pars['interval'] = None vaccine_pars['label'] = vaccine @@ -266,7 +266,7 @@ def initialize(self, sim): for strain in range(ts-1): circulating_strains.append(sim['strains'][strain].strain_label) - if self.NAb_pars is None : + if self.NAb_init is None : errormsg = f'Did not provide parameters for this vaccine' raise ValueError(errormsg) diff --git a/covasim/sim.py b/covasim/sim.py index 7fc387d0c..5c4d5b788 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -479,7 +479,7 @@ def init_vaccines(self): for ind, vacc in enumerate(self['vaccines']): self['vaccine_info']['rel_imm'][ind,:] = vacc.rel_imm self['vaccine_info']['doses'] = vacc.doses - self['vaccine_info']['NAb_pars'] = vacc.NAb_pars + self['vaccine_info']['NAb_init'] = vacc.NAb_init return def rescale(self): diff --git a/tests/devtests/test_immunity.py b/tests/devtests/test_immunity.py index 848b0b8c4..310beec1a 100644 --- a/tests/devtests/test_immunity.py +++ b/tests/devtests/test_immunity.py @@ -56,33 +56,6 @@ def test_reinfection_scens(do_plot=False, do_show=True, do_save=False): return sim -def test_reinfection(do_plot=False, do_show=True, do_save=False): - sc.heading('Run a basic sim with 1 strain and no reinfections') - sc.heading('Setting up...') - - pars = { - 'beta': 0.015, - 'n_days': 120, -# 'rel_imm': dict(asymptomatic=0.7, mild=0.8, severe=1.) - } - - pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer') - sim = cv.Sim( - pars=pars, - interventions=pfizer - ) - sim.run() - - to_plot = sc.objdict({ - 'New infections': ['new_infections'], - 'Cumulative infections': ['cum_infections'], - 'New reinfections': ['new_reinfections'], - }) - if do_plot: - sim.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_reinfection.png', to_plot=to_plot) - - return sim - #%% Run as a script @@ -95,7 +68,6 @@ def test_reinfection(do_plot=False, do_show=True, do_save=False): sim.run() # Run more complex tests - sim1 = test_reinfection(**plot_args) #scens1 = test_reinfection_scens(**plot_args) sc.toc() diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 403cf023a..b2ea88442 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -10,6 +10,141 @@ do_save = 0 +def test_import1strain(do_plot=False, do_show=True, do_save=False): + sc.heading('Test introducing a new strain partway through a sim') + sc.heading('Setting up...') + + strain_labels = [ + 'Strain 1', + 'Strain 2: 1.5x more transmissible' + ] + + strain_pars = { + 'rel_beta': 1.5, + } + pars = { + 'beta': 0.01 + } + strain = cv.Strain(strain_pars, days=1, n_imports=20) + sim = cv.Sim(pars=pars, strains=strain) + sim.run() + + if do_plot: + plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (cross immunity)', filename='test_importstrain1', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_shares(sim, key='new_infections', title='Shares of new infections by strain', filename='test_importstrain1_shares', do_show=do_show, do_save=do_save) + return sim + + +def test_import2strains(do_plot=False, do_show=True, do_save=False): + sc.heading('Test introducing 2 new strains partway through a sim') + sc.heading('Setting up...') + + b117 = cv.Strain('b117', days=1, n_imports=20) + p1 = cv.Strain('sa variant', days=2, n_imports=20) + sim = cv.Sim(strains=[b117, p1], label='With imported infections') + sim.run() + + strain_labels = [ + 'Strain 1: Wild Type', + 'Strain 2: UK Variant on day 10', + 'Strain 3: SA Variant on day 30' + ] + + if do_plot: + plot_results(sim, key='incidence_by_strain', title='Imported strains', filename='test_importstrain2', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_shares(sim, key='new_infections', title='Shares of new infections by strain', + filename='test_importstrain2_shares', do_show=do_show, do_save=do_save) + return sim + + +def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): + sc.heading('Test introducing a new strain with longer duration partway through a sim') + sc.heading('Setting up...') + + strain_labels = [ + 'Strain 1: beta 0.016', + 'Strain 2: beta 0.025' + ] + + pars = { + 'n_days': 120, + } + + strain_pars = { + 'rel_beta': 1.5, + 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} + } + + strain = cv.Strain(strain=strain_pars, strain_label='Custom strain', days=10, n_imports=30) + sim = cv.Sim(pars=pars, strains=strain, label='With imported infections') + sim.run() + + if do_plot: + plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (longer duration)', filename='test_importstrain1', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_shares(sim, key='new_infections', title='Shares of new infections by strain', filename='test_importstrain_longerdur_shares', do_show=do_show, do_save=do_save) + return sim + + +def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): + sc.heading('Test introducing 2 new strains partway through a sim, with a change_beta intervention') + sc.heading('Setting up...') + + strain2 = {'rel_beta': 1.5, + 'rel_severe_prob': 1.3} + + strain3 = {'rel_beta': 2, + 'rel_symp_prob': 1.6} + + intervs = cv.change_beta(days=[5, 20, 40], changes=[0.8, 0.7, 0.6]) + strains = [cv.Strain(strain=strain2, days=10, n_imports=20), + cv.Strain(strain=strain3, days=30, n_imports=20), + ] + sim = cv.Sim(interventions=intervs, strains=strains, label='With imported infections') + sim.run() + + strain_labels = [ + f'Strain 1: beta {sim["beta"]}', + f'Strain 2: beta {sim["beta"]*sim["rel_beta"][1]}, 20 imported day 10', + f'Strain 3: beta {sim["beta"]*sim["rel_beta"][2]}, 20 imported day 30' + ] + + if do_plot: + plot_results(sim, key='incidence_by_strain', title='Imported strains', filename='test_import2strains_changebeta', labels=strain_labels, do_show=do_show, do_save=do_save) + plot_shares(sim, key='new_infections', title='Shares of new infections by strain', + filename='test_import2strains_changebeta_shares', do_show=do_show, do_save=do_save) + return sim + + + +#%% Vaccination tests + +def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): + sc.heading('Test vaccination with a single strain') + sc.heading('Setting up...') + + pars = { + 'beta': 0.015, + 'n_days': 120, + } + + pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer') + sim = cv.Sim( + pars=pars, + interventions=pfizer + ) + sim.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'Cumulative infections': ['cum_infections'], + 'New reinfections': ['new_reinfections'], + }) + if do_plot: + sim.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_reinfection.png', to_plot=to_plot) + + return sim + + def test_synthpops(): sim = cv.Sim(pop_size=5000, pop_type='synthpops') sim.popdict = cv.make_synthpop(sim, with_facilities=True, layer_mapping={'LTCF': 'f'}) @@ -27,7 +162,10 @@ def test_synthpops(): return sim -def test_vaccine_1strain(do_plot=True, do_show=True, do_save=False): + +#%% Multisim and scenario tests + +def test_vaccine_1strain_scen(do_plot=True, do_show=True, do_save=False): sc.heading('Run a basic sim with 1 strain, pfizer vaccine') sc.heading('Setting up...') @@ -78,7 +216,7 @@ def test_vaccine_1strain(do_plot=True, do_show=True, do_save=False): return scens -def test_vaccine_2strains(do_plot=True, do_show=True, do_save=False): +def test_vaccine_2strains_scen(do_plot=True, do_show=True, do_save=False): sc.heading('Run a basic sim with b117 strain on day 10, pfizer vaccine day 20') sc.heading('Setting up...') @@ -141,7 +279,7 @@ def test_vaccine_2strains(do_plot=True, do_show=True, do_save=False): return scens -def test_strainduration(do_plot=False, do_show=True, do_save=False): +def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') sc.heading('Setting up...') @@ -186,110 +324,28 @@ def test_strainduration(do_plot=False, do_show=True, do_save=False): return scens -def test_import1strain(do_plot=False, do_show=True, do_save=False): - sc.heading('Test introducing a new strain partway through a sim') - sc.heading('Setting up...') - - strain_labels = [ - 'Strain 1', - 'Strain 2: 1.5x more transmissible' - ] - - strain_pars = { - 'rel_beta': 1.5, - } - pars = { - 'beta': 0.01 - } - strain = cv.Strain(strain_pars, days=1, n_imports=20) - sim = cv.Sim(pars=pars, strains=strain) - sim.run() - - if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (cross immunity)', filename='test_importstrain1', labels=strain_labels, do_show=do_show, do_save=do_save) - plot_shares(sim, key='new_infections', title='Shares of new infections by strain', filename='test_importstrain1_shares', do_show=do_show, do_save=do_save) - return sim - - -def test_import2strains(do_plot=False, do_show=True, do_save=False): - sc.heading('Test introducing 2 new strains partway through a sim') - sc.heading('Setting up...') - - b117 = cv.Strain('b117', days=1, n_imports=20) - p1 = cv.Strain('sa variant', days=2, n_imports=20) - sim = cv.Sim(strains=[b117, p1], label='With imported infections') - sim.run() - - strain_labels = [ - 'Strain 1: Wild Type', - 'Strain 2: UK Variant on day 10', - 'Strain 3: SA Variant on day 30' - ] - - if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strains', filename='test_importstrain2', labels=strain_labels, do_show=do_show, do_save=do_save) - plot_shares(sim, key='new_infections', title='Shares of new infections by strain', - filename='test_importstrain2_shares', do_show=do_show, do_save=do_save) - return sim - - -def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): - sc.heading('Test introducing a new strain with longer duration partway through a sim') - sc.heading('Setting up...') - - strain_labels = [ - 'Strain 1: beta 0.016', - 'Strain 2: beta 0.025' - ] - - pars = { - 'n_days': 120, - } - - strain_pars = { - 'rel_beta': 1.5, - 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} - } - - strain = cv.Strain(strain=strain_pars, strain_label='Custom strain', days=10, n_imports=30) - sim = cv.Sim(pars=pars, strains=strain, label='With imported infections') - sim.run() - - if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (longer duration)', filename='test_importstrain1', labels=strain_labels, do_show=do_show, do_save=do_save) - plot_shares(sim, key='new_infections', title='Shares of new infections by strain', filename='test_importstrain_longerdur_shares', do_show=do_show, do_save=do_save) - return sim - - -def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): - sc.heading('Test introducing 2 new strains partway through a sim, with a change_beta intervention') - sc.heading('Setting up...') +def test_msim(): + # basic test for vaccine + b117 = cv.Strain('b117', days=0) + sim = cv.Sim(strains=[b117]) + msim = cv.MultiSim(sim, n_runs=2) + msim.run() + msim.reduce() - strain2 = {'rel_beta': 1.5, - 'rel_severe_prob': 1.3} + to_plot = sc.objdict({ - strain3 = {'rel_beta': 2, - 'rel_symp_prob': 1.6} + 'Total infections': ['cum_infections'], + 'New infections per day': ['new_infections'], + 'New Re-infections per day': ['new_reinfections'], + 'New infections by strain': ['new_infections_by_strain'] + }) - intervs = cv.change_beta(days=[5, 20, 40], changes=[0.8, 0.7, 0.6]) - strains = [cv.Strain(strain=strain2, days=10, n_imports=20), - cv.Strain(strain=strain3, days=30, n_imports=20), - ] - sim = cv.Sim(interventions=intervs, strains=strains, label='With imported infections') - sim.run() + msim.plot(to_plot=to_plot, do_save=0, do_show=1, legend_args={'loc': 'upper left'}, axis_args={'hspace': 0.4}, interval=35) - strain_labels = [ - f'Strain 1: beta {sim["beta"]}', - f'Strain 2: beta {sim["beta"]*sim["rel_beta"][1]}, 20 imported day 10', - f'Strain 3: beta {sim["beta"]*sim["rel_beta"][2]}, 20 imported day 30' - ] + return msim - if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strains', filename='test_import2strains_changebeta', labels=strain_labels, do_show=do_show, do_save=do_save) - plot_shares(sim, key='new_infections', title='Shares of new infections by strain', - filename='test_import2strains_changebeta_shares', do_show=do_show, do_save=do_save) - return sim +#%% Plotting and utilities def plot_results(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): @@ -371,50 +427,31 @@ def get_ind_of_min_value(list, time): return ind -def test_msim(): - # basic test for vaccine - b117 = cv.Strain('b117', days=0) - sim = cv.Sim(strains=[b117]) - msim = cv.MultiSim(sim, n_runs=2) - msim.run() - msim.reduce() - - to_plot = sc.objdict({ - - 'Total infections': ['cum_infections'], - 'New infections per day': ['new_infections'], - 'New Re-infections per day': ['new_reinfections'], - 'New infections by strain': ['new_infections_by_strain'] - }) - - msim.plot(to_plot=to_plot, do_save=0, do_show=1, legend_args={'loc': 'upper left'}, axis_args={'hspace': 0.4}, interval=35) - - return msim - #%% Run as a script if __name__ == '__main__': sc.tic() # Run simplest possible test - if 0: + if 1: sim = cv.Sim() sim.run() - # sim0 = test_synthpops() - + # Run more complex single-sim tests + sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim0 = test_msim() + # Run Vaccine tests + sim4 = test_synthpops() + sim5 = test_vaccine_1strain() - # Run more complex tests - sim1 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim2 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim3 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim4 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - # scens2 = test_strainduration(do_plot=do_plot, do_save=do_save, do_show=do_show) + # Run multisim and scenario tests + scens0 = test_vaccine_1strain_scen() + scens1 = test_vaccine_2strains_scen() + scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) + msim0 = test_msim() - # Run Vaccine tests - # sim5 = test_vaccine_1strain() - # sim6 = test_vaccine_2strains() sc.toc() From 1777b858eae7f03659d1b81ca09fafad0a12a974 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 05:30:17 -0700 Subject: [PATCH 225/569] tests pass --- CHANGELOG.rst | 3 ++ covasim/analysis.py | 84 +++++++++++++++++++++++++++++++----------- tests/test_analysis.py | 10 ++--- 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cfea6860d..200159bd6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,9 @@ These are the major improvements we are currently working on. If there is a spec Latest versions (2.0.x) ~~~~~~~~~~~~~~~~~~~~~~~ +sim.plot(dpi=150, rotation=45, start_day='2020-03-01', end_day=55, interval=7) + + Version 2.0.4 (2021-03-19) -------------------------- diff --git a/covasim/analysis.py b/covasim/analysis.py index 81da0c790..10b7c1f4a 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1273,8 +1273,9 @@ def __init__(self, sim, to_networkx=False, **kwargs): self.source_dates[target] = date # Each target has at most one source self.target_dates[source].append(date) # Each source can have multiple targets - # Count the number of targets each person has - self.n_targets = self.count_targets() + # Count the number of targets each person has, and the list of transmissions + self.count_targets() + self.count_transmissions() # Include the detailed transmission tree as well, as a list and as a dataframe self.make_detailed(people) @@ -1311,20 +1312,6 @@ def __len__(self): return 0 - @property - def transmissions(self): - """ - Iterable over edges corresponding to transmission events - - This excludes edges corresponding to seeded infections without a source - """ - output = [] - for d in self.infection_log: - if d['source'] is not None: - output.append([d['source'], d['target']]) - return output - - def day(self, day=None, which=None): ''' Convenience function for converting an input to an integer day ''' if day is not None: @@ -1359,9 +1346,32 @@ def count_targets(self, start_day=None, end_day=None): n_targets[i] = len(self.targets[i]) n_target_inds = sc.findinds(~np.isnan(n_targets)) n_targets = n_targets[n_target_inds] + self.n_targets = n_targets return n_targets + def count_transmissions(self): + """ + Iterable over edges corresponding to transmission events + + This excludes edges corresponding to seeded infections without a source + """ + source_inds = [] + target_inds = [] + transmissions = [] + for d in self.infection_log: + if d['source'] is not None: + src = d['source'] + trg = d['target'] + source_inds.append(src) + target_inds.append(trg) + transmissions.append([src, trg]) + self.transmissions = transmissions + self.source_inds = source_inds + self.target_inds = target_inds + return transmissions + + def make_detailed(self, people, reset=False): ''' Construct a detailed transmission tree, with additional information for each person ''' @@ -1411,28 +1421,57 @@ def make_detailed(self, people, reset=False): ddf.loc[v_trg, src+attr] = people[attr][v_src] # Replace nan with false - def fillna(cols): + def fillna(nadf, cols): cols = sc.promotetolist(cols) filldict = {k:False for k in cols} - ddf.fillna(value=filldict, inplace=True) + nadf.fillna(value=filldict, inplace=True) return # Pull out valid indices for source and target ddf.loc[v_trg, src+'is_quarantined'] = (ddf.loc[v_trg, src+'date_quarantined'] <= vinfdates) & ~(ddf.loc[v_trg, src+'date_quarantined'] <= vinfdates) - fillna(src+'is_quarantined') + fillna(ddf, src+'is_quarantined') for is_attr,date_attr in zip(is_attrs, date_attrs): ddf.loc[v_trg, src+is_attr] = (ddf.loc[v_trg, src+date_attr] <= vinfdates) - fillna(src+is_attr) + fillna(ddf, src+is_attr) # Populate remaining properties ddf.loc[v_trg, src+'is_asymp'] = np.isnan(ddf.loc[v_trg, src+'date_symptomatic']) ddf.loc[v_trg, src+'is_presymp'] = ~ddf.loc[v_trg, src+'is_asymp'] & ~ddf.loc[v_trg, src+'is_symptomatic'] ddf.loc[trg_inds, trg+'is_quarantined'] = (ddf.loc[trg_inds, trg+'date_quarantined'] <= ainfdates) & ~(ddf.loc[trg_inds, trg+'date_end_quarantine'] <= ainfdates) - fillna(trg+'is_quarantined') + fillna(ddf, trg+'is_quarantined') # Store self.detailed = ddf + # Also re-parse the log and convert to a simpler dataframe + targets = np.array(self.target_inds) + df = pd.DataFrame(index=np.arange(len(targets))) + infdates = ddf.loc[targets, 'date'].values + df.loc[:, 'date'] = infdates + df.loc[:, 'layer'] = ddf.loc[targets, 'layer'].values + df.loc[:, 's_asymp'] = np.isnan(ddf.loc[targets, 'src_date_symptomatic'].values) + df.loc[:, 's_presymp'] = ~(df.loc[:, 's_asymp'].values) & (infdates < ddf.loc[targets, 'src_date_symptomatic'].values) + fillna(df, 's_presymp') + df.loc[:, 's_sev'] = ddf.loc[targets, 'src_date_severe'].values < infdates + df.loc[:, 's_crit'] = ddf.loc[targets, 'src_date_critical'].values < infdates + df.loc[:, 's_diag'] = ddf.loc[targets, 'src_date_diagnosed'].values < infdates + df.loc[:, 's_quar'] = (ddf.loc[targets, 'src_date_quarantined'].values < infdates) & ~(ddf.loc[targets, 'src_date_end_quarantine'].values <= infdates) + df.loc[:, 't_quar'] = (ddf.loc[targets, 'trg_date_quarantined'].values < infdates) & ~(ddf.loc[targets, 'trg_date_end_quarantine'].values <= infdates) + fillna(df, ['s_sev', 's_crit', 's_diag', 's_quar', 't_quar']) + + df = df.rename(columns={'date': 'Day'}) # For use in plotting + df = df.loc[df['layer'] != 'seed_infection'] + + df['Stage'] = 'Symptomatic' + df.loc[df['s_asymp'], 'Stage'] = 'Asymptomatic' + df.loc[df['s_presymp'], 'Stage'] = 'Presymptomatic' + + df['Severity'] = 'Mild' + df.loc[df['s_sev'], 'Severity'] = 'Severe' + df.loc[df['s_crit'], 'Severity'] = 'Critical' + + self.df = df + return @@ -1563,7 +1602,8 @@ def animate(self, *args, **kwargs): source_ind = ddict['source'] # Index of the person who infected the target target_date = ddict['date'] - if source_ind is not None: # Seed infections and importations won't have a source + if ~np.isnan(source_ind): # Seed infections and importations won't have a source + source_ind = int(source_ind) source_date = detailed[source_ind]['date'] else: source_ind = 0 diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 7c637c058..4b60a0711 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -160,11 +160,11 @@ def test_transtree(): cv.options.set(interactive=do_plot) T = sc.tic() - snapshot = test_snapshot() - agehist = test_age_hist() - daily_age = test_daily_age() - daily = test_daily_stats() - fit = test_fit() + # snapshot = test_snapshot() + # agehist = test_age_hist() + # daily_age = test_daily_age() + # daily = test_daily_stats() + # fit = test_fit() transtree = test_transtree() print('\n'*2) From 6c7c54244959da6f97636b4658454bb877a9b29d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 05:32:18 -0700 Subject: [PATCH 226/569] uncomment tests --- tests/test_analysis.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 4b60a0711..7c637c058 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -160,11 +160,11 @@ def test_transtree(): cv.options.set(interactive=do_plot) T = sc.tic() - # snapshot = test_snapshot() - # agehist = test_age_hist() - # daily_age = test_daily_age() - # daily = test_daily_stats() - # fit = test_fit() + snapshot = test_snapshot() + agehist = test_age_hist() + daily_age = test_daily_age() + daily = test_daily_stats() + fit = test_fit() transtree = test_transtree() print('\n'*2) From bac4416ed6315abfd8e500cc01f83b6c14d6ee3f Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 23 Mar 2021 13:55:26 +0100 Subject: [PATCH 227/569] nabs negative, not good! --- covasim/parameters.py | 24 +++++++++++------------- tests/devtests/test_variants.py | 28 ++++++++++++++-------------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 4894aea1a..87f381e1a 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -63,23 +63,22 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Parameters that control settings and defaults for multi-strain runs pars['n_strains'] = 1 # The number of strains currently circulating in the population pars['total_strains'] = None # Set during sim initialization, once strains have been specified and processed - pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor - pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py - pars['immune_degree'] = None # Pre-loaded array mapping from NAb titre to efficacy, set in Immunity.py (based on Fig 1A of https://www.medrxiv.org/content/10.1101/2021.03.09.21252641v1.full.pdf) - pars['vaccine_info'] = None # Vaccine info in a more easily accessible format - # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains - pars['rel_beta'] = 1.0 - pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 - pars['NAb_init'] = dict(dist='lognormal', par1= 0.5, par2= 1) # Parameters for the distribution of the initial level of NAbs following natural infection - pars['NAb_decay'] = dict(form='log_linear_exp_decay', pars={'ll_rate': 1/180, 'll_length': 250, 'exp_rate': 1/100}) # Parameters describing the kinetics of decay of NAbs over time - pars['NAb_boost'] = 3 # Multiplicative factor applied to a person's NAb levels if they get reinfected - - pars['rel_imm'] = {} + # Parameters used to calculate immunity + pars['NAb_init'] = dict(dist='lognormal', par1= 0.5, par2= 1) # Parameters for the distribution of the initial level of NAbs following natural infection + pars['NAb_decay'] = dict(form='log_linear_exp_decay', pars={'ll_rate': 1/180, 'll_length': 250, 'exp_rate': 1/100}) # Parameters describing the kinetics of decay of NAbs over time + pars['NAb_boost'] = 3 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source + pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains + pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms pars['rel_imm']['asymptomatic'] = 0.5 pars['rel_imm']['mild'] = 0.85 pars['rel_imm']['severe'] = 1 + pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py + pars['vaccine_info'] = None # Vaccine info in a more easily accessible format + # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains + pars['rel_beta'] = 1.0 + pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 pars['dur'] = {} # Duration parameters: time for disease progression pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.6, par2=4.8) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, subtracting inf2sym duration @@ -92,7 +91,6 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['dur']['sev2rec'] = dict(dist='lognormal_int', par1=14.0, par2=2.4) # Duration for people with severe symptoms to recover, 22.6 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf pars['dur']['crit2rec'] = dict(dist='lognormal_int', par1=14.0, par2=2.4) # Duration for people with critical symptoms to recover, 22.6 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf pars['dur']['crit2die'] = dict(dist='lognormal_int', par1=6.2, par2=1.7) # Duration from critical symptoms to death, 17.8 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf - # Severity parameters: probabilities of symptom progression pars['rel_symp_prob'] = 1.0 # Scale factor for proportion of symptomatic cases pars['rel_severe_prob'] = 1.0 # Scale factor for proportion of symptomatic cases that become severe diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index b2ea88442..dcc48b8a5 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -432,25 +432,25 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - if 1: + if 0: sim = cv.Sim() sim.run() # Run more complex single-sim tests sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - - # Run Vaccine tests - sim4 = test_synthpops() - sim5 = test_vaccine_1strain() - - # Run multisim and scenario tests - scens0 = test_vaccine_1strain_scen() - scens1 = test_vaccine_2strains_scen() - scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) - msim0 = test_msim() + # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + # + # # Run Vaccine tests + # sim4 = test_synthpops() + # sim5 = test_vaccine_1strain() + # + # # Run multisim and scenario tests + # scens0 = test_vaccine_1strain_scen() + # scens1 = test_vaccine_2strains_scen() + # scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) + # msim0 = test_msim() sc.toc() From 632bec3ebdfff3b3062dd9f0753ab8ef34af9dfa Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 23 Mar 2021 21:41:55 +0100 Subject: [PATCH 228/569] implement awful double exponential --- covasim/immunity.py | 30 ++++++++++++++++-------------- covasim/parameters.py | 4 ++-- tests/devtests/test_variants.py | 28 ++++++++++++++-------------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 39a1f12bc..a4f8902bf 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -344,7 +344,7 @@ def compute_nab(people, inds, prior_inf=True): n_days = people.pars['n_days'] days_left = n_days - day+1 # how many days left in sim NAb_waning = pre_compute_waning(length=days_left, form=NAb_decay['form'], pars=NAb_decay['pars']) - people.NAb[day:, inds] = np.add.outer(NAb_waning, people.NAb[day, inds]) + people.NAb[day:, inds] = np.multiply.outer(NAb_waning,2**people.NAb[day, inds]) return @@ -502,10 +502,10 @@ def check_immunity(people, strain, sus=True, inds=None): # %% Methods for computing waning __all__ += ['pre_compute_waning'] -def pre_compute_waning(length, form='log_linear_exp_decay', pars=None): +def pre_compute_waning(length, form='nab_decay', pars=None): ''' Process functional form and parameters into values - - 'log_linear_exp_decay': log-linear decay followed by exponential decay. The default choice, taken from https://doi.org/10.1101/2021.03.09.21252641 + - 'nab_decay' : specific decay function taken from https://doi.org/10.1101/2021.03.09.21252641 - 'exp_decay' : exponential decay. Parameters should be init_val and half_life (half_life can be None/nan) - 'logistic_decay' : logistic decay (TODO fill in details) - 'linear' : linear decay (TODO fill in details) @@ -520,7 +520,7 @@ def pre_compute_waning(length, form='log_linear_exp_decay', pars=None): ''' choices = [ - 'log_linear_exp_decay', # Default if no form is provided + 'nab_decay', # Default if no form is provided 'exp_decay', 'logistic_decay', 'linear_growth', @@ -528,8 +528,8 @@ def pre_compute_waning(length, form='log_linear_exp_decay', pars=None): ] # Process inputs - if form is None or form == 'log_linear_exp_decay': - output = log_linear_exp_decay(length, **pars) + if form is None or form == 'nab_decay': + output = nab_decay(length, **pars) elif form == 'exp_decay': if pars['half_life'] is None: pars['half_life'] = np.nan @@ -552,17 +552,19 @@ def pre_compute_waning(length, form='log_linear_exp_decay', pars=None): return output -def log_linear_exp_decay(length, ll_rate, ll_length, exp_rate): +def nab_decay(length, init_decay_rate, init_decay_time, decay_decay_rate): ''' - Returns an array of length 'length' with containing the evaluated function at each point + Returns an array of length 'length' containing the evaluated function NAb decay + function at each point + Uses exponential decay, with the rate of exponential decay also set to exponentilly decay (!) after 250 days ''' - ll_part = lambda t, ll_rate: -ll_rate*t - exp_part = lambda t, init_val, exp_rate: init_val - np.exp(-t*exp_rate) - t = np.arange(length, dtype=cvd.default_int) - y1 = ll_part(cvu.true(tll_length), y1[-1], exp_rate) - y = np.concatenate([y1,y2]) + f1 = lambda t, init_decay_rate: np.exp(-t*init_decay_rate) + f2 = lambda t, init_decay_rate, init_decay_time, decay_decay_rate: np.exp(-t*(init_decay_rate*np.exp(-(t-init_decay_time)*decay_decay_rate))) + t = np.arange(length, dtype=cvd.default_int) + y1 = f1(cvu.true(tinit_decay_time), init_decay_rate, init_decay_time, decay_decay_rate) + y = np.concatenate([y1,y2]) return y diff --git a/covasim/parameters.py b/covasim/parameters.py index 87f381e1a..512b183fa 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -65,8 +65,8 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['total_strains'] = None # Set during sim initialization, once strains have been specified and processed # Parameters used to calculate immunity - pars['NAb_init'] = dict(dist='lognormal', par1= 0.5, par2= 1) # Parameters for the distribution of the initial level of NAbs following natural infection - pars['NAb_decay'] = dict(form='log_linear_exp_decay', pars={'ll_rate': 1/180, 'll_length': 250, 'exp_rate': 1/100}) # Parameters describing the kinetics of decay of NAbs over time + pars['NAb_init'] = dict(dist='normal', par1= 0, par2= 2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 + pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_boost'] = 3 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index dcc48b8a5..b2ea88442 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -432,25 +432,25 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - if 0: + if 1: sim = cv.Sim() sim.run() # Run more complex single-sim tests sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - # - # # Run Vaccine tests - # sim4 = test_synthpops() - # sim5 = test_vaccine_1strain() - # - # # Run multisim and scenario tests - # scens0 = test_vaccine_1strain_scen() - # scens1 = test_vaccine_2strains_scen() - # scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) - # msim0 = test_msim() + sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + + # Run Vaccine tests + sim4 = test_synthpops() + sim5 = test_vaccine_1strain() + + # Run multisim and scenario tests + scens0 = test_vaccine_1strain_scen() + scens1 = test_vaccine_2strains_scen() + scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) + msim0 = test_msim() sc.toc() From 80580eba1a559a3232d26c3815de78c2fec1190c Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Tue, 23 Mar 2021 21:42:40 +0100 Subject: [PATCH 229/569] check tests --- tests/devtests/test_variants.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index b2ea88442..dcc48b8a5 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -432,25 +432,25 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - if 1: + if 0: sim = cv.Sim() sim.run() # Run more complex single-sim tests sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - - # Run Vaccine tests - sim4 = test_synthpops() - sim5 = test_vaccine_1strain() - - # Run multisim and scenario tests - scens0 = test_vaccine_1strain_scen() - scens1 = test_vaccine_2strains_scen() - scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) - msim0 = test_msim() + # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + # + # # Run Vaccine tests + # sim4 = test_synthpops() + # sim5 = test_vaccine_1strain() + # + # # Run multisim and scenario tests + # scens0 = test_vaccine_1strain_scen() + # scens1 = test_vaccine_2strains_scen() + # scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) + # msim0 = test_msim() sc.toc() From 2c516328e1300389b540cd76020d2379c4f96bfc Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 17:16:48 -0700 Subject: [PATCH 230/569] redoing transmission tree --- covasim/analysis.py | 74 ++++++++++++++++++++++++--------------------- covasim/utils.py | 2 +- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 10b7c1f4a..bb7e48047 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1344,7 +1344,7 @@ def count_targets(self, start_day=None, end_day=None): if self.sources[i] is not None: if self.source_dates[i] >= start_day and self.source_dates[i] <= end_day: n_targets[i] = len(self.targets[i]) - n_target_inds = sc.findinds(~np.isnan(n_targets)) + n_target_inds = sc.findinds(np.isfinite(n_targets)) n_targets = n_targets[n_target_inds] self.n_targets = n_targets return n_targets @@ -1375,8 +1375,15 @@ def count_transmissions(self): def make_detailed(self, people, reset=False): ''' Construct a detailed transmission tree, with additional information for each person ''' - # Convert infection log to a dataframe and initialize - inf_df = sc.dcp(pd.DataFrame(self.infection_log)) + def df_to_arrdict(df): + ''' Convert a dataframe to a dictionary of arrays ''' + arrdict = dict() + for col in df.columns: + arrdict[col] = df[col].values + return arrdict + + # Convert infection log to a dataframe and from there to a dict of arrays + inflog = df_to_arrdict(sc.dcp(pd.DataFrame(self.infection_log))) # Initialization n_people = len(people) @@ -1386,45 +1393,43 @@ def make_detailed(self, people, reset=False): quar_attrs = ['date_quarantined', 'date_end_quarantine'] date_attrs = [attr for attr in attrs if attr.startswith('date_')] is_attrs = [attr.replace('date_', 'is_') for attr in date_attrs] - ddf = pd.DataFrame(index=np.arange(n_people)) + dd_arr = lambda: np.nan*np.zeros(n_people) + dd = sc.odict(defaultdict=dd_arr) # Data dictionary, to be converted to a dataframe later # Handle indices - trg_inds = np.array(inf_df['target'].values, dtype=np.int64) - src_inds = np.array(inf_df['source'].values) - date_vals = np.array(inf_df['date'].values) - layer_vals = np.array(inf_df['layer'].values) - src_arr = np.nan*np.zeros(n_people) - trg_arr = np.nan*np.zeros(n_people) - infdate_arr = np.nan*np.zeros(n_people) + src_arr = dd_arr() + trg_arr = dd_arr() + date_arr = dd_arr() # Map onto arrays - src_arr[trg_inds] = src_inds - trg_arr[trg_inds] = trg_inds - infdate_arr[trg_inds] = date_vals + t_inds = np.array(inflog['target'], dtype=np.int64) + src_arr[t_inds] = inflog['source'] + trg_arr[t_inds] = t_inds + date_arr[t_inds] = inflog['date'] # Further index wrangling - ts_inds = sc.findinds(~np.isnan(trg_arr) * ~np.isnan(src_arr)) # Valid target-source indices - v_src = np.array(src_arr[ts_inds], dtype=np.int64) # Valid source indices - v_trg = np.array(trg_arr[ts_inds], dtype=np.int64) # Valid target indices - vinfdates = infdate_arr[v_trg] # Valid target-source pair infection dates - ainfdates = infdate_arr[trg_inds] # All infection dates + vts_inds = sc.findinds(np.isfinite(trg_arr) * np.isfinite(src_arr)) # Valid target-source indices + vs_inds = np.array(src_arr[vts_inds], dtype=np.int64) # Valid source indices + vt_inds = np.array(trg_arr[vts_inds], dtype=np.int64) # Valid target indices + vinfdates = date_arr[vt_inds] # Valid target-source pair infection dates + ainfdates = date_arr[t_inds] # All infection dates # Populate main columns - ddf.loc[v_trg, 'source'] = v_src - ddf.loc[trg_inds, 'target'] = trg_inds - ddf.loc[trg_inds, 'date'] = ainfdates - ddf.loc[trg_inds, 'layer'] = layer_vals + dd['source'][vt_inds] = vs_inds + dd['target'][t_inds] = t_inds + dd['date'][t_inds] = ainfdates + dd['layer'][t_inds] = inflog['layer'] # Populate from people for attr in attrs+quar_attrs: - ddf.loc[:, trg+attr] = people[attr][:] - ddf.loc[v_trg, src+attr] = people[attr][v_src] + dd[trg+attr] = people[attr][:] + dd[src+attr][vt_inds] = people[attr][vs_inds] # Replace nan with false - def fillna(nadf, cols): + def fillna(arrdict, cols, value=False): cols = sc.promotetolist(cols) - filldict = {k:False for k in cols} - nadf.fillna(value=filldict, inplace=True) + for col in cols: + arrdict[col][np.isnan(arrdict[col])] = value return # Pull out valid indices for source and target @@ -1441,11 +1446,12 @@ def fillna(nadf, cols): fillna(ddf, trg+'is_quarantined') # Store - self.detailed = ddf + self.detailed = pd.DataFrame(dd) # Also re-parse the log and convert to a simpler dataframe targets = np.array(self.target_inds) df = pd.DataFrame(index=np.arange(len(targets))) + ddft = ddf.loc[targets].reset_index() # Pull out only target values DO ABOVE TOO infdates = ddf.loc[targets, 'date'].values df.loc[:, 'date'] = infdates df.loc[:, 'layer'] = ddf.loc[targets, 'layer'].values @@ -1597,12 +1603,12 @@ def animate(self, *args, **kwargs): tdq = {} # Short for "tested, diagnosed, or quarantined" target_ind = ddict['target'] - if not np.isnan(ddict['date']): # If this person was infected + if np.isfinite(ddict['date']): # If this person was infected source_ind = ddict['source'] # Index of the person who infected the target target_date = ddict['date'] - if ~np.isnan(source_ind): # Seed infections and importations won't have a source + if np.isfinite(source_ind): # Seed infections and importations won't have a source source_ind = int(source_ind) source_date = detailed[source_ind]['date'] else: @@ -1623,11 +1629,11 @@ def animate(self, *args, **kwargs): date_t = ddict['trg_date_tested'] date_d = ddict['trg_date_diagnosed'] date_q = ddict['trg_date_known_contact'] - if ~np.isnan(date_t) and date_t < n: + if np.isfinite(date_t) and date_t < n: tests[int(date_t)].append(tdq) - if ~np.isnan(date_d) and date_d < n: + if np.isfinite(date_d) and date_d < n: diags[int(date_d)].append(tdq) - if ~np.isnan(date_q) and date_q < n: + if np.isfinite(date_q) and date_q < n: quars[int(date_q)].append(tdq) else: diff --git a/covasim/utils.py b/covasim/utils.py index f9ad881f4..891870cfc 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -546,7 +546,7 @@ def ifalse(arr, inds): def idefined(arr, inds): ''' - Returns the indices that are true in the array -- name is short for indices[defined] + Returns the indices that are defined in the array -- name is short for indices[defined] Args: arr (array): any array, used as a filter From 0bdaa60f9fe79b422ad88a30a927ae185f63d391 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 17:55:17 -0700 Subject: [PATCH 231/569] finish transtree reimplementation --- covasim/analysis.py | 81 ++++++++++++---------------- tests/devtests/dev_test_transtree.py | 20 ++++--- 2 files changed, 48 insertions(+), 53 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index bb7e48047..960e0ad80 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1393,7 +1393,7 @@ def df_to_arrdict(df): quar_attrs = ['date_quarantined', 'date_end_quarantine'] date_attrs = [attr for attr in attrs if attr.startswith('date_')] is_attrs = [attr.replace('date_', 'is_') for attr in date_attrs] - dd_arr = lambda: np.nan*np.zeros(n_people) + dd_arr = lambda: np.nan*np.zeros(n_people) # Create an empty array of the right size dd = sc.odict(defaultdict=dd_arr) # Data dictionary, to be converted to a dataframe later # Handle indices @@ -1402,69 +1402,56 @@ def df_to_arrdict(df): date_arr = dd_arr() # Map onto arrays - t_inds = np.array(inflog['target'], dtype=np.int64) - src_arr[t_inds] = inflog['source'] - trg_arr[t_inds] = t_inds - date_arr[t_inds] = inflog['date'] + ti = np.array(inflog['target'], dtype=np.int64) # "Target indices", short since used so much + src_arr[ti] = inflog['source'] + trg_arr[ti] = ti + date_arr[ti] = inflog['date'] # Further index wrangling vts_inds = sc.findinds(np.isfinite(trg_arr) * np.isfinite(src_arr)) # Valid target-source indices vs_inds = np.array(src_arr[vts_inds], dtype=np.int64) # Valid source indices - vt_inds = np.array(trg_arr[vts_inds], dtype=np.int64) # Valid target indices - vinfdates = date_arr[vt_inds] # Valid target-source pair infection dates - ainfdates = date_arr[t_inds] # All infection dates + vi = np.array(trg_arr[vts_inds], dtype=np.int64) # Valid target indices, short since used so much + vinfdates = date_arr[vi] # Valid target-source pair infection dates + tinfdates = date_arr[ti] # All target infection dates # Populate main columns - dd['source'][vt_inds] = vs_inds - dd['target'][t_inds] = t_inds - dd['date'][t_inds] = ainfdates - dd['layer'][t_inds] = inflog['layer'] + dd['source'][vi] = vs_inds + dd['target'][ti] = ti + dd['date'][ti] = tinfdates + dd['layer'] = np.array(dd['layer'], dtype=object) + dd['layer'][ti] = inflog['layer'] # Populate from people for attr in attrs+quar_attrs: dd[trg+attr] = people[attr][:] - dd[src+attr][vt_inds] = people[attr][vs_inds] - - # Replace nan with false - def fillna(arrdict, cols, value=False): - cols = sc.promotetolist(cols) - for col in cols: - arrdict[col][np.isnan(arrdict[col])] = value - return + dd[src+attr][vi] = people[attr][vs_inds] # Pull out valid indices for source and target - ddf.loc[v_trg, src+'is_quarantined'] = (ddf.loc[v_trg, src+'date_quarantined'] <= vinfdates) & ~(ddf.loc[v_trg, src+'date_quarantined'] <= vinfdates) - fillna(ddf, src+'is_quarantined') + lnot = np.logical_not # Shorten since used heavily + dd[src+'is_quarantined'][vi] = (dd[src+'date_quarantined'][vi] <= vinfdates) & lnot(dd[src+'date_quarantined'][vi] <= vinfdates) for is_attr,date_attr in zip(is_attrs, date_attrs): - ddf.loc[v_trg, src+is_attr] = (ddf.loc[v_trg, src+date_attr] <= vinfdates) - fillna(ddf, src+is_attr) + dd[src+is_attr][vi] = np.array(dd[src+date_attr][vi] <= vinfdates, dtype=bool) # Populate remaining properties - ddf.loc[v_trg, src+'is_asymp'] = np.isnan(ddf.loc[v_trg, src+'date_symptomatic']) - ddf.loc[v_trg, src+'is_presymp'] = ~ddf.loc[v_trg, src+'is_asymp'] & ~ddf.loc[v_trg, src+'is_symptomatic'] - ddf.loc[trg_inds, trg+'is_quarantined'] = (ddf.loc[trg_inds, trg+'date_quarantined'] <= ainfdates) & ~(ddf.loc[trg_inds, trg+'date_end_quarantine'] <= ainfdates) - fillna(ddf, trg+'is_quarantined') - - # Store - self.detailed = pd.DataFrame(dd) + dd[src+'is_asymp'][vi] = np.isnan(dd[src+'date_symptomatic'][vi]) + dd[src+'is_presymp'][vi] = lnot(dd[src+'is_asymp'][vi]) & lnot(dd[src+'is_symptomatic'][vi]) + dd[trg+'is_quarantined'][ti] = (dd[trg+'date_quarantined'][ti] <= tinfdates) & lnot(dd[trg+'date_end_quarantine'][ti] <= tinfdates) # Also re-parse the log and convert to a simpler dataframe targets = np.array(self.target_inds) - df = pd.DataFrame(index=np.arange(len(targets))) - ddft = ddf.loc[targets].reset_index() # Pull out only target values DO ABOVE TOO - infdates = ddf.loc[targets, 'date'].values - df.loc[:, 'date'] = infdates - df.loc[:, 'layer'] = ddf.loc[targets, 'layer'].values - df.loc[:, 's_asymp'] = np.isnan(ddf.loc[targets, 'src_date_symptomatic'].values) - df.loc[:, 's_presymp'] = ~(df.loc[:, 's_asymp'].values) & (infdates < ddf.loc[targets, 'src_date_symptomatic'].values) - fillna(df, 's_presymp') - df.loc[:, 's_sev'] = ddf.loc[targets, 'src_date_severe'].values < infdates - df.loc[:, 's_crit'] = ddf.loc[targets, 'src_date_critical'].values < infdates - df.loc[:, 's_diag'] = ddf.loc[targets, 'src_date_diagnosed'].values < infdates - df.loc[:, 's_quar'] = (ddf.loc[targets, 'src_date_quarantined'].values < infdates) & ~(ddf.loc[targets, 'src_date_end_quarantine'].values <= infdates) - df.loc[:, 't_quar'] = (ddf.loc[targets, 'trg_date_quarantined'].values < infdates) & ~(ddf.loc[targets, 'trg_date_end_quarantine'].values <= infdates) - fillna(df, ['s_sev', 's_crit', 's_diag', 's_quar', 't_quar']) - + dtr = dict() + infdates = dd['date'][targets] + dtr['date'] = infdates + dtr['layer'] = dd['layer'][targets] + dtr['s_asymp'] = np.isnan(dd['src_date_symptomatic'][targets]) + dtr['s_presymp'] = ~(dtr['s_asymp'][:]) & (infdates < dd['src_date_symptomatic'][targets]) + dtr['s_sev'] = dd['src_date_severe'][targets] < infdates + dtr['s_crit'] = dd['src_date_critical'][targets] < infdates + dtr['s_diag'] = dd['src_date_diagnosed'][targets] < infdates + dtr['s_quar'] = (dd['src_date_quarantined'][targets] < infdates) & lnot(dd['src_date_end_quarantine'][targets] <= infdates) + dtr['t_quar'] = (dd['trg_date_quarantined'][targets] < infdates) & lnot(dd['trg_date_end_quarantine'][targets] <= infdates) + + df = pd.DataFrame(dtr) df = df.rename(columns={'date': 'Day'}) # For use in plotting df = df.loc[df['layer'] != 'seed_infection'] @@ -1476,6 +1463,8 @@ def fillna(arrdict, cols, value=False): df.loc[df['s_sev'], 'Severity'] = 'Severe' df.loc[df['s_crit'], 'Severity'] = 'Critical' + # Store + self.detailed = pd.DataFrame(dd) self.df = df return diff --git a/tests/devtests/dev_test_transtree.py b/tests/devtests/dev_test_transtree.py index 2676793ad..f64c962ed 100644 --- a/tests/devtests/dev_test_transtree.py +++ b/tests/devtests/dev_test_transtree.py @@ -88,7 +88,7 @@ def fillna(cols): sc.toc() -sc.heading('Old implementation (dicts)...') +sc.heading('Original implementation (dicts)...') sc.tic() @@ -130,30 +130,36 @@ def fillna(cols): sc.toc() + sc.heading('Validation...') sc.tic() for i in range(len(detailed)): - sc.percentcomplete(step=i, maxsteps=len(detailed), stepsize=10) + sc.percentcomplete(step=i, maxsteps=len(detailed)-1, stepsize=5) d_entry = detailed[i] df_entry = ddf.iloc[i].to_dict() + tt_entry = tt.detailed.iloc[i].to_dict() if d_entry is None: # If in the dict it's None, it should be nan in the dataframe - assert np.isnan(df_entry['target']) + for entry in [df_entry, tt_entry]: + assert np.isnan(entry['target']) else: - dkeys = list(d_entry.keys()) + dkeys = list(d_entry.keys()) dfkeys = list(df_entry.keys()) + ttkeys = list(tt_entry.keys()) + assert dfkeys == ttkeys assert all([dk in dfkeys for dk in dkeys]) # The dataframe can have extra keys, but not the dict for k in dkeys: v_d = d_entry[k] v_df = df_entry[k] + v_tt = tt_entry[k] try: - assert np.isclose(v_d, v_df, equal_nan=True) # If it's numeric, check they're close + assert np.isclose(v_d, v_df, v_tt, equal_nan=True) # If it's numeric, check they're close except TypeError: if v_d is None: - assert np.isnan(v_df) # If in the dict it's None, it should be nan in the dataframe + assert all(np.isnan([v_df, v_tt])) # If in the dict it's None, it should be nan in the dataframe else: - assert v_d == v_df # In all other cases, it should be an exact match + assert v_d == v_df == v_tt # In all other cases, it should be an exact match print('\nValidation passed.') From d112c1a0cc460b2c2f0f33a75543a3fca46f2936 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 18:00:19 -0700 Subject: [PATCH 232/569] test updates --- tests/devtests/dev_test_transtree.py | 57 ++++++++++++++-------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/tests/devtests/dev_test_transtree.py b/tests/devtests/dev_test_transtree.py index f64c962ed..c225686ff 100644 --- a/tests/devtests/dev_test_transtree.py +++ b/tests/devtests/dev_test_transtree.py @@ -8,15 +8,16 @@ import numpy as np # Create a sim -sim = cv.Sim(pop_size=10e3, n_days=100).run() +sim = cv.Sim(pop_size=100e3, n_days=100).run() people = sim.people -sc.heading('Built-in implementation (pandas)...') -sc.tic() +sc.heading('Built-in implementation (Numpy)...') tt = sim.make_transtree() +sc.tic() +tt.make_detailed(sim.people) sc.toc() @@ -135,31 +136,31 @@ def fillna(cols): sc.tic() -for i in range(len(detailed)): - sc.percentcomplete(step=i, maxsteps=len(detailed)-1, stepsize=5) - d_entry = detailed[i] - df_entry = ddf.iloc[i].to_dict() - tt_entry = tt.detailed.iloc[i].to_dict() - if d_entry is None: # If in the dict it's None, it should be nan in the dataframe - for entry in [df_entry, tt_entry]: - assert np.isnan(entry['target']) - else: - dkeys = list(d_entry.keys()) - dfkeys = list(df_entry.keys()) - ttkeys = list(tt_entry.keys()) - assert dfkeys == ttkeys - assert all([dk in dfkeys for dk in dkeys]) # The dataframe can have extra keys, but not the dict - for k in dkeys: - v_d = d_entry[k] - v_df = df_entry[k] - v_tt = tt_entry[k] - try: - assert np.isclose(v_d, v_df, v_tt, equal_nan=True) # If it's numeric, check they're close - except TypeError: - if v_d is None: - assert all(np.isnan([v_df, v_tt])) # If in the dict it's None, it should be nan in the dataframe - else: - assert v_d == v_df == v_tt # In all other cases, it should be an exact match +# for i in range(len(detailed)): +# sc.percentcomplete(step=i, maxsteps=len(detailed)-1, stepsize=5) +# d_entry = detailed[i] +# df_entry = ddf.iloc[i].to_dict() +# tt_entry = tt.detailed.iloc[i].to_dict() +# if d_entry is None: # If in the dict it's None, it should be nan in the dataframe +# for entry in [df_entry, tt_entry]: +# assert np.isnan(entry['target']) +# else: +# dkeys = list(d_entry.keys()) +# dfkeys = list(df_entry.keys()) +# ttkeys = list(tt_entry.keys()) +# assert dfkeys == ttkeys +# assert all([dk in dfkeys for dk in dkeys]) # The dataframe can have extra keys, but not the dict +# for k in dkeys: +# v_d = d_entry[k] +# v_df = df_entry[k] +# v_tt = tt_entry[k] +# try: +# assert np.isclose(v_d, v_df, v_tt, equal_nan=True) # If it's numeric, check they're close +# except TypeError: +# if v_d is None: +# assert all(np.isnan([v_df, v_tt])) # If in the dict it's None, it should be nan in the dataframe +# else: +# assert v_d == v_df == v_tt # In all other cases, it should be an exact match print('\nValidation passed.') From 062aafd5b785e834cbc407be0b7e0b6930af4eed Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 19:01:36 -0700 Subject: [PATCH 233/569] working on docs --- docs/_static/theme_overrides.css | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css index 062cfb21e..82f1de029 100644 --- a/docs/_static/theme_overrides.css +++ b/docs/_static/theme_overrides.css @@ -45,6 +45,7 @@ div.document span.search-highlight { margin-bottom: 10px; } +/* CK: alternating table row colors */ .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td, .wy-table-backed, .wy-table-odd td, .wy-table-striped tr:nth-child(2n-1) td { background-color: #acf; } @@ -52,4 +53,11 @@ tr.row-even { background-color: #def; } -.highlight { background: #D9F0FF; } \ No newline at end of file +/* CK: Change the color of code blocks */ +.highlight { background: #D9F0FF; } + +/* CK: Change the color of inline code */ +code.literal { + color: #e01e5a !important; + background-color: #f6f6f6 !important; +} \ No newline at end of file From 9778252d436269bae95f0a99b4ed20e9ec1c7260 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 19:16:04 -0700 Subject: [PATCH 234/569] fixed styling inconsistency --- docs/_static/theme_overrides.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css index 82f1de029..b1084bc0d 100644 --- a/docs/_static/theme_overrides.css +++ b/docs/_static/theme_overrides.css @@ -21,7 +21,7 @@ h1 { margin-bottom: 1.0em; } -h2 { +h2, .toc-backref { font-size: 125%; color: #0055af !important; margin-bottom: 1.0em; From 18a1c824c7b2f965eb505f4849f77fe4528585dd Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 20:39:07 -0700 Subject: [PATCH 235/569] working on distributions --- CHANGELOG.rst | 6 ++- covasim/utils.py | 4 +- docs/_static/theme_overrides.css | 1 + tests/test_utils.py | 78 ++++++++++++++++++++++++++------ 4 files changed, 70 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 200159bd6..c27ea3a98 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,10 +24,12 @@ These are the major improvements we are currently working on. If there is a spec ~~~~~~~~~~~~~~~~~~~~~~~ -Latest versions (2.0.x) +Latest versions (2.x) ~~~~~~~~~~~~~~~~~~~~~~~ -sim.plot(dpi=150, rotation=45, start_day='2020-03-01', end_day=55, interval=7) +Version 2.1.0 (2021-03-23) +-------------------------- +- ``sim.plot(dpi=150, rotation=45, start_day='2020-03-01', end_day=55, interval=7)`` diff --git a/covasim/utils.py b/covasim/utils.py index 891870cfc..20afb76d0 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -189,8 +189,8 @@ def sample(dist=None, par1=None, par2=None, size=None, **kwargs): elif dist == 'neg_binomial': samples = n_neg_binomial(rate=par1, dispersion=par2, n=size, **kwargs) # Use custom version below elif dist in ['lognormal', 'lognormal_int']: if par1>0: - mean = np.log(par1**2 / np.sqrt(par2 + par1**2)) # Computes the mean of the underlying normal distribution - sigma = np.sqrt(np.log(par2/par1**2 + 1)) # Computes sigma for the underlying normal distribution + mean = np.log(par1**2 / np.sqrt(par2**2 + par1**2)) # Computes the mean of the underlying normal distribution + sigma = np.sqrt(np.log(par2**2/par1**2 + 1)) # Computes sigma for the underlying normal distribution samples = np.random.lognormal(mean=mean, sigma=sigma, size=size, **kwargs) else: samples = np.zeros(size) diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css index b1084bc0d..8574a6e10 100644 --- a/docs/_static/theme_overrides.css +++ b/docs/_static/theme_overrides.css @@ -21,6 +21,7 @@ h1 { margin-bottom: 1.0em; } +/* CK: added toc-backref since otherwise overrides this */ h2, .toc-backref { font-size: 125%; color: #0055af !important; diff --git a/tests/test_utils.py b/tests/test_utils.py index 3bdff4b02..1d4bdbd0a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -71,11 +71,11 @@ def test_poisson(): return s3 -def test_samples(do_plot=False): +def test_samples(do_plot=False, verbose=True): sc.heading('Samples distribution') - n = 10000 - nbins = 40 + n = 200_000 + nbins = 100 # Warning, must match utils.py! choices = [ @@ -85,6 +85,7 @@ def test_samples(do_plot=False): 'normal_pos', 'normal_int', 'lognormal_int', + 'poisson', 'neg_binomial' ] @@ -93,28 +94,75 @@ def test_samples(do_plot=False): # Run the samples nchoices = len(choices) - nsqr = np.ceil(np.sqrt(nchoices)) - results = {} + nsqr, _ = sc.get_rows_cols(nchoices) + results = sc.objdict() + mean = 11 + std = 7 + low = 3 + high = 9 + normal_dists = ['normal', 'normal_pos', 'normal_int', 'lognormal', 'lognormal_int'] for c,choice in enumerate(choices): - if choice == 'neg_binomial': - par1 = 10 - par2 = 0.5 - elif choice in ['lognormal', 'lognormal_int']: - par1 = 1 - par2 = 0.5 + kw = {} + if choice in normal_dists: + par1 = mean + par2 = std + elif choice == 'neg_binomial': + par1 = mean + par2 = 1.2 + kw['step'] = 0.1 + elif choice == 'poisson': + par1 = mean + par2 = 0 + elif choice == 'uniform': + par1 = low + par2 = high else: - par1 = 0 - par2 = 5 - results[choice] = cv.sample(dist=choice, par1=par1, par2=par2, size=n) + errormsg = f'Choice "{choice}" not implemented' + raise NotImplementedError(errormsg) + # Compute + results[choice] = cv.sample(dist=choice, par1=par1, par2=par2, size=n, **kw) + + # Optionally plot if do_plot: pl.subplot(nsqr, nsqr, c+1) - pl.hist(x=results[choice], bins=nbins) + plotbins = np.unique(results[choice]) if (choice=='poisson' or '_int' in choice) else nbins + pl.hist(x=results[choice], bins=plotbins, width=0.8) pl.title(f'dist={choice}, par1={par1}, par2={par2}') with pytest.raises(NotImplementedError): cv.sample(dist='not_found') + # Do statistical tests + tol = 1/np.sqrt(n/50/len(choices)) # Define acceptable tolerance -- broad to avoid false positives + + def isclose(choice, tol=tol, **kwargs): + key = list(kwargs.keys())[0] + ref = list(kwargs.values())[0] + npfunc = getattr(np, key) + value = npfunc(results[choice]) + msg = f'Test for {choice:14s}: expecting {key:4s} = {ref:8.4f} ± {tol*ref:8.4f} and got {value:8.4f}' + if verbose: + print(msg) + assert np.isclose(value, ref, rtol=tol), msg + return True + + # Normal + for choice in normal_dists: + isclose(choice, mean=mean) + if all([k not in choice for k in ['_pos', '_int']]): # These change the variance + isclose(choice, std=std) + + # Negative binomial + isclose('neg_binomial', mean=mean) + + # Poisson + isclose('poisson', mean=mean) + isclose('poisson', var=mean) + + # Uniform + isclose('uniform', mean=(low+high)/2) + return results From 6e41f87af5ba9430ef763f6e6b75d7960629e3b2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 21:02:02 -0700 Subject: [PATCH 236/569] update regression --- covasim/analysis.py | 2 +- covasim/regression/pars_v2.1.0.json | 203 ++++++++++++++++++++++++++++ covasim/sim.py | 2 +- covasim/version.py | 4 +- tests/baseline.json | 72 +++++----- tests/benchmark.json | 6 +- 6 files changed, 246 insertions(+), 43 deletions(-) create mode 100644 covasim/regression/pars_v2.1.0.json diff --git a/covasim/analysis.py b/covasim/analysis.py index 960e0ad80..4425fb1dc 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1228,7 +1228,7 @@ class TransTree(Analyzer): tt.plot() tt.plot_histograms() - New in version 2.0.5: ``tt.detailed`` is a dataframe rather than a list of dictionaries; + New in version 2.1.0: ``tt.detailed`` is a dataframe rather than a list of dictionaries; for the latter, use ``tt.detailed.to_dict('records')``. ''' diff --git a/covasim/regression/pars_v2.1.0.json b/covasim/regression/pars_v2.1.0.json new file mode 100644 index 000000000..6437ab382 --- /dev/null +++ b/covasim/regression/pars_v2.1.0.json @@ -0,0 +1,203 @@ +{ + "pop_size": 20000.0, + "pop_infected": 20, + "pop_type": "random", + "location": null, + "start_day": "2020-03-01", + "end_day": null, + "n_days": 60, + "rand_seed": 1, + "verbose": 0.1, + "pop_scale": 1, + "rescale": true, + "rescale_threshold": 0.05, + "rescale_factor": 1.2, + "beta": 0.016, + "contacts": { + "a": 20 + }, + "dynam_layer": { + "a": 0 + }, + "beta_layer": { + "a": 1.0 + }, + "n_imports": 0, + "beta_dist": { + "dist": "neg_binomial", + "par1": 1.0, + "par2": 0.45, + "step": 0.01 + }, + "viral_dist": { + "frac_time": 0.3, + "load_ratio": 2, + "high_cap": 4 + }, + "asymp_factor": 1.0, + "iso_factor": { + "a": 0.2 + }, + "quar_factor": { + "a": 0.3 + }, + "quar_period": 14, + "dur": { + "exp2inf": { + "dist": "lognormal_int", + "par1": 4.6, + "par2": 4.8 + }, + "inf2sym": { + "dist": "lognormal_int", + "par1": 1.0, + "par2": 0.9 + }, + "sym2sev": { + "dist": "lognormal_int", + "par1": 6.6, + "par2": 4.9 + }, + "sev2crit": { + "dist": "lognormal_int", + "par1": 3.0, + "par2": 7.4 + }, + "asym2rec": { + "dist": "lognormal_int", + "par1": 8.0, + "par2": 2.0 + }, + "mild2rec": { + "dist": "lognormal_int", + "par1": 8.0, + "par2": 2.0 + }, + "sev2rec": { + "dist": "lognormal_int", + "par1": 14.0, + "par2": 2.4 + }, + "crit2rec": { + "dist": "lognormal_int", + "par1": 14.0, + "par2": 2.4 + }, + "crit2die": { + "dist": "lognormal_int", + "par1": 6.2, + "par2": 1.7 + } + }, + "rel_symp_prob": 1.0, + "rel_severe_prob": 1.0, + "rel_crit_prob": 1.0, + "rel_death_prob": 1.0, + "prog_by_age": true, + "prognoses": { + "age_cutoffs": [ + 0, + 10, + 20, + 30, + 40, + 50, + 60, + 70, + 80, + 90 + ], + "sus_ORs": [ + 0.34, + 0.67, + 1.0, + 1.0, + 1.0, + 1.0, + 1.24, + 1.47, + 1.47, + 1.47 + ], + "trans_ORs": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "comorbidities": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "symp_probs": [ + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.9 + ], + "severe_probs": [ + 0.001, + 0.0029999999999999996, + 0.012, + 0.032, + 0.049, + 0.102, + 0.16599999999999998, + 0.24300000000000002, + 0.273, + 0.273 + ], + "crit_probs": [ + 0.06, + 0.04848484848484849, + 0.05, + 0.049999999999999996, + 0.06297376093294461, + 0.12196078431372549, + 0.2740210843373494, + 0.43200193657709995, + 0.708994708994709, + 0.708994708994709 + ], + "death_probs": [ + 0.6666666666666667, + 0.25, + 0.2777777777777778, + 0.30769230769230776, + 0.45370370370370366, + 0.2840300107181136, + 0.2104973893926903, + 0.2733385632634764, + 0.47600459242250287, + 0.9293915040183697 + ] + }, + "interventions": [], + "analyzers": [], + "timelimit": null, + "stopping_func": null, + "n_beds_hosp": null, + "n_beds_icu": null, + "no_hosp_factor": 2.0, + "no_icu_factor": 2.0 +} \ No newline at end of file diff --git a/covasim/sim.py b/covasim/sim.py index 7de93b49e..ab74fb931 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -1127,7 +1127,7 @@ def plot(self, *args, **kwargs): sim.run() sim.plot() - New in version 2.0.5: argument passing, date_args, and mpl_args + New in version 2.1.0: argument passing, date_args, and mpl_args ''' fig = cvplt.plot_sim(sim=self, *args, **kwargs) return fig diff --git a/covasim/version.py b/covasim/version.py index 522717cb0..d18ab2c6b 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,6 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '2.0.5' -__versiondate__ = '2021-03-22' +__version__ = '2.1.0' +__versiondate__ = '2021-03-23' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' diff --git a/tests/baseline.json b/tests/baseline.json index 4ddd7caad..f81be5f62 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -1,41 +1,41 @@ { "summary": { - "cum_infections": 8873.0, - "cum_infectious": 8675.0, - "cum_tests": 10454.0, - "cum_diagnoses": 3385.0, - "cum_recoveries": 7578.0, - "cum_symptomatic": 5824.0, - "cum_severe": 436.0, - "cum_critical": 111.0, - "cum_deaths": 22.0, - "cum_quarantined": 4416.0, - "new_infections": 16.0, - "new_infectious": 65.0, - "new_tests": 201.0, - "new_diagnoses": 55.0, - "new_recoveries": 180.0, - "new_symptomatic": 57.0, - "new_severe": 7.0, - "new_critical": 3.0, - "new_deaths": 2.0, - "new_quarantined": 230.0, - "n_susceptible": 11127.0, - "n_exposed": 1273.0, - "n_infectious": 1075.0, - "n_symptomatic": 768.0, - "n_severe": 195.0, - "n_critical": 49.0, - "n_diagnosed": 3385.0, - "n_quarantined": 4279.0, - "n_alive": 19978.0, - "n_preinfectious": 198.0, - "n_removed": 7600.0, - "prevalence": 0.06372009210131144, - "incidence": 0.001437943740451155, - "r_eff": 0.13863684353019246, + "cum_infections": 9990.0, + "cum_infectious": 9747.0, + "cum_tests": 10766.0, + "cum_diagnoses": 3909.0, + "cum_recoveries": 8778.0, + "cum_symptomatic": 6662.0, + "cum_severe": 527.0, + "cum_critical": 125.0, + "cum_deaths": 41.0, + "cum_quarantined": 3711.0, + "new_infections": 24.0, + "new_infectious": 54.0, + "new_tests": 198.0, + "new_diagnoses": 41.0, + "new_recoveries": 138.0, + "new_symptomatic": 39.0, + "new_severe": 8.0, + "new_critical": 1.0, + "new_deaths": 0.0, + "new_quarantined": 165.0, + "n_susceptible": 10010.0, + "n_exposed": 1171.0, + "n_infectious": 928.0, + "n_symptomatic": 683.0, + "n_severe": 158.0, + "n_critical": 35.0, + "n_diagnosed": 3909.0, + "n_quarantined": 3577.0, + "n_alive": 19959.0, + "n_preinfectious": 243.0, + "n_removed": 8819.0, + "prevalence": 0.05867027406182675, + "incidence": 0.0023976023976023976, + "r_eff": 0.2434089263397735, "doubling_time": 30.0, - "test_yield": 0.2736318407960199, - "rel_test_yield": 4.223602915654286 + "test_yield": 0.20707070707070707, + "rel_test_yield": 3.5813414315569485 } } \ No newline at end of file diff --git a/tests/benchmark.json b/tests/benchmark.json index e93ac84d1..1fee3c306 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,12 +1,12 @@ { "time": { - "initialize": 0.429, - "run": 0.496 + "initialize": 0.38, + "run": 0.487 }, "parameters": { "pop_size": 20000, "pop_type": "hybrid", "n_days": 60 }, - "cpu_performance": 0.9461125294823827 + "cpu_performance": 0.9960878831767238 } \ No newline at end of file From 91b11eb2803ea41ac572eeb3c45324733e1ced29 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 21:48:57 -0700 Subject: [PATCH 237/569] updated regression --- covasim/misc.py | 41 +++++++++++++++++++++++++++++++++++++++-- covasim/parameters.py | 4 ++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index f342078f2..49bfa9b94 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -98,7 +98,7 @@ def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=T return data -def load(*args, do_migrate=True, **kwargs): +def load(*args, do_migrate=True, update=True, verbose=True, **kwargs): ''' Convenience method for sc.loadobj() and equivalent to cv.Sim.load() or cv.Scenarios.load(). @@ -106,6 +106,8 @@ def load(*args, do_migrate=True, **kwargs): Args: filename (str): file to load do_migrate (bool): whether to migrate if loading an old object + update (bool): whether to modify the object to reflect the new version + verbose (bool): whether to print migration information args (list): passed to sc.loadobj() kwargs (dict): passed to sc.loadobj() @@ -125,7 +127,7 @@ def load(*args, do_migrate=True, **kwargs): if cmp != 0: print(f'Note: you have Covasim v{v_curr}, but are loading an object from v{v_obj}') if do_migrate: - obj = migrate(obj, v_obj, v_curr) + obj = migrate(obj, update=update, verbose=verbose) return obj @@ -152,6 +154,33 @@ def save(*args, **kwargs): return filepath +def migrate_lognormal(pars, revert=False, verbose=True): + ''' + Small helper function to automatically migrate the standard deviation of lognormal + distributions to match pre-v2.1.0 runs (where it was treated as the variance instead). + To undo the migration, run with revert=True. + ''' + # Convert each value to the square root, since squared in the new version + for key,dur in pars['dur'].items(): + if 'lognormal' in dur['dist']: + old = dur['par2'] + if revert: + new = old**2 + else: + new = np.sqrt(old) + dur['par2'] = new + if verbose > 1: + print(f' Updating {key} std from {old:0.2f} to {new:0.2f}') + + # Store whether migration has occurred so we don't accidentally do it twice + if not revert: + pars['migrated_lognormal'] = True + else: + pars.pop('migrated_lognormal', None) + + return + + def migrate(obj, update=True, verbose=True, die=False): ''' Define migrations allowing compatibility between different versions of saved @@ -174,6 +203,7 @@ def migrate(obj, update=True, verbose=True, die=False): sims = cv.load('my-list-of-sims.obj') sims = [cv.migrate(sim) for sim in sims] ''' + # Import here to avoid recursion from . import base as cvb from . import run as cvr from . import interventions as cvi @@ -203,6 +233,13 @@ def migrate(obj, update=True, verbose=True, die=False): except: pass + # Migration from <2.1.0 to 2.1.0 + if sc.compareversions(sim.version, '2.1.0') == -1: # Migrate from <2.0 to 2.0 + if verbose: + print(f'Migrating sim from version {sim.version} to version {cvv.__version__}') + print('Note: updating lognormal stds to restore previous behavior; see v2.1.0 changelog for details') + migrate_lognormal(sim.pars, verbose=verbose) + # Migrations for People elif isinstance(obj, cvb.BasePeople): # pragma: no cover ppl = obj diff --git a/covasim/parameters.py b/covasim/parameters.py index d4a9c1ce4..605bb460c 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -109,6 +109,10 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): if key in version_pars: # Only replace keys that exist in the old version pars[key] = version_pars[key] + # Handle code change migration + if sc.compareversions(version, '2.1.0') == -1 and 'migrate_lognormal' not in pars: + cvm.migrate_lognormal(pars, verbose=pars['verbose']) + return pars From a87b4bfd8c0f63201926bef67ed2a2a5679d14a6 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 23:10:54 -0700 Subject: [PATCH 238/569] starting on changelog --- CHANGELOG.rst | 12 ++--- tests/devtests/dev_test_transtree.py | 66 ++++++++++++++-------------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c27ea3a98..8f67a371a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,16 +9,14 @@ All notable changes to the codebase are documented in this file. Changes that ma :depth: 1 -~~~~~~~~~~~~~~~~~~~~ -Future release plans -~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~ +Coming soon +~~~~~~~~~~~ These are the major improvements we are currently working on. If there is a specific bugfix or feature you would like to see, please `create an issue `__. -- Additional flexibility in plotting options (e.g. date ranges, per-plot DPI) +- Mechanistic handling of different strains, and improved handling of vaccination, including more detailed targeting options, waning immunity, etc.. This will be Covasim 3.0, which is slated for release early April. - Expanded tutorials (health care workers, vaccination, calibration, exercises, etc.) -- Improved handling of vaccination, including more detailed targeting options, waning immunity, etc. -- Mechanistic handling of different strains - Multi-region and geospatial support - Economics and costing analysis @@ -30,6 +28,8 @@ Latest versions (2.x) Version 2.1.0 (2021-03-23) -------------------------- - ``sim.plot(dpi=150, rotation=45, start_day='2020-03-01', end_day=55, interval=7)`` +- *Regression information*: +- *GitHub info*: PR `859 `__ diff --git a/tests/devtests/dev_test_transtree.py b/tests/devtests/dev_test_transtree.py index c225686ff..4e7673b12 100644 --- a/tests/devtests/dev_test_transtree.py +++ b/tests/devtests/dev_test_transtree.py @@ -7,8 +7,11 @@ import sciris as sc import numpy as np +# Whether to validate (slow!) +validate = 1 + # Create a sim -sim = cv.Sim(pop_size=100e3, n_days=100).run() +sim = cv.Sim(pop_size=20e3, n_days=100).run() people = sim.people @@ -132,37 +135,36 @@ def fillna(cols): sc.toc() -sc.heading('Validation...') - -sc.tic() +if validate: + sc.heading('Validation...') + sc.tic() + for i in range(len(detailed)): + sc.percentcomplete(step=i, maxsteps=len(detailed)-1, stepsize=5) + d_entry = detailed[i] + df_entry = ddf.iloc[i].to_dict() + tt_entry = tt.detailed.iloc[i].to_dict() + if d_entry is None: # If in the dict it's None, it should be nan in the dataframe + for entry in [df_entry, tt_entry]: + assert np.isnan(entry['target']) + else: + dkeys = list(d_entry.keys()) + dfkeys = list(df_entry.keys()) + ttkeys = list(tt_entry.keys()) + assert dfkeys == ttkeys + assert all([dk in dfkeys for dk in dkeys]) # The dataframe can have extra keys, but not the dict + for k in dkeys: + v_d = d_entry[k] + v_df = df_entry[k] + v_tt = tt_entry[k] + try: + assert np.isclose(v_d, v_df, v_tt, equal_nan=True) # If it's numeric, check they're close + except TypeError: + if v_d is None: + assert all(np.isnan([v_df, v_tt])) # If in the dict it's None, it should be nan in the dataframe + else: + assert v_d == v_df == v_tt # In all other cases, it should be an exact match + sc.toc() + print('\nValidation passed.') -# for i in range(len(detailed)): -# sc.percentcomplete(step=i, maxsteps=len(detailed)-1, stepsize=5) -# d_entry = detailed[i] -# df_entry = ddf.iloc[i].to_dict() -# tt_entry = tt.detailed.iloc[i].to_dict() -# if d_entry is None: # If in the dict it's None, it should be nan in the dataframe -# for entry in [df_entry, tt_entry]: -# assert np.isnan(entry['target']) -# else: -# dkeys = list(d_entry.keys()) -# dfkeys = list(df_entry.keys()) -# ttkeys = list(tt_entry.keys()) -# assert dfkeys == ttkeys -# assert all([dk in dfkeys for dk in dkeys]) # The dataframe can have extra keys, but not the dict -# for k in dkeys: -# v_d = d_entry[k] -# v_df = df_entry[k] -# v_tt = tt_entry[k] -# try: -# assert np.isclose(v_d, v_df, v_tt, equal_nan=True) # If it's numeric, check they're close -# except TypeError: -# if v_d is None: -# assert all(np.isnan([v_df, v_tt])) # If in the dict it's None, it should be nan in the dataframe -# else: -# assert v_d == v_df == v_tt # In all other cases, it should be an exact match - -print('\nValidation passed.') -sc.toc() print('Done.') \ No newline at end of file From e230d84666367ef156b2a7528fd6bd38adc728d8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 23:46:34 -0700 Subject: [PATCH 239/569] update changelog --- CHANGELOG.rst | 48 +++++++++++++++++++++++++++++++++++++++++---- covasim/misc.py | 10 ++++++++++ covasim/plotting.py | 4 ++-- covasim/settings.py | 5 ----- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8f67a371a..531a0e369 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,14 +21,54 @@ These are the major improvements we are currently working on. If there is a spec - Economics and costing analysis -~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~ Latest versions (2.x) -~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~ Version 2.1.0 (2021-03-23) -------------------------- -- ``sim.plot(dpi=150, rotation=45, start_day='2020-03-01', end_day=55, interval=7)`` -- *Regression information*: + +This is the last release before the Covasim 3.0 launch (vaccines and variants). + +Highlights +^^^^^^^^^^ +- **Updated lognormal distributions**: Lognormal distributions had been inadvertently using the variance instead of the standard deviation as the second parameter, resulting in too small variance. This has been fixed. +- **Expanded plotting features**: You now have much more flexibility with passing arguments to ``sim.plot()``, such as to temporarily set global Matplotlib options (such as DPI), modify axis styles and limits, etc. For example, you can now do things like this: ``cv.Sim().run().plot(dpi=150, rotation=30, start_day='2020-03-01', end_day=55, interval=7)``. +- **Improved analyzers**: Transmission trees can be computed 20x faster, Fit objects are more forgiving for data problems, and analyzers can now be exported to JSON. + +Bugfixes +^^^^^^^^ +- Previously, the lognormal distributions were unintentionally using the variance of the distribution, instead of the standard deviation, as the second parameter. This makes a small difference to the results (slightly higher transmission due to the increased variance). Old simulations that are loaded will automatically have their parameters updated so they give the same results; however, new simulations will now give slightly different results than they did previously. (Thanks to Ace Thompson for identifying this.) +- If a results object has low and high values, these are now exported to JSON (and also to Excel). +- MultiSim and Scenarios ``run.()`` methods now return themselves, as Sim does. This means that just as you can do ``sim.run().plot()``, you can also now do ``msim.run().plot()``. + +Plotting and options +^^^^^^^^^^^^^^^^^^^^ +- Standard plots now accept keyword arguments that will be passed around to all available subfunctions. For example, if you specify ``dpi=150``, Covasim knows that this is a Matplotlib setting and will configure it accordingly; likewise things like ``bottom`` (only for axes), ``frameon`` (only for legends), etc. If you pass an ambiguous keyword (e.g. ``alpha``, which is used for line and scatter plots), it will only be used for the *first* one. +- There is a new keyword argument, ``date_args``, that will format the x-axis: options include ``dateformat`` (e.g. ``%Y-%m-%d``), ``rotation`` (to avoid label collisions), and ``start_day`` and ``end_day``. +- Default plotting styles have updated, including less intrusive lines for interventions. + +Other changes +^^^^^^^^^^^^^ +- MultiSims now have ``to_json()`` and ``to_excel()`` methods, which are shortcuts for calling these methods on the base sim. +- If no label is supplied to an analyzer or intervention, it will use its class name (e.g. the default label for ``cv.change_beta`` is ``'change_beta'``). +- Analyzers now have a ``to_json()`` method. +- The ``cv.Fit`` and ``cv.TransTree`` classes now derive from ``Analyzer``, giving them some new methods and attributes. +- ``cv.sim.compute_fit()`` has a new keyword argument, ``die``, that will print warnings rather than raise exceptions if no matching data is found. Exceptions are now caught and helpful error messages are provided (e.g., if dates don't match). +- The algorithm for ``cv.TransTree`` has been rewritten, and now runs 20x as fast. The detailed transmission tree, in ``tt.detailed``, is now a pandas dataframe rather than a list of dictionaries. To restore something close to the previous version, use ``tt.detailed.to_dict('records')``. +- A data file with an integer rather than date "date" index can now be loaded; these will be counted relative to the simulation's start day. +- ``cv.load()`` has two new keyword arguments, ``update`` and ``verbose``, than are passed to ``cv.migrate()``. +- ``cv.options`` has new a ``get_default()`` method which returns the value of that parameter when Covasim was first loaded. + +Documentation and testing +^^^^^^^^^^^^^^^^^^^^^^^^^ +- An extra tutorial has been added on "Deployment", covering how to use it with `Dask `__ and for using Covasim with interactive notebooks and websites. +- Tutorials 7 and 10 have been updated so they work on Windows machines. +- Additional unit tests have been written to check the statistical properties of the sampling algorithms. + +Regression information +^^^^^^^^^^^^^^^^^^^^^^ + - *GitHub info*: PR `859 `__ diff --git a/covasim/misc.py b/covasim/misc.py index 49bfa9b94..9baaca45e 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -159,7 +159,17 @@ def migrate_lognormal(pars, revert=False, verbose=True): Small helper function to automatically migrate the standard deviation of lognormal distributions to match pre-v2.1.0 runs (where it was treated as the variance instead). To undo the migration, run with revert=True. + + Args: + pars (dict): the parameters dictionary; or, alternatively, the sim object the parameters will be taken from + revert (bool): whether to reverse the update rather than make it + verbose (bool): whether to print out the old and new values ''' + # Handle different input types + from . import base as cvb + if isinstance(pars, cvb.BaseSim): + pars = pars.pars # It's actually a sim, not a pars object + # Convert each value to the square root, since squared in the new version for key,dur in pars['dur'].items(): if 'lognormal' in dur['dist']: diff --git a/covasim/plotting.py b/covasim/plotting.py index 0ab0fa2eb..ce90f78c5 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -283,9 +283,9 @@ def reset_ticks(ax, sim=None, date_args=None, start_day=None): # Handle start and end days xmin,xmax = ax.get_xlim() if date_args.start_day: - xmin = float(sc.day(date_args.start_day), start_day=start_day) # Keep original type (float) + xmin = float(sc.day(date_args.start_day, start_day=start_day)) # Keep original type (float) if date_args.end_day: - xmax = float(sc.day(date_args.end_day), start_day=start_day) + xmax = float(sc.day(date_args.end_day, start_day=start_day)) ax.set_xlim([xmin, xmax]) # Set the x-axis intervals diff --git a/covasim/settings.py b/covasim/settings.py index 829618736..6160491ea 100644 --- a/covasim/settings.py +++ b/covasim/settings.py @@ -151,11 +151,6 @@ def get_default(key=None): return orig_options[key] -def get_option(key=None): - ''' Helper function to get the current value of an option ''' - return options[key] - - def get_help(output=False): ''' Print information about options. From 7f31cb5ef5d28ae01835a2a025c44a35ce48d580 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 23:56:29 -0700 Subject: [PATCH 240/569] update changelog --- CHANGELOG.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 531a0e369..d84dffab3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,7 @@ These are the major improvements we are currently working on. If there is a spec Latest versions (2.x) ~~~~~~~~~~~~~~~~~~~~~ + Version 2.1.0 (2021-03-23) -------------------------- @@ -32,9 +33,9 @@ This is the last release before the Covasim 3.0 launch (vaccines and variants). Highlights ^^^^^^^^^^ -- **Updated lognormal distributions**: Lognormal distributions had been inadvertently using the variance instead of the standard deviation as the second parameter, resulting in too small variance. This has been fixed. -- **Expanded plotting features**: You now have much more flexibility with passing arguments to ``sim.plot()``, such as to temporarily set global Matplotlib options (such as DPI), modify axis styles and limits, etc. For example, you can now do things like this: ``cv.Sim().run().plot(dpi=150, rotation=30, start_day='2020-03-01', end_day=55, interval=7)``. -- **Improved analyzers**: Transmission trees can be computed 20x faster, Fit objects are more forgiving for data problems, and analyzers can now be exported to JSON. +- **Updated lognormal distributions**: Lognormal distributions had been inadvertently using the variance instead of the standard deviation as the second parameter, resulting in too small variance. This has been fixed. This has a small but nonzero impact on the results (e.g., the time to peak infections is about 5-10% sooner now). +- **Expanded plotting features**: You now have much more flexibility with passing arguments to ``sim.plot()`` and other plotting functions, such as to temporarily set global Matplotlib options (such as DPI), modify axis styles and limits, etc. For example, you can now do things like this: ``cv.Sim().run().plot(dpi=150, rotation=30, start_day='2020-03-01', end_day=55, interval=7)``. +- **Improved analyzers**: Transmission trees can be computed 20 times faster, Fit objects are more forgiving for data problems, and analyzers can now be exported to JSON. Bugfixes ^^^^^^^^ @@ -68,11 +69,11 @@ Documentation and testing Regression information ^^^^^^^^^^^^^^^^^^^^^^ - +- To restore previous behavior on a simulation (i.e. using variance instead of standard deviation for lognormal distributions), call ``cv.misc.migrate_lognormal(sim)``. This is done automatically when loading a saved sim from disk. To undo a migration, type ``cv.misc.migrate_lognormal(sim, revert=True)``. In practice, this loops over the duration parameters and replaces ``par2`` with its square root. If you have used lognormal distributions elsewhere, you will need to update them manually. +- Code that was designed to parse transmission trees will likely need to be revised. The object ``tt.detailed`` is now a dataframe; calling ``tt.detailed.to_dict('records')`` will bring it very close to what it used to be, with the exception that for a given row, ``'t'`` and ``'s'`` used to be nested dictionaries, whereas now they are prefixes. For example, whereas before the "is quarantined" state of the source of the 45th infection would have been ``tt.detailed[45]['s']['is_quarantined']``, it is now ``tt.detailed.iloc[45]['src_is_quarantined']``. - *GitHub info*: PR `859 `__ - Version 2.0.4 (2021-03-19) -------------------------- - Added a new analyzer, ``cv.daily_age_stats()``, which will compute statistics by age for each day of the simulation (compared to ``cv.age_histogram()``, which only looks at particular points in time). From f2110136f2fea2781fda536d8b8ec8cf2dd35266 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 24 Mar 2021 00:14:54 -0700 Subject: [PATCH 241/569] fixed unit tests --- tests/unittests/test_disease_mortality.py | 68 ++++------- tests/unittests/test_disease_progression.py | 24 ++-- tests/unittests/test_disease_transmission.py | 10 +- tests/unittests/test_population_types.py | 24 ++-- tests/unittests/test_simulation_parameter.py | 94 +++++++-------- .../unittests/test_specific_interventions.py | 32 ++--- tests/unittests/unittest_support_classes.py | 114 +++++++++--------- 7 files changed, 169 insertions(+), 197 deletions(-) diff --git a/tests/unittests/test_disease_mortality.py b/tests/unittests/test_disease_mortality.py index df832b175..184c1e05f 100644 --- a/tests/unittests/test_disease_mortality.py +++ b/tests/unittests/test_disease_mortality.py @@ -3,12 +3,14 @@ ../../covasim/README.md """ -from unittest_support_classes import CovaSimTest, TestProperties +import covasim as cv +import unittest +from unittest_support_classes import CovaSimTest, TProps -DProgKeys = TestProperties.ParameterKeys.ProgressionKeys -TransKeys = TestProperties.ParameterKeys.TransmissionKeys -TSimKeys = TestProperties.ParameterKeys.SimulationKeys -ResKeys = TestProperties.ResultsDataKeys +DProgKeys = TProps.ParKeys.ProgKeys +TransKeys = TProps.ParKeys.TransKeys +TSimKeys = TProps.ParKeys.SimKeys +ResKeys = TProps.ResKeys class DiseaseMortalityTests(CovaSimTest): @@ -25,33 +27,13 @@ def test_default_death_prob_one(self): Infect lots of people with cfr one and short time to die duration. Verify that everyone dies, no recoveries. """ - total_agents = 500 - self.set_everyone_is_going_to_die(num_agents=total_agents) - self.run_sim() - recoveries_at_timestep_channel = self.get_full_result_channel( - ResKeys.recovered_at_timestep - ) - recoveries_cumulative_channel = self.get_full_result_channel( - ResKeys.recovered_cumulative - ) - recovery_channels = [ - recoveries_at_timestep_channel, - recoveries_cumulative_channel - ] - for c in recovery_channels: - for t in range(len(c)): - self.assertEqual(0, c[t], - msg=f"There should be no recoveries" - f" with death_prob 1.0. Channel {c} had " - f" bad data at t: {t}") - pass - pass - cumulative_deaths = self.get_day_final_channel_value( - ResKeys.deaths_cumulative - ) - self.assertEqual(cumulative_deaths, total_agents, - msg="Everyone should die") - pass + pop_size = 200 + n_days = 90 + sim = cv.Sim(pop_size=pop_size, pop_infected=pop_size, n_days=n_days) + for key in ['rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob']: + sim[key] = 1e6 + sim.run() + assert sim.summary.cum_deaths == pop_size def test_default_death_prob_zero(self): """ @@ -62,7 +44,7 @@ def test_default_death_prob_zero(self): total_agents = 500 self.set_everyone_is_going_to_die(num_agents=total_agents) prob_dict = { - DProgKeys.ProbabilityKeys.RelativeProbKeys.crt_to_death_probability: 0.0 + DProgKeys.ProbKeys.RelProbKeys.crt_to_death_probability: 0.0 } self.set_simulation_prognosis_probability(prob_dict) self.run_sim() @@ -102,22 +84,14 @@ def test_default_death_prob_scaling(self): death_probs = [0.01, 0.05, 0.10, 0.15] old_cumulative_deaths = 0 for death_prob in death_probs: - prob_dict = { - DProgKeys.ProbabilityKeys.RelativeProbKeys.crt_to_death_probability: death_prob - } + prob_dict = {DProgKeys.ProbKeys.RelProbKeys.crt_to_death_probability: death_prob} self.set_simulation_prognosis_probability(prob_dict) self.run_sim() - deaths_at_timestep_channel = self.get_full_result_channel( - ResKeys.deaths_daily - ) - recoveries_at_timestep_channel = self.get_full_result_channel( - ResKeys.recovered_at_timestep - ) - cumulative_deaths = self.get_day_final_channel_value( - ResKeys.deaths_cumulative - ) - self.assertGreaterEqual(cumulative_deaths, old_cumulative_deaths, - msg="Should be more deaths with higer ratio") + cumulative_deaths = self.get_day_final_channel_value(ResKeys.deaths_cumulative) + self.assertGreaterEqual(cumulative_deaths, old_cumulative_deaths, msg="Should be more deaths with higer ratio") old_cumulative_deaths = cumulative_deaths pass +# Run unit tests if called as a script +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/unittests/test_disease_progression.py b/tests/unittests/test_disease_progression.py index e05657e3c..d71781fd0 100644 --- a/tests/unittests/test_disease_progression.py +++ b/tests/unittests/test_disease_progression.py @@ -4,10 +4,10 @@ """ import unittest -from unittest_support_classes import CovaSimTest, TestProperties +from unittest_support_classes import CovaSimTest, TProps -ResKeys = TestProperties.ResultsDataKeys -ParamKeys = TestProperties.ParameterKeys +ResKeys = TProps.ResKeys +ParamKeys = TProps.ParKeys class DiseaseProgressionTests(CovaSimTest): @@ -33,16 +33,16 @@ def test_exposure_to_infectiousness_delay_scaling(self): std_dev = 0 for exposed_delay in exposed_delays: self.set_duration_distribution_parameters( - duration_in_question=ParamKeys.ProgressionKeys.DurationKeys.exposed_to_infectious, + duration_in_question=ParamKeys.ProgKeys.DurKeys.exposed_to_infectious, par1=exposed_delay, par2=std_dev ) prob_dict = { - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.inf_to_symptomatic_probability: 0 + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.inf_to_symptomatic_probability: 0 } self.set_simulation_prognosis_probability(prob_dict) serial_delay = { - TestProperties.ParameterKeys.SimulationKeys.number_simulated_days: sim_dur + TProps.ParKeys.SimKeys.number_simulated_days: sim_dur } self.run_sim(serial_delay) infectious_channel = self.get_full_result_channel( @@ -76,7 +76,7 @@ def test_mild_infection_duration_scaling(self): self.set_everyone_infectious_same_day(num_agents=total_agents, days_to_infectious=exposed_delay) prob_dict = { - ParamKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.inf_to_symptomatic_probability: 0.0 + ParamKeys.ProgKeys.ProbKeys.RelProbKeys.inf_to_symptomatic_probability: 0.0 } self.set_simulation_prognosis_probability(prob_dict) infectious_durations = [1, 2, 5, 10, 20] # Keep values in order @@ -84,13 +84,13 @@ def test_mild_infection_duration_scaling(self): for TEST_dur in infectious_durations: recovery_day = exposed_delay + TEST_dur self.set_duration_distribution_parameters( - duration_in_question=ParamKeys.ProgressionKeys.DurationKeys.infectious_asymptomatic_to_recovered, + duration_in_question=ParamKeys.ProgKeys.DurKeys.infectious_asymptomatic_to_recovered, par1=TEST_dur, par2=infectious_duration_stddev ) self.run_sim() recoveries_channel = self.get_full_result_channel( - TestProperties.ResultsDataKeys.recovered_at_timestep + TProps.ResKeys.recovered_at_timestep ) recoveries_on_recovery_day = recoveries_channel[recovery_day] if self.is_debugging: @@ -107,7 +107,7 @@ def test_time_to_die_duration_scaling(self): total_agents = 500 self.set_everyone_critical(num_agents=500, constant_delay=0) prob_dict = { - ParamKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.crt_to_death_probability: 1.0 + ParamKeys.ProgKeys.ProbKeys.RelProbKeys.crt_to_death_probability: 1.0 } self.set_simulation_prognosis_probability(prob_dict) @@ -116,13 +116,13 @@ def test_time_to_die_duration_scaling(self): for TEST_dur in time_to_die_durations: self.set_duration_distribution_parameters( - duration_in_question=ParamKeys.ProgressionKeys.DurationKeys.critical_to_death, + duration_in_question=ParamKeys.ProgKeys.DurKeys.critical_to_death, par1=TEST_dur, par2=time_to_die_stddev ) self.run_sim() deaths_today_channel = self.get_full_result_channel( - TestProperties.ResultsDataKeys.deaths_daily + TProps.ResKeys.deaths_daily ) for t in range(len(deaths_today_channel)): curr_deaths = deaths_today_channel[t] diff --git a/tests/unittests/test_disease_transmission.py b/tests/unittests/test_disease_transmission.py index 59c837ceb..da5844bea 100644 --- a/tests/unittests/test_disease_transmission.py +++ b/tests/unittests/test_disease_transmission.py @@ -3,10 +3,10 @@ ../../covasim/README.md """ -from unittest_support_classes import CovaSimTest, TestProperties +from unittest_support_classes import CovaSimTest, TProps -TKeys = TestProperties.ParameterKeys.TransmissionKeys -Hightrans = TestProperties.SpecializedSimulations.Hightransmission +TKeys = TProps.ParKeys.TransKeys +Hightrans = TProps.SpecialSims.Hightransmission class DiseaseTransmissionTests(CovaSimTest): """ @@ -33,7 +33,7 @@ def test_beta_zero(self): } self.run_sim(beta_zero) exposed_today_channel = self.get_full_result_channel( - TestProperties.ResultsDataKeys.exposed_at_timestep + TProps.ResKeys.exposed_at_timestep ) prev_exposed = exposed_today_channel[0] self.assertEqual(prev_exposed, Hightrans.pop_infected, @@ -47,7 +47,7 @@ def test_beta_zero(self): pass infections_channel = self.get_full_result_channel( - TestProperties.ResultsDataKeys.infections_at_timestep + TProps.ResKeys.infections_at_timestep ) for t in range(len(infections_channel)): today_infectious = infections_channel[t] diff --git a/tests/unittests/test_population_types.py b/tests/unittests/test_population_types.py index 0ef8746b2..08ae8459c 100644 --- a/tests/unittests/test_population_types.py +++ b/tests/unittests/test_population_types.py @@ -1,6 +1,6 @@ -from unittest_support_classes import CovaSimTest, TestProperties +from unittest_support_classes import CovaSimTest, TProps -TPKeys = TestProperties.ParameterKeys.SimulationKeys +TParKeys = TProps.ParKeys.SimKeys class PopulationTypeTests(CovaSimTest): @@ -16,9 +16,9 @@ def test_different_pop_types(self): pop_types = ['random', 'hybrid'] #, 'synthpops'] results = {} short_sample = { - TPKeys.number_agents: 1000, - TPKeys.number_simulated_days: 10, - TPKeys.initial_infected_count: 50 + TParKeys.number_agents: 1000, + TParKeys.number_simulated_days: 10, + TParKeys.initial_infected_count: 50 } for poptype in pop_types: self.run_sim(short_sample, population_type=poptype) @@ -28,15 +28,15 @@ def test_different_pop_types(self): for k in results: these_results = results[k] self.assertIsNotNone(these_results) - day_0_susceptible = these_results[TestProperties.ResultsDataKeys.susceptible_at_timestep][0] - day_0_exposed = these_results[TestProperties.ResultsDataKeys.exposed_at_timestep][0] + day_0_susceptible = these_results[TProps.ResKeys.susceptible_at_timestep][0] + day_0_exposed = these_results[TProps.ResKeys.exposed_at_timestep][0] - self.assertEqual(day_0_susceptible + day_0_exposed, short_sample[TPKeys.number_agents], + self.assertEqual(day_0_susceptible + day_0_exposed, short_sample[TParKeys.number_agents], msg=f"Day 0 population should be as specified in params. Poptype {k} was different.") - self.assertGreater(these_results[TestProperties.ResultsDataKeys.infections_cumulative][-1], - these_results[TestProperties.ResultsDataKeys.infections_cumulative][0], + self.assertGreater(these_results[TProps.ResKeys.infections_cumulative][-1], + these_results[TProps.ResKeys.infections_cumulative][0], msg=f"Should see infections increase. Pop type {k} didn't do that.") - self.assertGreater(these_results[TestProperties.ResultsDataKeys.symptomatic_cumulative][-1], - these_results[TestProperties.ResultsDataKeys.symptomatic_cumulative][0], + self.assertGreater(these_results[TProps.ResKeys.symptomatic_cumulative][-1], + these_results[TProps.ResKeys.symptomatic_cumulative][0], msg=f"Should see symptomatic counts increase. Pop type {k} didn't do that.") diff --git a/tests/unittests/test_simulation_parameter.py b/tests/unittests/test_simulation_parameter.py index d5b9666ab..7b5bdd1b6 100644 --- a/tests/unittests/test_simulation_parameter.py +++ b/tests/unittests/test_simulation_parameter.py @@ -4,10 +4,10 @@ """ import unittest -from unittest_support_classes import CovaSimTest, TestProperties +from unittest_support_classes import CovaSimTest, TProps -TPKeys = TestProperties.ParameterKeys.SimulationKeys -ResKeys = TestProperties.ResultsDataKeys +TParKeys = TProps.ParKeys.SimKeys +ResKeys = TProps.ResKeys class SimulationParameterTests(CovaSimTest): def setUp(self): @@ -25,32 +25,32 @@ def test_population_size(self): Depends on run default simulation """ - TPKeys = TestProperties.ParameterKeys.SimulationKeys + TParKeys = TProps.ParKeys.SimKeys pop_2_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 2, - TPKeys.number_contacts: {'a': 1}, - TPKeys.initial_infected_count: 0 + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 1, + TParKeys.number_agents: 2, + TParKeys.number_contacts: {'a': 1}, + TParKeys.initial_infected_count: 0 } pop_10_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 10, - TPKeys.number_contacts: {'a': 4}, - TPKeys.initial_infected_count: 0 + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 1, + TParKeys.number_agents: 10, + TParKeys.number_contacts: {'a': 4}, + TParKeys.initial_infected_count: 0 } pop_123_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 123, - TPKeys.initial_infected_count: 0 + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 1, + TParKeys.number_agents: 123, + TParKeys.initial_infected_count: 0 } pop_1234_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 1234, - TPKeys.initial_infected_count: 0 + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 1, + TParKeys.number_agents: 1234, + TParKeys.initial_infected_count: 0 } self.run_sim(pop_2_one_day) pop_2_pop = self.get_day_zero_channel_value() @@ -61,10 +61,10 @@ def test_population_size(self): self.run_sim(pop_1234_one_day) pop_1234_pop = self.get_day_zero_channel_value() - self.assertEqual(pop_2_pop, pop_2_one_day[TPKeys.number_agents]) - self.assertEqual(pop_10_pop, pop_10_one_day[TPKeys.number_agents]) - self.assertEqual(pop_123_pop, pop_123_one_day[TPKeys.number_agents]) - self.assertEqual(pop_1234_pop, pop_1234_one_day[TPKeys.number_agents]) + self.assertEqual(pop_2_pop, pop_2_one_day[TParKeys.number_agents]) + self.assertEqual(pop_10_pop, pop_10_one_day[TParKeys.number_agents]) + self.assertEqual(pop_123_pop, pop_123_one_day[TParKeys.number_agents]) + self.assertEqual(pop_1234_pop, pop_1234_one_day[TParKeys.number_agents]) pass @@ -73,10 +73,10 @@ def test_population_size_ranges(self): Intent is to test zero, negative, and excessively large pop sizes """ pop_neg_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: -10, - TPKeys.initial_infected_count: 0 + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 1, + TParKeys.number_agents: -10, + TParKeys.initial_infected_count: 0 } with self.assertRaises(ValueError) as context: self.run_sim(pop_neg_one_day) @@ -84,10 +84,10 @@ def test_population_size_ranges(self): self.assertIn("negative", error_message) pop_zero_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 100, - TPKeys.number_agents: 0, - TPKeys.initial_infected_count: 0 + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 100, + TParKeys.number_agents: 0, + TParKeys.initial_infected_count: 0 } self.run_sim(pop_zero_one_day) self.assertEqual(self.simulation_result['results'][ResKeys.susceptible_at_timestep][-1], 0) @@ -103,21 +103,21 @@ def test_population_scaling(self): Depends on population_size """ scale_1_one_day = { - TPKeys.number_agents: 100, - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1 + TParKeys.number_agents: 100, + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 1 } scale_2_one_day = { - TPKeys.number_agents: 100, - TPKeys.population_scaling_factor: 2, - TPKeys.population_rescaling: False, - TPKeys.number_simulated_days: 1 + TParKeys.number_agents: 100, + TParKeys.population_scaling_factor: 2, + TParKeys.population_rescaling: False, + TParKeys.number_simulated_days: 1 } scale_10_one_day = { - TPKeys.number_agents: 100, - TPKeys.population_scaling_factor: 10, - TPKeys.population_rescaling: False, - TPKeys.number_simulated_days: 1 + TParKeys.number_agents: 100, + TParKeys.population_scaling_factor: 10, + TParKeys.population_rescaling: False, + TParKeys.number_simulated_days: 1 } self.run_sim(scale_1_one_day) scale_1_pop = self.get_day_zero_channel_value() @@ -140,10 +140,10 @@ def test_random_seed(self): """ self.set_smallpop_hightransmission() seed_1_params = { - TPKeys.random_seed: 1 + TParKeys.random_seed: 1 } seed_2_params = { - TPKeys.random_seed: 2 + TParKeys.random_seed: 2 } self.run_sim(seed_1_params) infectious_seed_1_v1 = self.get_full_result_channel( diff --git a/tests/unittests/test_specific_interventions.py b/tests/unittests/test_specific_interventions.py index 29c254efc..aa7d4c610 100644 --- a/tests/unittests/test_specific_interventions.py +++ b/tests/unittests/test_specific_interventions.py @@ -1,5 +1,5 @@ from unittest_support_classes import CovaSimTest -from unittest_support_classes import TestProperties +from unittest_support_classes import TProps from math import sqrt import json import numpy as np @@ -9,8 +9,8 @@ AGENT_COUNT = 1000 -ResultsKeys = TestProperties.ResultsDataKeys -SimKeys = TestProperties.ParameterKeys.SimulationKeys +ResultsKeys = TProps.ResKeys +SimKeys = TProps.ParKeys.SimKeys class InterventionTests(CovaSimTest): def setUp(self): super().setUp() @@ -26,7 +26,7 @@ def test_brutal_change_beta_intervention(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) day_of_change = 30 change_days = [day_of_change] change_multipliers = [0.0] @@ -57,7 +57,7 @@ def test_change_beta_days(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) # Do a 0.0 intervention / 1.0 intervention on different days days = [ 30, 32, 40, 42, 50] multipliers = [0.0, 1.0, 0.0, 1.0, 0.0] @@ -105,7 +105,7 @@ def test_change_beta_multipliers(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 40 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) day_of_change = 20 change_days = [day_of_change] change_multipliers = [1.0, 0.8, 0.6, 0.4, 0.2] @@ -162,7 +162,7 @@ def test_change_beta_layers_clustered(self): } if len(seed_list) > 1: self.expected_result_filename = f"DEBUG_{self.id()}_{seed}.json" - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) day_of_change = 25 change_multipliers = [0.0] layer_keys = ['c','h','s','w'] @@ -231,7 +231,7 @@ def test_change_beta_layers_random(self): SimKeys.number_simulated_days: 60, SimKeys.initial_infected_count: initial_infected } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) if len(seed_list) > 1: self.expected_result_filename = f"DEBUG_{self.id()}_{seed}.json" day_of_change = 25 @@ -291,7 +291,7 @@ def test_change_beta_layers_hybrid(self): } if len(seed_list) > 1: self.expected_result_filename = f"DEBUG_{self.id()}_{seed}.json" - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) day_of_change = 25 change_multipliers = [0.0] layer_keys = ['c','s','w','h'] @@ -418,7 +418,7 @@ def test_test_prob_perfect_asymptomatic(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) asymptomatic_probability_of_test = 1.0 test_sensitivity = 1.0 @@ -455,7 +455,7 @@ def test_test_prob_perfect_symptomatic(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) symptomatic_probability_of_test = 1.0 test_sensitivity = 1.0 @@ -489,7 +489,7 @@ def test_test_prob_perfect_not_quarantined(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) asymptomatic_probability_of_test = 1.0 symptomatic_probability_of_test = 1.0 @@ -526,7 +526,7 @@ def test_test_prob_sensitivity(self, subtract_today_recoveries=False): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 31 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) symptomatic_probability_of_test = 1.0 test_sensitivities = [0.9, 0.7, 0.6, 0.2] @@ -608,7 +608,7 @@ def test_test_prob_symptomatic_prob_of_test(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 31 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) symptomatic_probabilities_of_test = [0.9, 0.7, 0.6, 0.2] test_sensitivity = 1.0 @@ -659,7 +659,7 @@ def test_brutal_contact_tracing(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 55 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) intervention_list = [] @@ -723,7 +723,7 @@ def test_contact_tracing_perfect_school_layer(self): 'quar_period': 10, SimKeys.initial_infected_count: initial_infected } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) sequence_days = [30, 40] sequence_interventions = [] diff --git a/tests/unittests/unittest_support_classes.py b/tests/unittests/unittest_support_classes.py index ef503b3b2..c81f9e7cc 100644 --- a/tests/unittests/unittest_support_classes.py +++ b/tests/unittests/unittest_support_classes.py @@ -15,9 +15,9 @@ from covasim import Sim, parameters, change_beta, test_prob, contact_tracing, sequence -class TestProperties: - class ParameterKeys: - class SimulationKeys: +class TProps: + class ParKeys: + class SimKeys: number_agents = 'pop_size' number_contacts = 'contacts' population_scaling_factor = 'pop_scale' @@ -34,7 +34,7 @@ class SimulationKeys: # stopping_function = 'stop_func' pass - class TransmissionKeys: + class TransKeys: beta = 'beta' asymptomatic_fraction = 'asym_prop' asymptomatic_transmission_multiplier = 'asym_factor' @@ -45,12 +45,12 @@ class TransmissionKeys: contacts_population_specific = 'contacts_pop' pass - class ProgressionKeys: + class ProgKeys: durations = "dur" param_1 = "par1" param_2 = "par2" - class DurationKeys: + class DurKeys: exposed_to_infectious = 'exp2inf' infectious_to_symptomatic = 'inf2sym' infectious_asymptomatic_to_recovered = 'asym2rec' @@ -63,9 +63,9 @@ class DurationKeys: critical_to_death = 'crit2die' pass - class ProbabilityKeys: + class ProbKeys: progression_by_age = 'prog_by_age' - class RelativeProbKeys: + class RelProbKeys: inf_to_symptomatic_probability = 'rel_symp_prob' sym_to_severe_probability = 'rel_severe_prob' sev_to_critical_probability = 'rel_crit_prob' @@ -86,7 +86,7 @@ class DiagnosticTestingKeys: pass pass - class SpecializedSimulations: + class SpecialSims: class Microsim: n = 10 pop_infected = 1 @@ -111,7 +111,7 @@ class HighMortality: # timetodie_std = 2 pass - class ResultsDataKeys: + class ResKeys: deaths_cumulative = 'cum_deaths' deaths_daily = 'new_deaths' diagnoses_cumulative = 'cum_diagnoses' @@ -135,7 +135,7 @@ class ResultsDataKeys: pass -DurationKeys = TestProperties.ParameterKeys.ProgressionKeys.DurationKeys +DurKeys = TProps.ParKeys.ProgKeys.DurKeys class CovaSimTest(unittest.TestCase): @@ -159,7 +159,7 @@ def tearDown(self): pass # region configuration methods - def set_simulation_parameters(self, params_dict=None): + def set_sim_pars(self, params_dict=None): """ Overrides all of the default sim parameters with the ones in the dictionary @@ -181,16 +181,16 @@ def set_simulation_prognosis_probability(self, params_dict): Allows for testing prognoses probability as absolute rather than relative. NOTE: You can only call this once per test or you will overwrite your stuff. """ - ProbKeys = TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys - RelativeProbabilityKeys = ProbKeys.RelativeProbKeys + ProbKeys = TProps.ParKeys.ProgKeys.ProbKeys + RelProbKeys = ProbKeys.RelProbKeys supported_probabilities = [ - RelativeProbabilityKeys.inf_to_symptomatic_probability, - RelativeProbabilityKeys.sym_to_severe_probability, - RelativeProbabilityKeys.sev_to_critical_probability, - RelativeProbabilityKeys.crt_to_death_probability + RelProbKeys.inf_to_symptomatic_probability, + RelProbKeys.sym_to_severe_probability, + RelProbKeys.sev_to_critical_probability, + RelProbKeys.crt_to_death_probability ] if not self.simulation_parameters: - self.set_simulation_parameters() + self.set_sim_pars() pass if not self.simulation_prognoses: @@ -200,13 +200,13 @@ def set_simulation_prognosis_probability(self, params_dict): for k in params_dict: prognosis_in_question = None expected_prob = params_dict[k] - if k == RelativeProbabilityKeys.inf_to_symptomatic_probability: + if k == RelProbKeys.inf_to_symptomatic_probability: prognosis_in_question = PrognosisKeys.symptomatic_probabilities - elif k == RelativeProbabilityKeys.sym_to_severe_probability: + elif k == RelProbKeys.sym_to_severe_probability: prognosis_in_question = PrognosisKeys.severe_probabilities - elif k == RelativeProbabilityKeys.sev_to_critical_probability: + elif k == RelProbKeys.sev_to_critical_probability: prognosis_in_question = PrognosisKeys.critical_probabilities - elif k == RelativeProbabilityKeys.crt_to_death_probability: + elif k == RelProbKeys.crt_to_death_probability: prognosis_in_question = PrognosisKeys.death_probs else: raise KeyError(f"Key {k} not found in {supported_probabilities}.") @@ -218,7 +218,7 @@ def set_simulation_prognosis_probability(self, params_dict): def set_duration_distribution_parameters(self, duration_in_question, par1, par2): if not self.simulation_parameters: - self.set_simulation_parameters() + self.set_sim_pars() pass duration_node = self.simulation_parameters["dur"] duration_node[duration_in_question] = { @@ -229,12 +229,12 @@ def set_duration_distribution_parameters(self, duration_in_question, params_dict = { "dur": duration_node } - self.set_simulation_parameters(params_dict=params_dict) + self.set_sim_pars(params_dict=params_dict) def run_sim(self, params_dict=None, write_results_json=False, population_type=None): if not self.simulation_parameters or params_dict: # If we need one, or have one here - self.set_simulation_parameters(params_dict=params_dict) + self.set_sim_pars(params_dict=params_dict) pass self.simulation_parameters['interventions'] = self.interventions @@ -243,7 +243,7 @@ def run_sim(self, params_dict=None, write_results_json=False, population_type=No datafile=None) if not self.simulation_prognoses: self.simulation_prognoses = parameters.get_prognoses( - self.simulation_parameters[TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.progression_by_age] + self.simulation_parameters[TProps.ParKeys.ProgKeys.ProbKeys.progression_by_age] ) pass @@ -263,7 +263,7 @@ def get_full_result_channel(self, channel): result_data = self.simulation_result["results"][channel] return result_data - def get_day_zero_channel_value(self, channel=TestProperties.ResultsDataKeys.susceptible_at_timestep): + def get_day_zero_channel_value(self, channel=TProps.ResKeys.susceptible_at_timestep): """ Args: @@ -329,26 +329,26 @@ def intervention_build_sequence(self, # region specialized simulation methods def set_microsim(self): - Simkeys = TestProperties.ParameterKeys.SimulationKeys - Micro = TestProperties.SpecializedSimulations.Microsim + Simkeys = TProps.ParKeys.SimKeys + Micro = TProps.SpecialSims.Microsim microsim_parameters = { Simkeys.number_agents : Micro.n, Simkeys.initial_infected_count: Micro.pop_infected, Simkeys.number_simulated_days: Micro.n_days } - self.set_simulation_parameters(microsim_parameters) + self.set_sim_pars(microsim_parameters) pass def set_everyone_infected(self, agent_count=1000): - Simkeys = TestProperties.ParameterKeys.SimulationKeys + Simkeys = TProps.ParKeys.SimKeys everyone_infected = { Simkeys.number_agents: agent_count, Simkeys.initial_infected_count: agent_count } - self.set_simulation_parameters(params_dict=everyone_infected) + self.set_sim_pars(params_dict=everyone_infected) pass - DurationKeys = TestProperties.ParameterKeys.ProgressionKeys.DurationKeys + DurKeys = TProps.ParKeys.ProgKeys.DurKeys def set_everyone_infectious_same_day(self, num_agents, days_to_infectious=1, num_days=60): """ @@ -359,18 +359,18 @@ def set_everyone_infectious_same_day(self, num_agents, days_to_infectious=1, num """ self.set_everyone_infected(agent_count=num_agents) prob_dict = { - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.inf_to_symptomatic_probability: 0 + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.inf_to_symptomatic_probability: 0 } self.set_simulation_prognosis_probability(prob_dict) test_config = { - TestProperties.ParameterKeys.SimulationKeys.number_simulated_days: num_days + TProps.ParKeys.SimKeys.number_simulated_days: num_days } self.set_duration_distribution_parameters( - duration_in_question=DurationKeys.exposed_to_infectious, + duration_in_question=DurKeys.exposed_to_infectious, par1=days_to_infectious, par2=0 ) - self.set_simulation_parameters(params_dict=test_config) + self.set_sim_pars(params_dict=test_config) pass def set_everyone_symptomatic(self, num_agents, constant_delay:int=None): @@ -383,13 +383,13 @@ def set_everyone_symptomatic(self, num_agents, constant_delay:int=None): self.set_everyone_infectious_same_day(num_agents=num_agents, days_to_infectious=0) prob_dict = { - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.inf_to_symptomatic_probability: 1.0, - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.sym_to_severe_probability: 0 + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.inf_to_symptomatic_probability: 1.0, + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.sym_to_severe_probability: 0 } self.set_simulation_prognosis_probability(prob_dict) if constant_delay is not None: self.set_duration_distribution_parameters( - duration_in_question=DurationKeys.infectious_to_symptomatic, + duration_in_question=DurKeys.infectious_to_symptomatic, par1=constant_delay, par2=0 ) @@ -401,7 +401,7 @@ def set_everyone_is_going_to_die(self, num_agents): Args: num_agents: Number of agents to simulate """ - ProbKeys = TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys + ProbKeys = TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys self.set_everyone_infectious_same_day(num_agents=num_agents) prob_dict = { ProbKeys.inf_to_symptomatic_probability: 1, @@ -415,13 +415,13 @@ def set_everyone_is_going_to_die(self, num_agents): def set_everyone_severe(self, num_agents, constant_delay:int=None): self.set_everyone_symptomatic(num_agents=num_agents, constant_delay=constant_delay) prob_dict = { - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.sym_to_severe_probability: 1.0, - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.sev_to_critical_probability: 0.0 + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.sym_to_severe_probability: 1.0, + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.sev_to_critical_probability: 0.0 } self.set_simulation_prognosis_probability(prob_dict) if constant_delay is not None: self.set_duration_distribution_parameters( - duration_in_question=DurationKeys.symptomatic_to_severe, + duration_in_question=DurKeys.symptomatic_to_severe, par1=constant_delay, par2=0 ) @@ -433,13 +433,13 @@ def set_everyone_critical(self, num_agents, constant_delay:int=None): """ self.set_everyone_severe(num_agents=num_agents, constant_delay=constant_delay) prob_dict = { - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.sev_to_critical_probability: 1.0, - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.crt_to_death_probability: 0.0 + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.sev_to_critical_probability: 1.0, + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.crt_to_death_probability: 0.0 } self.set_simulation_prognosis_probability(prob_dict) if constant_delay is not None: self.set_duration_distribution_parameters( - duration_in_question=DurationKeys.severe_to_critical, + duration_in_question=DurKeys.severe_to_critical, par1=constant_delay, par2=0 ) @@ -450,24 +450,22 @@ def set_smallpop_hightransmission(self): """ Creates a small population with lots of transmission """ - Simkeys = TestProperties.ParameterKeys.SimulationKeys - Transkeys = TestProperties.ParameterKeys.TransmissionKeys - Hightrans = TestProperties.SpecializedSimulations.Hightransmission + Simkeys = TProps.ParKeys.SimKeys + Transkeys = TProps.ParKeys.TransKeys + Hightrans = TProps.SpecialSims.Hightransmission hightrans_parameters = { Simkeys.number_agents : Hightrans.n, Simkeys.initial_infected_count: Hightrans.pop_infected, Simkeys.number_simulated_days: Hightrans.n_days, Transkeys.beta : Hightrans.beta } - self.set_simulation_parameters(hightrans_parameters) + self.set_sim_pars(hightrans_parameters) pass # endregion pass - - class TestSupportTests(CovaSimTest): def test_run_vanilla_simulation(self): """ @@ -489,7 +487,7 @@ def test_everyone_infected(self): total_agents = 500 self.set_everyone_infected(agent_count=total_agents) self.run_sim() - exposed_channel = TestProperties.ResultsDataKeys.exposed_at_timestep + exposed_channel = TProps.ResKeys.exposed_at_timestep day_0_exposed = self.get_day_zero_channel_value(exposed_channel) self.assertEqual(day_0_exposed, total_agents) pass @@ -508,7 +506,7 @@ def test_run_small_hightransmission_sim(self): self.assertIsNotNone(self.sim) self.assertIsNotNone(self.simulation_parameters) exposed_today_channel = self.get_full_result_channel( - TestProperties.ResultsDataKeys.exposed_at_timestep + TProps.ResKeys.exposed_at_timestep ) prev_exposed = exposed_today_channel[0] for t in range(1, 10): @@ -520,10 +518,10 @@ def test_run_small_hightransmission_sim(self): prev_exposed = today_exposed pass infections_channel = self.get_full_result_channel( - TestProperties.ResultsDataKeys.infections_at_timestep + TProps.ResKeys.infections_at_timestep ) self.assertGreaterEqual(sum(infections_channel), 150, - msg=f"Should have at least 150 infections") + msg="Should have at least 150 infections") pass pass From 3c0dfb2db978d62823acd315b8dd6d5a84e3f332 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 24 Mar 2021 00:16:35 -0700 Subject: [PATCH 242/569] fixing unit tests --- tests/unittests/test_population_types.py | 10 +-- tests/unittests/test_simulation_parameter.py | 90 ++++++++++---------- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/unittests/test_population_types.py b/tests/unittests/test_population_types.py index 08ae8459c..2c06fb7c3 100644 --- a/tests/unittests/test_population_types.py +++ b/tests/unittests/test_population_types.py @@ -1,6 +1,6 @@ from unittest_support_classes import CovaSimTest, TProps -TParKeys = TProps.ParKeys.SimKeys +TPKeys = TProps.ParKeys.SimKeys class PopulationTypeTests(CovaSimTest): @@ -16,9 +16,9 @@ def test_different_pop_types(self): pop_types = ['random', 'hybrid'] #, 'synthpops'] results = {} short_sample = { - TParKeys.number_agents: 1000, - TParKeys.number_simulated_days: 10, - TParKeys.initial_infected_count: 50 + TPKeys.number_agents: 1000, + TPKeys.number_simulated_days: 10, + TPKeys.initial_infected_count: 50 } for poptype in pop_types: self.run_sim(short_sample, population_type=poptype) @@ -31,7 +31,7 @@ def test_different_pop_types(self): day_0_susceptible = these_results[TProps.ResKeys.susceptible_at_timestep][0] day_0_exposed = these_results[TProps.ResKeys.exposed_at_timestep][0] - self.assertEqual(day_0_susceptible + day_0_exposed, short_sample[TParKeys.number_agents], + self.assertEqual(day_0_susceptible + day_0_exposed, short_sample[TPKeys.number_agents], msg=f"Day 0 population should be as specified in params. Poptype {k} was different.") self.assertGreater(these_results[TProps.ResKeys.infections_cumulative][-1], these_results[TProps.ResKeys.infections_cumulative][0], diff --git a/tests/unittests/test_simulation_parameter.py b/tests/unittests/test_simulation_parameter.py index 7b5bdd1b6..6d2e80bcd 100644 --- a/tests/unittests/test_simulation_parameter.py +++ b/tests/unittests/test_simulation_parameter.py @@ -6,7 +6,7 @@ from unittest_support_classes import CovaSimTest, TProps -TParKeys = TProps.ParKeys.SimKeys +TPKeys = TProps.ParKeys.SimKeys ResKeys = TProps.ResKeys class SimulationParameterTests(CovaSimTest): @@ -25,32 +25,32 @@ def test_population_size(self): Depends on run default simulation """ - TParKeys = TProps.ParKeys.SimKeys + TPKeys = TProps.ParKeys.SimKeys pop_2_one_day = { - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 1, - TParKeys.number_agents: 2, - TParKeys.number_contacts: {'a': 1}, - TParKeys.initial_infected_count: 0 + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 1, + TPKeys.number_agents: 2, + TPKeys.number_contacts: {'a': 1}, + TPKeys.initial_infected_count: 0 } pop_10_one_day = { - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 1, - TParKeys.number_agents: 10, - TParKeys.number_contacts: {'a': 4}, - TParKeys.initial_infected_count: 0 + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 1, + TPKeys.number_agents: 10, + TPKeys.number_contacts: {'a': 4}, + TPKeys.initial_infected_count: 0 } pop_123_one_day = { - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 1, - TParKeys.number_agents: 123, - TParKeys.initial_infected_count: 0 + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 1, + TPKeys.number_agents: 123, + TPKeys.initial_infected_count: 0 } pop_1234_one_day = { - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 1, - TParKeys.number_agents: 1234, - TParKeys.initial_infected_count: 0 + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 1, + TPKeys.number_agents: 1234, + TPKeys.initial_infected_count: 0 } self.run_sim(pop_2_one_day) pop_2_pop = self.get_day_zero_channel_value() @@ -61,10 +61,10 @@ def test_population_size(self): self.run_sim(pop_1234_one_day) pop_1234_pop = self.get_day_zero_channel_value() - self.assertEqual(pop_2_pop, pop_2_one_day[TParKeys.number_agents]) - self.assertEqual(pop_10_pop, pop_10_one_day[TParKeys.number_agents]) - self.assertEqual(pop_123_pop, pop_123_one_day[TParKeys.number_agents]) - self.assertEqual(pop_1234_pop, pop_1234_one_day[TParKeys.number_agents]) + self.assertEqual(pop_2_pop, pop_2_one_day[TPKeys.number_agents]) + self.assertEqual(pop_10_pop, pop_10_one_day[TPKeys.number_agents]) + self.assertEqual(pop_123_pop, pop_123_one_day[TPKeys.number_agents]) + self.assertEqual(pop_1234_pop, pop_1234_one_day[TPKeys.number_agents]) pass @@ -73,10 +73,10 @@ def test_population_size_ranges(self): Intent is to test zero, negative, and excessively large pop sizes """ pop_neg_one_day = { - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 1, - TParKeys.number_agents: -10, - TParKeys.initial_infected_count: 0 + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 1, + TPKeys.number_agents: -10, + TPKeys.initial_infected_count: 0 } with self.assertRaises(ValueError) as context: self.run_sim(pop_neg_one_day) @@ -84,10 +84,10 @@ def test_population_size_ranges(self): self.assertIn("negative", error_message) pop_zero_one_day = { - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 100, - TParKeys.number_agents: 0, - TParKeys.initial_infected_count: 0 + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 100, + TPKeys.number_agents: 0, + TPKeys.initial_infected_count: 0 } self.run_sim(pop_zero_one_day) self.assertEqual(self.simulation_result['results'][ResKeys.susceptible_at_timestep][-1], 0) @@ -103,21 +103,21 @@ def test_population_scaling(self): Depends on population_size """ scale_1_one_day = { - TParKeys.number_agents: 100, - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 1 + TPKeys.number_agents: 100, + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 1 } scale_2_one_day = { - TParKeys.number_agents: 100, - TParKeys.population_scaling_factor: 2, - TParKeys.population_rescaling: False, - TParKeys.number_simulated_days: 1 + TPKeys.number_agents: 100, + TPKeys.population_scaling_factor: 2, + TPKeys.population_rescaling: False, + TPKeys.number_simulated_days: 1 } scale_10_one_day = { - TParKeys.number_agents: 100, - TParKeys.population_scaling_factor: 10, - TParKeys.population_rescaling: False, - TParKeys.number_simulated_days: 1 + TPKeys.number_agents: 100, + TPKeys.population_scaling_factor: 10, + TPKeys.population_rescaling: False, + TPKeys.number_simulated_days: 1 } self.run_sim(scale_1_one_day) scale_1_pop = self.get_day_zero_channel_value() @@ -140,10 +140,10 @@ def test_random_seed(self): """ self.set_smallpop_hightransmission() seed_1_params = { - TParKeys.random_seed: 1 + TPKeys.random_seed: 1 } seed_2_params = { - TParKeys.random_seed: 2 + TPKeys.random_seed: 2 } self.run_sim(seed_1_params) infectious_seed_1_v1 = self.get_full_result_channel( From f10defe80feb0b986dbab2c68c71ddcdfcf0915d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 24 Mar 2021 00:17:06 -0700 Subject: [PATCH 243/569] fixing unit tests --- tests/unittests/experiment_test_disease_mortality.py | 4 ++-- tests/unittests/test_disease_mortality.py | 4 ++-- tests/unittests/test_disease_progression.py | 4 ++-- tests/unittests/test_disease_transmission.py | 4 ++-- tests/unittests/test_miscellaneous_features.py | 4 ++-- tests/unittests/test_population_types.py | 4 ++-- tests/unittests/test_simulation_parameter.py | 4 ++-- tests/unittests/test_specific_interventions.py | 4 ++-- tests/unittests/unittest_support_classes.py | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/unittests/experiment_test_disease_mortality.py b/tests/unittests/experiment_test_disease_mortality.py index 954abcc4b..6d35933b7 100644 --- a/tests/unittests/experiment_test_disease_mortality.py +++ b/tests/unittests/experiment_test_disease_mortality.py @@ -1,5 +1,5 @@ import covasim as cv -from unittest_support_classes import CovaSimTest +from unittest_support_classes import CovaTest class SimKeys: @@ -46,7 +46,7 @@ def BaseSim(): return base_sim -class ExperimentalDiseaseMortalityTests(CovaSimTest): +class ExperimentalDiseaseMortalityTests(CovaTest): ''' Define the actual tests ''' def test_zero_deaths(self): diff --git a/tests/unittests/test_disease_mortality.py b/tests/unittests/test_disease_mortality.py index 184c1e05f..cc18ff442 100644 --- a/tests/unittests/test_disease_mortality.py +++ b/tests/unittests/test_disease_mortality.py @@ -5,7 +5,7 @@ import covasim as cv import unittest -from unittest_support_classes import CovaSimTest, TProps +from unittest_support_classes import CovaTest, TProps DProgKeys = TProps.ParKeys.ProgKeys TransKeys = TProps.ParKeys.TransKeys @@ -13,7 +13,7 @@ ResKeys = TProps.ResKeys -class DiseaseMortalityTests(CovaSimTest): +class DiseaseMortalityTests(CovaTest): def setUp(self): super().setUp() pass diff --git a/tests/unittests/test_disease_progression.py b/tests/unittests/test_disease_progression.py index d71781fd0..e6ce9fbae 100644 --- a/tests/unittests/test_disease_progression.py +++ b/tests/unittests/test_disease_progression.py @@ -4,13 +4,13 @@ """ import unittest -from unittest_support_classes import CovaSimTest, TProps +from unittest_support_classes import CovaTest, TProps ResKeys = TProps.ResKeys ParamKeys = TProps.ParKeys -class DiseaseProgressionTests(CovaSimTest): +class DiseaseProgressionTests(CovaTest): def setUp(self): super().setUp() pass diff --git a/tests/unittests/test_disease_transmission.py b/tests/unittests/test_disease_transmission.py index da5844bea..c2f3c13fa 100644 --- a/tests/unittests/test_disease_transmission.py +++ b/tests/unittests/test_disease_transmission.py @@ -3,12 +3,12 @@ ../../covasim/README.md """ -from unittest_support_classes import CovaSimTest, TProps +from unittest_support_classes import CovaTest, TProps TKeys = TProps.ParKeys.TransKeys Hightrans = TProps.SpecialSims.Hightransmission -class DiseaseTransmissionTests(CovaSimTest): +class DiseaseTransmissionTests(CovaTest): """ Tests of the parameters involved in transmission pre requisites simulation parameter tests diff --git a/tests/unittests/test_miscellaneous_features.py b/tests/unittests/test_miscellaneous_features.py index 2512e8dcd..1c885a415 100644 --- a/tests/unittests/test_miscellaneous_features.py +++ b/tests/unittests/test_miscellaneous_features.py @@ -4,11 +4,11 @@ import unittest import pandas as pd -from unittest_support_classes import CovaSimTest +from unittest_support_classes import CovaTest from covasim import Sim, parameters import os -class MiscellaneousFeatureTests(CovaSimTest): +class MiscellaneousFeatureTests(CovaTest): def setUp(self): super().setUp() self.sim = Sim() diff --git a/tests/unittests/test_population_types.py b/tests/unittests/test_population_types.py index 2c06fb7c3..5d00712a4 100644 --- a/tests/unittests/test_population_types.py +++ b/tests/unittests/test_population_types.py @@ -1,9 +1,9 @@ -from unittest_support_classes import CovaSimTest, TProps +from unittest_support_classes import CovaTest, TProps TPKeys = TProps.ParKeys.SimKeys -class PopulationTypeTests(CovaSimTest): +class PopulationTypeTests(CovaTest): def setUp(self): super().setUp() pass diff --git a/tests/unittests/test_simulation_parameter.py b/tests/unittests/test_simulation_parameter.py index 6d2e80bcd..ba3f871b7 100644 --- a/tests/unittests/test_simulation_parameter.py +++ b/tests/unittests/test_simulation_parameter.py @@ -4,12 +4,12 @@ """ import unittest -from unittest_support_classes import CovaSimTest, TProps +from unittest_support_classes import CovaTest, TProps TPKeys = TProps.ParKeys.SimKeys ResKeys = TProps.ResKeys -class SimulationParameterTests(CovaSimTest): +class SimulationParameterTests(CovaTest): def setUp(self): super().setUp() pass diff --git a/tests/unittests/test_specific_interventions.py b/tests/unittests/test_specific_interventions.py index aa7d4c610..3a3a2e76b 100644 --- a/tests/unittests/test_specific_interventions.py +++ b/tests/unittests/test_specific_interventions.py @@ -1,4 +1,4 @@ -from unittest_support_classes import CovaSimTest +from unittest_support_classes import CovaTest from unittest_support_classes import TProps from math import sqrt import json @@ -11,7 +11,7 @@ ResultsKeys = TProps.ResKeys SimKeys = TProps.ParKeys.SimKeys -class InterventionTests(CovaSimTest): +class InterventionTests(CovaTest): def setUp(self): super().setUp() pass diff --git a/tests/unittests/unittest_support_classes.py b/tests/unittests/unittest_support_classes.py index c81f9e7cc..1bc6a7692 100644 --- a/tests/unittests/unittest_support_classes.py +++ b/tests/unittests/unittest_support_classes.py @@ -138,7 +138,7 @@ class ResKeys: DurKeys = TProps.ParKeys.ProgKeys.DurKeys -class CovaSimTest(unittest.TestCase): +class CovaTest(unittest.TestCase): def setUp(self): self.is_debugging = False @@ -466,7 +466,7 @@ def set_smallpop_hightransmission(self): pass -class TestSupportTests(CovaSimTest): +class TestSupportTests(CovaTest): def test_run_vanilla_simulation(self): """ Runs an uninteresting but predictable From 3b4a23f63590fa6f0b8bb7c67d821378d891dd59 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 24 Mar 2021 00:31:00 -0700 Subject: [PATCH 244/569] update changelog --- CHANGELOG.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d84dffab3..3c29311bd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -33,7 +33,7 @@ This is the last release before the Covasim 3.0 launch (vaccines and variants). Highlights ^^^^^^^^^^ -- **Updated lognormal distributions**: Lognormal distributions had been inadvertently using the variance instead of the standard deviation as the second parameter, resulting in too small variance. This has been fixed. This has a small but nonzero impact on the results (e.g., the time to peak infections is about 5-10% sooner now). +- **Updated lognormal distributions**: Lognormal distributions had been inadvertently using the variance instead of the standard deviation as the second parameter, resulting in too small variance. This has been fixed. This has a small but nonzero impact on the results (e.g. with default parameters, the time to peak infections is about 5-10% sooner now). - **Expanded plotting features**: You now have much more flexibility with passing arguments to ``sim.plot()`` and other plotting functions, such as to temporarily set global Matplotlib options (such as DPI), modify axis styles and limits, etc. For example, you can now do things like this: ``cv.Sim().run().plot(dpi=150, rotation=30, start_day='2020-03-01', end_day=55, interval=7)``. - **Improved analyzers**: Transmission trees can be computed 20 times faster, Fit objects are more forgiving for data problems, and analyzers can now be exported to JSON. @@ -69,8 +69,8 @@ Documentation and testing Regression information ^^^^^^^^^^^^^^^^^^^^^^ -- To restore previous behavior on a simulation (i.e. using variance instead of standard deviation for lognormal distributions), call ``cv.misc.migrate_lognormal(sim)``. This is done automatically when loading a saved sim from disk. To undo a migration, type ``cv.misc.migrate_lognormal(sim, revert=True)``. In practice, this loops over the duration parameters and replaces ``par2`` with its square root. If you have used lognormal distributions elsewhere, you will need to update them manually. -- Code that was designed to parse transmission trees will likely need to be revised. The object ``tt.detailed`` is now a dataframe; calling ``tt.detailed.to_dict('records')`` will bring it very close to what it used to be, with the exception that for a given row, ``'t'`` and ``'s'`` used to be nested dictionaries, whereas now they are prefixes. For example, whereas before the "is quarantined" state of the source of the 45th infection would have been ``tt.detailed[45]['s']['is_quarantined']``, it is now ``tt.detailed.iloc[45]['src_is_quarantined']``. +- To restore previous behavior for a simulation (i.e. using variance instead of standard deviation for lognormal distributions), call ``cv.misc.migrate_lognormal(sim)``. This is done automatically when loading a saved sim from disk. To undo a migration, type ``cv.misc.migrate_lognormal(sim, revert=True)``. What this function does is loop over the duration parameters and replace ``par2`` with its square root. If you have used lognormal distributions elsewhere, you will need to update them manually. +- Code that was designed to parse transmission trees will likely need to be revised. The object ``tt.detailed`` is now a dataframe; calling ``tt.detailed.to_dict('records')`` will bring it very close to what it used to be, with the exception that for a given row, ``'t'`` and ``'s'`` used to be nested dictionaries, whereas now they are prefixes. For example, whereas before the 45th person's source's "is quarantined" state would have been ``tt.detailed[45]['s']['is_quarantined']``, it is now ``tt.detailed.iloc[45]['src_is_quarantined']``. - *GitHub info*: PR `859 `__ From 2998ff0168418940101b70970d08ec3190a4751f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 24 Mar 2021 01:09:35 -0700 Subject: [PATCH 245/569] update t07 --- docs/conf.py | 2 +- docs/tutorials/t07.ipynb | 11 +++++++---- examples/t07_optuna_calibration.py | 1 + 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4fb3b9529..d1feeaaf7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -231,7 +231,7 @@ # Configure nbsphinx nbsphinx_kernel_name = "python" -nbsphinx_timeout = 60 # Time in seconds; use -1 for no timeout +nbsphinx_timeout = 90 # Time in seconds; use -1 for no timeout nbsphinx_execute_arguments = [ "--InlineBackend.figure_formats={'svg', 'pdf'}", "--InlineBackend.rc=figure.dpi=96", diff --git a/docs/tutorials/t07.ipynb b/docs/tutorials/t07.ipynb index c3151ebbe..b22bd0bc1 100644 --- a/docs/tutorials/t07.ipynb +++ b/docs/tutorials/t07.ipynb @@ -2,7 +2,9 @@ "cells": [ { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "tags": [] + }, "source": [ "# T7 - Calibration\n", "\n", @@ -170,6 +172,7 @@ "def run_sim(pars, label=None, return_sim=False):\n", " ''' Create and run a simulation '''\n", " pars = dict(\n", + " pop_size = 10_000,\n", " start_day = '2020-02-01',\n", " end_day = '2020-04-11',\n", " beta = pars[\"beta\"],\n", @@ -204,7 +207,7 @@ "\n", "def run_workers():\n", " ''' Run multiple workers in parallel '''\n", - " output = sc.parallelize(worker, g.n_workers)\n", + " output = sc.parallelize(worker, g.n_workers, ncpus=1)\n", " return output\n", "\n", "\n", @@ -260,8 +263,8 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (Spyder)", - "language": "python3", + "display_name": "Python 3", + "language": "python", "name": "python3" }, "language_info": { diff --git a/examples/t07_optuna_calibration.py b/examples/t07_optuna_calibration.py index bae478731..81f2674cc 100644 --- a/examples/t07_optuna_calibration.py +++ b/examples/t07_optuna_calibration.py @@ -20,6 +20,7 @@ def run_sim(pars, label=None, return_sim=False): ''' Create and run a simulation ''' print(f'Running sim for beta={pars["beta"]}, rel_death_prob={pars["rel_death_prob"]}') pars = dict( + pop_size = 10_000, start_day = '2020-02-01', end_day = '2020-04-11', beta = pars["beta"], From 4013452a1c08bed95329c4df8c0156601637f927 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 24 Mar 2021 01:27:40 -0700 Subject: [PATCH 246/569] update workers --- docs/tutorials/t07.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/t07.ipynb b/docs/tutorials/t07.ipynb index b22bd0bc1..e46a33fb6 100644 --- a/docs/tutorials/t07.ipynb +++ b/docs/tutorials/t07.ipynb @@ -207,7 +207,7 @@ "\n", "def run_workers():\n", " ''' Run multiple workers in parallel '''\n", - " output = sc.parallelize(worker, g.n_workers, ncpus=1)\n", + " output = sc.parallelize(worker, g.n_workers)\n", " return output\n", "\n", "\n", From 1184b2fd401693de14cf45ebb578e949775811b9 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 24 Mar 2021 11:24:21 +0100 Subject: [PATCH 247/569] refactoring calcs --- covasim/defaults.py | 1 + covasim/immunity.py | 72 +++++++++++++++++++-------------- covasim/interventions.py | 2 +- covasim/parameters.py | 1 + covasim/people.py | 4 +- covasim/sim.py | 7 +++- tests/devtests/test_variants.py | 22 +++++----- 7 files changed, 61 insertions(+), 48 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 894e74609..6c81aab75 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -55,6 +55,7 @@ class PeopleMeta(sc.prettyobj): 'prior_symptoms', # Float 'vaccinations', # Number of doses given per person 'vaccine_source', # index of vaccine that individual received + 'init_NAb', # Initial neutralization titre relative to convalescent plasma 'NAb', # Current neutralization titre relative to convalescent plasma ] diff --git a/covasim/immunity.py b/covasim/immunity.py index a4f8902bf..6ea9605bf 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -289,9 +289,9 @@ def initialize(self, sim): # %% NAb methods -__all__ += ['compute_nab', 'nab_to_efficacy'] +__all__ += ['init_nab', 'check_nab', 'nab_to_efficacy'] -def compute_nab(people, inds, prior_inf=True): +def init_nab(people, inds, prior_inf=True): ''' Draws an initial NAb level for individuals and pre-computes NAb waning over time. Can come from a natural infection or vaccination and depends on if there is prior immunity: @@ -301,16 +301,14 @@ def compute_nab(people, inds, prior_inf=True): depending upon vaccine source. If individual has existing NAb, multiply booster impact ''' - day = people.t # timestep we are on - NAb_arrays = people.NAb[day, inds] + NAb_arrays = people.NAb[inds] + prior_NAb_inds = cvu.idefined(NAb_arrays, inds) # Find people with prior NAbs + no_prior_NAb_inds = np.setdiff1d(inds, prior_NAb_inds) # Find people without prior NAbs - prior_NAb_inds = cvu.itrue(NAb_arrays > 0, inds) # Find people with prior NAbs - no_prior_NAb_inds = cvu.itrue(NAb_arrays == 0, inds) # Find people without prior NAbs - - prior_NAb = people.NAb[day, prior_NAb_inds] # Array of NAb levels on this timestep for people with some NAbs + prior_NAb = people.NAb[prior_NAb_inds] # Array of NAb levels on this timestep for people with some NAbs NAb_boost = people.pars['NAb_boost'] # Boosting factor - # PART A: compute the initial NAb level (depends on prior infection/vaccination history) + # NAbs from infection if prior_inf: # 1) No prior NAb: draw NAb from a distribution and compute @@ -318,33 +316,45 @@ def compute_nab(people, inds, prior_inf=True): init_NAb = cvu.sample(**people.pars['NAb_init'], size=len(no_prior_NAb_inds)) prior_symp = people.prior_symptoms[no_prior_NAb_inds] no_prior_NAb = init_NAb * prior_symp - people.NAb[day, no_prior_NAb_inds] = no_prior_NAb + people.init_NAb[no_prior_NAb_inds] = no_prior_NAb # 2) Prior NAb: multiply existing NAb by boost factor if len(prior_NAb_inds): init_NAb = prior_NAb * NAb_boost - people.NAb[day, prior_NAb_inds] = init_NAb + people.init_NAb[prior_NAb_inds] = init_NAb + # NAbs from a vaccine else: - # NAbs coming from a vaccine # 1) No prior NAb: draw NAb from a distribution and compute if len(no_prior_NAb_inds): init_NAb = cvu.sample(**people.pars['vaccine_info']['NAb_init'], size=len(no_prior_NAb_inds)) - people.NAb[day, no_prior_NAb_inds] = init_NAb + people.init_NAb[no_prior_NAb_inds] = init_NAb # 2) Prior NAb (from natural or vaccine dose 1): multiply existing NAb by boost factor if len(prior_NAb_inds): init_NAb = prior_NAb * NAb_boost - people.NAb[day, prior_NAb_inds] = init_NAb + people.NAb[prior_NAb_inds] = init_NAb + + return + + +def check_nab(t, people, inds=None): + ''' Determines current NAbs based on date since recovered/vaccinated.''' + # Indices of people who've had some NAb event + rec_inds = cvu.defined(people.date_recovered[inds]) + vac_inds = cvu.defined(people.date_vaccinated[inds]) + both_inds = np.intersect1d(rec_inds, vac_inds) - # PART B: compute NAb levels over time using waning functions - NAb_decay = people.pars['NAb_decay'] - n_days = people.pars['n_days'] - days_left = n_days - day+1 # how many days left in sim - NAb_waning = pre_compute_waning(length=days_left, form=NAb_decay['form'], pars=NAb_decay['pars']) - people.NAb[day:, inds] = np.multiply.outer(NAb_waning,2**people.NAb[day, inds]) + # Time since boost + t_since_boost = np.full(len(inds), np.nan, dtype=cvd.default_int) + t_since_boost[rec_inds] = t-people.date_recovered[inds[rec_inds]] + t_since_boost[vac_inds] = t-people.date_vaccinated[inds[vac_inds]] + t_since_boost[both_inds] = t-np.maximum(people.date_recovered[inds[both_inds]],people.date_vaccinated[inds[both_inds]]) + + # Set current NAbs + people.NAb[inds] = people.pars['NAb_kin'][t_since_boost] * 2**people.init_NAb[inds] return @@ -423,6 +433,11 @@ def init_immunity(sim, create=False): errormsg = f'Type of immunity["sus"] not understood: you provided {type(sim["immunity"]["sus"])}, but it should be an array or dict.' raise ValueError(errormsg) + # Next, precompute the NAb kinetics and store these for access during the sim + sim['NAb_kin'] = pre_compute_waning(length=sim['n_days'], form=sim['NAb_decay']['form'], pars=sim['NAb_decay']['pars']) + + return + def check_immunity(people, strain, sus=True, inds=None): ''' @@ -457,11 +472,11 @@ def check_immunity(people, strain, sus=True, inds=None): if len(is_sus_vacc): vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] - current_NAbs = people.NAb[people.t, is_sus_vacc] + current_NAbs = people.NAb[is_sus_vacc] people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus') if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain - current_NAbs = people.NAb[people.t, is_sus_was_inf_same] + current_NAbs = people.NAb[is_sus_was_inf_same] people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, strain], 'sus') if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain @@ -469,7 +484,7 @@ def check_immunity(people, strain, sus=True, inds=None): prior_strains_unique = cvd.default_int(np.unique(prior_strains)) for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] - current_NAbs = people.NAb[people.t, unique_inds] + current_NAbs = people.NAb[unique_inds] people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus') else: @@ -481,18 +496,13 @@ def check_immunity(people, strain, sus=True, inds=None): if len(is_inf_vacc): # Immunity for infected people who've been vaccinated vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] - current_NAbs = people.NAb[people.t, is_inf_vacc] + current_NAbs = people.NAb[is_inf_vacc] people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['symp'][strain], 'symp') people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['sev'][strain], 'sev') if len(was_inf): # Immunity for reinfected people - current_NAbs = people.NAb[people.t, was_inf] - try: people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp') - except: - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() + current_NAbs = people.NAb[was_inf] + people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp') people.sev_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['sev'][strain], 'sev') return diff --git a/covasim/interventions.py b/covasim/interventions.py index d45e16bf2..39494b503 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1215,5 +1215,5 @@ def update_vaccine_info(self, sim, vacc_inds): # Update vaccine attributes in sim sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] sim.people.date_vaccinated[vacc_inds] = self.vaccination_dates[vacc_inds] - cvi.compute_nab(sim.people, vacc_inds, prior_inf=False) + cvi.init_nab(sim.people, vacc_inds, prior_inf=False) return \ No newline at end of file diff --git a/covasim/parameters.py b/covasim/parameters.py index 512b183fa..4a2e4c5ba 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -67,6 +67,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Parameters used to calculate immunity pars['NAb_init'] = dict(dist='normal', par1= 0, par2= 2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 + pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters pars['NAb_boost'] = 3 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms diff --git a/covasim/people.py b/covasim/people.py index 415e74ba7..8777c5497 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -61,8 +61,6 @@ def __init__(self, pars, strict=True, **kwargs): self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif 'imm' in key: # everyone starts out with no immunity self[key] = np.full((self.pars['n_strains'], self.pop_size), 0, dtype=cvd.default_float) - elif 'NAb' in key: # everyone starts out with no NAb - self[key] = np.full(((self.pars['n_days']+1), self.pop_size), 0, dtype=cvd.default_float) elif key == 'vaccinations': self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) else: @@ -272,7 +270,7 @@ def check_recovery(self): self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] # self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] # if len(inds): - cvi.compute_nab(self, inds, prior_inf=True) + cvi.init_nab(self, inds, prior_inf=True) # Now reset all disease states self.exposed[inds] = False diff --git a/covasim/sim.py b/covasim/sim.py index 5c4d5b788..0b0242a01 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -463,7 +463,7 @@ def init_strains(self): def init_immunity(self, create=False): - ''' Initialize immunity matrices and precompute immunity waning for each strain ''' + ''' Initialize immunity matrices and precompute NAb waning for each strain ''' cvimm.init_immunity(self, create=create) return @@ -575,10 +575,13 @@ def step(self): strain_pars = dict() ns = self['n_strains'] # Shorten number of strains + # Check NAbs. Take intersection with recovered peopl so we don't compute NAbs for anyone currently infected + has_nabs = np.intersect1d(cvu.defined(people.init_NAb), cvu.defined(people.date_recovered)) + if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) + # Iterate through n_strains to calculate infections for strain in range(ns): - # Check immunity cvimm.check_immunity(people, strain, sus=True) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index dcc48b8a5..07924c220 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -6,8 +6,8 @@ do_plot = 1 -do_show = 1 -do_save = 0 +do_show = 0 +do_save = 1 def test_import1strain(do_plot=False, do_show=True, do_save=False): @@ -431,21 +431,21 @@ def get_ind_of_min_value(list, time): if __name__ == '__main__': sc.tic() - # Run simplest possible test - if 0: - sim = cv.Sim() - sim.run() - - # Run more complex single-sim tests + # # Run simplest possible test + # if 1: + # sim = cv.Sim() + # sim.run() + # + # # Run more complex single-sim tests sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - # - # # Run Vaccine tests + + # Run Vaccine tests # sim4 = test_synthpops() # sim5 = test_vaccine_1strain() - # + # # Run multisim and scenario tests # scens0 = test_vaccine_1strain_scen() # scens1 = test_vaccine_2strains_scen() From 141f1cf8fd0532e8de4b9133847f35df12f62937 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 24 Mar 2021 12:14:59 +0100 Subject: [PATCH 248/569] tests ok --- tests/devtests/test_variants.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 07924c220..414d2decc 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -3,6 +3,8 @@ import sciris as sc import matplotlib.pyplot as plt import numpy as np +import pandas as pd +import seaborn as sns do_plot = 1 @@ -26,12 +28,13 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): 'beta': 0.01 } strain = cv.Strain(strain_pars, days=1, n_imports=20) - sim = cv.Sim(pars=pars, strains=strain) + sim = cv.Sim(pars=pars, strains=strain, analyzers=cv.snapshot(30, 60)) sim.run() if do_plot: plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (cross immunity)', filename='test_importstrain1', labels=strain_labels, do_show=do_show, do_save=do_save) plot_shares(sim, key='new_infections', title='Shares of new infections by strain', filename='test_importstrain1_shares', do_show=do_show, do_save=do_save) + return sim From c954eb0d2aaa18cd450c58a75bb9bf3ba56c9b41 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 24 Mar 2021 12:19:08 +0100 Subject: [PATCH 249/569] tidy up comment --- covasim/immunity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 6ea9605bf..3de6104de 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -293,11 +293,11 @@ def initialize(self, sim): def init_nab(people, inds, prior_inf=True): ''' - Draws an initial NAb level for individuals and pre-computes NAb waning over time. + Draws an initial NAb level for individuals. Can come from a natural infection or vaccination and depends on if there is prior immunity: - 1) a natural infection. If individual has no existing NAb, draw from lognormal distribution + 1) a natural infection. If individual has no existing NAb, draw from distribution depending upon symptoms. If individual has existing NAb, multiply booster impact - 2) Vaccination. If individual has no existing NAb, draw from lognormal distribution + 2) Vaccination. If individual has no existing NAb, draw from distribution depending upon vaccine source. If individual has existing NAb, multiply booster impact ''' From 02e5708628cbe070d92cc8a8db049aaf2fdee725 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 24 Mar 2021 14:49:44 +0100 Subject: [PATCH 250/569] fix line 580 in sim --- covasim/immunity.py | 7 ++++++- covasim/sim.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 3de6104de..2820b7146 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -343,7 +343,12 @@ def check_nab(t, people, inds=None): ''' Determines current NAbs based on date since recovered/vaccinated.''' # Indices of people who've had some NAb event - rec_inds = cvu.defined(people.date_recovered[inds]) + try: rec_inds = cvu.defined(people.date_recovered[inds]) + except: + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() vac_inds = cvu.defined(people.date_vaccinated[inds]) both_inds = np.intersect1d(rec_inds, vac_inds) diff --git a/covasim/sim.py b/covasim/sim.py index 0b0242a01..f66d6efc4 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -575,8 +575,8 @@ def step(self): strain_pars = dict() ns = self['n_strains'] # Shorten number of strains - # Check NAbs. Take intersection with recovered peopl so we don't compute NAbs for anyone currently infected - has_nabs = np.intersect1d(cvu.defined(people.init_NAb), cvu.defined(people.date_recovered)) + # Check NAbs. Take set difference so we don't compute NAbs for anyone currently infected + has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.true(inf)) if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) # Iterate through n_strains to calculate infections From bcd5b4efd647959e38da300b6bc8c34b84e21d86 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 24 Mar 2021 14:50:18 +0100 Subject: [PATCH 251/569] remove debug --- covasim/immunity.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 2820b7146..3de6104de 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -343,12 +343,7 @@ def check_nab(t, people, inds=None): ''' Determines current NAbs based on date since recovered/vaccinated.''' # Indices of people who've had some NAb event - try: rec_inds = cvu.defined(people.date_recovered[inds]) - except: - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() + rec_inds = cvu.defined(people.date_recovered[inds]) vac_inds = cvu.defined(people.date_vaccinated[inds]) both_inds = np.intersect1d(rec_inds, vac_inds) From c7a548543615669b3d557f036a759e2fb99e923e Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 24 Mar 2021 15:14:11 +0100 Subject: [PATCH 252/569] need to transform init_nab vals --- covasim/immunity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 3de6104de..25d68c002 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -315,7 +315,7 @@ def init_nab(people, inds, prior_inf=True): if len(no_prior_NAb_inds): init_NAb = cvu.sample(**people.pars['NAb_init'], size=len(no_prior_NAb_inds)) prior_symp = people.prior_symptoms[no_prior_NAb_inds] - no_prior_NAb = init_NAb * prior_symp + no_prior_NAb = (2**init_NAb) * prior_symp people.init_NAb[no_prior_NAb_inds] = no_prior_NAb # 2) Prior NAb: multiply existing NAb by boost factor @@ -329,7 +329,7 @@ def init_nab(people, inds, prior_inf=True): # 1) No prior NAb: draw NAb from a distribution and compute if len(no_prior_NAb_inds): init_NAb = cvu.sample(**people.pars['vaccine_info']['NAb_init'], size=len(no_prior_NAb_inds)) - people.init_NAb[no_prior_NAb_inds] = init_NAb + people.init_NAb[no_prior_NAb_inds] = 2**init_NAb # 2) Prior NAb (from natural or vaccine dose 1): multiply existing NAb by boost factor if len(prior_NAb_inds): From 3239b45d6e3d95f06b32d5a26fd2e1dfd4e43b2f Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 24 Mar 2021 15:22:21 +0100 Subject: [PATCH 253/569] fix to negative nabs --- covasim/immunity.py | 10 +++++----- covasim/sim.py | 2 +- tests/devtests/test_variants.py | 34 ++++++++++++++++----------------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 25d68c002..30f132847 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -215,7 +215,7 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on pfizer if vaccine in choices['pfizer']: vaccine_pars = dict() - vaccine_pars['NAb_init'] = dict(dist='lognormal', par1=2, par2= 2) + vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2= 2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -223,7 +223,7 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on moderna elif vaccine in choices['moderna']: vaccine_pars = dict() - vaccine_pars['NAb_init'] = dict(dist='lognormal', par1=2, par2=2) + vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 29 vaccine_pars['label'] = vaccine @@ -231,7 +231,7 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on az elif vaccine in choices['az']: vaccine_pars = dict() - vaccine_pars['NAb_init'] = dict(dist='lognormal', par1=2, par2=2) + vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['label'] = vaccine @@ -239,7 +239,7 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on j&j elif vaccine in choices['j&j']: vaccine_pars = dict() - vaccine_pars['NAb_init'] = dict(dist='lognormal', par1=2, par2=2) + vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) vaccine_pars['doses'] = 1 vaccine_pars['interval'] = None vaccine_pars['label'] = vaccine @@ -354,7 +354,7 @@ def check_nab(t, people, inds=None): t_since_boost[both_inds] = t-np.maximum(people.date_recovered[inds[both_inds]],people.date_vaccinated[inds[both_inds]]) # Set current NAbs - people.NAb[inds] = people.pars['NAb_kin'][t_since_boost] * 2**people.init_NAb[inds] + people.NAb[inds] = people.pars['NAb_kin'][t_since_boost] * people.init_NAb[inds] return diff --git a/covasim/sim.py b/covasim/sim.py index f66d6efc4..76433027c 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -576,7 +576,7 @@ def step(self): ns = self['n_strains'] # Shorten number of strains # Check NAbs. Take set difference so we don't compute NAbs for anyone currently infected - has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.true(inf)) + has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.true(people.exposed)) if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) # Iterate through n_strains to calculate infections diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 414d2decc..fc2826eb9 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -434,26 +434,26 @@ def get_ind_of_min_value(list, time): if __name__ == '__main__': sc.tic() - # # Run simplest possible test - # if 1: - # sim = cv.Sim() - # sim.run() - # - # # Run more complex single-sim tests + # Run simplest possible test + if 1: + sim = cv.Sim() + sim.run() + + # Run more complex single-sim tests sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) # Run Vaccine tests - # sim4 = test_synthpops() - # sim5 = test_vaccine_1strain() - - # # Run multisim and scenario tests - # scens0 = test_vaccine_1strain_scen() - # scens1 = test_vaccine_2strains_scen() - # scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) - # msim0 = test_msim() + sim4 = test_synthpops() + sim5 = test_vaccine_1strain() + + # Run multisim and scenario tests + scens0 = test_vaccine_1strain_scen() + scens1 = test_vaccine_2strains_scen() + scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) + msim0 = test_msim() sc.toc() From 26d08dc7b99eb95f406fa7d5343944994ac497be Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 24 Mar 2021 15:28:07 +0100 Subject: [PATCH 254/569] check tests --- covasim/immunity.py | 7 ++++++- covasim/sim.py | 2 +- tests/devtests/test_variants.py | 28 ++++++++++++++-------------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 30f132847..36021cb43 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -354,7 +354,12 @@ def check_nab(t, people, inds=None): t_since_boost[both_inds] = t-np.maximum(people.date_recovered[inds[both_inds]],people.date_vaccinated[inds[both_inds]]) # Set current NAbs - people.NAb[inds] = people.pars['NAb_kin'][t_since_boost] * people.init_NAb[inds] + try: people.NAb[inds] = people.pars['NAb_kin'][t_since_boost] * people.init_NAb[inds] + except: + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() return diff --git a/covasim/sim.py b/covasim/sim.py index 76433027c..897a97691 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -576,7 +576,7 @@ def step(self): ns = self['n_strains'] # Shorten number of strains # Check NAbs. Take set difference so we don't compute NAbs for anyone currently infected - has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.true(people.exposed)) + has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.false(sus)) if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) # Iterate through n_strains to calculate infections diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index fc2826eb9..10da99e4a 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -435,25 +435,25 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - if 1: + if 0: sim = cv.Sim() sim.run() # Run more complex single-sim tests sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - - # Run Vaccine tests - sim4 = test_synthpops() - sim5 = test_vaccine_1strain() - - # Run multisim and scenario tests - scens0 = test_vaccine_1strain_scen() - scens1 = test_vaccine_2strains_scen() - scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) - msim0 = test_msim() + # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + # + # # Run Vaccine tests + # sim4 = test_synthpops() + # sim5 = test_vaccine_1strain() + # + # # Run multisim and scenario tests + # scens0 = test_vaccine_1strain_scen() + # scens1 = test_vaccine_2strains_scen() + # scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) + # msim0 = test_msim() sc.toc() From 825c74626a889718e03c09f644850ff0215c10a9 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 24 Mar 2021 15:29:26 +0100 Subject: [PATCH 255/569] clean up --- covasim/immunity.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 36021cb43..30f132847 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -354,12 +354,7 @@ def check_nab(t, people, inds=None): t_since_boost[both_inds] = t-np.maximum(people.date_recovered[inds[both_inds]],people.date_vaccinated[inds[both_inds]]) # Set current NAbs - try: people.NAb[inds] = people.pars['NAb_kin'][t_since_boost] * people.init_NAb[inds] - except: - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() + people.NAb[inds] = people.pars['NAb_kin'][t_since_boost] * people.init_NAb[inds] return From 06e95bbe45106ab946c6ce9511f79a4edd33be7d Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 24 Mar 2021 17:55:44 -0400 Subject: [PATCH 256/569] script to generate plots for immune trajectories, WIP --- tests/devtests/test_immunity.py | 65 ++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/tests/devtests/test_immunity.py b/tests/devtests/test_immunity.py index 310beec1a..51c20b910 100644 --- a/tests/devtests/test_immunity.py +++ b/tests/devtests/test_immunity.py @@ -5,39 +5,62 @@ import numpy as np -plot_args = dict(do_plot=1, do_show=0, do_save=1) +plot_args = dict(do_plot=1, do_show=1, do_save=1) def test_reinfection_scens(do_plot=False, do_show=True, do_save=False): - sc.heading('Run a basic sim with 1 strain, varying reinfection risk') + sc.heading('Run a basic sim with varying reinfection risk') sc.heading('Setting up...') # Define baseline parameters base_pars = { 'beta': 0.1, # Make beta higher than usual so people get infected quickly - 'n_days': 240, + 'n_days': 350, } n_runs = 3 base_sim = cv.Sim(base_pars) + b1351 = cv.Strain('b1351', days=100, n_imports=20) # Define the scenarios scenarios = { - 'baseline': { - 'name':'No reinfection', - 'pars': {'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': None}) for k in cvd.immunity_axes}, - 'rel_imm': {k: 1 for k in cvd.immunity_sources} - }, + # 'baseline': { + # 'name':'Baseline', + # 'pars': {'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001})}, + # }, + # 'slower': { + # 'name':'Slower', + # 'pars': {'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/120, 'init_decay_time': 250, 'decay_decay_rate': 0.001})}, + # }, + # 'faster': { + # 'name':'Faster', + # 'pars': {'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/30, 'init_decay_time': 150, 'decay_decay_rate': 0.01})}, + # }, + 'baseline_b1351': { + 'name': 'Baseline, B1351 on day 40', + 'pars': {'strains': [b1351]}, + }, - 'med_halflife': { - 'name':'3 month waning susceptibility', - 'pars': {'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 90}) for k in cvd.immunity_axes}}, + 'slower_b1351': { + 'name': 'Slower, B1351 on day 40', + 'pars': {'NAb_decay': dict(form='nab_decay', + pars={'init_decay_rate': np.log(2) / 120, 'init_decay_time': 250, + 'decay_decay_rate': 0.001}), + 'strains': [b1351]}, }, - 'med_halflife_bysev': { - 'name':'2 month waning susceptibility for symptomatics only', - 'pars': {'rel_imm': {'asymptomatic': 0, 'mild': 1, 'severe': 1}, - 'imm_pars': {k: dict(form='exp_decay', pars={'init_val': 1., 'half_life': 60}) for k in cvd.immunity_axes} - } + 'faster_b1351': { + 'name': 'Faster, B1351 on day 40', + 'pars': {'NAb_decay': dict(form='nab_decay', + pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 150, + 'decay_decay_rate': 0.01}), + 'strains': [b1351]}, }, + # 'even_faster_b1351': { + # 'name': 'Even faster, B1351 on day 40', + # 'pars': {'NAb_decay': dict(form='nab_decay', + # pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 50, + # 'decay_decay_rate': 0.1}), + # 'strains': [b1351]}, + # }, } metapars = {'n_runs': n_runs} @@ -53,7 +76,7 @@ def test_reinfection_scens(do_plot=False, do_show=True, do_save=False): if do_plot: scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_basic_reinfection.png', to_plot=to_plot) - return sim + return scens @@ -63,12 +86,12 @@ def test_reinfection_scens(do_plot=False, do_show=True, do_save=False): sc.tic() # Run simplest possible test - if 0: - sim = cv.Sim() - sim.run() + # if 0: + # sim = cv.Sim() + # sim.run() # Run more complex tests - #scens1 = test_reinfection_scens(**plot_args) + scens1 = test_reinfection_scens(**plot_args) sc.toc() From 5989262057044769684f4276baf06641eef52098 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 24 Mar 2021 17:56:37 -0400 Subject: [PATCH 257/569] adding in vaccine vs infection specific boosts on NAb --- covasim/immunity.py | 19 ++++++++++++++++--- covasim/parameters.py | 8 ++------ covasim/sim.py | 2 ++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 30f132847..b5229eec3 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -167,6 +167,7 @@ def __init__(self, vaccine=None): self.doses = None self.interval = None self.NAb_init = None + self.NAb_boost = None self.vaccine_strain_info = self.init_strain_vaccine_info() self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine) for par, val in self.vaccine_pars.items(): @@ -218,6 +219,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2= 2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 + vaccine_pars['NAb_boost'] = 3 vaccine_pars['label'] = vaccine # Known parameters on moderna @@ -226,6 +228,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 29 + vaccine_pars['NAb_boost'] = 3 vaccine_pars['label'] = vaccine # Known parameters on az @@ -234,6 +237,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 + vaccine_pars['NAb_boost'] = 3 vaccine_pars['label'] = vaccine # Known parameters on j&j @@ -242,6 +246,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) vaccine_pars['doses'] = 1 vaccine_pars['interval'] = None + vaccine_pars['NAb_boost'] = 3 vaccine_pars['label'] = vaccine else: @@ -306,11 +311,11 @@ def init_nab(people, inds, prior_inf=True): no_prior_NAb_inds = np.setdiff1d(inds, prior_NAb_inds) # Find people without prior NAbs prior_NAb = people.NAb[prior_NAb_inds] # Array of NAb levels on this timestep for people with some NAbs - NAb_boost = people.pars['NAb_boost'] # Boosting factor + # NAbs from infection if prior_inf: - + NAb_boost = people.pars['NAb_boost'] # Boosting factor for natural infection # 1) No prior NAb: draw NAb from a distribution and compute if len(no_prior_NAb_inds): init_NAb = cvu.sample(**people.pars['NAb_init'], size=len(no_prior_NAb_inds)) @@ -325,7 +330,7 @@ def init_nab(people, inds, prior_inf=True): # NAbs from a vaccine else: - + NAb_boost = people.pars['vaccine_info']['NAb_boost'] # Boosting factor for vaccination # 1) No prior NAb: draw NAb from a distribution and compute if len(no_prior_NAb_inds): init_NAb = cvu.sample(**people.pars['vaccine_info']['NAb_init'], size=len(no_prior_NAb_inds)) @@ -409,6 +414,14 @@ def init_immunity(sim, create=False): np.fill_diagonal(immunity[ax], 1) # Default for own-immunity else: # Progression and transmission are matrices of scalars of size sim['n_strains'] immunity[ax] = np.full(ts, 1, dtype=cvd.default_float) + + known_strains = ['wild', 'b117', 'b1351', 'p1'] + cross_immunity = create_cross_immunity(circulating_strains) + for i in range(ts): + for j in range(ts): + if i != j: + if circulating_strains[i] in known_strains and circulating_strains[j] in known_strains: + immunity['sus'][j][i] = cross_immunity[circulating_strains[j]][circulating_strains[i]] sim['immunity'] = immunity else: diff --git a/covasim/parameters.py b/covasim/parameters.py index 4a2e4c5ba..2b14848ac 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -68,7 +68,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['NAb_init'] = dict(dist='normal', par1= 0, par2= 2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters - pars['NAb_boost'] = 3 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source + pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms pars['rel_imm']['asymptomatic'] = 0.5 @@ -316,11 +316,7 @@ def update_sub_key_pars(pars, default_pars): else: pars[par] = sc.promotetolist(sc.mergenested(oldval, newval)) else: - if isinstance(val, dict): # Update the dictionary, don't just overwrite it - if isinstance(default_pars[par], dict): - pars[par] = sc.mergenested(default_pars[par], val) - else: # If the default isn't a disctionary, just overwrite it (TODO: could make this more robust) - pars[par] = val + pars[par] = val return pars diff --git a/covasim/sim.py b/covasim/sim.py index 897a97691..e21faa082 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -480,6 +480,8 @@ def init_vaccines(self): self['vaccine_info']['rel_imm'][ind,:] = vacc.rel_imm self['vaccine_info']['doses'] = vacc.doses self['vaccine_info']['NAb_init'] = vacc.NAb_init + self['vaccine_info']['NAb_boost'] = vacc.NAb_boost + return def rescale(self): From 13bd65b5a879bddcb30a0a3fc08c23353c8be7bf Mon Sep 17 00:00:00 2001 From: Romesh Abeysuriya Date: Thu, 25 Mar 2021 10:27:57 +1100 Subject: [PATCH 258/569] Add Layer.to_graph() method --- covasim/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/covasim/base.py b/covasim/base.py index 26bd80ab0..7792e7986 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1433,6 +1433,14 @@ def from_df(self, df): return self + def to_graph(self): + ''' Convert to a networkx DiGraph''' + import networkx as nx + G = nx.DiGraph() + G.add_weighted_edges_from(zip(self['p1'],self['p2'],self['beta']), 'beta') + return G + + def find_contacts(self, inds, as_array=True): """ Find all contacts of the specified people From b13c866a12dfa0ab87574c351b97d3716a748b13 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 25 Mar 2021 16:00:47 -0400 Subject: [PATCH 259/569] manaus repo --- covasim/immunity.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index b5229eec3..0ad4eb392 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -182,13 +182,14 @@ def init_strain_vaccine_info(self): for vx in rel_imm['known_vaccines']: rel_imm[vx] = {} rel_imm[vx]['wild'] = 1 - rel_imm[vx]['b117'] = 1 - rel_imm['pfizer']['b1351'] = .5 - rel_imm['pfizer']['p1'] = .5 + rel_imm['pfizer']['b117'] = 1/2 + rel_imm['pfizer']['b1351'] = 1/6.7 + rel_imm['pfizer']['p1'] = 1/6.5 - rel_imm['moderna']['b1351'] = .5 - rel_imm['moderna']['p1'] = .5 + rel_imm['moderna']['b117'] = 1/1.8 + rel_imm['moderna']['b1351'] = 1/4.5 + rel_imm['moderna']['p1'] = 1/8.6 rel_imm['az']['b1351'] = .5 rel_imm['az']['p1'] = .5 From 40122d8f223f0c3e4ca28d44f96e5d00b700a75e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 25 Mar 2021 18:59:19 -0700 Subject: [PATCH 260/569] update themes --- docs/_static/theme_overrides.css | 11 +++++++++-- docs/tutorials/t07.ipynb | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css index 8574a6e10..ab3281e8b 100644 --- a/docs/_static/theme_overrides.css +++ b/docs/_static/theme_overrides.css @@ -17,17 +17,24 @@ h1, h2, .rst-content .toctree-wrapper p.caption, h3, h4, h5, h6, legend { margin-bottom: 0.5em; } +/* CK: added toc-backref since otherwise overrides this */ +.toc-backref, .rst-content { + font-size: inherit !important; + color: inherit !important; + margin-bottom: inherit !important; +} + h1 { margin-bottom: 1.0em; } -/* CK: added toc-backref since otherwise overrides this */ -h2, .toc-backref { +h2 { font-size: 125%; color: #0055af !important; margin-bottom: 1.0em; } + h3 { font-size: 115%; color: #38761d; diff --git a/docs/tutorials/t07.ipynb b/docs/tutorials/t07.ipynb index e46a33fb6..9552a4f45 100644 --- a/docs/tutorials/t07.ipynb +++ b/docs/tutorials/t07.ipynb @@ -263,8 +263,8 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", - "language": "python", + "display_name": "Python 3 (Spyder)", + "language": "python3", "name": "python3" }, "language_info": { From 804ce5ec1e138bc19d56777328b4fb3df9980a81 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 26 Mar 2021 13:17:03 +0100 Subject: [PATCH 261/569] merge variants changlog updates --- CHANGELOG.rst | 60 ++++++++- covasim/analysis.py | 195 ++++++++++++++++++++++++++--- covasim/base.py | 113 +++++++++++------ covasim/interventions.py | 54 ++++---- covasim/parameters.py | 1 - covasim/people.py | 25 +--- covasim/plotting.py | 49 ++++++-- covasim/sim.py | 9 -- docs/tutorials/t5.ipynb | 1 - examples/t5_custom_intervention.py | 1 - tests/test_analysis.py | 18 +++ tests/test_other.py | 60 ++++++--- 12 files changed, 440 insertions(+), 146 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index db6d7e140..134e73d91 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,16 +15,70 @@ Future release plans These are the major improvements we are currently working on. If there is a specific bugfix or feature you would like to see, please `create an issue `__. -- Improved handling of vaccination, including more detailed targeting options, waning immunity, etc. -- Mechanistic handling of different strains - Additional flexibility in plotting options (e.g. date ranges, per-plot DPI) - Expanded tutorials (health care workers, vaccination, calibration, exercises, etc.) +- Improved handling of vaccination, including more detailed targeting options, waning immunity, etc. +- Mechanistic handling of different strains +- Multi-region and geospatial support +- Economics and costing analysis ~~~~~~~~~~~~~~~~~~~~~~~ -Latest versions (2.0.x) +Latest versions (3.0.x) ~~~~~~~~~~~~~~~~~~~~~~~ +Version 3.0.0 (2021-XX-XX) +-------------------------- +This version contains a number of major updates. + +Highlights +^^^^^^^^^^ +- **Model structure**: The model now follows an "SEIS"-type structure, instead of the previous "SEIR" structure. This means that after recovering from an infection, agents return to the "susceptible" compartment. Each agent in the simulation has properties ``sus_imm``, ``trans_imm`` and ``prog_imm``, which respectively determine their immunity to acquiring an infection, transmitting an infection, or developing a more severe case of COVID-19. All these immunity levels are initially zero. They can be boosted by either natural infection or vaccination, and thereafter they can wane over time or remain permanently elevated. +- **Multi-strain modeling**: Model functionality has been extended to allow for modeling of multiple different co-circulating strains with different properties. This means you can now do e.g. ``b117 = cv.Strain('b117', days=1, n_imports=20)`` followed by ``sim = cv.Sim(strains=b117)`` to import strain B117. Further examples are contained in ``devtests/test_variants.py``. +- **New methods for vaccine modeling**: A new ``vaccinate`` intervention has been added, which allows more flexible modeling of vaccinations. Vaccines, like natural infections, are assumed to boost agents' immunity. + +State changes +^^^^^^^^^^^^^ +- The ``recovered`` state has been removed. + +Parameter changes +^^^^^^^^^^^^^^^^^ +- A subset of existing parameters have been made strain-specific, meaning that they are allowed to differ by strain. These include: ``rel_beta``, which specifies the relative transmissibility of a new strain compared to the wild strain; ``asymp_factor``, all of the ``dur`` parameters, ``rel_symp_prob``, ``rel_severe_prob``, ``rel_crit_prob``, and the newly-added immunity parameters ``imm_pars`` (see next point). The list of parameters that can vary by strain is specified in ``covasim/defaults.py``. +- Two new parameters have been added to hold information about the strains in the simulation: + - The parameter ``n_strains`` is an integer, updated on each time-step, that specifies how many strains are in circulation at that time-step. + - The parameter ``total_strains`` is an integer that specifies how many strains will be in ciruclation at some point during the course of the simulation. +- Five new parameters have been added to characterize agents' immunity levels: + - The parameter ``imm_pars`` is a dictionary with keys ``sus``, ``trans`` and ``prog``, for describing how a person's immunity to acquiring an infection, transmitting an infection, or developing a more severe case of COVID-19 evolves over time. Each of these is also a dictionary, with keys ``form`` and ``pars`` for specifying the functional form of immunity decay (e.g. ``exp_decay``) and the associated parameters (e.g. ``init_val`` and ``half_life``). By default, infection with SARS-CoV-2 is assumed to grant lasting perfect immunity to reinfection with the same strain (``dict(form='exp_decay', pars={'init_val': 1., 'half_life': None})``). Immunity levels can alternatively be set to wane over time by changing the functional form of the ``imm_pars`` immunity functions. + - The parameter ``immune_degree`` is a dictionary with keys ``sus``, ``trans`` and ``prog`` as above. The values of this dictionary are pre-computed evaluations of the immunity functions described above over time. + - The parameter ``cross_immunity``. By default, infection with one strain of SARS-CoV-2 is assumed to grant 50% immunity to infection with a different strain. This default assumption of 50% cross-immunity can be modified via this parameter (which will then apply to all strains in the simulation), or it can be modified on a per-strain basis using the ``immunity`` parameter described below. + - The parameter ``immunity`` is a matrix of size ``total_strains`` by ``total_strains``. Row ``i`` specifies the immunity levels that people who have been infected with strain ``i`` have to other strains. The entries of this matrix are then multiplied by the time-dependent immunity levels contained in the ``immune_degree`` parameter to determine a person's immunity at each time-step. By default, this will be ``[[1]]`` for a single-strain simulation and ``[[1, 0.5],[0.5, 1]]`` a 2-strain simulation. + - The parameter ``rel_imm`` is a dictionary with keys ``asymptomatic``, ``mild`` and ``severe``. These contain scalars specifying the relative immunity levels for someone who had an asymptomatic, mild, or severe infection. By default, values of 0.98, 0.99, and 1.0 are used. +- The parameter ``strains`` contains information about any circulating strains that have been specified as additional to the default strain, and the parameter ``vaccines`` contains information about any vaccines in use. These are initialized as ``None`` and then populated by the user. + +New functions, methods and classes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +- The newly-added file ``immunity.py`` contains functions, methods, and classes related to calculating immunity. This includes the ``Strain`` class, the ``Vaccine`` class. +- A new ``vaccinate`` intervention has been added. Compared to the previous ``vaccine`` intervention, this new intervention allows vaccination to boost agents' immunity against infection, transmission, and progression. + +Changes to results +^^^^^^^^^^^^^^^^^^ +- ``results[n_recovered]``, ``results[cum_recovered]`` and ``results[new_recovered]`` have all been removed, since the ``recovered`` state has been removed. However, ``results[recoveries]`` still exists, and stores information about how many people cleared their infection at each time-step. +- New results have been added to store information by strain: ``cum_infections_by_strain``, ``cum_infectious_by_strain``, ``new_reinfections``, ``new_infections_by_strain``, ``new_infectious_by_strain``, ``prevalence_by_strain``, ``incidence_by_strain``. + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Versions 2.0.x (2.0.0 – 2.0.4) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Version 2.0.4 (2021-03-19) +-------------------------- +- Added a new analyzer, ``cv.daily_age_stats()``, which will compute statistics by age for each day of the simulation (compared to ``cv.age_histogram()``, which only looks at particular points in time). +- Added a new function, ``cv.date_formatter()``, which may be useful in quickly formatting axes using dates. +- Removed the need for ``self._store_args()`` in interventions; now custom interventions only need to implement ``super().__init__(**kwargs)`` rather than both. +- Changed how custom interventions print out by default (a short representation rather than the jsonified version used by built-in interventions). +- Added an ``update()`` method to ``Layer``, to allow greater flexibility for dynamic updating. +- *GitHub info*: PR `854 `__ + Version 2.0.3 (2021-03-11) -------------------------- diff --git a/covasim/analysis.py b/covasim/analysis.py index e9fc24799..cfbd1ad18 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -11,9 +11,10 @@ from . import misc as cvm from . import interventions as cvi from . import settings as cvset +from . import plotting as cvpl -__all__ = ['Analyzer', 'snapshot', 'age_histogram', 'daily_stats', 'Fit', 'TransTree'] +__all__ = ['Analyzer', 'snapshot', 'age_histogram', 'daily_age_stats', 'daily_stats', 'Fit', 'TransTree'] class Analyzer(sc.prettyobj): @@ -151,10 +152,7 @@ def get(self, key=None): class age_histogram(Analyzer): ''' - Analyzer that takes a "snapshot" of the sim.people array at specified points - in time, and saves them to itself. To retrieve them, you can either access - the dictionary directly, or use the get() method. You can also apply this - analyzer directly to a sim object. + Calculate statistics across age bins, including histogram plotting functionality. Args: days (list): list of ints/strings/date objects, the days on which to calculate the histograms (default: last day) @@ -169,9 +167,10 @@ class age_histogram(Analyzer): sim = cv.Sim(analyzers=cv.age_histogram()) sim.run() - agehist = sim.get_analyzer() - agehist = cv.age_histogram(sim=sim) + agehist = sim.get_analyzer() + agehist = cv.age_histogram(sim=sim) # Alternate method + agehist.plot() ''' def __init__(self, days=None, states=None, edges=None, datafile=None, sim=None, die=True, **kwargs): @@ -223,7 +222,7 @@ def initialize(self, sim): # Handle states if self.states is None: - self.states = ['exposed', 'dead', 'tested', 'diagnosed'] + self.states = ['exposed', 'severe', 'dead', 'tested', 'diagnosed'] self.states = sc.promotetolist(self.states) for s,state in enumerate(self.states): self.states[s] = state.replace('date_', '') # Allow keys starting with date_ as input, but strip it off here @@ -337,21 +336,178 @@ def plot(self, windows=False, width=0.8, color='#F8A493', fig_args=None, axis_ar bins = hists['bins'] barwidth = width*(bins[1] - bins[0]) # Assume uniform width for s,state in enumerate(self.states): - pl.subplot(n_rows, n_cols, s+1) - pl.bar(bins, hists[state], width=barwidth, facecolor=color, label=f'Number {state}') + ax = pl.subplot(n_rows, n_cols, s+1) + ax.bar(bins, hists[state], width=barwidth, facecolor=color, label=f'Number {state}') if self.data and state in self.data: data = self.data[state] - pl.bar(bins+d_args.offset, data, width=barwidth*d_args.width, facecolor=d_args.color, label='Data') - pl.xlabel('Age') - pl.ylabel('Count') - pl.xticks(ticks=bins) - pl.legend() + ax.bar(bins+d_args.offset, data, width=barwidth*d_args.width, facecolor=d_args.color, label='Data') + ax.set_xlabel('Age') + ax.set_ylabel('Count') + ax.set_xticks(ticks=bins) + ax.legend() preposition = 'from' if windows else 'by' - pl.title(f'Number of people {state} {preposition} {date}') + ax.set_title(f'Number of people {state} {preposition} {date}') return figs +class daily_age_stats(Analyzer): + ''' + Calculate daily counts by age, saving for each day of the simulation. Can + plot either time series by age or a histogram over all time. + + Args: + states (list): which states of people to record (default: ['diagnoses', 'deaths', 'tests', 'severe']) + edges (list): edges of age bins to use (default: 10 year bins from 0 to 100) + kwargs (dict): passed to Analyzer() + + **Examples**:: + + sim = cv.Sim(analyzers=cv.daily_age_stats()) + sim = cv.Sim(pars, analyzers=daily_age) + sim.run() + daily_age = sim.get_analyzer() + daily_age.plot() + daily_age.plot(total=True) + + ''' + + def __init__(self, states=None, edges=None, **kwargs): + super().__init__(**kwargs) + self.edges = edges + self.bins = None # Age bins, calculated from edges + self.states = states + self.results = sc.odict() + self.start_day = None + self.df = None + self.total_df = None + return + + + def initialize(self, sim): + + if self.states is None: + self.states = ['exposed', 'severe', 'dead', 'tested', 'diagnosed'] + + # Handle edges and age bins + if self.edges is None: # Default age bins + self.edges = np.linspace(0, 100, 11) + self.bins = self.edges[:-1] # Don't include the last edge in the bins + + self.start_day = sim['start_day'] + self.initialized = True + return + + + def apply(self, sim): + df_entry = {} + for state in self.states: + inds = sc.findinds(sim.people[f'date_{state}'], sim.t) + b, _ = np.histogram(sim.people.age[inds], self.edges) + df_entry.update({state: b * sim.rescale_vec[sim.t]}) + df_entry.update({'day':sim.t, 'age': self.bins}) + self.results.update({sim.date(sim.t): df_entry}) + + + def to_df(self): + '''Create dataframe totals for each day''' + mapper = {f'{k}': f'new_{k}' for k in self.states} + df = pd.DataFrame() + for date, k in self.results.items(): + df_ = pd.DataFrame(k) + df_['date'] = date + df_.rename(mapper, inplace=True, axis=1) + df = pd.concat((df, df_)) + cols = list(df.columns.values) + cols = [cols[-1]] + [cols[-2]] + cols[:-2] + self.df = df[cols] + return self.df + + + def to_total_df(self): + ''' Create dataframe totals across days ''' + if self.df is None: + self.to_df() + cols = list(self.df.columns) + cum_cols = [c for c in cols if c.split('_')[0] == 'new'] + mapper = {f'new_{c.split("_")[1]}': f'cum_{c.split("_")[1]}' for c in cum_cols} + df_dict = {'age': []} + df_dict.update({c: [] for c in mapper.values()}) + for age, group in self.df.groupby('age'): + cum_vals = group.sum() + df_dict['age'].append(age) + for k, v in mapper.items(): + df_dict[v].append(cum_vals[k]) + df = pd.DataFrame(df_dict) + if ('cum_diagnoses' in df.columns) and ('cum_tests' in df.columns): + df['yield'] = df['cum_diagnoses'] / df['cum_tests'] + self.total_df = df + return df + + + def plot(self, total=False, do_show=None, fig_args=None, axis_args=None, plot_args=None, dateformat='%b-%d', width=0.8, color='#F8A493', data_args=None): + ''' + Plot the results. + + Args: + total (bool): whether to plot the total histograms rather than time series + do_show (bool): whether to show the plot + fig_args (dict): passed to pl.figure() + axis_args (dict): passed to pl.subplots_adjust() + plot_args (dict): passed to pl.plot() + dateformat (str): the format to use for the x-axes (only used for time series) + width (float): width of bars (only used for histograms) + color (hex/rgb): the color of the bars (only used for histograms) + ''' + if self.df is None: + self.to_df() + if self.total_df is None: + self.to_total_df() + + fig_args = sc.mergedicts(dict(figsize=(18,11)), fig_args) + axis_args = sc.mergedicts(dict(left=0.05, right=0.95, bottom=0.05, top=0.95, wspace=0.25, hspace=0.4), axis_args) + plot_args = sc.mergedicts(dict(lw=2, alpha=0.5, marker='o'), plot_args) + + nplots = len(self.states) + nrows, ncols = sc.get_rows_cols(nplots) + fig, axs = pl.subplots(nrows=nrows, ncols=ncols, **fig_args) + pl.subplots_adjust(**axis_args) + + for count,state in enumerate(self.states): + row,col = np.unravel_index(count, (nrows,ncols)) + ax = axs[row,col] + ax.set_title(state.title()) + ages = self.df.age.unique() + + # Plot time series + if not total: + colors = sc.vectocolor(len(ages)) + has_data = False + for a,age in enumerate(ages): + label = f'Age {age}' + df = self.df[self.df.age==age] + ax.plot(df.day, df[f'new_{state}'], c=colors[a], label=label) + has_data = has_data or len(df) + if has_data: + ax.legend() + ax.set_xlabel('Day') + ax.set_ylabel('Count') + cvpl.date_formatter(start_day=self.start_day, dateformat=dateformat, ax=ax) + + # Plot total histograms + else: + df = self.total_df + barwidth = width*(df.age[1] - df.age[0]) # Assume uniform width + ax.bar(df.age, df[f'cum_{state}'], width=barwidth, facecolor=color) + ax.set_xlabel('Age') + ax.set_ylabel('Count') + ax.set_xticks(ticks=df.age) + + cvset.handle_show(do_show) # Whether or not to call pl.show() + + return fig + + class daily_stats(Analyzer): ''' Print out daily statistics about the simulation. Note that this analyzer takes @@ -883,8 +1039,7 @@ def compute_mismatch(self, use_median=False): return self.mismatch - def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, - plot_args=None, do_show=None, fig=None): + def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=None, do_show=None, fig=None): ''' Plot the fit of the model to the data. For each result, plot the data and the model; the difference; and the loss (weighted difference). Also @@ -1015,7 +1170,9 @@ def __init__(self, sim, to_networkx=False): # Check that rescaling is not on if sim['rescale'] and sim['pop_scale']>1: - warningmsg = 'Warning: transmission tree results are unreliable when dynamic rescaling is on, since agents are reused! Please rerun with rescale=False and pop_scale=1 for reliable results.' + warningmsg = 'Warning: transmission tree results are unreliable when' \ + 'dynamic rescaling is on, since agents are reused! Please '\ + 'rerun with rescale=False and pop_scale=1 for reliable results.' print(warningmsg) # Include the basic line list diff --git a/covasim/base.py b/covasim/base.py index 6a18c65c9..0583eef70 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -15,7 +15,7 @@ from . import parameters as cvpar # Specify all externally visible classes this file defines -__all__ = ['ParsObj', 'Result', 'Par', 'BaseSim', 'BasePeople', 'Person', 'FlexDict', 'Contacts', 'Layer'] +__all__ = ['Result', 'Par', 'BaseSim', 'BasePeople', 'Person', 'FlexDict', 'Contacts', 'Layer'] #%% Define simulation classes @@ -58,38 +58,7 @@ def brief(self, output=False): print(string) else: return string - - -class ParsObj(FlexPretty): - ''' - A class based around performing operations on a self.pars dict. - ''' - - def __init__(self, pars): - self.update_pars(pars, create=True) - return - - - def __getitem__(self, key): - ''' Allow sim['par_name'] instead of sim.pars['par_name'] ''' - try: - return self.pars[key] - except: - all_keys = '\n'.join(list(self.pars.keys())) - errormsg = f'Key "{key}" not found; available keys:\n{all_keys}' - raise sc.KeyNotFoundError(errormsg) - - - def __setitem__(self, key, value): - ''' Ditto ''' - if key in self.pars: - self.pars[key] = value - else: - all_keys = '\n'.join(list(self.pars.keys())) - errormsg = f'Key "{key}" not found; available keys:\n{all_keys}' - raise sc.KeyNotFoundError(errormsg) - return - + def update_pars(self, pars=None, create=False): ''' @@ -1177,7 +1146,12 @@ def add_contacts(self, contacts, lkey=None, beta=None): # Create the layer if it doesn't yet exist if lkey not in self.contacts: - self.contacts[lkey] = Layer() + if self.pars['dynam_layer'].get(lkey, False): + # Equivalent to previous functionality, but might be better if make_randpop() returned Layer objects instead of just dicts, that + # way the population creation function could have control over both the contacts and the update algorithm + self.contacts[lkey] = RandomLayer() + else: + self.contacts[lkey] = Layer() # Actually include them, and update properties if supplied for col in self.contacts[lkey].keys(): # Loop over the supplied columns @@ -1342,7 +1316,41 @@ def pop_layer(self, *args): class Layer(FlexDict): - ''' A small class holding a single layer of contacts ''' + ''' + A small class holding a single layer of contact edges (connections) between people. + + The input is typically three arrays: person 1 of the connection, person 2 of + the connection, and the weight of the connection. Connections are undirected; + each person is both a source and sink. + + This class is usually not invoked directly by the user, but instead is called + as part of the population creation. + + Args: + p1 (array): an array of N connections, representing people on one side of the connection + p2 (array): an array of people on the other side of the connection + beta (array): an array of weights for each connection + kwargs (dict): other keys copied directly into the layer + + Note that all arguments must be arrays of the same length, although not all + have to be supplied at the time of creation (they must all be the same at the + time of initialization, though, or else validation will fail). + + **Examples**:: + + # Generate an average of 10 contacts for 1000 people + n = 10_000 + n_people = 1000 + p1 = np.random.randint(n_people, size=n) + p2 = np.random.randint(n_people, size=n) + beta = np.ones(n) + layer = cv.Layer(p1=p1, p2=p2, beta=beta) + + # Convert one layer to another with an extra column + index = np.arange(n) + self_conn = p1 == p2 + layer2 = cv.Layer(**layer, index=index, self_conn=self_conn) + ''' def __init__(self, **kwargs): self.meta = { @@ -1358,7 +1366,7 @@ def __init__(self, **kwargs): # Set data, if provided for key,value in kwargs.items(): - self[key] = np.array(value, dtype=self.meta[key]) + self[key] = np.array(value, dtype=self.meta.get(key)) return @@ -1372,8 +1380,9 @@ def __len__(self): def __repr__(self): ''' Convert to a dataframe for printing ''' + label = self.__class__.__name__ keys_str = ', '.join(self.keys()) - output = f'Layer({keys_str})\n' + output = f'{label}({keys_str})\n' # e.g. Layer(p1, p2, beta) output += self.to_df().__repr__() return output @@ -1504,3 +1513,33 @@ def find_contacts(self, inds, as_array=True): contact_inds.sort() # Sorting ensures that the results are reproducible for a given seed as well as being identical to previous versions of Covasim return contact_inds + + + def update(self, people, frac=1.0): + ''' + Regenerate contacts on each timestep. + + This method gets called if the layer appears in ``sim.pars['dynam_lkeys']``. + The Layer implements the update procedure so that derived classes can customize + the update e.g. implementing over-dispersion/other distributions, random + clusters, etc. + + Typically, this method also takes in the ``people`` object so that the + update can depend on person attributes that may change over time (e.g. + changing contacts for people that are severe/critical). + + Args: + frac (float): the fraction of contacts to update on each timestep + ''' + # Choose how many contacts to make + pop_size = len(people) # Total number of people + n_contacts = len(self) # Total number of contacts + n_new = int(np.round(n_contacts*frac)) # Since these get looped over in both directions later + inds = cvu.choose(n_contacts, n_new) + + # Create the contacts, not skipping self-connections + self['p1'][inds] = np.array(cvu.choose_r(max_n=pop_size, n=n_new), dtype=cvd.default_int) # Choose with replacement + self['p2'][inds] = np.array(cvu.choose_r(max_n=pop_size, n=n_new), dtype=cvd.default_int) + self['beta'][inds] = np.ones(n_new, dtype=cvd.default_float) + return + diff --git a/covasim/interventions.py b/covasim/interventions.py index 39494b503..38eeb3fa8 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -92,6 +92,7 @@ class Intervention: line_args (dict): arguments passed to pl.axvline() when plotting ''' def __init__(self, label=None, show_label=True, do_plot=None, line_args=None): + self._store_args() # Store the input arguments so the intervention can be recreated self.label = label # e.g. "Close schools" self.show_label = show_label # Show the label by default self.do_plot = do_plot if do_plot is not None else True # Plot the intervention, including if None @@ -101,37 +102,42 @@ def __init__(self, label=None, show_label=True, do_plot=None, line_args=None): return - def __repr__(self): - ''' Return a JSON-friendly output if possible, else revert to pretty repr ''' - try: - json = self.to_json() - which = json['which'] - pars = json['pars'] - parstr = ', '.join([f'{k}={v}' for k,v in pars.items()]) - output = f"cv.{which}({parstr})" - except Exception as E: - output = type(self) + f' ({str(E)})' # If that fails, print why - return output + def __repr__(self, jsonify=False): + ''' Return a JSON-friendly output if possible, else revert to short repr ''' + + if self.__class__.__name__ in __all__ or jsonify: + try: + json = self.to_json() + which = json['which'] + pars = json['pars'] + parstr = ', '.join([f'{k}={v}' for k,v in pars.items()]) + output = f"cv.{which}({parstr})" + except Exception as E: + output = type(self) + f' (error: {str(E)})' # If that fails, print why + return output + else: + return f'{self.__module__}.{self.__class__.__name__}()' def disp(self): ''' Print a detailed representation of the intervention ''' - return print(sc.prepr(self)) + return sc.pr(self) def _store_args(self): ''' Store the user-supplied arguments for later use in to_json ''' f0 = inspect.currentframe() # This "frame", i.e. Intervention.__init__() f1 = inspect.getouterframes(f0) # The list of outer frames - parent = f1[1].frame # The parent frame, e.g. change_beta.__init__() + parent = f1[2].frame # The parent frame, e.g. change_beta.__init__() _,_,_,values = inspect.getargvalues(parent) # Get the values of the arguments - self.input_args = {} - for key,value in values.items(): - if key == 'kwargs': # Store additional kwargs directly - for k2,v2 in value.items(): - self.input_args[k2] = v2 # These are already a dict - elif key not in ['self', '__class__']: # Everything else, but skip these - self.input_args[key] = value + if values: + self.input_args = {} + for key,value in values.items(): + if key == 'kwargs': # Store additional kwargs directly + for k2,v2 in value.items(): + self.input_args[k2] = v2 # These are already a dict + elif key not in ['self', '__class__']: # Everything else, but skip these + self.input_args[key] = value return @@ -245,7 +251,6 @@ def __init__(self, pars=None, **kwargs): # Do standard initialization super().__init__(**kwargs) # Initialize the Intervention object - self._store_args() # Store the input arguments so the intervention can be recreated # Handle the rest of the initialization subkeys = ['days', 'vals'] @@ -297,7 +302,6 @@ class sequence(Intervention): def __init__(self, days, interventions, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object - self._store_args() # Store the input arguments so the intervention can be recreated assert len(days) == len(interventions) self.days = days self.interventions = interventions @@ -379,7 +383,6 @@ class change_beta(Intervention): def __init__(self, days, changes, layers=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object - self._store_args() # Store the input arguments so the intervention can be recreated self.days = sc.dcp(days) self.changes = sc.dcp(changes) self.layers = sc.dcp(layers) @@ -447,7 +450,6 @@ class clip_edges(Intervention): def __init__(self, days, changes, layers=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object - self._store_args() # Store the input arguments so the intervention can be recreated self.days = sc.dcp(days) self.changes = sc.dcp(changes) self.layers = sc.dcp(layers) @@ -654,7 +656,6 @@ def __init__(self, daily_tests, symp_test=100.0, quar_test=1.0, quar_policy=None ili_prev=None, sensitivity=1.0, loss_prob=0, test_delay=0, start_day=0, end_day=None, swab_delay=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object - self._store_args() # Store the input arguments so the intervention can be recreated self.daily_tests = daily_tests # Should be a list of length matching time self.symp_test = symp_test # Set probability of testing symptomatics self.quar_test = quar_test # Probability of testing people in quarantine @@ -785,7 +786,6 @@ class test_prob(Intervention): def __init__(self, symp_prob, asymp_prob=0.0, symp_quar_prob=None, asymp_quar_prob=None, quar_policy=None, subtarget=None, ili_prev=None, sensitivity=1.0, loss_prob=0.0, test_delay=0, start_day=0, end_day=None, swab_delay=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object - self._store_args() # Store the input arguments so the intervention can be recreated self.symp_prob = symp_prob self.asymp_prob = asymp_prob self.symp_quar_prob = symp_quar_prob if symp_quar_prob is not None else symp_prob @@ -900,7 +900,6 @@ class contact_tracing(Intervention): ''' def __init__(self, trace_probs=None, trace_time=None, start_day=0, end_day=None, presumptive=False, quar_period=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object - self._store_args() # Store the input arguments so the intervention can be recreated self.trace_probs = trace_probs self.trace_time = trace_time self.start_day = start_day @@ -1059,7 +1058,6 @@ class vaccine(Intervention): ''' def __init__(self, days, prob=1.0, rel_sus=0.0, rel_symp=0.0, subtarget=None, cumulative=False, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object - self._store_args() # Store the input arguments so the intervention can be recreated self.days = sc.dcp(days) self.prob = prob self.rel_sus = rel_sus diff --git a/covasim/parameters.py b/covasim/parameters.py index 2b14848ac..62696a9f7 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -53,7 +53,6 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['contacts'] = None # The number of contacts per layer; set by reset_layer_pars() below pars['dynam_layer'] = None # Which layers are dynamic; set by reset_layer_pars() below pars['beta_layer'] = None # Transmissibility per layer; set by reset_layer_pars() below - pars['n_imports'] = 0 # Average daily number of imported cases (actual number is drawn from Poisson distribution) # Basic disease transmission parameters pars['beta_dist'] = dict(dist='neg_binomial', par1=1.0, par2=0.45, step=0.01) # Distribution to draw individual level transmissibility; dispersion from https://www.researchsquare.com/article/rs-29548/v1 diff --git a/covasim/people.py b/covasim/people.py index 8777c5497..092232a4b 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -186,29 +186,10 @@ def update_states_post(self): def update_contacts(self): ''' Refresh dynamic contacts, e.g. community ''' - # Figure out if anything needs to be done -- e.g. {'h':False, 'c':True} - dynam_keys = [lkey for lkey,is_dynam in self.pars['dynam_layer'].items() if is_dynam] - - # Loop over dynamic keys - for lkey in dynam_keys: - # Remove existing contacts - self.contacts.pop(lkey) - - # Choose how many contacts to make - pop_size = len(self) - n_contacts = self.pars['contacts'][lkey] - n_new = int(n_contacts*pop_size/2) # Since these get looped over in both directions later - - # Create the contacts - new_contacts = {} # Initialize - new_contacts['p1'] = np.array(cvu.choose_r(max_n=pop_size, n=n_new), dtype=cvd.default_int) # Choose with replacement - new_contacts['p2'] = np.array(cvu.choose_r(max_n=pop_size, n=n_new), dtype=cvd.default_int) - new_contacts['beta'] = np.ones(n_new, dtype=cvd.default_float) - - # Add to contacts - self.add_contacts(new_contacts, lkey=lkey) - self.contacts[lkey].validate() + for lkey, is_dynam in self.pars['dynam_layer'].items(): + if is_dynam: + self.contacts[lkey].update(self) return self.contacts diff --git a/covasim/plotting.py b/covasim/plotting.py index 69b56e2a6..f7e0dc7fd 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -16,7 +16,7 @@ from . import settings as cvset -__all__ = ['plot_sim', 'plot_scens', 'plot_result', 'plot_compare', 'plot_people', 'plotly_sim', 'plotly_people', 'plotly_animate'] +__all__ = ['date_formatter', 'plot_sim', 'plot_scens', 'plot_result', 'plot_compare', 'plot_people', 'plotly_sim', 'plotly_people', 'plotly_animate'] #%% Plotting helper functions @@ -188,13 +188,50 @@ def title_grid_legend(ax, title, grid, commaticks, setylim, legend_args, show_le return -def reset_ticks(ax, sim, interval, as_dates, dateformat): - ''' Set the tick marks, using dates by default ''' +def date_formatter(start_day=None, dateformat=None, ax=None): + ''' + Create an automatic date formatter based on a number of days and a start day. + + Wrapper for Matplotlib's date formatter. Note, start_day is not required if the + axis uses dates already. To be used in conjunction with setting the x-axis + tick label formatter. + + Args: + start_day (str/date): the start day, either as a string or date object + dateformat (str): the date format + ax (axes): if supplied, automatically set the x-axis formatter for this axis + + **Example**:: + + formatter = date_formatter(start_day='2020-04-04', dateformat='%Y-%m-%d') + ax.xaxis.set_major_formatter(formatter) + + ''' # Set the default -- "Mar-01" if dateformat is None: dateformat = '%b-%d' + # Convert to a date object + start_day = sc.date(start_day) + + @ticker.FuncFormatter + def mpl_formatter(x, pos): + if sc.isnumber(x): + return (start_day + dt.timedelta(days=x)).strftime(dateformat) + else: + return x.strftime(dateformat) + + if ax is not None: + ax.xaxis.set_major_formatter(mpl_formatter) + + return mpl_formatter + + + +def reset_ticks(ax, sim, interval, as_dates, dateformat): + ''' Set the tick marks, using dates by default ''' + # Set the x-axis intervals if interval: xmin,xmax = ax.get_xlim() @@ -203,11 +240,7 @@ def reset_ticks(ax, sim, interval, as_dates, dateformat): # Set xticks as dates if as_dates: - @ticker.FuncFormatter - def date_formatter(x, pos): - return (sim['start_day'] + dt.timedelta(days=x)).strftime(dateformat) - - ax.xaxis.set_major_formatter(date_formatter) + ax.xaxis.set_major_formatter(date_formatter(start_day=sim['start_day'], dateformat=dateformat)) if not interval: ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) diff --git a/covasim/sim.py b/covasim/sim.py index e21faa082..aa5945070 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -527,15 +527,6 @@ def step(self): hosp_max = people.count('severe') > self['n_beds_hosp'] if self['n_beds_hosp'] else False # Check for acute bed constraint icu_max = people.count('critical') > self['n_beds_icu'] if self['n_beds_icu'] else False # Check for ICU bed constraint - # Randomly infect some people (imported infections) - imports = cvu.n_poisson(self['n_imports'], self['n_strains']) # Imported cases - for strain, n_imports in enumerate(imports): - if n_imports>0: - susceptible_inds = cvu.true(people.susceptible) - importation_inds = np.random.choice(susceptible_inds, n_imports) - people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation', - strain=strain) - # Add strains for strain in self['strains']: if isinstance(strain, cvimm.Strain): diff --git a/docs/tutorials/t5.ipynb b/docs/tutorials/t5.ipynb index 36dabd52d..f10a59dcb 100644 --- a/docs/tutorials/t5.ipynb +++ b/docs/tutorials/t5.ipynb @@ -355,7 +355,6 @@ "\n", " def __init__(self, start_day=None, end_day=None, age_cutoff=70, rel_sus=0.0, *args, **kwargs):\n", " super().__init__(**kwargs) # This line must be included\n", - " self._store_args() # So must this one\n", " self.start_day = start_day\n", " self.end_day = end_day\n", " self.age_cutoff = age_cutoff\n", diff --git a/examples/t5_custom_intervention.py b/examples/t5_custom_intervention.py index 93f47e9c1..15fc8de14 100644 --- a/examples/t5_custom_intervention.py +++ b/examples/t5_custom_intervention.py @@ -10,7 +10,6 @@ class protect_elderly(cv.Intervention): def __init__(self, start_day=None, end_day=None, age_cutoff=70, rel_sus=0.0, *args, **kwargs): super().__init__(**kwargs) # This line must be included - self._store_args() # So must this one self.start_day = start_day self.end_day = end_day self.age_cutoff = age_cutoff diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 5c83d72fe..7c637c058 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -58,9 +58,26 @@ def test_age_hist(): plots = agehist.plot(windows=True) assert len(plots) == len(day_list), "Number of plots generated should equal number of days" + # Check daily age histogram + daily_age = cv.daily_age_stats() + sim = cv.Sim(pars, analyzers=daily_age) + sim.run() + + return agehist +def test_daily_age(): + sc.heading('Testing daily age analyzer') + sim = cv.Sim(pars, analyzers=cv.daily_age_stats()) + sim.run() + daily_age = sim.get_analyzer() + if do_plot: + daily_age.plot() + daily_age.plot(total=True) + return daily_age + + def test_daily_stats(): sc.heading('Testing daily stats analyzer') ds = cv.daily_stats(days=['2020-04-04', '2020-04-14'], save_inds=True) @@ -145,6 +162,7 @@ def test_transtree(): snapshot = test_snapshot() agehist = test_age_hist() + daily_age = test_daily_age() daily = test_daily_stats() fit = test_fit() transtree = test_transtree() diff --git a/tests/test_other.py b/tests/test_other.py index 5bebf6126..b43f21fca 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -6,6 +6,7 @@ #%% Imports and settings import os import pytest +import numpy as np import sciris as sc import covasim as cv @@ -29,7 +30,7 @@ def remove_files(*args): #%% Define the tests def test_base(): - sc.heading('Testing base.py...') + sc.heading('Testing base.py sim...') json_path = 'base_tests.json' sim_path = 'base_tests.sim' @@ -67,6 +68,19 @@ def test_base(): sim.save(filename=sim_path, keep_people=keep_people) cv.Sim.load(sim_path) + # Tidy up + remove_files(json_path, sim_path) + + return + + +def test_basepeople(): + sc.heading('Testing base.py people and contacts...') + + # Create a small sim for later use + sim = cv.Sim(pop_size=100, verbose=verbose) + sim.initialize() + # BasePeople methods ppl = sim.people ppl.get(['susceptible', 'infectious']) @@ -104,8 +118,33 @@ def test_base(): df = hospitals_layer.to_df() hospitals_layer.from_df(df) - # Tidy up - remove_files(json_path, sim_path) + # Generate an average of 10 contacts for 1000 people + n = 10_000 + n_people = 1000 + p1 = np.random.randint(n_people, size=n) + p2 = np.random.randint(n_people, size=n) + beta = np.ones(n) + layer = cv.Layer(p1=p1, p2=p2, beta=beta) + + # Convert one layer to another with extra columns + index = np.arange(n) + self_conn = p1 == p2 + layer2 = cv.Layer(**layer, index=index, self_conn=self_conn) + assert len(layer2) == n + assert len(layer2.keys()) == 5 + + # Test dynamic layers, plotting, and stories + pars = dict(pop_size=1000, n_days=50, verbose=verbose, pop_type='hybrid') + s1 = cv.Sim(pars, dynam_layer={'c':1}) + s1.run() + s1.people.plot() + for person in [25, 79]: + sim.people.story(person) + + # Run without dynamic layers and assert that the results are different + s2 = cv.Sim(pars, dynam_layer={'c':0}) + s2.run() + assert cv.diff_sims(s1, s2, output=True) return @@ -198,19 +237,6 @@ def test_misc(): return -def test_people(): - sc.heading('Testing people') - - # Test dynamic layers - sim = cv.Sim(pop_size=100, n_days=10, verbose=verbose, dynam_layer={'a':1}) - sim.run() - sim.people.plot() - for person in [25, 79]: - sim.people.story(person) - - return - - def test_plotting(): sc.heading('Testing plotting') @@ -427,8 +453,8 @@ def test_settings(): T = sc.tic() test_base() + test_basepeople() test_misc() - test_people() test_plotting() test_population() test_requirements() From 02214acf78279e53eb605792428e05df2132b580 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 26 Mar 2021 13:35:31 +0100 Subject: [PATCH 262/569] fix t5 --- docs/tutorials/t05.ipynb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/tutorials/t05.ipynb b/docs/tutorials/t05.ipynb index bf1ac7ffe..beb97361c 100644 --- a/docs/tutorials/t05.ipynb +++ b/docs/tutorials/t05.ipynb @@ -369,11 +369,7 @@ "class protect_elderly(cv.Intervention):\n", "\n", " def __init__(self, start_day=None, end_day=None, age_cutoff=70, rel_sus=0.0, *args, **kwargs):\n", -<<<<<<< HEAD:docs/tutorials/t5.ipynb - " super().__init__(**kwargs) # This line must be included\n", -======= " super().__init__(**kwargs) # NB: This line must be included\n", ->>>>>>> master:docs/tutorials/t05.ipynb " self.start_day = start_day\n", " self.end_day = end_day\n", " self.age_cutoff = age_cutoff\n", From 1a2789d39947d16bea0dd6881878d33dac03c50d Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 26 Mar 2021 13:39:40 +0100 Subject: [PATCH 263/569] plotting changes --- covasim/plotting.py | 42 +----------------------------------------- covasim/version.py | 9 ++------- 2 files changed, 3 insertions(+), 48 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 070e7682d..7d974bb81 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -228,11 +228,7 @@ def title_grid_legend(ax, title, grid, commaticks, setylim, legend_args, show_le return -<<<<<<< HEAD -def date_formatter(start_day=None, dateformat=None, ax=None): -======= def date_formatter(start_day=None, dateformat=None, ax=None, sim=None): ->>>>>>> master ''' Create an automatic date formatter based on a number of days and a start day. @@ -244,14 +240,6 @@ def date_formatter(start_day=None, dateformat=None, ax=None, sim=None): start_day (str/date): the start day, either as a string or date object dateformat (str): the date format ax (axes): if supplied, automatically set the x-axis formatter for this axis -<<<<<<< HEAD - - **Example**:: - - formatter = date_formatter(start_day='2020-04-04', dateformat='%Y-%m-%d') - ax.xaxis.set_major_formatter(formatter) - -======= sim (Sim): if supplied, get the start day from this **Examples**:: @@ -260,7 +248,7 @@ def date_formatter(start_day=None, dateformat=None, ax=None, sim=None): formatter = cv.date_formatter(start_day='2020-04-04', dateformat='%Y-%m-%d') # Manually configure ax.xaxis.set_major_formatter(formatter) ->>>>>>> master + ''' # Set the default -- "Mar-01" @@ -268,24 +256,15 @@ def date_formatter(start_day=None, dateformat=None, ax=None, sim=None): dateformat = '%b-%d' # Convert to a date object -<<<<<<< HEAD -======= if start_day is None and sim is not None: start_day = sim['start_day'] ->>>>>>> master start_day = sc.date(start_day) @ticker.FuncFormatter def mpl_formatter(x, pos): -<<<<<<< HEAD - if sc.isnumber(x): - return (start_day + dt.timedelta(days=x)).strftime(dateformat) - else: -======= if sc.isnumber(x): # If the axis doesn't have date units return (start_day + dt.timedelta(days=int(x))).strftime(dateformat) else: # If the axis does ->>>>>>> master return x.strftime(dateformat) if ax is not None: @@ -294,12 +273,6 @@ def mpl_formatter(x, pos): return mpl_formatter -<<<<<<< HEAD - -def reset_ticks(ax, sim, interval, as_dates, dateformat): - ''' Set the tick marks, using dates by default ''' - -======= def reset_ticks(ax, sim=None, date_args=None, start_day=None): ''' Set the tick marks, using dates by default ''' @@ -316,23 +289,15 @@ def reset_ticks(ax, sim=None, date_args=None, start_day=None): xmax = float(sc.day(date_args.end_day, start_day=start_day)) ax.set_xlim([xmin, xmax]) ->>>>>>> master # Set the x-axis intervals if date_args.interval: ax.set_xticks(pl.arange(xmin, xmax+1, date_args.interval)) # Set xticks as dates -<<<<<<< HEAD - if as_dates: - - ax.xaxis.set_major_formatter(date_formatter(start_day=sim['start_day'], dateformat=dateformat)) - if not interval: -======= if date_args.as_dates: date_formatter(start_day=start_day, dateformat=date_args.dateformat, ax=ax) if not date_args.interval: ->>>>>>> master ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) # Handle rotation @@ -428,12 +393,7 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot if args.show['data']: plot_data(sim, ax, reskey, args.scatter, color=color) # Plot the data if args.show['ticks']: -<<<<<<< HEAD - reset_ticks(ax, sim, interval, as_dates, - dateformat) # Optionally reset tick marks (useful for e.g. plotting weeks/months) -======= reset_ticks(ax, sim, args.date) # Optionally reset tick marks (useful for e.g. plotting weeks/months) ->>>>>>> master if args.show['interventions']: plot_interventions(sim, ax) # Plot the interventions if args.show['legend']: diff --git a/covasim/version.py b/covasim/version.py index de55b715a..1b58a1d16 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,11 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -<<<<<<< HEAD -__version__ = '3.0' -__versiondate__ = '2020-03-01' -======= -__version__ = '2.1.0' -__versiondate__ = '2021-03-23' ->>>>>>> master +__version__ = '3.0.0' +__versiondate__ = '2020-04-XX' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From a2afa2510064545628eddd6e78b2ccc58c7e9d5c Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 26 Mar 2021 13:40:44 +0100 Subject: [PATCH 264/569] interventions changes --- covasim/interventions.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 5754585f9..60755dedf 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -92,11 +92,8 @@ class Intervention: line_args (dict): arguments passed to pl.axvline() when plotting ''' def __init__(self, label=None, show_label=True, do_plot=None, line_args=None): -<<<<<<< HEAD -======= if label is None: label = self.__class__.__name__ # Use the class name if no label is supplied ->>>>>>> master self._store_args() # Store the input arguments so the intervention can be recreated self.label = label # e.g. "Close schools" self.show_label = show_label # Show the label by default From ee17082b1e998df93776f4771d8a358de3ea2430 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 26 Mar 2021 13:41:24 +0100 Subject: [PATCH 265/569] analyses --- covasim/analysis.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 3eda5632a..4425fb1dc 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1099,11 +1099,7 @@ def compute_mismatch(self, use_median=False): return self.mismatch -<<<<<<< HEAD - def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=None, do_show=None, fig=None): -======= def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=None, date_args=None, do_show=None, fig=None): ->>>>>>> master ''' Plot the fit of the model to the data. For each result, plot the data and the model; the difference; and the loss (weighted difference). Also From fd5178a87817fd30c2d4c5f9d4e241a4d7c291ce Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 26 Mar 2021 13:43:41 +0100 Subject: [PATCH 266/569] resolve changes in base --- covasim/base.py | 55 +------------------------------------------------ 1 file changed, 1 insertion(+), 54 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 814c5794e..f9c79eb2c 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -15,7 +15,7 @@ from . import parameters as cvpar # Specify all externally visible classes this file defines -__all__ = ['Result', 'Par', 'BaseSim', 'BasePeople', 'Person', 'FlexDict', 'Contacts', 'Layer'] +__all__ = ['ParsObj', 'Result', 'BaseSim', 'BasePeople', 'Person', 'FlexDict', 'Contacts', 'Layer'] #%% Define simulation classes @@ -146,59 +146,6 @@ def npts(self): return len(self.values) -class Par(object): - ''' - NB NOT USED ANYWHERE YET- PLACEHOLDER CLASS IN CASE USEFUL - Stores a single parameter -- by default, acts like an array. - Args: - name (str): name of this parameter, e.g. beta - val: value(s) of the parameter - can take various forms - by_age: whether the parameter is differentiated by age - is_dist: whether the parameter is a distribution - by_strain (bool): whether or not the parameter varies by strain - ''' - - def __init__(self, name=None, val=None, by_age=False, is_dist=False, by_strain=False): - self.name = name # Name of this parameter - self.val = sc.promotetolist(val) # Value of this parameter - self.by_strain = by_strain # Whether or not the parameter varies by strain - self.by_age = by_age # Whether or not the parameter varies by age - self.is_dist = is_dist # Whether or not the parameter is stored as a distribution - return - - def __repr__(self, *args, **kwargs): - ''' Use pretty repr, like sc.prettyobj, but displaying full values ''' - output = sc.prepr(self, use_repr=False) - return output - - def __getitem__(self, *args, **kwargs): - ''' To allow e.g. par[2] instead of par.val[5] ''' - return self.val.__getitem__(*args, **kwargs) - - def __setitem__(self, *args, **kwargs): - ''' To allow e.g. par[:] = 1 instead of par.val[:] = 1 ''' - return self.val.__setitem__(*args, **kwargs) - - def __len__(self): - ''' To allow len(par) instead of len(par.val) ''' - return len(self.val) - - @property - def n_strains(self): - return len(self.val) - - def add_strain(self, new_val=None): - self.val.append(new_val) - - def get(self, strain=0, n=None): - if self.by_strain: - if not self.is_dist: output = self.val[strain] - else: output = cvu.sample(**self.val[strain], size=n) - else: - output = self.val - return output - - def set_metadata(obj): ''' Set standard metadata for an object ''' obj.created = sc.now() From 9a10b2e4e1ef4b40a5a15a4ba5aebf0149cb29d8 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 26 Mar 2021 13:49:59 +0100 Subject: [PATCH 267/569] tests pass --- covasim/base.py | 31 +++++++++++++++++++++++++++++-- tests/devtests/test_variants.py | 2 +- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index f9c79eb2c..e2232cf01 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -58,12 +58,39 @@ def brief(self, output=False): print(string) else: return string - + + +class ParsObj(FlexPretty): + ''' + A class based around performing operations on a self.pars dict. + ''' + + def __init__(self, pars): + self.update_pars(pars, create=True) + return + + def __getitem__(self, key): + ''' Allow sim['par_name'] instead of sim.pars['par_name'] ''' + try: + return self.pars[key] + except: + all_keys = '\n'.join(list(self.pars.keys())) + errormsg = f'Key "{key}" not found; available keys:\n{all_keys}' + raise sc.KeyNotFoundError(errormsg) + + def __setitem__(self, key, value): + ''' Ditto ''' + if key in self.pars: + self.pars[key] = value + else: + all_keys = '\n'.join(list(self.pars.keys())) + errormsg = f'Key "{key}" not found; available keys:\n{all_keys}' + raise sc.KeyNotFoundError(errormsg) + return def update_pars(self, pars=None, create=False): ''' Update internal dict with new pars. - Args: pars (dict): the parameters to update (if None, do nothing) create (bool): if create is False, then raise a KeyNotFoundError if the key does not already exist diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 10da99e4a..7c3c8d383 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -448,7 +448,7 @@ def get_ind_of_min_value(list, time): # # Run Vaccine tests # sim4 = test_synthpops() # sim5 = test_vaccine_1strain() - # + # # Run multisim and scenario tests # scens0 = test_vaccine_1strain_scen() # scens1 = test_vaccine_2strains_scen() From 2ce1091929dfd7b9f80cc0a577f38de996ea6a26 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 26 Mar 2021 08:39:48 -0700 Subject: [PATCH 268/569] date formatter interval --- covasim/plotting.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index ce90f78c5..5b62f4547 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -228,7 +228,7 @@ def title_grid_legend(ax, title, grid, commaticks, setylim, legend_args, show_le return -def date_formatter(start_day=None, dateformat=None, ax=None, sim=None): +def date_formatter(start_day=None, dateformat=None, interval=None, ax=None, sim=None): ''' Create an automatic date formatter based on a number of days and a start day. @@ -238,7 +238,8 @@ def date_formatter(start_day=None, dateformat=None, ax=None, sim=None): Args: start_day (str/date): the start day, either as a string or date object - dateformat (str): the date format + dateformat (str): the date format (default '%b-%d') + interval (int): if supplied, the interval between ticks (must supply an axis also to take effect) ax (axes): if supplied, automatically set the x-axis formatter for this axis sim (Sim): if supplied, get the start day from this @@ -268,6 +269,9 @@ def mpl_formatter(x, pos): if ax is not None: ax.xaxis.set_major_formatter(mpl_formatter) + if interval: # Set the x-axis intervals + xmin,xmax = ax.get_xlim() + ax.set_xticks(np.arange(xmin, xmax+1, interval)) return mpl_formatter @@ -290,7 +294,7 @@ def reset_ticks(ax, sim=None, date_args=None, start_day=None): # Set the x-axis intervals if date_args.interval: - ax.set_xticks(pl.arange(xmin, xmax+1, date_args.interval)) + ax.set_xticks(np.arange(xmin, xmax+1, date_args.interval)) # Set xticks as dates if date_args.as_dates: From ceebd6dc61329a1fc7d699f835e291fdce50b626 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 28 Mar 2021 12:09:25 -0700 Subject: [PATCH 269/569] update match map --- covasim/misc.py | 3 ++- docs/conf.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index 9baaca45e..74fd07e1a 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -503,7 +503,8 @@ def get_version_pars(version, verbose=True): '1.5.0': [f'1.5.{i}' for i in range(4)], '1.6.0': [f'1.6.{i}' for i in range(2)], '1.7.0': [f'1.7.{i}' for i in range(7)], - '2.0.0': [f'2.0.{i}' for i in range(3)], + '2.0.0': [f'2.0.{i}' for i in range(4)], + '2.1.0': [f'2.0.{i}' for i in range(1)], } # Find and check the match diff --git a/docs/conf.py b/docs/conf.py index d1feeaaf7..724a2af3f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ # Rename "covasim package" to "API reference" filename = 'modules.rst' # This must match the Makefile -with open(filename) as f: # Read exitsting file +with open(filename) as f: # Read existing file lines = f.readlines() lines[0] = "API reference\n" # Blast away the existing heading and replace with this lines[1] = "=============\n" # Ensure the heading is the right length From e79a1798ce132acd0ed8acc91d537a8f462eadc1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 28 Mar 2021 16:18:47 -0700 Subject: [PATCH 270/569] precalculate infections --- covasim/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/covasim/utils.py b/covasim/utils.py index 20afb76d0..96deccbbe 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -90,7 +90,9 @@ def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, @nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): # pragma: no cover ''' The heaviest step of the model -- figure out who gets infected on this timestep. Cannot be parallelized since random numbers are used ''' - betas = beta * layer_betas * rel_trans[sources] * rel_sus[targets] # Calculate the raw transmission probabilities + source_trans = rel_trans[sources] + inf_inds = source_trans.nonzero()[0] # Find nonzero entries + betas = beta * layer_betas[inf_inds] * source_trans[inf_inds] * rel_sus[targets[inf_inds]] # Calculate the raw transmission probabilities nonzero_inds = betas.nonzero()[0] # Find nonzero entries nonzero_betas = betas[nonzero_inds] # Remove zero entries from beta nonzero_sources = sources[nonzero_inds] # Remove zero entries from the sources From f5a1841073a6b98cfcf86206ce4cc726d22738db Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 28 Mar 2021 17:12:04 -0700 Subject: [PATCH 271/569] update version --- covasim/misc.py | 2 +- covasim/version.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index 74fd07e1a..288128925 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -504,7 +504,7 @@ def get_version_pars(version, verbose=True): '1.6.0': [f'1.6.{i}' for i in range(2)], '1.7.0': [f'1.7.{i}' for i in range(7)], '2.0.0': [f'2.0.{i}' for i in range(4)], - '2.1.0': [f'2.0.{i}' for i in range(1)], + '2.1.0': [f'2.0.{i}' for i in range(2)], } # Find and check the match diff --git a/covasim/version.py b/covasim/version.py index d18ab2c6b..0bfc1ab6f 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,6 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '2.1.0' -__versiondate__ = '2021-03-23' +__version__ = '2.1.1' +__versiondate__ = '2021-03-28' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From 730ae9d46e7dcfa46cae92ead3cc7a337d6d2707 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 28 Mar 2021 19:34:32 -0700 Subject: [PATCH 272/569] parameter updates --- covasim/parameters.py | 14 +++++++------- covasim/utils.py | 25 +++++++++++++++---------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 605bb460c..815f5fac8 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -64,17 +64,17 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Duration parameters: time for disease progression pars['dur'] = {} - pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.6, par2=4.8) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, subtracting inf2sym duration - pars['dur']['inf2sym'] = dict(dist='lognormal_int', par1=1.0, par2=0.9) # Duration from infectious to symptomatic; see Linton et al., https://doi.org/10.3390/jcm9020538 - pars['dur']['sym2sev'] = dict(dist='lognormal_int', par1=6.6, par2=4.9) # Duration from symptomatic to severe symptoms; see Linton et al., https://doi.org/10.3390/jcm9020538 - pars['dur']['sev2crit'] = dict(dist='lognormal_int', par1=3.0, par2=7.4) # Duration from severe symptoms to requiring ICU; see Wang et al., https://jamanetwork.com/journals/jama/fullarticle/2761044 + pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.5, par2=1.5) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, appendix table S2, subtracting inf2sym duration + pars['dur']['inf2sym'] = dict(dist='lognormal_int', par1=1.1, par2=0.9) # Duration from infectious to symptomatic; see Linton et al., https://doi.org/10.3390/jcm9020538, from Table 2, 5.6 day incubation period - 4.5 day exp2inf from Lauer et al. + pars['dur']['sym2sev'] = dict(dist='lognormal_int', par1=6.6, par2=4.9) # Duration from symptomatic to severe symptoms; see Linton et al., https://doi.org/10.3390/jcm9020538, from Table 2, 6.6 day onset to hospital admission (deceased); cf. Wang et al., 7 days (Table 1) + pars['dur']['sev2crit'] = dict(dist='lognormal_int', par1=1.5, par2=2.0) # Duration from severe symptoms to requiring ICU; average of 1.9 and 1.0; see Chen et al., https://www.sciencedirect.com/science/article/pii/S0163445320301195, 8.5 days total - 6.6 days sym2sev = 1.9 days; see also Wang et al., https://jamanetwork.com/journals/jama/fullarticle/2761044, Table 3, 1 day, IQR 0-3 days # Duration parameters: time for disease recovery pars['dur']['asym2rec'] = dict(dist='lognormal_int', par1=8.0, par2=2.0) # Duration for asymptomatic people to recover; see Wölfel et al., https://www.nature.com/articles/s41586-020-2196-x pars['dur']['mild2rec'] = dict(dist='lognormal_int', par1=8.0, par2=2.0) # Duration for people with mild symptoms to recover; see Wölfel et al., https://www.nature.com/articles/s41586-020-2196-x - pars['dur']['sev2rec'] = dict(dist='lognormal_int', par1=14.0, par2=2.4) # Duration for people with severe symptoms to recover, 22.6 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf - pars['dur']['crit2rec'] = dict(dist='lognormal_int', par1=14.0, par2=2.4) # Duration for people with critical symptoms to recover, 22.6 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf - pars['dur']['crit2die'] = dict(dist='lognormal_int', par1=6.2, par2=1.7) # Duration from critical symptoms to death, 17.8 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf + pars['dur']['sev2rec'] = dict(dist='lognormal_int', par1=18.1, par2=6.3) # Duration for people with severe symptoms to recover, 24.7 days total; see Verity et al., https://www.thelancet.com/journals/laninf/article/PIIS1473-3099(20)30243-7/fulltext; 18.1 days = 24.7 onset-to-recovery - 6.6 sym2sev; 6.3 = 0.35 coefficient of variation * 18.1; see also https://doi.org/10.1017/S0950268820001259 (22 days) and https://doi.org/10.3390/ijerph17207560 (3-10 days) + pars['dur']['crit2rec'] = dict(dist='lognormal_int', par1=18.1, par2=6.3) # Duration for people with critical symptoms to recover; as above (Verity et al.) + pars['dur']['crit2die'] = dict(dist='lognormal_int', par1=10.7, par2=4.8) # Duration from critical symptoms to death, 18.8 days total; see Verity et al., https://www.thelancet.com/journals/laninf/article/PIIS1473-3099(20)30243-7/fulltext; 10.7 = 18.8 onset-to-death - 6.6 sym2sev - 1.5 sev2crit; 4.8 = 0.45 coefficient of variation * 10.7 # Severity parameters: probabilities of symptom progression pars['rel_symp_prob'] = 1.0 # Scale factor for proportion of symptomatic cases diff --git a/covasim/utils.py b/covasim/utils.py index 96deccbbe..6fe300e48 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -170,33 +170,38 @@ def sample(dist=None, par1=None, par2=None, size=None, **kwargs): the mean. ''' + # Some of these have aliases, but these are the "official" names choices = [ 'uniform', 'normal', - 'lognormal', 'normal_pos', 'normal_int', + 'lognormal', 'lognormal_int', 'poisson', 'neg_binomial', - ] + ] + + # Ensure it's an integer + if size is not None: + size = int(size) # Compute distribution parameters and draw samples # NB, if adding a new distribution, also add to choices above - if dist == 'uniform': samples = np.random.uniform(low=par1, high=par2, size=size, **kwargs) - elif dist == 'normal': samples = np.random.normal(loc=par1, scale=par2, size=size, **kwargs) - elif dist == 'normal_pos': samples = np.abs(np.random.normal(loc=par1, scale=par2, size=size, **kwargs)) - elif dist == 'normal_int': samples = np.round(np.abs(np.random.normal(loc=par1, scale=par2, size=size, **kwargs))) - elif dist == 'poisson': samples = n_poisson(rate=par1, n=size, **kwargs) # Use Numba version below for speed - elif dist == 'neg_binomial': samples = n_neg_binomial(rate=par1, dispersion=par2, n=size, **kwargs) # Use custom version below - elif dist in ['lognormal', 'lognormal_int']: + if dist in ['unif', 'uniform']: samples = np.random.uniform(low=par1, high=par2, size=size, **kwargs) + elif dist in ['norm', 'normal']: samples = np.random.normal(loc=par1, scale=par2, size=size, **kwargs) + elif dist == 'normal_pos': samples = np.abs(np.random.normal(loc=par1, scale=par2, size=size, **kwargs)) + elif dist == 'normal_int': samples = np.round(np.abs(np.random.normal(loc=par1, scale=par2, size=size, **kwargs))) + elif dist == 'poisson': samples = n_poisson(rate=par1, n=size, **kwargs) # Use Numba version below for speed + elif dist == 'neg_binomial': samples = n_neg_binomial(rate=par1, dispersion=par2, n=size, **kwargs) # Use custom version below + elif dist in ['lognorm', 'lognormal', 'lognorm_int', 'lognormal_int']: if par1>0: mean = np.log(par1**2 / np.sqrt(par2**2 + par1**2)) # Computes the mean of the underlying normal distribution sigma = np.sqrt(np.log(par2**2/par1**2 + 1)) # Computes sigma for the underlying normal distribution samples = np.random.lognormal(mean=mean, sigma=sigma, size=size, **kwargs) else: samples = np.zeros(size) - if dist == 'lognormal_int': + if '_int' in dist: samples = np.round(samples) else: choicestr = '\n'.join(choices) From 2d27de3fa3efca2cc5a7027159b46ba2589121fe Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 28 Mar 2021 20:25:13 -0700 Subject: [PATCH 273/569] fix transtree test --- covasim/analysis.py | 2 +- covasim/utils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 4425fb1dc..2903c7e7a 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1585,7 +1585,7 @@ def animate(self, *args, **kwargs): # Construct each frame of the animation detailed = self.detailed.to_dict('records') # Convert to the old style for ddict in detailed: # Loop over every person - if ddict is None: + if np.isnan(ddict['source']): continue # Skip the 'None' node corresponding to seeded infections frame = {} diff --git a/covasim/utils.py b/covasim/utils.py index 6fe300e48..5d17e4184 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -90,8 +90,8 @@ def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, @nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): # pragma: no cover ''' The heaviest step of the model -- figure out who gets infected on this timestep. Cannot be parallelized since random numbers are used ''' - source_trans = rel_trans[sources] - inf_inds = source_trans.nonzero()[0] # Find nonzero entries + source_trans = rel_trans[sources] # Pull out the transmissibility of the sources (0 for non-infectious people) + inf_inds = source_trans.nonzero()[0] # Remove noninfectious people betas = beta * layer_betas[inf_inds] * source_trans[inf_inds] * rel_sus[targets[inf_inds]] # Calculate the raw transmission probabilities nonzero_inds = betas.nonzero()[0] # Find nonzero entries nonzero_betas = betas[nonzero_inds] # Remove zero entries from beta From f879a4ad38fc24fe1d2d5de0e456cc899821d761 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 28 Mar 2021 21:04:35 -0700 Subject: [PATCH 274/569] concatenation is slower --- covasim/sim.py | 10 +++++++--- covasim/utils.py | 40 +++++++++++++++++++++++++++------------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index ab74fb931..9165d25db 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -532,9 +532,13 @@ def step(self): rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor) # Calculate actual transmission - for sources,targets in [[p1,p2], [p2,p1]]: # Loop over the contact network from p1->p2 and p2->p1 - source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! - people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey) # Actually infect people + # for sources,targets in [[p1,p2], [p2,p1]]: # Loop over the contact network from p1->p2 and p2->p1 + # source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! + # people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey) # Actually infect people + + source_inds, target_inds = cvu.compute_infections(beta, p1, p2, betas, rel_trans, rel_sus) # Calculate transmission! + people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey) # Actually infect people + # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): diff --git a/covasim/utils.py b/covasim/utils.py index 5d17e4184..449711060 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -87,19 +87,33 @@ def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, return rel_trans, rel_sus -@nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) -def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): # pragma: no cover - ''' The heaviest step of the model -- figure out who gets infected on this timestep. Cannot be parallelized since random numbers are used ''' - source_trans = rel_trans[sources] # Pull out the transmissibility of the sources (0 for non-infectious people) - inf_inds = source_trans.nonzero()[0] # Remove noninfectious people - betas = beta * layer_betas[inf_inds] * source_trans[inf_inds] * rel_sus[targets[inf_inds]] # Calculate the raw transmission probabilities - nonzero_inds = betas.nonzero()[0] # Find nonzero entries - nonzero_betas = betas[nonzero_inds] # Remove zero entries from beta - nonzero_sources = sources[nonzero_inds] # Remove zero entries from the sources - nonzero_targets = targets[nonzero_inds] # Remove zero entries from the targets - transmissions = (np.random.random(len(nonzero_betas)) < nonzero_betas).nonzero()[0] # Compute the actual infections! - source_inds = nonzero_sources[transmissions] - target_inds = nonzero_targets[transmissions] # Filter the targets on the actual infections +@nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) +def compute_infections(beta, rsources, rtargets, rlayer_betas, rel_trans, rel_sus): # pragma: no cover + ''' + Compute who infects whom + + The heaviest step of the model, taking about 50% of the total time -- figure + out who gets infected on this timestep. Cannot be easily parallelized since + random numbers are used. + ''' + # sources = rsources + # targets = rtargets + sources = np.concatenate((rsources, rtargets), axis=0) + targets = np.concatenate((rtargets, rsources), axis=0) + layer_betas = np.concatenate((rlayer_betas, rlayer_betas), axis=0) + + source_trans = rel_trans[sources] # Pull out the transmissibility of the sources (0 for non-infectious people) + inf_inds = source_trans.nonzero()[0] # Infectious indices -- remove noninfectious people + betas = beta * layer_betas[inf_inds] * source_trans[inf_inds] * rel_sus[targets[inf_inds]] # Calculate the raw transmission probabilities + nonzero_inds = betas.nonzero()[0] # Find nonzero entries + nonzero_inf_inds = inf_inds[nonzero_inds] # Map onto original indices + nonzero_betas = betas[nonzero_inds] # Remove zero entries from beta + nonzero_sources = sources[nonzero_inf_inds] # Remove zero entries from the sources + nonzero_targets = targets[nonzero_inf_inds] # Remove zero entries from the targets + transmissions = (np.random.random(len(nonzero_betas)) < nonzero_betas).nonzero()[0] # Compute the actual infections! + source_inds = nonzero_sources[transmissions] + target_inds = nonzero_targets[transmissions] # Filter the targets on the actual infections + # print(len(inf_inds), len(nonzero_inds), len(source_inds), len(target_inds)) return source_inds, target_inds From 62d9a713e6e4dba974ee4d6473ef426dc7222015 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 28 Mar 2021 21:58:32 -0700 Subject: [PATCH 275/569] faster, but only 5% --- covasim/sim.py | 10 +++++----- covasim/utils.py | 42 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 9165d25db..caa4258fa 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -532,12 +532,12 @@ def step(self): rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor) # Calculate actual transmission - # for sources,targets in [[p1,p2], [p2,p1]]: # Loop over the contact network from p1->p2 and p2->p1 - # source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! - # people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey) # Actually infect people + for sources,targets in [[p1,p2], [p2,p1]]: # Loop over the contact network from p1->p2 and p2->p1 + source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! + people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey) # Actually infect people - source_inds, target_inds = cvu.compute_infections(beta, p1, p2, betas, rel_trans, rel_sus) # Calculate transmission! - people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey) # Actually infect people + # source_inds, target_inds = cvu.compute_infections(beta, p1, p2, betas, rel_trans, rel_sus) # Calculate transmission! + # people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey) # Actually infect people # Update counts for this time step: stocks diff --git a/covasim/utils.py b/covasim/utils.py index 449711060..69944f4b3 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -88,7 +88,7 @@ def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, @nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) -def compute_infections(beta, rsources, rtargets, rlayer_betas, rel_trans, rel_sus): # pragma: no cover +def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): # pragma: no cover ''' Compute who infects whom @@ -96,12 +96,6 @@ def compute_infections(beta, rsources, rtargets, rlayer_betas, rel_trans, out who gets infected on this timestep. Cannot be easily parallelized since random numbers are used. ''' - # sources = rsources - # targets = rtargets - sources = np.concatenate((rsources, rtargets), axis=0) - targets = np.concatenate((rtargets, rsources), axis=0) - layer_betas = np.concatenate((rlayer_betas, rlayer_betas), axis=0) - source_trans = rel_trans[sources] # Pull out the transmissibility of the sources (0 for non-infectious people) inf_inds = source_trans.nonzero()[0] # Infectious indices -- remove noninfectious people betas = beta * layer_betas[inf_inds] * source_trans[inf_inds] * rel_sus[targets[inf_inds]] # Calculate the raw transmission probabilities @@ -113,10 +107,42 @@ def compute_infections(beta, rsources, rtargets, rlayer_betas, rel_trans, transmissions = (np.random.random(len(nonzero_betas)) < nonzero_betas).nonzero()[0] # Compute the actual infections! source_inds = nonzero_sources[transmissions] target_inds = nonzero_targets[transmissions] # Filter the targets on the actual infections - # print(len(inf_inds), len(nonzero_inds), len(source_inds), len(target_inds)) return source_inds, target_inds +# @nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) +# def compute_infections(beta, rsources, rtargets, layer_betas, rel_trans, rel_sus): # pragma: no cover +# ''' +# Compute who infects whom + +# The heaviest step of the model, taking about 50% of the total time -- figure +# out who gets infected on this timestep. Cannot be easily parallelized since +# random numbers are used. +# ''' +# slist = np.empty(0, dtype=nbint) +# tlist = np.empty(0, dtype=nbint) +# for sources,targets in [[rsources,rtargets], [rtargets,rsources]]: + +# source_trans = rel_trans[sources] # Pull out the transmissibility of the sources (0 for non-infectious people) +# inf_inds = source_trans.nonzero()[0] # Infectious indices -- remove noninfectious people +# betas = beta * layer_betas[inf_inds] * source_trans[inf_inds] * rel_sus[targets[inf_inds]] # Calculate the raw transmission probabilities +# nonzero_inds = betas.nonzero()[0] # Find nonzero entries +# nonzero_inf_inds = inf_inds[nonzero_inds] # Map onto original indices +# nonzero_betas = betas[nonzero_inds] # Remove zero entries from beta +# nonzero_sources = sources[nonzero_inf_inds] # Remove zero entries from the sources +# nonzero_targets = targets[nonzero_inf_inds] # Remove zero entries from the targets +# transmissions = (np.random.random(len(nonzero_betas)) < nonzero_betas).nonzero()[0] # Compute the actual infections! +# source_inds = nonzero_sources[transmissions] +# target_inds = nonzero_targets[transmissions] # Filter the targets on the actual infections +# slist = np.concatenate((slist, source_inds), axis=0) +# tlist = np.concatenate((tlist, target_inds), axis=0) +# # tlist.append(target_inds) +# # source_inds = np.concatenate(tuple(slist), axis=0) +# # target_inds = np.concatenate(tuple(tlist), axis=0) +# # print(len(inf_inds), len(nonzero_inds), len(source_inds), len(target_inds)) +# return slist,tlist #source_inds, target_inds + + @nb.njit((nbint[:], nbint[:], nb.int64[:]), cache=cache) def find_contacts(p1, p2, inds): # pragma: no cover """ From 9d6b0e515739b89379cf549ecafcdee64ff05a08 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 28 Mar 2021 22:32:24 -0700 Subject: [PATCH 276/569] update baseline --- covasim/sim.py | 4 --- covasim/utils.py | 33 -------------------- tests/baseline.json | 72 ++++++++++++++++++++++---------------------- tests/benchmark.json | 6 ++-- 4 files changed, 39 insertions(+), 76 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index caa4258fa..ab74fb931 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -536,10 +536,6 @@ def step(self): source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey) # Actually infect people - # source_inds, target_inds = cvu.compute_infections(beta, p1, p2, betas, rel_trans, rel_sus) # Calculate transmission! - # people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey) # Actually infect people - - # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): self.results[f'n_{key}'][t] = people.count(key) diff --git a/covasim/utils.py b/covasim/utils.py index 69944f4b3..1095bd16e 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -110,39 +110,6 @@ def compute_infections(beta, sources, targets, layer_betas, rel_trans, r return source_inds, target_inds -# @nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) -# def compute_infections(beta, rsources, rtargets, layer_betas, rel_trans, rel_sus): # pragma: no cover -# ''' -# Compute who infects whom - -# The heaviest step of the model, taking about 50% of the total time -- figure -# out who gets infected on this timestep. Cannot be easily parallelized since -# random numbers are used. -# ''' -# slist = np.empty(0, dtype=nbint) -# tlist = np.empty(0, dtype=nbint) -# for sources,targets in [[rsources,rtargets], [rtargets,rsources]]: - -# source_trans = rel_trans[sources] # Pull out the transmissibility of the sources (0 for non-infectious people) -# inf_inds = source_trans.nonzero()[0] # Infectious indices -- remove noninfectious people -# betas = beta * layer_betas[inf_inds] * source_trans[inf_inds] * rel_sus[targets[inf_inds]] # Calculate the raw transmission probabilities -# nonzero_inds = betas.nonzero()[0] # Find nonzero entries -# nonzero_inf_inds = inf_inds[nonzero_inds] # Map onto original indices -# nonzero_betas = betas[nonzero_inds] # Remove zero entries from beta -# nonzero_sources = sources[nonzero_inf_inds] # Remove zero entries from the sources -# nonzero_targets = targets[nonzero_inf_inds] # Remove zero entries from the targets -# transmissions = (np.random.random(len(nonzero_betas)) < nonzero_betas).nonzero()[0] # Compute the actual infections! -# source_inds = nonzero_sources[transmissions] -# target_inds = nonzero_targets[transmissions] # Filter the targets on the actual infections -# slist = np.concatenate((slist, source_inds), axis=0) -# tlist = np.concatenate((tlist, target_inds), axis=0) -# # tlist.append(target_inds) -# # source_inds = np.concatenate(tuple(slist), axis=0) -# # target_inds = np.concatenate(tuple(tlist), axis=0) -# # print(len(inf_inds), len(nonzero_inds), len(source_inds), len(target_inds)) -# return slist,tlist #source_inds, target_inds - - @nb.njit((nbint[:], nbint[:], nb.int64[:]), cache=cache) def find_contacts(p1, p2, inds): # pragma: no cover """ diff --git a/tests/baseline.json b/tests/baseline.json index f81be5f62..2c7e9b581 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -1,41 +1,41 @@ { "summary": { - "cum_infections": 9990.0, - "cum_infectious": 9747.0, - "cum_tests": 10766.0, - "cum_diagnoses": 3909.0, - "cum_recoveries": 8778.0, - "cum_symptomatic": 6662.0, - "cum_severe": 527.0, - "cum_critical": 125.0, - "cum_deaths": 41.0, - "cum_quarantined": 3711.0, - "new_infections": 24.0, - "new_infectious": 54.0, - "new_tests": 198.0, - "new_diagnoses": 41.0, - "new_recoveries": 138.0, - "new_symptomatic": 39.0, - "new_severe": 8.0, - "new_critical": 1.0, - "new_deaths": 0.0, - "new_quarantined": 165.0, - "n_susceptible": 10010.0, - "n_exposed": 1171.0, - "n_infectious": 928.0, - "n_symptomatic": 683.0, - "n_severe": 158.0, - "n_critical": 35.0, - "n_diagnosed": 3909.0, - "n_quarantined": 3577.0, - "n_alive": 19959.0, - "n_preinfectious": 243.0, - "n_removed": 8819.0, - "prevalence": 0.05867027406182675, - "incidence": 0.0023976023976023976, - "r_eff": 0.2434089263397735, + "cum_infections": 9829.0, + "cum_infectious": 9688.0, + "cum_tests": 10783.0, + "cum_diagnoses": 3867.0, + "cum_recoveries": 8551.0, + "cum_symptomatic": 6581.0, + "cum_severe": 468.0, + "cum_critical": 129.0, + "cum_deaths": 30.0, + "cum_quarantined": 4092.0, + "new_infections": 14.0, + "new_infectious": 47.0, + "new_tests": 195.0, + "new_diagnoses": 45.0, + "new_recoveries": 157.0, + "new_symptomatic": 34.0, + "new_severe": 6.0, + "new_critical": 2.0, + "new_deaths": 3.0, + "new_quarantined": 153.0, + "n_susceptible": 10171.0, + "n_exposed": 1248.0, + "n_infectious": 1107.0, + "n_symptomatic": 809.0, + "n_severe": 248.0, + "n_critical": 64.0, + "n_diagnosed": 3867.0, + "n_quarantined": 3938.0, + "n_alive": 19970.0, + "n_preinfectious": 141.0, + "n_removed": 8581.0, + "prevalence": 0.06249374061091637, + "incidence": 0.0013764624913971094, + "r_eff": 0.12219744828926875, "doubling_time": 30.0, - "test_yield": 0.20707070707070707, - "rel_test_yield": 3.5813414315569485 + "test_yield": 0.23076923076923078, + "rel_test_yield": 3.356889722743382 } } \ No newline at end of file diff --git a/tests/benchmark.json b/tests/benchmark.json index 1fee3c306..1ea181e6a 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,12 +1,12 @@ { "time": { - "initialize": 0.38, - "run": 0.487 + "initialize": 0.408, + "run": 0.446 }, "parameters": { "pop_size": 20000, "pop_type": "hybrid", "n_days": 60 }, - "cpu_performance": 0.9960878831767238 + "cpu_performance": 1.01777919829902 } \ No newline at end of file From f313748e7052b9da15a05ab3670e6e87f4d7a47a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 28 Mar 2021 22:32:38 -0700 Subject: [PATCH 277/569] add regression parameters --- covasim/regression/pars_v2.1.1.json | 203 ++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 covasim/regression/pars_v2.1.1.json diff --git a/covasim/regression/pars_v2.1.1.json b/covasim/regression/pars_v2.1.1.json new file mode 100644 index 000000000..fd6dab297 --- /dev/null +++ b/covasim/regression/pars_v2.1.1.json @@ -0,0 +1,203 @@ +{ + "pop_size": 20000.0, + "pop_infected": 20, + "pop_type": "random", + "location": null, + "start_day": "2020-03-01", + "end_day": null, + "n_days": 60, + "rand_seed": 1, + "verbose": 0.1, + "pop_scale": 1, + "rescale": true, + "rescale_threshold": 0.05, + "rescale_factor": 1.2, + "beta": 0.016, + "contacts": { + "a": 20 + }, + "dynam_layer": { + "a": 0 + }, + "beta_layer": { + "a": 1.0 + }, + "n_imports": 0, + "beta_dist": { + "dist": "neg_binomial", + "par1": 1.0, + "par2": 0.45, + "step": 0.01 + }, + "viral_dist": { + "frac_time": 0.3, + "load_ratio": 2, + "high_cap": 4 + }, + "asymp_factor": 1.0, + "iso_factor": { + "a": 0.2 + }, + "quar_factor": { + "a": 0.3 + }, + "quar_period": 14, + "dur": { + "exp2inf": { + "dist": "lognormal_int", + "par1": 4.5, + "par2": 1.5 + }, + "inf2sym": { + "dist": "lognormal_int", + "par1": 1.1, + "par2": 0.9 + }, + "sym2sev": { + "dist": "lognormal_int", + "par1": 6.6, + "par2": 4.9 + }, + "sev2crit": { + "dist": "lognormal_int", + "par1": 1.5, + "par2": 2.0 + }, + "asym2rec": { + "dist": "lognormal_int", + "par1": 8.0, + "par2": 2.0 + }, + "mild2rec": { + "dist": "lognormal_int", + "par1": 8.0, + "par2": 2.0 + }, + "sev2rec": { + "dist": "lognormal_int", + "par1": 18.1, + "par2": 6.3 + }, + "crit2rec": { + "dist": "lognormal_int", + "par1": 18.1, + "par2": 6.3 + }, + "crit2die": { + "dist": "lognormal_int", + "par1": 10.7, + "par2": 4.8 + } + }, + "rel_symp_prob": 1.0, + "rel_severe_prob": 1.0, + "rel_crit_prob": 1.0, + "rel_death_prob": 1.0, + "prog_by_age": true, + "prognoses": { + "age_cutoffs": [ + 0, + 10, + 20, + 30, + 40, + 50, + 60, + 70, + 80, + 90 + ], + "sus_ORs": [ + 0.34, + 0.67, + 1.0, + 1.0, + 1.0, + 1.0, + 1.24, + 1.47, + 1.47, + 1.47 + ], + "trans_ORs": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "comorbidities": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "symp_probs": [ + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.9 + ], + "severe_probs": [ + 0.001, + 0.0029999999999999996, + 0.012, + 0.032, + 0.049, + 0.102, + 0.16599999999999998, + 0.24300000000000002, + 0.273, + 0.273 + ], + "crit_probs": [ + 0.06, + 0.04848484848484849, + 0.05, + 0.049999999999999996, + 0.06297376093294461, + 0.12196078431372549, + 0.2740210843373494, + 0.43200193657709995, + 0.708994708994709, + 0.708994708994709 + ], + "death_probs": [ + 0.6666666666666667, + 0.25, + 0.2777777777777778, + 0.30769230769230776, + 0.45370370370370366, + 0.2840300107181136, + 0.2104973893926903, + 0.2733385632634764, + 0.47600459242250287, + 0.9293915040183697 + ] + }, + "interventions": [], + "analyzers": [], + "timelimit": null, + "stopping_func": null, + "n_beds_hosp": null, + "n_beds_icu": null, + "no_hosp_factor": 2.0, + "no_icu_factor": 2.0 +} \ No newline at end of file From 4c6db32826fd594ca459a26025db52734a124241 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 28 Mar 2021 23:45:49 -0700 Subject: [PATCH 278/569] pin line_profiler --- .github/workflows/tests.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7a3c1d6a0..a2d5a493f 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -19,7 +19,9 @@ jobs: python-version: ${{ matrix.python-version }} architecture: x64 - name: Install Covasim - run: python setup.py develop + run: | + pip install line_profiler==3.1 + python setup.py develop - name: Run integration tests working-directory: ./tests run: | From 43c942e3c4785ce2efa3f8e4f85d9592d348ec49 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 00:38:42 -0700 Subject: [PATCH 279/569] graph methods --- CHANGELOG.rst | 14 ++++++++++++-- covasim/base.py | 41 +++++++++++++++++++++++++++++++++++++---- covasim/parameters.py | 2 +- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3c29311bd..e58725d31 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,12 +25,22 @@ These are the major improvements we are currently working on. If there is a spec Latest versions (2.x) ~~~~~~~~~~~~~~~~~~~~~ - -Version 2.1.0 (2021-03-23) +Version 2.1.1 (2021-03-29) -------------------------- This is the last release before the Covasim 3.0 launch (vaccines and variants). +- **Duration updates**: All duration parameters have been updated from the literature. While most are similar to what they were before, there are some differences: in particular, durations of severe and critical disease (either to recovery or death) have increased; for example, duration from symptom onset to death has increased from 15.8±3.8 days to 18.8±7.2 days. +- Contact layers now have a new method, ``to_graph()``, that will return a ``networkx`` graph (requires ``networkx`` to be installed, of course). For example, ``nx.draw(cv.Sim(pop_size=100).run().people.contacts.to_graph())`` will draw all connections between 100 default people. +- A bug was fixed with ``cv.TransTree.animate`` failing in some cases. + + +In the previous version, a bug was discovered + + +Version 2.1.0 (2021-03-23) +-------------------------- + Highlights ^^^^^^^^^^ - **Updated lognormal distributions**: Lognormal distributions had been inadvertently using the variance instead of the standard deviation as the second parameter, resulting in too small variance. This has been fixed. This has a small but nonzero impact on the results (e.g. with default parameters, the time to peak infections is about 5-10% sooner now). diff --git a/covasim/base.py b/covasim/base.py index 6b5522796..d956c7ed7 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1278,6 +1278,27 @@ def pop_layer(self, *args): return + def to_graph(self): # pragma: no cover + ''' + Convert all layers to a networkx DiGraph + + **Example**:: + + import networkx as nx + sim = cv.Sim(pop_size=100, pop_type='hybrid').run() + G = sim.people.contacts.to_graph() + nx.draw(G) + ''' + import networkx as nx + H = nx.Graph() + for lkey,layer in self.items(): + G = layer.to_graph() + nx.set_edge_attributes(G, lkey, name='layer') + H = nx.compose(H, G) + return H + + + class Layer(FlexDict): ''' A small class holding a single layer of contact edges (connections) between people. @@ -1293,6 +1314,7 @@ class Layer(FlexDict): p1 (array): an array of N connections, representing people on one side of the connection p2 (array): an array of people on the other side of the connection beta (array): an array of weights for each connection + label (str): the name of the layer kwargs (dict): other keys copied directly into the layer Note that all arguments must be arrays of the same length, although not all @@ -1315,13 +1337,14 @@ class Layer(FlexDict): layer2 = cv.Layer(**layer, index=index, self_conn=self_conn) ''' - def __init__(self, **kwargs): + def __init__(self, label=None, **kwargs): self.meta = { 'p1': cvd.default_int, # Person 1 'p2': cvd.default_int, # Person 2 'beta': cvd.default_float, # Default transmissibility for this contact type } self.basekey = 'p1' # Assign a base key for calculating lengths and performing other operations + self.label = label # Initialize the keys of the layers for key,dtype in self.meta.items(): @@ -1437,11 +1460,21 @@ def from_df(self, df): return self - def to_graph(self): - ''' Convert to a networkx DiGraph''' + def to_graph(self): # pragma: no cover + ''' + Convert to a networkx DiGraph + + **Example**:: + + import networkx as nx + sim = cv.Sim(pop_size=20, pop_type='hybrid').run() + G = sim.people.contacts['h'].to_graph() + nx.draw(G) + ''' import networkx as nx G = nx.DiGraph() - G.add_weighted_edges_from(zip(self['p1'],self['p2'],self['beta']), 'beta') + G.add_weighted_edges_from(zip(self['p1'], self['p2'], self['beta']), 'beta') + nx.set_edge_attributes(G, self.label, name='layer') return G diff --git a/covasim/parameters.py b/covasim/parameters.py index 815f5fac8..b5d6b62ee 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -66,7 +66,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['dur'] = {} pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.5, par2=1.5) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, appendix table S2, subtracting inf2sym duration pars['dur']['inf2sym'] = dict(dist='lognormal_int', par1=1.1, par2=0.9) # Duration from infectious to symptomatic; see Linton et al., https://doi.org/10.3390/jcm9020538, from Table 2, 5.6 day incubation period - 4.5 day exp2inf from Lauer et al. - pars['dur']['sym2sev'] = dict(dist='lognormal_int', par1=6.6, par2=4.9) # Duration from symptomatic to severe symptoms; see Linton et al., https://doi.org/10.3390/jcm9020538, from Table 2, 6.6 day onset to hospital admission (deceased); cf. Wang et al., 7 days (Table 1) + pars['dur']['sym2sev'] = dict(dist='lognormal_int', par1=6.6, par2=4.9) # Duration from symptomatic to severe symptoms; see Linton et al., https://doi.org/10.3390/jcm9020538, from Table 2, 6.6 day onset to hospital admission (deceased); see also Wang et al., https://jamanetwork.com/journals/jama/fullarticle/2761044, 7 days (Table 1) pars['dur']['sev2crit'] = dict(dist='lognormal_int', par1=1.5, par2=2.0) # Duration from severe symptoms to requiring ICU; average of 1.9 and 1.0; see Chen et al., https://www.sciencedirect.com/science/article/pii/S0163445320301195, 8.5 days total - 6.6 days sym2sev = 1.9 days; see also Wang et al., https://jamanetwork.com/journals/jama/fullarticle/2761044, Table 3, 1 day, IQR 0-3 days # Duration parameters: time for disease recovery From 36f59974c3c255604cc137b6c00faff378f16655 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 01:24:55 -0700 Subject: [PATCH 280/569] working with date axes --- covasim/plotting.py | 45 +++++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 5b62f4547..bbb210cd8 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -10,7 +10,7 @@ import pylab as pl import sciris as sc import datetime as dt -import matplotlib.ticker as ticker +import matplotlib as mpl from . import misc as cvm from . import defaults as cvd from . import settings as cvset @@ -228,7 +228,7 @@ def title_grid_legend(ax, title, grid, commaticks, setylim, legend_args, show_le return -def date_formatter(start_day=None, dateformat=None, interval=None, ax=None, sim=None): +def date_formatter(start_day=None, dateformat=None, interval=None, start=None, end=None, is_numeric=True, ax=None, sim=None): ''' Create an automatic date formatter based on a number of days and a start day. @@ -240,15 +240,21 @@ def date_formatter(start_day=None, dateformat=None, interval=None, ax=None, sim= start_day (str/date): the start day, either as a string or date object dateformat (str): the date format (default '%b-%d') interval (int): if supplied, the interval between ticks (must supply an axis also to take effect) + start (str/int): if supplied, the lower limit of the axis + end (str/int): if supplied, the upper limit of the axis + is_numeric (bool): whether the x-axis was originally plotted with numeric units (set to False if dates were supplied) ax (axes): if supplied, automatically set the x-axis formatter for this axis sim (Sim): if supplied, get the start day from this **Examples**:: - cv.date_formatter(sim=sim, ax=ax) # Automatically configure the axis with default options + # Automatically configure the axis with default option + cv.date_formatter(sim=sim, ax=ax) s - formatter = cv.date_formatter(start_day='2020-04-04', dateformat='%Y-%m-%d') # Manually configure - ax.xaxis.set_major_formatter(formatter) + # Manually configure + ax = pl.subplot(111) + ax.plot(np.arange(60), np.random.random(60)) + formatter = cv.date_formatter(start_day='2020-04-04', interval=7, start='2020-05-01', end=50, dateformat='%Y-%m-%d', ax=ax) ''' # Set the default -- "Mar-01" @@ -256,22 +262,37 @@ def date_formatter(start_day=None, dateformat=None, interval=None, ax=None, sim= dateformat = '%b-%d' # Convert to a date object - if start_day is None and sim is not None: - start_day = sim['start_day'] - start_day = sc.date(start_day) + if start_day is not None: + start_day = sc.date(start_day) + else: + if sim is not None: + start_day = sim['start_day'] + elif ax is not None and not is_numeric: + xmin, xmax = ax.get_xlim() + start_day = mpl.dates.num2date(xmin) + else: + errormsg = 'If not supplying a start day or sim, the axis must already use dates and you must set is_numeric=False' + raise ValueError(errormsg) - @ticker.FuncFormatter + @mpl.ticker.FuncFormatter def mpl_formatter(x, pos): - if sc.isnumber(x): # If the axis doesn't have date units + if is_numeric: # If the axis doesn't have date units + print('sdfoiufd', x, pos) return (start_day + dt.timedelta(days=int(x))).strftime(dateformat) else: # If the axis does - return x.strftime(dateformat) + return mpl.dates.num2date(x).strftime(dateformat) if ax is not None: ax.xaxis.set_major_formatter(mpl_formatter) + xmin, xmax = ax.get_xlim() + print('hi, ', xmin, xmax) if interval: # Set the x-axis intervals - xmin,xmax = ax.get_xlim() ax.set_xticks(np.arange(xmin, xmax+1, interval)) + if start: + xmin = sc.day(start, start_day=start_day) + if end: + xmax = sc.day(end, start_day=start_day) + ax.set_xlim((xmin, xmax)) return mpl_formatter From 87877a74cb4aa86c3e4191df9361330ef220aa09 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 01:26:31 -0700 Subject: [PATCH 281/569] revert changes --- covasim/plotting.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index bbb210cd8..133c977cf 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -10,7 +10,7 @@ import pylab as pl import sciris as sc import datetime as dt -import matplotlib as mpl +import matplotlib.ticker as ticker from . import misc as cvm from . import defaults as cvd from . import settings as cvset @@ -228,7 +228,7 @@ def title_grid_legend(ax, title, grid, commaticks, setylim, legend_args, show_le return -def date_formatter(start_day=None, dateformat=None, interval=None, start=None, end=None, is_numeric=True, ax=None, sim=None): +def date_formatter(start_day=None, dateformat=None, interval=None, start=None, end=None, ax=None, sim=None): ''' Create an automatic date formatter based on a number of days and a start day. @@ -242,7 +242,6 @@ def date_formatter(start_day=None, dateformat=None, interval=None, start=None, e interval (int): if supplied, the interval between ticks (must supply an axis also to take effect) start (str/int): if supplied, the lower limit of the axis end (str/int): if supplied, the upper limit of the axis - is_numeric (bool): whether the x-axis was originally plotted with numeric units (set to False if dates were supplied) ax (axes): if supplied, automatically set the x-axis formatter for this axis sim (Sim): if supplied, get the start day from this @@ -262,25 +261,17 @@ def date_formatter(start_day=None, dateformat=None, interval=None, start=None, e dateformat = '%b-%d' # Convert to a date object - if start_day is not None: - start_day = sc.date(start_day) - else: - if sim is not None: - start_day = sim['start_day'] - elif ax is not None and not is_numeric: - xmin, xmax = ax.get_xlim() - start_day = mpl.dates.num2date(xmin) - else: - errormsg = 'If not supplying a start day or sim, the axis must already use dates and you must set is_numeric=False' - raise ValueError(errormsg) + if start_day is None and sim is not None: + start_day = sim['start_day'] + start_day = sc.date(start_day) - @mpl.ticker.FuncFormatter + @ticker.FuncFormatter def mpl_formatter(x, pos): - if is_numeric: # If the axis doesn't have date units - print('sdfoiufd', x, pos) + if sc.isnumber(x): # If the axis doesn't have date units + print() return (start_day + dt.timedelta(days=int(x))).strftime(dateformat) else: # If the axis does - return mpl.dates.num2date(x).strftime(dateformat) + return x.strftime(dateformat) if ax is not None: ax.xaxis.set_major_formatter(mpl_formatter) From d9ce7246f5e044d8486d691423e9749566c8eaec Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 01:32:56 -0700 Subject: [PATCH 282/569] date options --- covasim/plotting.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 133c977cf..88d03a204 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -263,28 +263,33 @@ def date_formatter(start_day=None, dateformat=None, interval=None, start=None, e # Convert to a date object if start_day is None and sim is not None: start_day = sim['start_day'] + if start_day is None: + errormsg = 'If not supplying a start day, you must supply a sim object' + raise ValueError(errormsg) start_day = sc.date(start_day) @ticker.FuncFormatter def mpl_formatter(x, pos): - if sc.isnumber(x): # If the axis doesn't have date units - print() - return (start_day + dt.timedelta(days=int(x))).strftime(dateformat) - else: # If the axis does - return x.strftime(dateformat) + return (start_day + dt.timedelta(days=int(x))).strftime(dateformat) + # Set initial tick marks (intervals and limits) if ax is not None: - ax.xaxis.set_major_formatter(mpl_formatter) + + # Handle limits xmin, xmax = ax.get_xlim() - print('hi, ', xmin, xmax) - if interval: # Set the x-axis intervals - ax.set_xticks(np.arange(xmin, xmax+1, interval)) if start: xmin = sc.day(start, start_day=start_day) if end: xmax = sc.day(end, start_day=start_day) ax.set_xlim((xmin, xmax)) + # Set the x-axis intervals + if interval: + ax.set_xticks(np.arange(xmin, xmax+1, interval)) + + # Set the formatter + ax.xaxis.set_major_formatter(mpl_formatter) + return mpl_formatter From 5f02afb2e2606eaedbf4cfb3d942f6e4d2e2d498 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 01:49:21 -0700 Subject: [PATCH 283/569] update changelog --- CHANGELOG.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e58725d31..a8c3fc68b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,11 +31,19 @@ Version 2.1.1 (2021-03-29) This is the last release before the Covasim 3.0 launch (vaccines and variants). - **Duration updates**: All duration parameters have been updated from the literature. While most are similar to what they were before, there are some differences: in particular, durations of severe and critical disease (either to recovery or death) have increased; for example, duration from symptom onset to death has increased from 15.8±3.8 days to 18.8±7.2 days. -- Contact layers now have a new method, ``to_graph()``, that will return a ``networkx`` graph (requires ``networkx`` to be installed, of course). For example, ``nx.draw(cv.Sim(pop_size=100).run().people.contacts.to_graph())`` will draw all connections between 100 default people. +- **Performance updates**: The innermost loop, ``cv.compute_infections()``, has been refactored to make more efficient use of array indexing. The speed increase will depend on the nature of the simulation (e.g., network type, interventions), but may be up to 1.5x faster. +- Contacts layers now have a new method, ``to_graph()``, that will return a ``networkx`` graph (requires ``networkx`` to be installed, of course). For example, ``nx.draw(cv.Sim(pop_size=100).run().people.contacts.to_graph())`` will draw all connections between 100 default people. - A bug was fixed with ``cv.TransTree.animate`` failing in some cases. +- *Regression information*: Parameters can be restored by using the ``version`` argument when creating a sim. Manually, the parameters have been changed as follows:: + exp2inf: par1 = 4.6 → 4.5, par2 = 4.8 → 1.5 + inf2sym: par1 = 1.0 → 1.1, par2 = 0.9 → 0.9 + sev2crit: par1 = 3.0 → 1.5, par2 = 7.4 → 2.0 + sev2rec: par1 = 14.0 → 18.1, par2 = 2.4 → 6.3 + crit2rec: par1 = 14.0 → 18.1, par2 = 2.4 → 6.3 + crit2die: par1 = 6.2 → 10.7, par2 = 1.7 → 4.8 -In the previous version, a bug was discovered +- *GitHub info*: PR `887 `__ Version 2.1.0 (2021-03-23) From 14abb5d17348c9eb285079e8203500d2a0c7f1f3 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 01:51:18 -0700 Subject: [PATCH 284/569] update changelog --- CHANGELOG.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a8c3fc68b..24283f8bc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,14 +34,14 @@ This is the last release before the Covasim 3.0 launch (vaccines and variants). - **Performance updates**: The innermost loop, ``cv.compute_infections()``, has been refactored to make more efficient use of array indexing. The speed increase will depend on the nature of the simulation (e.g., network type, interventions), but may be up to 1.5x faster. - Contacts layers now have a new method, ``to_graph()``, that will return a ``networkx`` graph (requires ``networkx`` to be installed, of course). For example, ``nx.draw(cv.Sim(pop_size=100).run().people.contacts.to_graph())`` will draw all connections between 100 default people. - A bug was fixed with ``cv.TransTree.animate`` failing in some cases. -- *Regression information*: Parameters can be restored by using the ``version`` argument when creating a sim. Manually, the parameters have been changed as follows:: - - exp2inf: par1 = 4.6 → 4.5, par2 = 4.8 → 1.5 - inf2sym: par1 = 1.0 → 1.1, par2 = 0.9 → 0.9 - sev2crit: par1 = 3.0 → 1.5, par2 = 7.4 → 2.0 - sev2rec: par1 = 14.0 → 18.1, par2 = 2.4 → 6.3 - crit2rec: par1 = 14.0 → 18.1, par2 = 2.4 → 6.3 - crit2die: par1 = 6.2 → 10.7, par2 = 1.7 → 4.8 +- *Regression information*: Parameters can be restored by using the ``version`` argument when creating a sim. Specifically, the parameters for the following distributions (all lognormal) have been changed as follows:: + + exp2inf: μ = 4.6 → 4.5, σ = 4.8 → 1.5 + inf2sym: μ = 1.0 → 1.1, σ = 0.9 → 0.9 + sev2crit: μ = 3.0 → 1.5, σ = 7.4 → 2.0 + sev2rec: μ = 14.0 → 18.1, σ = 2.4 → 6.3 + crit2rec: μ = 14.0 → 18.1, σ = 2.4 → 6.3 + crit2die: μ = 6.2 → 10.7, σ = 1.7 → 4.8 - *GitHub info*: PR `887 `__ From a07c02103a5180f82a16757b321d036acb0a4028 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 01:59:00 -0700 Subject: [PATCH 285/569] update changelog --- CHANGELOG.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 24283f8bc..411c0faf5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,7 +15,7 @@ Coming soon These are the major improvements we are currently working on. If there is a specific bugfix or feature you would like to see, please `create an issue `__. -- Mechanistic handling of different strains, and improved handling of vaccination, including more detailed targeting options, waning immunity, etc.. This will be Covasim 3.0, which is slated for release early April. +- Mechanistic handling of different strains, and improved handling of vaccination, including more detailed targeting options, waning immunity, etc. This will be Covasim 3.0, which is slated for release early April. - Expanded tutorials (health care workers, vaccination, calibration, exercises, etc.) - Multi-region and geospatial support - Economics and costing analysis @@ -31,9 +31,10 @@ Version 2.1.1 (2021-03-29) This is the last release before the Covasim 3.0 launch (vaccines and variants). - **Duration updates**: All duration parameters have been updated from the literature. While most are similar to what they were before, there are some differences: in particular, durations of severe and critical disease (either to recovery or death) have increased; for example, duration from symptom onset to death has increased from 15.8±3.8 days to 18.8±7.2 days. -- **Performance updates**: The innermost loop, ``cv.compute_infections()``, has been refactored to make more efficient use of array indexing. The speed increase will depend on the nature of the simulation (e.g., network type, interventions), but may be up to 1.5x faster. +- **Performance updates**: The innermost loop of Covasim, ``cv.compute_infections()``, has been refactored to make more efficient use of array indexing. The observed difference will depend on the nature of the simulation (e.g., network type, interventions), but runs may be up to 1.5x faster now. - Contacts layers now have a new method, ``to_graph()``, that will return a ``networkx`` graph (requires ``networkx`` to be installed, of course). For example, ``nx.draw(cv.Sim(pop_size=100).run().people.contacts.to_graph())`` will draw all connections between 100 default people. -- A bug was fixed with ``cv.TransTree.animate`` failing in some cases. +- A bug was fixed with ``cv.TransTree.animate()`` failing in some cases. +- ``cv.date_formatter()`` now takes ``interval``, ``start``, and ``end`` arguments. - *Regression information*: Parameters can be restored by using the ``version`` argument when creating a sim. Specifically, the parameters for the following distributions (all lognormal) have been changed as follows:: exp2inf: μ = 4.6 → 4.5, σ = 4.8 → 1.5 From 636cd9efdbae1e35a048a94cf0d8f95b261b2946 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 29 Mar 2021 12:06:22 +0200 Subject: [PATCH 286/569] fix conflicts --- CHANGELOG.rst | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b6f7af139..c7df687ca 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -67,14 +67,12 @@ Changes to results -~~~~~~~~~~~~~~~~~~~~~~~~~~ -Version 2.1.1 (2021-03-29) -~~~~~~~~~~~~~~~~~~~~~~~~~~ - +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Versions 2.1.x (2.1.0 – 2.1.1) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -~~~~~~~~~~~~~~~~~~~~~~~~~~ -Version 2.1.0 (2021-03-23) -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Version 2.1.1 (2021-03-29) +-------------------------- - **Duration updates**: All duration parameters have been updated from the literature. While most are similar to what they were before, there are some differences: in particular, durations of severe and critical disease (either to recovery or death) have increased; for example, duration from symptom onset to death has increased from 15.8±3.8 days to 18.8±7.2 days. - **Performance updates**: The innermost loop of Covasim, ``cv.compute_infections()``, has been refactored to make more efficient use of array indexing. The observed difference will depend on the nature of the simulation (e.g., network type, interventions), but runs may be up to 1.5x faster now. From 5380555592657c818ea9baebc1eb886b50909f16 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 29 Mar 2021 13:11:28 +0200 Subject: [PATCH 287/569] updates and benchmarking --- covasim/base.py | 14 ++--- covasim/run.py | 7 ++- covasim/sim.py | 18 ++++-- examples/t10_variants.py | 24 -------- tests/devtests/test_immunity.py | 100 -------------------------------- tests/devtests/test_variants.py | 4 +- tests/test_baselines.py | 2 +- 7 files changed, 25 insertions(+), 144 deletions(-) delete mode 100644 examples/t10_variants.py delete mode 100644 tests/devtests/test_immunity.py diff --git a/covasim/base.py b/covasim/base.py index 03861be60..a7baf84d9 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -69,6 +69,7 @@ def __init__(self, pars): self.update_pars(pars, create=True) return + def __getitem__(self, key): ''' Allow sim['par_name'] instead of sim.pars['par_name'] ''' try: @@ -78,6 +79,7 @@ def __getitem__(self, key): errormsg = f'Key "{key}" not found; available keys:\n{all_keys}' raise sc.KeyNotFoundError(errormsg) + def __setitem__(self, key, value): ''' Ditto ''' if key in self.pars: @@ -88,6 +90,7 @@ def __setitem__(self, key, value): raise sc.KeyNotFoundError(errormsg) return + def update_pars(self, pars=None, create=False): ''' Update internal dict with new pars. @@ -128,7 +131,7 @@ class Result(object): print(r1.values) ''' - def __init__(self, name=None, npts=None, scale=True, color=None, strain_color=None, max_strains=30): + def __init__(self, name=None, npts=None, scale=True, color=None, strain_color=None, total_strains=1): self.name = name # Name of this result self.scale = scale # Whether or not to scale the result by the scale factor if color is None: @@ -139,7 +142,7 @@ def __init__(self, name=None, npts=None, scale=True, color=None, strain_color=No if npts is None: npts = 0 if 'by_strain' in self.name or 'by strain' in self.name: - self.values = np.full((max_strains, npts), 0, dtype=cvd.result_float, order='F') + self.values = np.full((total_strains, npts), 0, dtype=cvd.result_float, order='F') else: self.values = np.array(np.zeros(int(npts)), dtype=cvd.result_float) self.low = None @@ -237,7 +240,7 @@ def _brief(self): return string - def update_pars(self, pars=None, create=False, defaults=None, **kwargs): + def update_pars(self, pars=None, create=False, **kwargs): ''' Ensure that metaparameters get used properly before being updated ''' pars = sc.mergedicts(pars, kwargs) @@ -246,11 +249,6 @@ def update_pars(self, pars=None, create=False, defaults=None, **kwargs): cvpar.reset_layer_pars(pars, force=False) if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses - - if defaults is not None: # Defaults have been provided: we are now doing updates - pars = cvpar.listify_strain_pars(pars) # Strain pars need to be lists - pars = cvpar.update_sub_key_pars(pars, defaults) # Update dict parameters by sub-key - combined_pars = sc.mergedicts(defaults, pars) # Now that subkeys have been updated, can merge the dicts together super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/run.py b/covasim/run.py index eb6f0b311..a24eb0a5b 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -954,12 +954,13 @@ def print_heading(string): scen_sim = sc.dcp(self.base_sim) scen_sim.label = scenkey - defaults = {par: scen_sim[par] for par in cvd.strain_pars} - scen_sim.update_pars(scenpars, defaults=defaults, **kwargs) # Update the parameters, if provided +# defaults = {par: scen_sim[par] for par in cvd.strain_pars} +# scen_sim.update_pars(scenpars, defaults=defaults, **kwargs) # Update the parameters, if provided + scen_sim.update_pars(scenpars, **kwargs) # Update the parameters, if provided if 'strains' in scenpars: # Process strains scen_sim.init_strains() scen_sim.init_immunity(create=True) - if 'imm_pars' in scenpars: # Process strains + if 'imm_pars' in scenpars: # Process immunity scen_sim.init_immunity(create=True) run_args = dict(n_runs=self['n_runs'], noise=self['noise'], noisepar=self['noisepar'], keep_people=keep_people, verbose=verbose) diff --git a/covasim/sim.py b/covasim/sim.py index 660fade52..1b2ca7413 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -75,7 +75,7 @@ def __init__(self, pars=None, datafile=None, datacols=None, label=None, simfile= # Now update everything self.set_metadata(simfile) # Set the simulation date and filename - self.update_pars(pars, defaults=default_pars, **kwargs) # Update the parameters, if provided + self.update_pars(pars, **kwargs) # Update the parameters, if provided self.load_data(datafile, datacols) # Load the data, if provided if self.load_pop: self.load_population(popfile) # Load the population, if provided @@ -280,23 +280,23 @@ def init_res(*args, **kwargs): # Flows and cumulative flows for key,label in cvd.result_flows.items(): - self.results[f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key], strain_color=strain_cols, max_strains=self['total_strains']) # Cumulative variables -- e.g. "Cumulative infections" + self.results[f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key], strain_color=strain_cols, total_strains=self['total_strains']) # Cumulative variables -- e.g. "Cumulative infections" for key,label in cvd.result_flows.items(): # Repeat to keep all the cumulative keys together - self.results[f'new_{key}'] = init_res(f'Number of new {label}', color=dcols[key], strain_color=strain_cols, max_strains=self['total_strains']) # Flow variables -- e.g. "Number of new infections" + self.results[f'new_{key}'] = init_res(f'Number of new {label}', color=dcols[key], strain_color=strain_cols, total_strains=self['total_strains']) # Flow variables -- e.g. "Number of new infections" # Stock variables for key,label in cvd.result_stocks.items(): - self.results[f'n_{key}'] = init_res(label, color=dcols[key], strain_color=strain_cols, max_strains=self['total_strains']) + self.results[f'n_{key}'] = init_res(label, color=dcols[key], strain_color=strain_cols, total_strains=self['total_strains']) # Other variables self.results['n_alive'] = init_res('Number of people alive', scale=False) self.results['n_preinfectious'] = init_res('Number preinfectious', scale=False, color=dcols.exposed) #self.results['n_removed'] = init_res('Number removed', scale=False, color=dcols.recovered) self.results['prevalence'] = init_res('Prevalence', scale=False) - self.results['prevalence_by_strain'] = init_res('Prevalence by strain', scale=False, max_strains=self['total_strains']) + self.results['prevalence_by_strain'] = init_res('Prevalence by strain', scale=False, total_strains=self['total_strains']) self.results['incidence'] = init_res('Incidence', scale=False) - self.results['incidence_by_strain'] = init_res('Incidence by strain', scale=False, max_strains=self['total_strains']) + self.results['incidence_by_strain'] = init_res('Incidence by strain', scale=False, total_strains=self['total_strains']) self.results['r_eff'] = init_res('Effective reproduction number', scale=False) self.results['doubling_time'] = init_res('Doubling time', scale=False) self.results['test_yield'] = init_res('Testing yield', scale=False) @@ -572,6 +572,12 @@ def step(self): has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.false(sus)) if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) + if t == 20: + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() + # Iterate through n_strains to calculate infections for strain in range(ns): diff --git a/examples/t10_variants.py b/examples/t10_variants.py deleted file mode 100644 index f5284f77c..000000000 --- a/examples/t10_variants.py +++ /dev/null @@ -1,24 +0,0 @@ -import covasim as cv - - -pars = {'n_strains': 2, - 'beta': [0.016, 0.035]} - - -sim = cv.Sim(pars=pars) -sim.run() - -sim.plot_result('cum_infections_by_strain', do_show=False, do_save=True, fig_path='results/sim1_cum_infections_by_strain') -sim.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim1_incidence_by_strain') -sim.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim1_prevalence_by_strain') - -# Run sim with a single strain initially, then introduce a new strain that's more transmissible on day 10 -pars = {'n_strains': 10, 'beta': [0.016]*10, 'max_strains': 11} -imports = cv.import_strain(day=10, n_imports=20, beta=0.05, rel_trans=1, rel_sus=1) -sim2 = cv.Sim(pars=pars, interventions=imports, label='With imported infections') -sim2.run() - -sim2.plot_result('cum_infections_by_strain', do_show=False, do_save=True, fig_path='results/sim2_cum_infections_by_strain') -sim2.plot_result('incidence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim2_incidence_by_strain') -sim2.plot_result('prevalence_by_strain', label = ['strain1', 'strain2'], do_show=False, do_save=True, fig_path='results/sim2_prevalence_by_strain') - diff --git a/tests/devtests/test_immunity.py b/tests/devtests/test_immunity.py deleted file mode 100644 index 51c20b910..000000000 --- a/tests/devtests/test_immunity.py +++ /dev/null @@ -1,100 +0,0 @@ -import covasim as cv -import covasim.defaults as cvd -import sciris as sc -import matplotlib.pyplot as plt -import numpy as np - - -plot_args = dict(do_plot=1, do_show=1, do_save=1) - -def test_reinfection_scens(do_plot=False, do_show=True, do_save=False): - sc.heading('Run a basic sim with varying reinfection risk') - sc.heading('Setting up...') - - # Define baseline parameters - base_pars = { - 'beta': 0.1, # Make beta higher than usual so people get infected quickly - 'n_days': 350, - } - - n_runs = 3 - base_sim = cv.Sim(base_pars) - b1351 = cv.Strain('b1351', days=100, n_imports=20) - - # Define the scenarios - scenarios = { - # 'baseline': { - # 'name':'Baseline', - # 'pars': {'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001})}, - # }, - # 'slower': { - # 'name':'Slower', - # 'pars': {'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/120, 'init_decay_time': 250, 'decay_decay_rate': 0.001})}, - # }, - # 'faster': { - # 'name':'Faster', - # 'pars': {'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/30, 'init_decay_time': 150, 'decay_decay_rate': 0.01})}, - # }, - 'baseline_b1351': { - 'name': 'Baseline, B1351 on day 40', - 'pars': {'strains': [b1351]}, - - }, - 'slower_b1351': { - 'name': 'Slower, B1351 on day 40', - 'pars': {'NAb_decay': dict(form='nab_decay', - pars={'init_decay_rate': np.log(2) / 120, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), - 'strains': [b1351]}, - }, - 'faster_b1351': { - 'name': 'Faster, B1351 on day 40', - 'pars': {'NAb_decay': dict(form='nab_decay', - pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 150, - 'decay_decay_rate': 0.01}), - 'strains': [b1351]}, - }, - # 'even_faster_b1351': { - # 'name': 'Even faster, B1351 on day 40', - # 'pars': {'NAb_decay': dict(form='nab_decay', - # pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 50, - # 'decay_decay_rate': 0.1}), - # 'strains': [b1351]}, - # }, - } - - metapars = {'n_runs': n_runs} - scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run() - - to_plot = sc.objdict({ - 'New infections': ['new_infections'], - 'Cumulative infections': ['cum_infections'], - 'New reinfections': ['new_reinfections'], - # 'Cumulative reinfections': ['cum_reinfections'], - }) - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_basic_reinfection.png', to_plot=to_plot) - - return scens - - - - -#%% Run as a script -if __name__ == '__main__': - sc.tic() - - # Run simplest possible test - # if 0: - # sim = cv.Sim() - # sim.run() - - # Run more complex tests - scens1 = test_reinfection_scens(**plot_args) - - sc.toc() - - -print('Done.') - diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 7c3c8d383..370293404 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -448,8 +448,8 @@ def get_ind_of_min_value(list, time): # # Run Vaccine tests # sim4 = test_synthpops() # sim5 = test_vaccine_1strain() - - # # Run multisim and scenario tests + # + # # # Run multisim and scenario tests # scens0 = test_vaccine_1strain_scen() # scens1 = test_vaccine_2strains_scen() # scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) diff --git a/tests/test_baselines.py b/tests/test_baselines.py index 99b110b40..9403eb514 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -6,7 +6,7 @@ import sciris as sc import covasim as cv -do_plot = 1 +do_plot = 0 do_save = 0 baseline_filename = sc.thisdir(__file__, 'baseline.json') benchmark_filename = sc.thisdir(__file__, 'benchmark.json') From 94ebb60d216fd662985241a9e799dcdad6244508 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 29 Mar 2021 13:28:41 +0200 Subject: [PATCH 288/569] need to determine best approach --- covasim/sim.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 1b2ca7413..6a2230868 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -572,12 +572,6 @@ def step(self): has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.false(sus)) if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) - if t == 20: - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() - # Iterate through n_strains to calculate infections for strain in range(ns): From 97854dedf0adb9e09895c34fb6518820d728b5cc Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 29 Mar 2021 16:11:23 +0200 Subject: [PATCH 289/569] add prelim immunity results --- covasim/base.py | 7 ++++++- covasim/defaults.py | 9 ++++++++- covasim/immunity.py | 1 - covasim/people.py | 7 +++++-- covasim/run.py | 5 ++--- covasim/sim.py | 8 +++++++- covasim/utils.py | 2 +- 7 files changed, 29 insertions(+), 10 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index a7baf84d9..f58fb0691 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -240,7 +240,7 @@ def _brief(self): return string - def update_pars(self, pars=None, create=False, **kwargs): + def update_pars(self, pars=None, create=False, defaults=None, **kwargs): ''' Ensure that metaparameters get used properly before being updated ''' pars = sc.mergedicts(pars, kwargs) @@ -249,6 +249,11 @@ def update_pars(self, pars=None, create=False, **kwargs): cvpar.reset_layer_pars(pars, force=False) if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses + + if defaults is not None: # Defaults have been provided: we are now doing updates + pars = cvpar.listify_strain_pars(pars) # Strain pars need to be lists + pars = cvpar.update_sub_key_pars(pars, defaults) # Update dict parameters by sub-key + combined_pars = sc.mergedicts(defaults, pars) # Now that subkeys have been updated, can merge the dicts together super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/covasim/defaults.py b/covasim/defaults.py index 6c81aab75..4e850efc3 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -116,7 +116,7 @@ class PeopleMeta(sc.prettyobj): 'critical': 'Number of critical cases', 'diagnosed': 'Number of confirmed cases', 'quarantined': 'Number in quarantine', - 'vaccinated': 'Number of people vaccinated' + 'vaccinated': 'Number of people vaccinated', } # The types of result that are counted as flows -- used in sim.py; value is the label suffix @@ -137,6 +137,11 @@ class PeopleMeta(sc.prettyobj): 'vaccinated': 'vaccinated people' } +result_imm = { + 'pop_nabs': 'Population average NAbs', + 'pop_protection': 'Population average protective immunity' +} + # Define these here as well new_result_flows = [f'new_{key}' for key in result_flows.keys()] cum_result_flows = [f'cum_{key}' for key in result_flows.keys()] @@ -213,6 +218,8 @@ def get_colors(): c.deaths = '#000000' c.dead = c.deaths c.default = '#000000' + c.pop_nabs = '#32733d' + c.pop_protection = '#9e1149' return c diff --git a/covasim/immunity.py b/covasim/immunity.py index 0ad4eb392..e6d367671 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -313,7 +313,6 @@ def init_nab(people, inds, prior_inf=True): prior_NAb = people.NAb[prior_NAb_inds] # Array of NAb levels on this timestep for people with some NAbs - # NAbs from infection if prior_inf: NAb_boost = people.pars['NAb_boost'] # Boosting factor for natural infection diff --git a/covasim/people.py b/covasim/people.py index 092232a4b..06ad17391 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -345,6 +345,9 @@ def make_susceptible(self, inds): for key in self.meta.dates + self.meta.durs: self[key][inds] = np.nan + self['init_NAb'][inds] = np.nan + self['NAb'][inds] = np.nan + return @@ -356,8 +359,8 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str * Infected people that develop symptoms are disaggregated into mild vs. severe (=requires hospitalization) vs. critical (=requires ICU) * Every asymptomatic, mildly symptomatic, and severely symptomatic person recovers * Critical cases either recover or die - - Method also deduplicates input arrays in case one agent is infected many times + + Method also deduplicates input arrays in case one agent is infected many times and stores who infected whom in infection_log list. Args: diff --git a/covasim/run.py b/covasim/run.py index a24eb0a5b..c362d5d09 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -954,9 +954,8 @@ def print_heading(string): scen_sim = sc.dcp(self.base_sim) scen_sim.label = scenkey -# defaults = {par: scen_sim[par] for par in cvd.strain_pars} -# scen_sim.update_pars(scenpars, defaults=defaults, **kwargs) # Update the parameters, if provided - scen_sim.update_pars(scenpars, **kwargs) # Update the parameters, if provided + defaults = {par: scen_sim[par] for par in cvd.strain_pars} + scen_sim.update_pars(scenpars, defaults=defaults, **kwargs) # Update the parameters, if provided if 'strains' in scenpars: # Process strains scen_sim.init_strains() scen_sim.init_immunity(create=True) diff --git a/covasim/sim.py b/covasim/sim.py index 6a2230868..bb48abb21 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -75,7 +75,7 @@ def __init__(self, pars=None, datafile=None, datacols=None, label=None, simfile= # Now update everything self.set_metadata(simfile) # Set the simulation date and filename - self.update_pars(pars, **kwargs) # Update the parameters, if provided + self.update_pars(pars, defaults=default_pars, **kwargs) # Update the parameters, if provided self.load_data(datafile, datacols) # Load the data, if provided if self.load_pop: self.load_population(popfile) # Load the population, if provided @@ -302,6 +302,8 @@ def init_res(*args, **kwargs): self.results['test_yield'] = init_res('Testing yield', scale=False) self.results['rel_test_yield'] = init_res('Relative testing yield', scale=False) self.results['share_vaccinated'] = init_res('Share Vaccinated', scale=False) + self.results['pop_nabs'] = init_res('Population average NAb levels', scale=False, color=dcols.pop_nabs) + self.results['pop_protection'] = init_res('Population average immunity protection', scale=False, color=dcols.pop_protection) # Populate the rest of the results if self['rescale']: @@ -627,6 +629,10 @@ def step(self): else: self.results[key][t] += count + # Update NAb and immunity for this time step + self.results['pop_nabs'][t] = np.sum(people.NAb[cvu.defined(people.NAb)])/len(people) + self.results['pop_protection'][t] = np.nanmean(people.sus_imm) + # Apply analyzers -- same syntax as interventions for i,analyzer in enumerate(self['analyzers']): if isinstance(analyzer, cva.Analyzer): diff --git a/covasim/utils.py b/covasim/utils.py index 514f0968b..8158a4e1a 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -76,7 +76,7 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=safe_parallel) +#@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=safe_parallel) def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] From dd663f790d989c87dd5259be6328b983cbd5d97d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 08:50:34 -0700 Subject: [PATCH 290/569] expand graph methods --- CHANGELOG.rst | 6 ++-- covasim/base.py | 82 ++++++++++++++++++++++++++++++++++--------------- covasim/sim.py | 2 +- 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 411c0faf5..440f445b7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -30,9 +30,9 @@ Version 2.1.1 (2021-03-29) This is the last release before the Covasim 3.0 launch (vaccines and variants). -- **Duration updates**: All duration parameters have been updated from the literature. While most are similar to what they were before, there are some differences: in particular, durations of severe and critical disease (either to recovery or death) have increased; for example, duration from symptom onset to death has increased from 15.8±3.8 days to 18.8±7.2 days. -- **Performance updates**: The innermost loop of Covasim, ``cv.compute_infections()``, has been refactored to make more efficient use of array indexing. The observed difference will depend on the nature of the simulation (e.g., network type, interventions), but runs may be up to 1.5x faster now. -- Contacts layers now have a new method, ``to_graph()``, that will return a ``networkx`` graph (requires ``networkx`` to be installed, of course). For example, ``nx.draw(cv.Sim(pop_size=100).run().people.contacts.to_graph())`` will draw all connections between 100 default people. +- **Duration updates:** All duration parameters have been updated from the literature. While most are similar to what they were before, there are some differences: in particular, durations of severe and critical disease (either to recovery or death) have increased; for example, duration from symptom onset to death has increased from 15.8±3.8 days to 18.8±7.2 days. +- **Performance updates:** The innermost loop of Covasim, ``cv.compute_infections()``, has been refactored to make more efficient use of array indexing. The observed difference will depend on the nature of the simulation (e.g., network type, interventions), but runs may be up to 1.5x faster now. +- **Graphs:** People, contacts, and contacts layers now have a new method, ``to_graph()``, that will return a ``networkx`` graph (requires ``networkx`` to be installed, of course). For example, ``nx.draw(cv.Sim(pop_size=100).run().people.to_graph())`` will draw all connections between 100 default people. See ``cv.Sim.people.to_graph()`` for full documentation. - A bug was fixed with ``cv.TransTree.animate()`` failing in some cases. - ``cv.date_formatter()`` now takes ``interval``, ``start``, and ``end`` arguments. - *Regression information*: Parameters can be restored by using the ``version`` argument when creating a sim. Specifically, the parameters for the following distributions (all lognormal) have been changed as follows:: diff --git a/covasim/base.py b/covasim/base.py index d956c7ed7..a68c39967 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1057,6 +1057,40 @@ def from_people(self, people, resize=True): return + def to_graph(self): # pragma: no cover + ''' + Convert all people to a networkx DiGraph, including all properties of + the people (nodes) and contacts (edges). + + **Example**:: + + import networkx as nx + sim = cv.Sim(pop_size=50, pop_type='hybrid').run() + G = sim.people.to_graph() + nodes = G.nodes(data=True) + edges = G.edges() + node_colors = [n['age'] for i,n in nodes] + layer_map = dict(h='#37b', s='#e11', w='#4a4', c='#a49') + edge_colors = [layer_map[G[u][v]['layer']] for u,v in edges] + edge_weights = [G[u][v]['beta'] for u,v in edges] + nx.draw(G, node_color=node_colors, edge_color=edge_colors, width=edge_weights, alpha=0.5) + ''' + import networkx as nx + + # Copy data from people into graph + G = self.contacts.to_graph() + for key in self.keys(): + data = {k:v for k,v in enumerate(self[key])} + nx.set_node_attributes(G, data, name=key) + + # Include global layer weights + for u,v in G.edges(): + edge = G[u][v] + edge['beta'] *= self.pars['beta_layer'][edge['layer']] + + return G + + def init_contacts(self, reset=False): ''' Initialize the contacts dataframe with the correct columns and data types ''' @@ -1109,12 +1143,7 @@ def add_contacts(self, contacts, lkey=None, beta=None): # Create the layer if it doesn't yet exist if lkey not in self.contacts: - if self.pars['dynam_layer'].get(lkey, False): - # Equivalent to previous functionality, but might be better if make_randpop() returned Layer objects instead of just dicts, that - # way the population creation function could have control over both the contacts and the update algorithm - self.contacts[lkey] = RandomLayer() - else: - self.contacts[lkey] = Layer() + self.contacts[lkey] = Layer(label=lkey) # Actually include them, and update properties if supplied for col in self.contacts[lkey].keys(): # Loop over the supplied columns @@ -1151,7 +1180,7 @@ def make_edgelist(self, contacts): # Turn into a dataframe for lkey in lkeys: - new_layer = Layer() + new_layer = Layer(label=lkey) for ckey,value in new_contacts[lkey].items(): new_layer[ckey] = np.array(value, dtype=new_layer.meta[ckey]) new_contacts[lkey] = new_layer @@ -1221,8 +1250,8 @@ class Contacts(FlexDict): ''' def __init__(self, layer_keys=None): if layer_keys is not None: - for key in layer_keys: - self[key] = Layer() + for lkey in layer_keys: + self[lkey] = Layer(label=lkey) return def __repr__(self): @@ -1253,7 +1282,7 @@ def add_layer(self, **kwargs): **Example**:: - hospitals_layer = cv.Layer() + hospitals_layer = cv.Layer(label='hosp') sim.people.contacts.add_layer(hospitals=hospitals_layer) ''' for lkey,layer in kwargs.items(): @@ -1285,15 +1314,14 @@ def to_graph(self): # pragma: no cover **Example**:: import networkx as nx - sim = cv.Sim(pop_size=100, pop_type='hybrid').run() + sim = cv.Sim(pop_size=50, pop_type='hybrid').run() G = sim.people.contacts.to_graph() nx.draw(G) ''' import networkx as nx - H = nx.Graph() + H = nx.DiGraph() for lkey,layer in self.items(): G = layer.to_graph() - nx.set_edge_attributes(G, lkey, name='layer') H = nx.compose(H, G) return H @@ -1314,12 +1342,12 @@ class Layer(FlexDict): p1 (array): an array of N connections, representing people on one side of the connection p2 (array): an array of people on the other side of the connection beta (array): an array of weights for each connection - label (str): the name of the layer + label (str): the name of the layer (optional) kwargs (dict): other keys copied directly into the layer - Note that all arguments must be arrays of the same length, although not all - have to be supplied at the time of creation (they must all be the same at the - time of initialization, though, or else validation will fail). + Note that all arguments (except for label) must be arrays of the same length, + although not all have to be supplied at the time of creation (they must all + be the same at the time of initialization, though, or else validation will fail). **Examples**:: @@ -1329,12 +1357,12 @@ class Layer(FlexDict): p1 = np.random.randint(n_people, size=n) p2 = np.random.randint(n_people, size=n) beta = np.ones(n) - layer = cv.Layer(p1=p1, p2=p2, beta=beta) + layer = cv.Layer(p1=p1, p2=p2, beta=beta, label='rand') - # Convert one layer to another with an extra column + # Convert one layer to another with extra columns index = np.arange(n) self_conn = p1 == p2 - layer2 = cv.Layer(**layer, index=index, self_conn=self_conn) + layer2 = cv.Layer(**layer, index=index, self_conn=self_conn, label=layer.label) ''' def __init__(self, label=None, **kwargs): @@ -1366,9 +1394,10 @@ def __len__(self): def __repr__(self): ''' Convert to a dataframe for printing ''' - label = self.__class__.__name__ + namestr = self.__class__.__name__ + labelstr = f'"{self.label}"' if self.label else '' keys_str = ', '.join(self.keys()) - output = f'{label}({keys_str})\n' # e.g. Layer(p1, p2, beta) + output = f'{namestr}({labelstr}, {keys_str})\n' # e.g. Layer("h", p1, p2, beta) output += self.to_df().__repr__() return output @@ -1453,9 +1482,11 @@ def to_df(self): return df - def from_df(self, df): + def from_df(self, df, keys=None): ''' Convert from a dataframe ''' - for key in self.meta_keys(): + if keys is None: + keys = self.meta_keys() + for key in keys: self[key] = df[key].to_numpy() return self @@ -1472,8 +1503,9 @@ def to_graph(self): # pragma: no cover nx.draw(G) ''' import networkx as nx + data = [np.array(self[k], dtype=dtype).tolist() for k,dtype in [('p1', int), ('p2', int), ('beta', float)]] G = nx.DiGraph() - G.add_weighted_edges_from(zip(self['p1'], self['p2'], self['beta']), 'beta') + G.add_weighted_edges_from(zip(*data), weight='beta') nx.set_edge_attributes(G, self.label, name='layer') return G diff --git a/covasim/sim.py b/covasim/sim.py index ab74fb931..07bc88845 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -116,7 +116,7 @@ def initialize(self, reset=False, **kwargs): self.initialized = True self.complete = False self.results_ready = False - return + return self def layer_keys(self): From 14731259b08596ab10ab651610a4b157704b5ed0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 08:55:53 -0700 Subject: [PATCH 291/569] update version date --- covasim/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/version.py b/covasim/version.py index 0bfc1ab6f..a297f93bc 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -5,5 +5,5 @@ __all__ = ['__version__', '__versiondate__', '__license__'] __version__ = '2.1.1' -__versiondate__ = '2021-03-28' +__versiondate__ = '2021-03-29' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From c484e63a11d9ff688ac89e4665ad608cde2bfd64 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 09:11:26 -0700 Subject: [PATCH 292/569] update graph example --- covasim/base.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index a68c39967..df4d6d11f 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1059,20 +1059,20 @@ def from_people(self, people, resize=True): def to_graph(self): # pragma: no cover ''' - Convert all people to a networkx DiGraph, including all properties of + Convert all people to a networkx MultiDiGraph, including all properties of the people (nodes) and contacts (edges). **Example**:: import networkx as nx - sim = cv.Sim(pop_size=50, pop_type='hybrid').run() + sim = cv.Sim(pop_size=50, pop_type='hybrid', contacts=dict(h=3, s=10, w=10, c=5)).run() G = sim.people.to_graph() nodes = G.nodes(data=True) - edges = G.edges() + edges = G.edges(keys=True) node_colors = [n['age'] for i,n in nodes] layer_map = dict(h='#37b', s='#e11', w='#4a4', c='#a49') - edge_colors = [layer_map[G[u][v]['layer']] for u,v in edges] - edge_weights = [G[u][v]['beta'] for u,v in edges] + edge_colors = [layer_map[G[i][j][k]['layer']] for i,j,k in edges] + edge_weights = [G[i][j][k]['beta']*5 for i,j,k in edges] nx.draw(G, node_color=node_colors, edge_color=edge_colors, width=edge_weights, alpha=0.5) ''' import networkx as nx @@ -1084,8 +1084,8 @@ def to_graph(self): # pragma: no cover nx.set_node_attributes(G, data, name=key) # Include global layer weights - for u,v in G.edges(): - edge = G[u][v] + for u,v,k in G.edges(keys=True): + edge = G[u][v][k] edge['beta'] *= self.pars['beta_layer'][edge['layer']] return G @@ -1309,7 +1309,7 @@ def pop_layer(self, *args): def to_graph(self): # pragma: no cover ''' - Convert all layers to a networkx DiGraph + Convert all layers to a networkx MultiDiGraph **Example**:: @@ -1319,10 +1319,10 @@ def to_graph(self): # pragma: no cover nx.draw(G) ''' import networkx as nx - H = nx.DiGraph() + H = nx.MultiDiGraph() for lkey,layer in self.items(): G = layer.to_graph() - H = nx.compose(H, G) + H = nx.compose(H, nx.MultiDiGraph(G)) return H From 09adda0feabd75ff5b72f10ea50b6cd7b5c78b5d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 09:33:23 -0700 Subject: [PATCH 293/569] tidy regression parameters --- covasim/base.py | 1 + covasim/misc.py | 10 +- covasim/parameters.py | 2 +- covasim/regression/pars_v1.6.0.json | 203 ---------------------------- covasim/regression/pars_v1.7.0.json | 203 ---------------------------- covasim/regression/pars_v2.1.0.json | 203 ---------------------------- covasim/utils.py | 2 +- 7 files changed, 7 insertions(+), 617 deletions(-) delete mode 100644 covasim/regression/pars_v1.6.0.json delete mode 100644 covasim/regression/pars_v1.7.0.json delete mode 100644 covasim/regression/pars_v2.1.0.json diff --git a/covasim/base.py b/covasim/base.py index df4d6d11f..630419a2d 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1064,6 +1064,7 @@ def to_graph(self): # pragma: no cover **Example**:: + import covasim as cv import networkx as nx sim = cv.Sim(pop_size=50, pop_type='hybrid', contacts=dict(h=3, s=10, w=10, c=5)).run() G = sim.people.to_graph() diff --git a/covasim/misc.py b/covasim/misc.py index 288128925..2a4c9379e 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -487,7 +487,7 @@ def get_version_pars(version, verbose=True): Dictionary of parameters from that version ''' - # Define mappings for available sets of parameters -- from the changelog + # Define mappings for available sets of parameters -- note that this must be manually updated from the changelog match_map = { '0.30.4': ['0.30.4'], '0.31.0': ['0.31.0'], @@ -500,11 +500,9 @@ def get_version_pars(version, verbose=True): '1.2.0': [f'1.2.{i}' for i in range(4)], '1.3.0': [f'1.3.{i}' for i in range(6)], '1.4.0': [f'1.4.{i}' for i in range(9)], - '1.5.0': [f'1.5.{i}' for i in range(4)], - '1.6.0': [f'1.6.{i}' for i in range(2)], - '1.7.0': [f'1.7.{i}' for i in range(7)], - '2.0.0': [f'2.0.{i}' for i in range(4)], - '2.1.0': [f'2.0.{i}' for i in range(2)], + '1.5.0': [f'1.5.{i}' for i in range(4)] + [f'1.6.{i}' for i in range(2)] + [f'1.7.{i}' for i in range(7)], + '2.0.0': [f'2.0.{i}' for i in range(5)] + ['2.1.0'], + '2.1.1': ['2.1.1'], } # Find and check the match diff --git a/covasim/parameters.py b/covasim/parameters.py index b5d6b62ee..4e562c580 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -67,7 +67,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.5, par2=1.5) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, appendix table S2, subtracting inf2sym duration pars['dur']['inf2sym'] = dict(dist='lognormal_int', par1=1.1, par2=0.9) # Duration from infectious to symptomatic; see Linton et al., https://doi.org/10.3390/jcm9020538, from Table 2, 5.6 day incubation period - 4.5 day exp2inf from Lauer et al. pars['dur']['sym2sev'] = dict(dist='lognormal_int', par1=6.6, par2=4.9) # Duration from symptomatic to severe symptoms; see Linton et al., https://doi.org/10.3390/jcm9020538, from Table 2, 6.6 day onset to hospital admission (deceased); see also Wang et al., https://jamanetwork.com/journals/jama/fullarticle/2761044, 7 days (Table 1) - pars['dur']['sev2crit'] = dict(dist='lognormal_int', par1=1.5, par2=2.0) # Duration from severe symptoms to requiring ICU; average of 1.9 and 1.0; see Chen et al., https://www.sciencedirect.com/science/article/pii/S0163445320301195, 8.5 days total - 6.6 days sym2sev = 1.9 days; see also Wang et al., https://jamanetwork.com/journals/jama/fullarticle/2761044, Table 3, 1 day, IQR 0-3 days + pars['dur']['sev2crit'] = dict(dist='lognormal_int', par1=1.5, par2=2.0) # Duration from severe symptoms to requiring ICU; average of 1.9 and 1.0; see Chen et al., https://www.sciencedirect.com/science/article/pii/S0163445320301195, 8.5 days total - 6.6 days sym2sev = 1.9 days; see also Wang et al., https://jamanetwork.com/journals/jama/fullarticle/2761044, Table 3, 1 day, IQR 0-3 days; std=2.0 is an estimate # Duration parameters: time for disease recovery pars['dur']['asym2rec'] = dict(dist='lognormal_int', par1=8.0, par2=2.0) # Duration for asymptomatic people to recover; see Wölfel et al., https://www.nature.com/articles/s41586-020-2196-x diff --git a/covasim/regression/pars_v1.6.0.json b/covasim/regression/pars_v1.6.0.json deleted file mode 100644 index e113ae5dd..000000000 --- a/covasim/regression/pars_v1.6.0.json +++ /dev/null @@ -1,203 +0,0 @@ -{ - "pop_size": 20000.0, - "pop_infected": 10, - "pop_type": "random", - "location": null, - "start_day": "2020-03-01", - "end_day": null, - "n_days": 60, - "rand_seed": 1, - "verbose": 1, - "pop_scale": 1, - "rescale": true, - "rescale_threshold": 0.05, - "rescale_factor": 1.2, - "beta": 0.016, - "contacts": { - "a": 20 - }, - "dynam_layer": { - "a": 0 - }, - "beta_layer": { - "a": 1.0 - }, - "n_imports": 0, - "beta_dist": { - "dist": "neg_binomial", - "par1": 1.0, - "par2": 0.45, - "step": 0.01 - }, - "viral_dist": { - "frac_time": 0.3, - "load_ratio": 2, - "high_cap": 4 - }, - "asymp_factor": 1.0, - "iso_factor": { - "a": 0.2 - }, - "quar_factor": { - "a": 0.3 - }, - "quar_period": 14, - "dur": { - "exp2inf": { - "dist": "lognormal_int", - "par1": 4.6, - "par2": 4.8 - }, - "inf2sym": { - "dist": "lognormal_int", - "par1": 1.0, - "par2": 0.9 - }, - "sym2sev": { - "dist": "lognormal_int", - "par1": 6.6, - "par2": 4.9 - }, - "sev2crit": { - "dist": "lognormal_int", - "par1": 3.0, - "par2": 7.4 - }, - "asym2rec": { - "dist": "lognormal_int", - "par1": 8.0, - "par2": 2.0 - }, - "mild2rec": { - "dist": "lognormal_int", - "par1": 8.0, - "par2": 2.0 - }, - "sev2rec": { - "dist": "lognormal_int", - "par1": 14.0, - "par2": 2.4 - }, - "crit2rec": { - "dist": "lognormal_int", - "par1": 14.0, - "par2": 2.4 - }, - "crit2die": { - "dist": "lognormal_int", - "par1": 6.2, - "par2": 1.7 - } - }, - "rel_symp_prob": 1.0, - "rel_severe_prob": 1.0, - "rel_crit_prob": 1.0, - "rel_death_prob": 1.0, - "prog_by_age": true, - "prognoses": { - "age_cutoffs": [ - 0, - 10, - 20, - 30, - 40, - 50, - 60, - 70, - 80, - 90 - ], - "sus_ORs": [ - 0.34, - 0.67, - 1.0, - 1.0, - 1.0, - 1.0, - 1.24, - 1.47, - 1.47, - 1.47 - ], - "trans_ORs": [ - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0 - ], - "comorbidities": [ - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0 - ], - "symp_probs": [ - 0.5, - 0.55, - 0.6, - 0.65, - 0.7, - 0.75, - 0.8, - 0.85, - 0.9, - 0.9 - ], - "severe_probs": [ - 0.001, - 0.0029999999999999996, - 0.012, - 0.032, - 0.049, - 0.102, - 0.16599999999999998, - 0.24300000000000002, - 0.273, - 0.273 - ], - "crit_probs": [ - 0.06, - 0.04848484848484849, - 0.05, - 0.049999999999999996, - 0.06297376093294461, - 0.12196078431372549, - 0.2740210843373494, - 0.43200193657709995, - 0.708994708994709, - 0.708994708994709 - ], - "death_probs": [ - 0.6666666666666667, - 0.75, - 0.8333333333333333, - 0.7692307692307694, - 0.6944444444444444, - 0.6430868167202572, - 0.6045616927727397, - 0.5715566513504426, - 0.5338691159586683, - 0.5338691159586683 - ] - }, - "interventions": [], - "analyzers": [], - "timelimit": null, - "stopping_func": null, - "n_beds_hosp": null, - "n_beds_icu": null, - "no_hosp_factor": 2.0, - "no_icu_factor": 2.0 -} \ No newline at end of file diff --git a/covasim/regression/pars_v1.7.0.json b/covasim/regression/pars_v1.7.0.json deleted file mode 100644 index e113ae5dd..000000000 --- a/covasim/regression/pars_v1.7.0.json +++ /dev/null @@ -1,203 +0,0 @@ -{ - "pop_size": 20000.0, - "pop_infected": 10, - "pop_type": "random", - "location": null, - "start_day": "2020-03-01", - "end_day": null, - "n_days": 60, - "rand_seed": 1, - "verbose": 1, - "pop_scale": 1, - "rescale": true, - "rescale_threshold": 0.05, - "rescale_factor": 1.2, - "beta": 0.016, - "contacts": { - "a": 20 - }, - "dynam_layer": { - "a": 0 - }, - "beta_layer": { - "a": 1.0 - }, - "n_imports": 0, - "beta_dist": { - "dist": "neg_binomial", - "par1": 1.0, - "par2": 0.45, - "step": 0.01 - }, - "viral_dist": { - "frac_time": 0.3, - "load_ratio": 2, - "high_cap": 4 - }, - "asymp_factor": 1.0, - "iso_factor": { - "a": 0.2 - }, - "quar_factor": { - "a": 0.3 - }, - "quar_period": 14, - "dur": { - "exp2inf": { - "dist": "lognormal_int", - "par1": 4.6, - "par2": 4.8 - }, - "inf2sym": { - "dist": "lognormal_int", - "par1": 1.0, - "par2": 0.9 - }, - "sym2sev": { - "dist": "lognormal_int", - "par1": 6.6, - "par2": 4.9 - }, - "sev2crit": { - "dist": "lognormal_int", - "par1": 3.0, - "par2": 7.4 - }, - "asym2rec": { - "dist": "lognormal_int", - "par1": 8.0, - "par2": 2.0 - }, - "mild2rec": { - "dist": "lognormal_int", - "par1": 8.0, - "par2": 2.0 - }, - "sev2rec": { - "dist": "lognormal_int", - "par1": 14.0, - "par2": 2.4 - }, - "crit2rec": { - "dist": "lognormal_int", - "par1": 14.0, - "par2": 2.4 - }, - "crit2die": { - "dist": "lognormal_int", - "par1": 6.2, - "par2": 1.7 - } - }, - "rel_symp_prob": 1.0, - "rel_severe_prob": 1.0, - "rel_crit_prob": 1.0, - "rel_death_prob": 1.0, - "prog_by_age": true, - "prognoses": { - "age_cutoffs": [ - 0, - 10, - 20, - 30, - 40, - 50, - 60, - 70, - 80, - 90 - ], - "sus_ORs": [ - 0.34, - 0.67, - 1.0, - 1.0, - 1.0, - 1.0, - 1.24, - 1.47, - 1.47, - 1.47 - ], - "trans_ORs": [ - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0 - ], - "comorbidities": [ - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0 - ], - "symp_probs": [ - 0.5, - 0.55, - 0.6, - 0.65, - 0.7, - 0.75, - 0.8, - 0.85, - 0.9, - 0.9 - ], - "severe_probs": [ - 0.001, - 0.0029999999999999996, - 0.012, - 0.032, - 0.049, - 0.102, - 0.16599999999999998, - 0.24300000000000002, - 0.273, - 0.273 - ], - "crit_probs": [ - 0.06, - 0.04848484848484849, - 0.05, - 0.049999999999999996, - 0.06297376093294461, - 0.12196078431372549, - 0.2740210843373494, - 0.43200193657709995, - 0.708994708994709, - 0.708994708994709 - ], - "death_probs": [ - 0.6666666666666667, - 0.75, - 0.8333333333333333, - 0.7692307692307694, - 0.6944444444444444, - 0.6430868167202572, - 0.6045616927727397, - 0.5715566513504426, - 0.5338691159586683, - 0.5338691159586683 - ] - }, - "interventions": [], - "analyzers": [], - "timelimit": null, - "stopping_func": null, - "n_beds_hosp": null, - "n_beds_icu": null, - "no_hosp_factor": 2.0, - "no_icu_factor": 2.0 -} \ No newline at end of file diff --git a/covasim/regression/pars_v2.1.0.json b/covasim/regression/pars_v2.1.0.json deleted file mode 100644 index 6437ab382..000000000 --- a/covasim/regression/pars_v2.1.0.json +++ /dev/null @@ -1,203 +0,0 @@ -{ - "pop_size": 20000.0, - "pop_infected": 20, - "pop_type": "random", - "location": null, - "start_day": "2020-03-01", - "end_day": null, - "n_days": 60, - "rand_seed": 1, - "verbose": 0.1, - "pop_scale": 1, - "rescale": true, - "rescale_threshold": 0.05, - "rescale_factor": 1.2, - "beta": 0.016, - "contacts": { - "a": 20 - }, - "dynam_layer": { - "a": 0 - }, - "beta_layer": { - "a": 1.0 - }, - "n_imports": 0, - "beta_dist": { - "dist": "neg_binomial", - "par1": 1.0, - "par2": 0.45, - "step": 0.01 - }, - "viral_dist": { - "frac_time": 0.3, - "load_ratio": 2, - "high_cap": 4 - }, - "asymp_factor": 1.0, - "iso_factor": { - "a": 0.2 - }, - "quar_factor": { - "a": 0.3 - }, - "quar_period": 14, - "dur": { - "exp2inf": { - "dist": "lognormal_int", - "par1": 4.6, - "par2": 4.8 - }, - "inf2sym": { - "dist": "lognormal_int", - "par1": 1.0, - "par2": 0.9 - }, - "sym2sev": { - "dist": "lognormal_int", - "par1": 6.6, - "par2": 4.9 - }, - "sev2crit": { - "dist": "lognormal_int", - "par1": 3.0, - "par2": 7.4 - }, - "asym2rec": { - "dist": "lognormal_int", - "par1": 8.0, - "par2": 2.0 - }, - "mild2rec": { - "dist": "lognormal_int", - "par1": 8.0, - "par2": 2.0 - }, - "sev2rec": { - "dist": "lognormal_int", - "par1": 14.0, - "par2": 2.4 - }, - "crit2rec": { - "dist": "lognormal_int", - "par1": 14.0, - "par2": 2.4 - }, - "crit2die": { - "dist": "lognormal_int", - "par1": 6.2, - "par2": 1.7 - } - }, - "rel_symp_prob": 1.0, - "rel_severe_prob": 1.0, - "rel_crit_prob": 1.0, - "rel_death_prob": 1.0, - "prog_by_age": true, - "prognoses": { - "age_cutoffs": [ - 0, - 10, - 20, - 30, - 40, - 50, - 60, - 70, - 80, - 90 - ], - "sus_ORs": [ - 0.34, - 0.67, - 1.0, - 1.0, - 1.0, - 1.0, - 1.24, - 1.47, - 1.47, - 1.47 - ], - "trans_ORs": [ - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0 - ], - "comorbidities": [ - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0 - ], - "symp_probs": [ - 0.5, - 0.55, - 0.6, - 0.65, - 0.7, - 0.75, - 0.8, - 0.85, - 0.9, - 0.9 - ], - "severe_probs": [ - 0.001, - 0.0029999999999999996, - 0.012, - 0.032, - 0.049, - 0.102, - 0.16599999999999998, - 0.24300000000000002, - 0.273, - 0.273 - ], - "crit_probs": [ - 0.06, - 0.04848484848484849, - 0.05, - 0.049999999999999996, - 0.06297376093294461, - 0.12196078431372549, - 0.2740210843373494, - 0.43200193657709995, - 0.708994708994709, - 0.708994708994709 - ], - "death_probs": [ - 0.6666666666666667, - 0.25, - 0.2777777777777778, - 0.30769230769230776, - 0.45370370370370366, - 0.2840300107181136, - 0.2104973893926903, - 0.2733385632634764, - 0.47600459242250287, - 0.9293915040183697 - ] - }, - "interventions": [], - "analyzers": [], - "timelimit": null, - "stopping_func": null, - "n_beds_hosp": null, - "n_beds_icu": null, - "no_hosp_factor": 2.0, - "no_icu_factor": 2.0 -} \ No newline at end of file diff --git a/covasim/utils.py b/covasim/utils.py index 1095bd16e..bf6a6768d 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -87,7 +87,7 @@ def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, return rel_trans, rel_sus -@nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) +@nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): # pragma: no cover ''' Compute who infects whom From 143da7b965c77e1ef3d582f9ebe1d445b86ca8f0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 09:54:16 -0700 Subject: [PATCH 294/569] pin line_profiler --- .github/workflows/tests.yaml | 4 +--- requirements.txt | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a2d5a493f..7a3c1d6a0 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -19,9 +19,7 @@ jobs: python-version: ${{ matrix.python-version }} architecture: x64 - name: Install Covasim - run: | - pip install line_profiler==3.1 - python setup.py develop + run: python setup.py develop - name: Run integration tests working-directory: ./tests run: | diff --git a/requirements.txt b/requirements.txt index acc074fbd..7773625e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ statsmodels matplotlib pandas xlrd==1.2.0 +line_profiler==3.1 sciris>=1.0.0 From 055d2792f2287c1115bcdc1209d68e000357bc0b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 10:00:55 -0700 Subject: [PATCH 295/569] update actions --- .github/workflows/tests.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7a3c1d6a0..cd5446b95 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -20,11 +20,11 @@ jobs: architecture: x64 - name: Install Covasim run: python setup.py develop + - name: Install tests + run: pip install -r tests/requirements_test.txt - name: Run integration tests working-directory: ./tests - run: | - pip install pytest - pytest test*.py --durations=0 # Run actual tests + run: pytest test_*.py --cov-config=.coveragerc --cov=../covasim --workers auto --durations=0 - name: Run unit tests working-directory: ./tests/unittests - run: pytest test*.py --durations=0 # Run actual tests + run: pytest test_*.py --cov=../covasim --workers auto --durations=0 From 9dad743aa7882a06a7ef614a212d69cd392ccf57 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 10:04:24 -0700 Subject: [PATCH 296/569] update actions --- .github/workflows/tests.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index cd5446b95..1af278dfc 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -21,10 +21,10 @@ jobs: - name: Install Covasim run: python setup.py develop - name: Install tests - run: pip install -r tests/requirements_test.txt + run: pip install pytest - name: Run integration tests working-directory: ./tests - run: pytest test_*.py --cov-config=.coveragerc --cov=../covasim --workers auto --durations=0 + run: pytest test_*.py --durations=0 - name: Run unit tests working-directory: ./tests/unittests - run: pytest test_*.py --cov=../covasim --workers auto --durations=0 + run: pytest test_*.py --durations=0 From 109478fc5e72449c2bf9654237d0f2fdc0dbcecc Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 29 Mar 2021 10:08:40 -0700 Subject: [PATCH 297/569] update changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 440f445b7..9062ace11 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,6 +35,7 @@ This is the last release before the Covasim 3.0 launch (vaccines and variants). - **Graphs:** People, contacts, and contacts layers now have a new method, ``to_graph()``, that will return a ``networkx`` graph (requires ``networkx`` to be installed, of course). For example, ``nx.draw(cv.Sim(pop_size=100).run().people.to_graph())`` will draw all connections between 100 default people. See ``cv.Sim.people.to_graph()`` for full documentation. - A bug was fixed with ``cv.TransTree.animate()`` failing in some cases. - ``cv.date_formatter()`` now takes ``interval``, ``start``, and ``end`` arguments. +- Temporarily pinned ``line_profiler`` to version 3.1 due to `this issue `__. - *Regression information*: Parameters can be restored by using the ``version`` argument when creating a sim. Specifically, the parameters for the following distributions (all lognormal) have been changed as follows:: exp2inf: μ = 4.6 → 4.5, σ = 4.8 → 1.5 From 32568627000c0f62fadc6621a98eb71047ce539c Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 29 Mar 2021 21:48:52 +0200 Subject: [PATCH 298/569] move nab_to_eff pars to main pars dict --- covasim/immunity.py | 30 +++++++++++++----------------- covasim/parameters.py | 1 + tests/devtests/test_variants.py | 18 +++++++++--------- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index e6d367671..d4867a240 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -364,7 +364,7 @@ def check_nab(t, people, inds=None): return -def nab_to_efficacy(nab, ax): +def nab_to_efficacy(nab, ax, slope, n_50, factors): ''' Convert NAb levels to immunity protection factors, using the functional form given in this paper: https://doi.org/10.1101/2021.03.09.21252641 @@ -375,17 +375,12 @@ def nab_to_efficacy(nab, ax): an array the same size as nab, containing the immunity protection factors for the specified axis ''' - choices = {'sus': -0.4, 'symp': 0, 'sev': 0.4} - if ax not in choices.keys(): + if ax not in ['sus', 'symp', 'sev']: errormsg = f'Choice provided not in list of choices' raise ValueError(errormsg) - # Temporary parameter values, pending confirmation - n_50 = 0.2 - slope = 2 - # put in here nab to efficacy mapping (logistic regression from fig 1a from https://doi.org/10.1101/2021.03.09.21252641) - efficacy = 1/(1+np.exp(-slope*(nab - n_50 + choices[ax]))) # from logistic regression computed in R using data from Khoury et al + efficacy = 1/(1+np.exp(-slope*(nab - n_50 + factors[ax]))) # from logistic regression computed in R using data from Khoury et al return efficacy @@ -466,6 +461,7 @@ def check_immunity(people, strain, sus=True, inds=None): is_vacc = cvu.true(people.vaccinated) # Vaccinated date_rec = people.date_recovered # Date recovered immunity = people.pars['immunity'] # cross-immunity/own-immunity scalars to be applied to NAb level before computing efficacy + nab_eff_pars = people.pars['NAb_eff'] # If vaccines are present, extract relevant information about them vacc_present = len(is_vacc) @@ -486,11 +482,11 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] current_NAbs = people.NAb[is_sus_vacc] - people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus') + people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus', **nab_eff_pars) if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain current_NAbs = people.NAb[is_sus_was_inf_same] - people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, strain], 'sus') + people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, strain], 'sus', **nab_eff_pars) if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain prior_strains = people.recovered_strain[is_sus_was_inf_diff] @@ -498,7 +494,7 @@ def check_immunity(people, strain, sus=True, inds=None): for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] current_NAbs = people.NAb[unique_inds] - people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus') + people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus', **nab_eff_pars) else: ### PART 2: @@ -510,13 +506,13 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] current_NAbs = people.NAb[is_inf_vacc] - people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['symp'][strain], 'symp') - people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['sev'][strain], 'sev') + people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['symp'][strain], 'symp', **nab_eff_pars) + people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['sev'][strain], 'sev', **nab_eff_pars) if len(was_inf): # Immunity for reinfected people current_NAbs = people.NAb[was_inf] - people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp') - people.sev_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['sev'][strain], 'sev') + people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp', **nab_eff_pars) + people.sev_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['sev'][strain], 'sev', **nab_eff_pars) return @@ -577,9 +573,9 @@ def pre_compute_waning(length, form='nab_decay', pars=None): def nab_decay(length, init_decay_rate, init_decay_time, decay_decay_rate): ''' - Returns an array of length 'length' containing the evaluated function NAb decay + Returns an array of length 'length' containing the evaluated NAb decay function at each point - Uses exponential decay, with the rate of exponential decay also set to exponentilly decay (!) after 250 days + Uses exponential decay, with the rate of exponential decay also set to exponentilly decay (!) after init_decay_time days ''' f1 = lambda t, init_decay_rate: np.exp(-t*init_decay_rate) diff --git a/covasim/parameters.py b/covasim/parameters.py index d96dd8031..e656d1c5d 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -68,6 +68,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source + pars['NAb_eff'] = {'slope': 2, 'n_50': 0.2, 'factors': {'sus': -0.01, 'symp': 0, 'sev': 0.01}} # Parameters to map NAbs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms pars['rel_imm']['asymptomatic'] = 0.5 diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 370293404..321ba0ad8 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -435,20 +435,20 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - if 0: + if 1: sim = cv.Sim() sim.run() # Run more complex single-sim tests sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - # - # # Run Vaccine tests - # sim4 = test_synthpops() - # sim5 = test_vaccine_1strain() - # + sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + + # Run Vaccine tests + sim4 = test_synthpops() + sim5 = test_vaccine_1strain() + # # # Run multisim and scenario tests # scens0 = test_vaccine_1strain_scen() # scens1 = test_vaccine_2strains_scen() From 40c9cd493dd0eddbb97dec7fef6e11dc0c2b65cf Mon Sep 17 00:00:00 2001 From: Romesh Abeysuriya Date: Tue, 30 Mar 2021 12:37:55 +1100 Subject: [PATCH 299/569] Add finalize method --- covasim/analysis.py | 9 +++++++++ covasim/interventions.py | 11 +++++++++++ covasim/sim.py | 12 ++++++++++++ 3 files changed, 32 insertions(+) diff --git a/covasim/analysis.py b/covasim/analysis.py index 4425fb1dc..60c0d36f1 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -45,6 +45,15 @@ def initialize(self, sim): self.initialized = True return + def finalize(self, sim): + ''' + Finalize analyzer + + This method is run once as part of `sim.finalize()` enabling the analyzer to perform any + final operations after the simulation is complete (e.g. rescaling) + ''' + self.finalized = True + return def apply(self, sim): ''' diff --git a/covasim/interventions.py b/covasim/interventions.py index 29d7dcd87..3df49df09 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -100,6 +100,7 @@ def __init__(self, label=None, show_label=True, do_plot=None, line_args=None): self.line_args = sc.mergedicts(dict(linestyle='--', c='#aaa', lw=1.0), line_args) # Do not set alpha by default due to the issue of overlapping interventions self.days = [] # The start and end days of the intervention self.initialized = False # Whether or not it has been initialized + self.finalized = False # Whether or not it has been initialized return @@ -150,6 +151,16 @@ def initialize(self, sim): self.initialized = True return + def finalize(self, sim): + ''' + Finalize intervention + + This method is run once as part of `sim.finalize()` enabling the intervention to perform any + final operations after the simulation is complete (e.g. rescaling) + ''' + self.finalized = True + return + def apply(self, sim): ''' diff --git a/covasim/sim.py b/covasim/sim.py index ab74fb931..84ec8bfec 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -426,6 +426,10 @@ def init_interventions(self): return + def finalize_interventions(self): + for intervention in self['interventions']: + if isinstance(intervention, cvi.Intervention): + intervention.finalize(self) def init_analyzers(self): ''' Initialize the analyzers ''' @@ -437,6 +441,10 @@ def init_analyzers(self): analyzer.initialize(self) return + def finalize_analyzers(self): + for analyzer in self['analyzers']: + if isinstance(analyzer, cva.Analyzer): + analyzer.finalize(self) def rescale(self): ''' Dynamically rescale the population -- used during step() ''' @@ -655,6 +663,10 @@ def finalize(self, verbose=None, restore_pars=True): self.results[f'cum_{key}'][:] = np.cumsum(self.results[f'new_{key}'][:]) self.results['cum_infections'].values += self['pop_infected']*self.rescale_vec[0] # Include initially infected people + # Finalize interventions and analyzers + self.finalize_interventions() + self.finalize_analyzers() + # Final settings self.results_ready = True # Set this first so self.summary() knows to print the results self.t -= 1 # During the run, this keeps track of the next step; restore this be the final day of the sim From 4b43662879bb9be9b598990a04803215fc896798 Mon Sep 17 00:00:00 2001 From: Romesh Abeysuriya Date: Tue, 30 Mar 2021 12:42:48 +1100 Subject: [PATCH 300/569] Move some commands into the finalize method --- covasim/analysis.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 60c0d36f1..e28186175 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -170,10 +170,9 @@ def apply(self, sim): date = self.dates[ind] self.snapshots[date] = sc.dcp(sim.people) # Take snapshot! - # On the final timestep, check that everything matches - if sim.t == sim.tvec[-1]: - validate_recorded_dates(sim, requested_dates=self.dates, recorded_dates=self.snapshots.keys(), die=self.die) - + def finalize(self, sim): + super().finalize(sim) + validate_recorded_dates(sim, requested_dates=self.dates, recorded_dates=self.snapshots.keys(), die=self.die) return @@ -294,10 +293,9 @@ def apply(self, sim): inds = sim.people.defined(f'date_{state}') # Pull out people for which this state is defined self.hists[date][state] = np.histogram(age[inds], bins=self.edges)[0]*scale # Actually count the people - # On the final timestep, check that everything matches - if sim.t == sim.tvec[-1]: - validate_recorded_dates(sim, requested_dates=self.dates, recorded_dates=self.hists.keys(), die=self.die) - + def finalize(self, sim): + super().finalize(sim) + validate_recorded_dates(sim, requested_dates=self.dates, recorded_dates=self.hists.keys(), die=self.die) return From 26421b5cb3bcc60768dffd90ca71582cdee23dd8 Mon Sep 17 00:00:00 2001 From: Romesh Abeysuriya Date: Tue, 30 Mar 2021 12:50:25 +1100 Subject: [PATCH 301/569] Add analyzer re-finalizing guard --- covasim/analysis.py | 4 ++++ covasim/interventions.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/covasim/analysis.py b/covasim/analysis.py index e28186175..db62b2ca7 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -35,6 +35,7 @@ def __init__(self, label=None): label = self.__class__.__name__ # Use the class name if no label is supplied self.label = label # e.g. "Record ages" self.initialized = False + self.finalized = False return @@ -43,6 +44,7 @@ def initialize(self, sim): Initialize the analyzer, e.g. convert date strings to integers. ''' self.initialized = True + self.initialized = False return def finalize(self, sim): @@ -52,6 +54,8 @@ def finalize(self, sim): This method is run once as part of `sim.finalize()` enabling the analyzer to perform any final operations after the simulation is complete (e.g. rescaling) ''' + if self.finalized: + raise Exception('Already finalized') # Raise an error because finalizing multiple times has a high probability of producing incorrect results e.g. applying rescale factors twice self.finalized = True return diff --git a/covasim/interventions.py b/covasim/interventions.py index 3df49df09..837c9ad5a 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -149,6 +149,7 @@ def initialize(self, sim): that can't be done until after the sim is created. ''' self.initialized = True + self.finalized = False return def finalize(self, sim): @@ -158,6 +159,8 @@ def finalize(self, sim): This method is run once as part of `sim.finalize()` enabling the intervention to perform any final operations after the simulation is complete (e.g. rescaling) ''' + if self.finalized: + raise Exception('Already finalized') # Raise an error because finalizing multiple times has a high probability of producing incorrect results e.g. applying rescale factors twice self.finalized = True return From 750bb62472fbf8c29f8c8458b9f3eaca08a3cc21 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 29 Mar 2021 22:02:56 -0400 Subject: [PATCH 302/569] a few quick fixes --- covasim/immunity.py | 5 +- covasim/parameters.py | 4 +- tests/devtests/test_variants.py | 99 ++++++++++++++++++++++++++++++--- 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index d4867a240..e9de23eb5 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -312,6 +312,7 @@ def init_nab(people, inds, prior_inf=True): no_prior_NAb_inds = np.setdiff1d(inds, prior_NAb_inds) # Find people without prior NAbs prior_NAb = people.NAb[prior_NAb_inds] # Array of NAb levels on this timestep for people with some NAbs + peak_NAb = people.init_NAb[prior_NAb_inds] # NAbs from infection if prior_inf: @@ -325,7 +326,7 @@ def init_nab(people, inds, prior_inf=True): # 2) Prior NAb: multiply existing NAb by boost factor if len(prior_NAb_inds): - init_NAb = prior_NAb * NAb_boost + init_NAb = peak_NAb * NAb_boost people.init_NAb[prior_NAb_inds] = init_NAb # NAbs from a vaccine @@ -338,7 +339,7 @@ def init_nab(people, inds, prior_inf=True): # 2) Prior NAb (from natural or vaccine dose 1): multiply existing NAb by boost factor if len(prior_NAb_inds): - init_NAb = prior_NAb * NAb_boost + init_NAb = peak_NAb * NAb_boost people.NAb[prior_NAb_inds] = init_NAb return diff --git a/covasim/parameters.py b/covasim/parameters.py index e656d1c5d..8c1e2a91c 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -71,8 +71,8 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['NAb_eff'] = {'slope': 2, 'n_50': 0.2, 'factors': {'sus': -0.01, 'symp': 0, 'sev': 0.01}} # Parameters to map NAbs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms - pars['rel_imm']['asymptomatic'] = 0.5 - pars['rel_imm']['mild'] = 0.85 + pars['rel_imm']['asymptomatic'] = 0.7 + pars['rel_imm']['mild'] = 0.9 pars['rel_imm']['severe'] = 1 pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py pars['vaccine_info'] = None # Vaccine info in a more easily accessible format diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 321ba0ad8..cdb1b50cf 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -8,10 +8,90 @@ do_plot = 1 -do_show = 0 +do_show = 1 do_save = 1 +def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): + sc.heading('Test varying properties of immunity') + sc.heading('Setting up...') + + # Define baseline parameters + base_pars = { + 'pop_size': 100000, + 'n_days': 400, + } + + n_runs = 3 + base_sim = cv.Sim(base_pars) + + # Define the scenarios + b1351 = cv.Strain('b1351', days=100, n_imports=20) + + scenarios = { + 'baseline': { + 'name': 'Default Immunity (decay at log(2)/90)', + 'pars': { + 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, + 'decay_decay_rate': 0.001}), + }, + }, + # 'slower_immunity': { + # 'name': 'Slower Immunity (decay at log(2)/150)', + # 'pars': { + # 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 150, 'init_decay_time': 250, + # 'decay_decay_rate': 0.001}), + # }, + # }, + 'faster_immunity': { + 'name': 'Faster Immunity (decay at log(2)/30)', + 'pars': { + 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, + 'decay_decay_rate': 0.001}), + }, + }, + 'baseline_b1351': { + 'name': 'Default Immunity (decay at log(2)/90), B1351 on day 100', + 'pars': { + 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, + 'decay_decay_rate': 0.001}), + 'strains': [b1351], + }, + }, + # 'slower_immunity_b1351': { + # 'name': 'Slower Immunity (decay at log(2)/150), B1351 on day 100', + # 'pars': { + # 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 150, 'init_decay_time': 250, + # 'decay_decay_rate': 0.001}), + # 'strains': [b1351], + # }, + # }, + 'faster_immunity_b1351': { + 'name': 'Faster Immunity (decay at log(2)/30), B1351 on day 100', + 'pars': { + 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, + 'decay_decay_rate': 0.001}), + 'strains': [b1351], + }, + }, + } + + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'New re-infections': ['new_reinfections'], + 'Population Nabs': ['pop_nabs'], + 'Population Immunity': ['pop_protection'], + }) + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_basic_immunity.png', to_plot=to_plot) + + return scens + + def test_import1strain(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing a new strain partway through a sim') sc.heading('Setting up...') @@ -435,19 +515,19 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - if 1: + if 0: sim = cv.Sim() sim.run() # Run more complex single-sim tests - sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) # Run Vaccine tests - sim4 = test_synthpops() - sim5 = test_vaccine_1strain() + # sim4 = test_synthpops() + # sim5 = test_vaccine_1strain() # # # Run multisim and scenario tests # scens0 = test_vaccine_1strain_scen() @@ -455,6 +535,9 @@ def get_ind_of_min_value(list, time): # scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) # msim0 = test_msim() + # Run immunity tests + sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) + sc.toc() From f57fcfba6d700a753635b303f4f2f3ef8c81d37c Mon Sep 17 00:00:00 2001 From: Romesh Abeysuriya Date: Tue, 30 Mar 2021 16:15:26 +1100 Subject: [PATCH 303/569] Use base class initialize() --- covasim/analysis.py | 2 +- covasim/interventions.py | 18 +++++++++--------- covasim/webapp/romesh_gunicorn.sh | 4 ++++ 3 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 covasim/webapp/romesh_gunicorn.sh diff --git a/covasim/analysis.py b/covasim/analysis.py index db62b2ca7..7eeecf008 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -55,7 +55,7 @@ def finalize(self, sim): final operations after the simulation is complete (e.g. rescaling) ''' if self.finalized: - raise Exception('Already finalized') # Raise an error because finalizing multiple times has a high probability of producing incorrect results e.g. applying rescale factors twice + raise Exception('Analyzer already finalized') # Raise an error because finalizing multiple times has a high probability of producing incorrect results e.g. applying rescale factors twice self.finalized = True return diff --git a/covasim/interventions.py b/covasim/interventions.py index 837c9ad5a..bd514bb3e 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -160,7 +160,7 @@ def finalize(self, sim): final operations after the simulation is complete (e.g. rescaling) ''' if self.finalized: - raise Exception('Already finalized') # Raise an error because finalizing multiple times has a high probability of producing incorrect results e.g. applying rescale factors twice + raise Exception('Intervention already finalized') # Raise an error because finalizing multiple times has a high probability of producing incorrect results e.g. applying rescale factors twice self.finalized = True return @@ -325,11 +325,11 @@ def __init__(self, days, interventions, **kwargs): def initialize(self, sim): ''' Fix the dates ''' + super().initialize(sim) self.days = [sim.day(day) for day in self.days] self.days_arr = np.array(self.days + [sim.npts]) for intervention in self.interventions: intervention.initialize(sim) - self.initialized = True return @@ -407,6 +407,7 @@ def __init__(self, days, changes, layers=None, **kwargs): def initialize(self, sim): ''' Fix days and store beta ''' + super().initialize(sim) self.days = process_days(sim, self.days) self.changes = process_changes(sim, self.changes, self.days) self.layers = sc.promotetolist(self.layers, keepnone=True) @@ -417,7 +418,6 @@ def initialize(self, sim): else: self.orig_betas[lkey] = sim['beta_layer'][lkey] - self.initialized = True return @@ -472,6 +472,7 @@ def __init__(self, days, changes, layers=None, **kwargs): def initialize(self, sim): + super().initialize(sim) self.days = process_days(sim, self.days) self.changes = process_changes(sim, self.changes, self.days) if self.layers is None: @@ -479,7 +480,6 @@ def initialize(self, sim): else: self.layers = sc.promotetolist(self.layers) self.contacts = cvb.Contacts(layer_keys=self.layers) - self.initialized = True return @@ -689,6 +689,8 @@ def initialize(self, sim): ''' Fix the dates and number of tests ''' # Handle days + super().initialize(sim) + self.start_day = sim.day(self.start_day) self.end_day = sim.day(self.end_day) self.days = [self.start_day, self.end_day] @@ -697,8 +699,6 @@ def initialize(self, sim): self.daily_tests = process_daily_data(self.daily_tests, sim, self.start_day) self.ili_prev = process_daily_data(self.ili_prev, sim, self.start_day) - self.initialized = True - return @@ -818,11 +818,11 @@ def __init__(self, symp_prob, asymp_prob=0.0, symp_quar_prob=None, asymp_quar_pr def initialize(self, sim): ''' Fix the dates ''' + super().initialize(sim) self.start_day = sim.day(self.start_day) self.end_day = sim.day(self.end_day) self.days = [self.start_day, self.end_day] self.ili_prev = process_daily_data(self.ili_prev, sim, self.start_day) - self.initialized = True return @@ -925,6 +925,7 @@ def __init__(self, trace_probs=None, trace_time=None, start_day=0, end_day=None, def initialize(self, sim): ''' Process the dates and dictionaries ''' + super().initialize(sim) self.start_day = sim.day(self.start_day) self.end_day = sim.day(self.end_day) self.days = [self.start_day, self.end_day] @@ -940,7 +941,6 @@ def initialize(self, sim): if sc.isnumber(self.trace_time): val = self.trace_time self.trace_time = {k:val for k in sim.people.layer_keys()} - self.initialized = True return @@ -1087,6 +1087,7 @@ def __init__(self, days, prob=1.0, rel_sus=0.0, rel_symp=0.0, subtarget=None, cu def initialize(self, sim): ''' Fix the dates and store the vaccinations ''' + super().initialize(sim) self.days = process_days(sim, self.days) self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person self.vaccination_dates = [[] for p in range(sim.n)] # Store the dates when people are vaccinated @@ -1094,7 +1095,6 @@ def initialize(self, sim): self.orig_symp_prob = sc.dcp(sim.people.symp_prob) # ...and symptom probability self.mod_rel_sus = np.ones(sim.n, dtype=cvd.default_float) # Store the final modifiers self.mod_symp_prob = np.ones(sim.n, dtype=cvd.default_float) # Store the final modifiers - self.initialized = True return diff --git a/covasim/webapp/romesh_gunicorn.sh b/covasim/webapp/romesh_gunicorn.sh new file mode 100644 index 000000000..4d1e8ebd1 --- /dev/null +++ b/covasim/webapp/romesh_gunicorn.sh @@ -0,0 +1,4 @@ +export MPLBACKEND=agg +gunicorn --reload --workers=2 --bind=127.0.0.1:8097 cova_app:flask_app + + From 5bef65de28cf7b56e7728ae7d4f15cb06149bdc1 Mon Sep 17 00:00:00 2001 From: Romesh Abeysuriya Date: Tue, 30 Mar 2021 16:17:42 +1100 Subject: [PATCH 304/569] Fix typo --- covasim/analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 7eeecf008..059ec4f71 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -44,7 +44,7 @@ def initialize(self, sim): Initialize the analyzer, e.g. convert date strings to integers. ''' self.initialized = True - self.initialized = False + self.finalized = False return def finalize(self, sim): From 24e0382e28f2d1c221b0c5f5442026ed42486f7b Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 30 Mar 2021 10:34:49 -0400 Subject: [PATCH 305/569] params from Khoury et al --- covasim/defaults.py | 1 + covasim/immunity.py | 2 +- covasim/parameters.py | 2 +- covasim/sim.py | 3 +++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 4e850efc3..f25f8a055 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -220,6 +220,7 @@ def get_colors(): c.default = '#000000' c.pop_nabs = '#32733d' c.pop_protection = '#9e1149' + c.pop_symp_protection = '#b86113' return c diff --git a/covasim/immunity.py b/covasim/immunity.py index e9de23eb5..46581d238 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -381,7 +381,7 @@ def nab_to_efficacy(nab, ax, slope, n_50, factors): raise ValueError(errormsg) # put in here nab to efficacy mapping (logistic regression from fig 1a from https://doi.org/10.1101/2021.03.09.21252641) - efficacy = 1/(1+np.exp(-slope*(nab - n_50 + factors[ax]))) # from logistic regression computed in R using data from Khoury et al + efficacy = 1/(1+np.exp(-slope*(np.log10(nab) - np.log10(n_50) - np.log10(factors[ax])))) # from logistic regression computed in R using data from Khoury et al return efficacy diff --git a/covasim/parameters.py b/covasim/parameters.py index 8c1e2a91c..7892c56c2 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -68,7 +68,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source - pars['NAb_eff'] = {'slope': 2, 'n_50': 0.2, 'factors': {'sus': -0.01, 'symp': 0, 'sev': 0.01}} # Parameters to map NAbs to efficacy + pars['NAb_eff'] = {'slope': 3.43297265, 'n_50': 0.19869944, 'factors': {'sus': 3, 'symp': 0, 'sev': 0.5}} # Parameters to map NAbs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms pars['rel_imm']['asymptomatic'] = 0.7 diff --git a/covasim/sim.py b/covasim/sim.py index fc4187784..174631bb9 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -304,6 +304,8 @@ def init_res(*args, **kwargs): self.results['share_vaccinated'] = init_res('Share Vaccinated', scale=False) self.results['pop_nabs'] = init_res('Population average NAb levels', scale=False, color=dcols.pop_nabs) self.results['pop_protection'] = init_res('Population average immunity protection', scale=False, color=dcols.pop_protection) + self.results['pop_symp_protection'] = init_res('Population average symptomatic immunity protection', scale=False, + color=dcols.pop_symp_protection) # Populate the rest of the results if self['rescale']: @@ -632,6 +634,7 @@ def step(self): # Update NAb and immunity for this time step self.results['pop_nabs'][t] = np.sum(people.NAb[cvu.defined(people.NAb)])/len(people) self.results['pop_protection'][t] = np.nanmean(people.sus_imm) + self.results['pop_symp_protection'][t] = np.nanmean(people.symp_imm) # Apply analyzers -- same syntax as interventions for i,analyzer in enumerate(self['analyzers']): From ff6e633491b1b0c1e09a3090cf281f9ff3a4f4a5 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 30 Mar 2021 22:03:52 -0400 Subject: [PATCH 306/569] manaus! WIP --- covasim/immunity.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/covasim/immunity.py b/covasim/immunity.py index 46581d238..fa03057b6 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -88,6 +88,9 @@ def parse_strain_pars(self, strain=None, strain_label=None): # Known parameters on Brazil variant elif strain in choices['p1']: strain_pars = dict() + strain_pars['rel_beta'] = 1.4 + strain_pars['rel_severe_prob'] = 1.4 + strain_pars['rel_death_prob'] = 1.4 strain_pars['rel_imm'] = 0.5 self.strain_label = strain From 587886d9c8d626658f238acda6c43fcbee984cd1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 30 Mar 2021 20:41:59 -0700 Subject: [PATCH 307/569] unpin line_profiler --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7773625e2..acc074fbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,4 @@ statsmodels matplotlib pandas xlrd==1.2.0 -line_profiler==3.1 sciris>=1.0.0 From 9b156ca2c5bb728fc92697db0c6511d376e3435d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 30 Mar 2021 22:00:47 -0700 Subject: [PATCH 308/569] undo intervention label change --- covasim/interventions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 29d7dcd87..4c4ac3a50 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -91,8 +91,6 @@ class Intervention: line_args (dict): arguments passed to pl.axvline() when plotting ''' def __init__(self, label=None, show_label=True, do_plot=None, line_args=None): - if label is None: - label = self.__class__.__name__ # Use the class name if no label is supplied self._store_args() # Store the input arguments so the intervention can be recreated self.label = label # e.g. "Close schools" self.show_label = show_label # Show the label by default @@ -445,7 +443,7 @@ class clip_edges(Intervention): **Examples**:: interv = cv.clip_edges(25, 0.3) # On day 25, reduce overall contacts by 70% to 0.3 - interv = cv.clip_edges([14, 28], [0.7, 1], layers='w') # On day 14, remove 30% of school contacts, and on day 28, restore them + interv = cv.clip_edges([14, 28], [0.7, 1], layers='s') # On day 14, remove 30% of school contacts, and on day 28, restore them ''' def __init__(self, days, changes, layers=None, **kwargs): From 6d7b565029fc64a5c7e73a12ea3fef8fda1d924d Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 31 Mar 2021 12:49:05 -0400 Subject: [PATCH 309/569] manaus and khoury updates --- covasim/immunity.py | 12 ++++++++---- covasim/parameters.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index fa03057b6..949aba99a 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -89,8 +89,10 @@ def parse_strain_pars(self, strain=None, strain_label=None): elif strain in choices['p1']: strain_pars = dict() strain_pars['rel_beta'] = 1.4 - strain_pars['rel_severe_prob'] = 1.4 - strain_pars['rel_death_prob'] = 1.4 + strain_pars['rel_severe_prob'] = 1.7 + strain_pars['rel_death_prob'] = 2.5 + strain_pars['dur'] = dict() + strain_pars['dur']['crit2die'] = dict(dist='lognormal_int', par1=3, par2=2) strain_pars['rel_imm'] = 0.5 self.strain_label = strain @@ -368,7 +370,7 @@ def check_nab(t, people, inds=None): return -def nab_to_efficacy(nab, ax, slope, n_50, factors): +def nab_to_efficacy(nab, ax, slope, n_50): ''' Convert NAb levels to immunity protection factors, using the functional form given in this paper: https://doi.org/10.1101/2021.03.09.21252641 @@ -383,8 +385,10 @@ def nab_to_efficacy(nab, ax, slope, n_50, factors): errormsg = f'Choice provided not in list of choices' raise ValueError(errormsg) + n_50 = n_50[ax] # put in here nab to efficacy mapping (logistic regression from fig 1a from https://doi.org/10.1101/2021.03.09.21252641) - efficacy = 1/(1+np.exp(-slope*(np.log10(nab) - np.log10(n_50) - np.log10(factors[ax])))) # from logistic regression computed in R using data from Khoury et al + efficacy = 1/(1+np.exp(-slope*(np.log10(nab) - np.log10(n_50)))) # from logistic regression in Khoury et al + # efficacy = 1/(1+np.exp(-(slope*np.log10(2))*(nab - np.log2(n_50)))) # from logistic regression in Khoury et al return efficacy diff --git a/covasim/parameters.py b/covasim/parameters.py index 7892c56c2..765115972 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -68,7 +68,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source - pars['NAb_eff'] = {'slope': 3.43297265, 'n_50': 0.19869944, 'factors': {'sus': 3, 'symp': 0, 'sev': 0.5}} # Parameters to map NAbs to efficacy + pars['NAb_eff'] = {'slope': 3.43297265, 'n_50': {'sus': 0.5, 'symp': 0.19869944, 'sev': 0.031}} # Parameters to map NAbs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms pars['rel_imm']['asymptomatic'] = 0.7 From 93a0d280a65c8b0a4ba36d098be12049d84031d6 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 31 Mar 2021 13:08:28 -0700 Subject: [PATCH 310/569] change install method --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 1af278dfc..cab8a59b7 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -19,7 +19,7 @@ jobs: python-version: ${{ matrix.python-version }} architecture: x64 - name: Install Covasim - run: python setup.py develop + run: pip install -e . - name: Install tests run: pip install pytest - name: Run integration tests From cb2c5cac148ed18924db0488a3868ec9af22e1f4 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 31 Mar 2021 13:32:12 -0700 Subject: [PATCH 311/569] update setup instructions --- .readthedocs.yml | 2 +- README.rst | 4 ++-- docs/requirements.txt | 3 +-- requirements.txt | 2 +- setup.py | 28 +++++++++++++--------------- 5 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 9f258f6bb..7e36f3990 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -24,7 +24,7 @@ python: version: 3.8 install: - requirements: docs/requirements.txt - - method: setuptools + - method: pip path: . system_packages: true diff --git a/README.rst b/README.rst index a66777855..ad0e3d2d5 100644 --- a/README.rst +++ b/README.rst @@ -89,11 +89,11 @@ If you would rather download the source code rather than using the ``pip`` packa * For normal installation (recommended):: - python setup.py develop + pip install -e . * To install Covasim and optional dependencies (be aware this may fail since it relies on nonstandard packages):: - python setup.py develop full + pip install -e .[full] The module should then be importable via ``import covasim as cv``. diff --git a/docs/requirements.txt b/docs/requirements.txt index 77934b658..d528c3f20 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -7,5 +7,4 @@ ipykernel nbsphinx pandoc pypandoc -optuna -numba==0.48 \ No newline at end of file +optuna \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index acc074fbd..f944675d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ numpy numba +pandas scipy statsmodels matplotlib -pandas xlrd==1.2.0 sciris>=1.0.0 diff --git a/setup.py b/setup.py index e98aeaef2..a7b63025d 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ ''' Covasim installation. Requirements are listed in requirements.txt. There are two options: - python setup.py develop # standard install, does not include optional libraries - python setup.py develop full # full install, including optional libraries (NB: these libraries are not available publicly yet) + pip install -e . # Standard install, does not include optional libraries + pip install -e .[full] # Full install, including optional libraries ''' import os @@ -14,18 +14,6 @@ with open('requirements.txt') as f: requirements = f.read().splitlines() -if 'full' in sys.argv: - print('Performing full installation, including optional dependencies') - sys.argv.remove('full') - full_reqs = [ - 'plotly', - 'fire', - 'optuna', - 'synthpops', - 'parestlib', - ] - requirements.extend(full_reqs) - # Get version cwd = os.path.abspath(os.path.dirname(__file__)) versionpath = os.path.join(cwd, 'covasim', 'version.py') @@ -43,6 +31,7 @@ "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules", "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ] @@ -55,11 +44,20 @@ long_description=long_description, long_description_content_type="text/x-rst", url='http://covasim.org', - keywords=["Covid-19", "coronavirus", "SARS-CoV-2", "stochastic", "agent-based model", "interventions", "epidemiology"], + keywords=["COVID", "COVID-19", "coronavirus", "SARS-CoV-2", "stochastic", "agent-based model", "interventions", "epidemiology"], platforms=["OS Independent"], classifiers=CLASSIFIERS, packages=find_packages(), include_package_data=True, install_requires=requirements + extras_require={ + 'full': [ + 'plotly', + 'fire', + 'optuna', + 'synthpops', + 'parestlib', + ], + } ) From 37769e231b35bb1781b9fb2277f616bf69d80b6f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 31 Mar 2021 13:35:08 -0700 Subject: [PATCH 312/569] update test workflow --- .github/workflows/tests.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index cab8a59b7..a90fb0a88 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -24,7 +24,7 @@ jobs: run: pip install pytest - name: Run integration tests working-directory: ./tests - run: pytest test_*.py --durations=0 + run: pytest -v test_*.py --durations=0 - name: Run unit tests working-directory: ./tests/unittests - run: pytest test_*.py --durations=0 + run: pytest -v test_*.py --durations=0 From 3c6ab3b33425a3e3f812de1c6733c7d4efd834cb Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 31 Mar 2021 13:36:43 -0700 Subject: [PATCH 313/569] update setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a7b63025d..5b2257c64 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ classifiers=CLASSIFIERS, packages=find_packages(), include_package_data=True, - install_requires=requirements + install_requires=requirements, extras_require={ 'full': [ 'plotly', From c90594da6eae606fdda62156ed282d8230c2d8ba Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 31 Mar 2021 13:44:19 -0700 Subject: [PATCH 314/569] updating changelog --- CHANGELOG.rst | 13 +++++++++++-- covasim/misc.py | 2 +- covasim/webapp/romesh_gunicorn.sh | 4 ---- 3 files changed, 12 insertions(+), 7 deletions(-) delete mode 100644 covasim/webapp/romesh_gunicorn.sh diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9062ace11..d2b7a0b11 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,10 +25,19 @@ These are the major improvements we are currently working on. If there is a spec Latest versions (2.x) ~~~~~~~~~~~~~~~~~~~~~ -Version 2.1.1 (2021-03-29) +Version 2.1.2 (2021-03-31) -------------------------- -This is the last release before the Covasim 3.0 launch (vaccines and variants). +This is (probably) the last release before the Covasim 3.0 launch (vaccines and variants). + +- Added a ``finalize()`` method to interventions and analyzers, to replace the ``if sim.t == sim.npts-1:`` blocks in ``apply()`` that had been being used to finalize. +- Changed setup instructions from ``python setup.py develop`` to ``pip install -e .``, and unpinned ``line_profiler``. +- *Regression information*: If you have any scripts/workflows that have been using ``python setup.py develop``, please update them to ``pip install -e .``. Likewise, ``python setup.py develop`` is now ``pip install -e .[full]``. +- *GitHub info*: PR `897 `__ + + +Version 2.1.1 (2021-03-29) +-------------------------- - **Duration updates:** All duration parameters have been updated from the literature. While most are similar to what they were before, there are some differences: in particular, durations of severe and critical disease (either to recovery or death) have increased; for example, duration from symptom onset to death has increased from 15.8±3.8 days to 18.8±7.2 days. - **Performance updates:** The innermost loop of Covasim, ``cv.compute_infections()``, has been refactored to make more efficient use of array indexing. The observed difference will depend on the nature of the simulation (e.g., network type, interventions), but runs may be up to 1.5x faster now. diff --git a/covasim/misc.py b/covasim/misc.py index 2a4c9379e..de7f15891 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -502,7 +502,7 @@ def get_version_pars(version, verbose=True): '1.4.0': [f'1.4.{i}' for i in range(9)], '1.5.0': [f'1.5.{i}' for i in range(4)] + [f'1.6.{i}' for i in range(2)] + [f'1.7.{i}' for i in range(7)], '2.0.0': [f'2.0.{i}' for i in range(5)] + ['2.1.0'], - '2.1.1': ['2.1.1'], + '2.1.1': [f'2.1.{i}' for i in range(1,3)], } # Find and check the match diff --git a/covasim/webapp/romesh_gunicorn.sh b/covasim/webapp/romesh_gunicorn.sh deleted file mode 100644 index 4d1e8ebd1..000000000 --- a/covasim/webapp/romesh_gunicorn.sh +++ /dev/null @@ -1,4 +0,0 @@ -export MPLBACKEND=agg -gunicorn --reload --workers=2 --bind=127.0.0.1:8097 cova_app:flask_app - - From 0c44a9cfcb981cf65e6dd258e8340d3772733fc7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 31 Mar 2021 14:55:48 -0700 Subject: [PATCH 315/569] add trigger options --- covasim/interventions.py | 327 ++++++++++++++++++++++----------------- 1 file changed, 185 insertions(+), 142 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 3ff413528..03e22aabf 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -21,20 +21,27 @@ __all__ = ['InterventionDict', 'Intervention', 'dynamic_pars', 'sequence'] -def find_day(arr, t=None, which='first'): +def find_day(arr, t=None, interv=None, sim=None, which='first'): ''' Helper function to find if the current simulation time matches any day in the intervention. Although usually never more than one index is returned, it is returned as a list for the sake of easy iteration. Args: - arr (list): list of days in the intervention, or else a boolean array + arr (list/function): list of days in the intervention, or a boolean array; or a function that returns these t (int): current simulation time (can be None if a boolean array is used) which (str): what to return: 'first', 'last', or 'all' indices + interv (intervention): the intervention object (usually self); only used if arr is callable + sim (sim): the simulation object; only used if arr is callable Returns: inds (list): list of matching days; length zero or one unless which is 'all' + + New in version 2.1.2: arr can be a function with arguments interv and sim. ''' + if callable(arr): + arr = arr(interv, sim) + arr = sc.promotetoarray(arr) all_inds = sc.findinds(arr=arr, val=t) if len(all_inds) == 0 or which == 'all': inds = all_inds @@ -48,6 +55,139 @@ def find_day(arr, t=None, which='first'): return inds +def preprocess_day(day, sim): + ''' + Preprocess a day: leave it as-is if it's a function, or try to convert it to + an integer if it's anything else. + ''' + if callable(day): # If it's callable, leave it as-is + return day + else: + day = sim.day(day) # Otherwise, convert it to an int + return day + + +def get_day(day, interv=None, sim=None): + ''' + Return the day if it's an integer, or call it if it's a function. + ''' + if callable(day): + return day(interv, sim) # If it's callable, call it + else: + return day # Otherwise, leave it as-is + + +def process_days(sim, days, return_dates=False): + ''' + Ensure lists of days are in consistent format. Used by change_beta, clip_edges, + and some analyzers. If day is 'end' or -1, use the final day of the simulation. + Optionally return dates as well as days. If days is callable, leave unchanged. + ''' + if callable(days): + return days + if sc.isstring(days) or not sc.isiterable(days): + days = sc.promotetolist(days) + for d,day in enumerate(days): + if day in ['end', -1]: + day = sim['end_day'] + days[d] = preprocess_day(day, sim) # Ensure it's an integer and not a string or something + days = np.sort(sc.promotetoarray(days)) # Ensure they're an array and in order + if return_dates: + dates = [sim.date(day) for day in days] # Store as date strings + return days, dates + else: + return days + + +def process_changes(sim, changes, days): + ''' + Ensure lists of changes are in consistent format. Used by change_beta and clip_edges. + ''' + changes = sc.promotetoarray(changes) + if sc.isiterable(days) and len(days) != len(changes): # pragma: no cover + errormsg = f'Number of days supplied ({len(days)}) does not match number of changes ({len(changes)})' + raise ValueError(errormsg) + return changes + + +def process_daily_data(daily_data, sim, start_day, as_int=False): + ''' + This function performs one of three things: if the daily test data are supplied as + a number, then it converts it to an array of the right length. If the daily + data are supplied as a Pandas series or dataframe with a date index, then it + reindexes it to match the start date of the simulation. If the daily data are + supplied as a string, then it will convert it to a column and try to read from + that. Otherwise, it does nothing. + + Args: + daily_data (str, number, dataframe, or series): the data to convert to standardized format + sim (Sim): the simulation object + start_day (date): the start day of the simulation, in already-converted datetime.date format + as_int (bool): whether to convert to an integer + ''' + # Handle string arguments + if sc.isstring(daily_data): + if daily_data == 'data': + daily_data = sim.data['new_tests'] # Use default name + else: + try: # pragma: no cover + daily_data = sim.data[daily_data] + except Exception as E: + errormsg = f'Tried to load testing data from sim.data["{daily_data}"], but that failed: {str(E)}.\nPlease ensure data are loaded into the sim and the column exists.' + raise ValueError(errormsg) from E + + # Handle other arguments + if sc.isnumber(daily_data): # If a number, convert to an array + if as_int: daily_data = int(daily_data) # Make it an integer + daily_data = np.array([daily_data] * sim.npts) + elif isinstance(daily_data, (pd.Series, pd.DataFrame)): + start_date = sim['start_day'] + dt.timedelta(days=start_day) + end_date = daily_data.index[-1] + dateindex = pd.date_range(start_date, end_date) + daily_data = daily_data.reindex(dateindex, fill_value=0).to_numpy() + + return daily_data + + +def get_subtargets(subtarget, sim): + ''' + A small helper function to see if subtargeting is a list of indices to use, + or a function that needs to be called. If a function, it must take a single + argument, a sim object, and return a list of indices. Also validates the values. + Currently designed for use with testing interventions, but could be generalized + to other interventions. Not typically called directly by the user. + + Args: + subtarget (dict): dict with keys 'inds' and 'vals'; see test_num() for examples of a valid subtarget dictionary + sim (Sim): the simulation object + ''' + + # Validation + if callable(subtarget): + subtarget = subtarget(sim) + + if 'inds' not in subtarget: # pragma: no cover + errormsg = f'The subtarget dict must have keys "inds" and "vals", but you supplied {subtarget}' + raise ValueError(errormsg) + + # Handle the two options of type + if callable(subtarget['inds']): # A function has been provided + subtarget_inds = subtarget['inds'](sim) # Call the function to get the indices + else: + subtarget_inds = subtarget['inds'] # The indices are supplied directly + + # Validate the values + if callable(subtarget['vals']): # A function has been provided + subtarget_vals = subtarget['vals'](sim) # Call the function to get the indices + else: + subtarget_vals = subtarget['vals'] # The indices are supplied directly + if sc.isiterable(subtarget_vals): + if len(subtarget_vals) != len(subtarget_inds): # pragma: no cover + errormsg = f'Length of subtargeting indices ({len(subtarget_inds)}) does not match length of values ({len(subtarget_vals)})' + raise ValueError(errormsg) + + return subtarget_inds, subtarget_vals + def InterventionDict(which, pars): ''' Generate an intervention from a dictionary. Although a function, it acts @@ -198,13 +338,14 @@ def plot_intervention(self, sim, ax=None, **kwargs): if self.do_plot or self.do_plot is None: if ax is None: ax = pl.gca() - for day in self.days: - if day is not None: - if self.show_label: # Choose whether to include the label in the legend - label = self.label - else: - label = None - ax.axvline(day, label=label, **line_args) + if sc.isiterable(self.days): + for day in self.days: + if day is not None: + if self.show_label: # Choose whether to include the label in the legend + label = self.label + else: + label = None + ax.axvline(day, label=label, **line_args) return @@ -274,10 +415,13 @@ def __init__(self, pars=None, **kwargs): raise sc.KeyNotFoundError(errormsg) if sc.isnumber(pars[parkey][subkey]): # Allow scalar values or dicts, but leave everything else unchanged pars[parkey][subkey] = sc.promotetoarray(pars[parkey][subkey]) - len_days = len(pars[parkey]['days']) - len_vals = len(pars[parkey]['vals']) - if len_days != len_vals: # pragma: no cover - raise ValueError(f'Length of days ({len_days}) does not match length of values ({len_vals}) for parameter {parkey}') + days = pars[parkey]['days'] + vals = pars[parkey]['vals'] + if sc.isiterable(days): + len_days = len(days) + len_vals = len(vals) + if len_days != len_vals: # pragma: no cover + raise ValueError(f'Length of days ({len_days}) does not match length of values ({len_vals}) for parameter {parkey}') self.pars = pars return @@ -286,7 +430,7 @@ def apply(self, sim): ''' Loop over the parameters, and then loop over the days, applying them if any are found ''' t = sim.t for parkey,parval in self.pars.items(): - for ind in find_day(parval['days'], t): + for ind in find_day(parval['days'], t, interv=self, sim=sim): self.days.append(t) val = parval['vals'][ind] if isinstance(val, dict): @@ -311,6 +455,8 @@ class sequence(Intervention): cv.test_num(n_tests=[100]*npts), cv.test_prob(symptomatic_prob=0.2, asymptomatic_prob=0.002), ]) + + Note: callable days are not supported. ''' def __init__(self, days, interventions, **kwargs): @@ -324,7 +470,7 @@ def __init__(self, days, interventions, **kwargs): def initialize(self, sim): ''' Fix the dates ''' super().initialize(sim) - self.days = [sim.day(day) for day in self.days] + self.days = [preprocess_day(day, sim) for day in self.days] self.days_arr = np.array(self.days + [sim.npts]) for intervention in self.interventions: intervention.initialize(sim) @@ -343,37 +489,6 @@ def apply(self, sim): __all__+= ['change_beta', 'clip_edges'] -def process_days(sim, days, return_dates=False): - ''' - Ensure lists of days are in consistent format. Used by change_beta, clip_edges, - and some analyzers. If day is 'end' or -1, use the final day of the simulation. - Optionally return dates as well as days. - ''' - if sc.isstring(days) or not sc.isiterable(days): - days = sc.promotetolist(days) - for d,day in enumerate(days): - if day in ['end', -1]: - day = sim['end_day'] - days[d] = sim.day(day) # Ensure it's an integer and not a string or something - days = np.sort(sc.promotetoarray(days)) # Ensure they're an array and in order - if return_dates: - dates = [sim.date(day) for day in days] # Store as date strings - return days, dates - else: - return days - - -def process_changes(sim, changes, days): - ''' - Ensure lists of changes are in consistent format. Used by change_beta and clip_edges. - ''' - changes = sc.promotetoarray(changes) - if len(days) != len(changes): # pragma: no cover - errormsg = f'Number of days supplied ({len(days)}) does not match number of changes ({len(changes)})' - raise ValueError(errormsg) - return changes - - class change_beta(Intervention): ''' The most basic intervention -- change beta (transmission) by a certain amount @@ -422,7 +537,7 @@ def initialize(self, sim): def apply(self, sim): # If this day is found in the list, apply the intervention - for ind in find_day(self.days, sim.t): + for ind in find_day(self.days, sim.t, interv=self, sim=sim): for lkey,new_beta in self.orig_betas.items(): new_beta = new_beta * self.changes[ind] if lkey == 'overall': @@ -484,7 +599,7 @@ def initialize(self, sim): def apply(self, sim): # If this day is found in the list, apply the intervention - for ind in find_day(self.days, sim.t): + for ind in find_day(self.days, sim.t, interv=self, sim=sim): # Do the contact moving for lkey in self.layers: @@ -523,85 +638,6 @@ def apply(self, sim): __all__+= ['test_num', 'test_prob', 'contact_tracing'] -def process_daily_data(daily_data, sim, start_day, as_int=False): - ''' - This function performs one of three things: if the daily test data are supplied as - a number, then it converts it to an array of the right length. If the daily - data are supplied as a Pandas series or dataframe with a date index, then it - reindexes it to match the start date of the simulation. If the daily data are - supplied as a string, then it will convert it to a column and try to read from - that. Otherwise, it does nothing. - - Args: - daily_data (str, number, dataframe, or series): the data to convert to standardized format - sim (Sim): the simulation object - start_day (date): the start day of the simulation, in already-converted datetime.date format - as_int (bool): whether to convert to an integer - ''' - # Handle string arguments - if sc.isstring(daily_data): - if daily_data == 'data': - daily_data = sim.data['new_tests'] # Use default name - else: - try: # pragma: no cover - daily_data = sim.data[daily_data] - except Exception as E: - errormsg = f'Tried to load testing data from sim.data["{daily_data}"], but that failed: {str(E)}.\nPlease ensure data are loaded into the sim and the column exists.' - raise ValueError(errormsg) from E - - # Handle other arguments - if sc.isnumber(daily_data): # If a number, convert to an array - if as_int: daily_data = int(daily_data) # Make it an integer - daily_data = np.array([daily_data] * sim.npts) - elif isinstance(daily_data, (pd.Series, pd.DataFrame)): - start_date = sim['start_day'] + dt.timedelta(days=start_day) - end_date = daily_data.index[-1] - dateindex = pd.date_range(start_date, end_date) - daily_data = daily_data.reindex(dateindex, fill_value=0).to_numpy() - - return daily_data - - -def get_subtargets(subtarget, sim): - ''' - A small helper function to see if subtargeting is a list of indices to use, - or a function that needs to be called. If a function, it must take a single - argument, a sim object, and return a list of indices. Also validates the values. - Currently designed for use with testing interventions, but could be generalized - to other interventions. Not typically called directly by the user. - - Args: - subtarget (dict): dict with keys 'inds' and 'vals'; see test_num() for examples of a valid subtarget dictionary - sim (Sim): the simulation object - ''' - - # Validation - if callable(subtarget): - subtarget = subtarget(sim) - - if 'inds' not in subtarget: # pragma: no cover - errormsg = f'The subtarget dict must have keys "inds" and "vals", but you supplied {subtarget}' - raise ValueError(errormsg) - - # Handle the two options of type - if callable(subtarget['inds']): # A function has been provided - subtarget_inds = subtarget['inds'](sim) # Call the function to get the indices - else: - subtarget_inds = subtarget['inds'] # The indices are supplied directly - - # Validate the values - if callable(subtarget['vals']): # A function has been provided - subtarget_vals = subtarget['vals'](sim) # Call the function to get the indices - else: - subtarget_vals = subtarget['vals'] # The indices are supplied directly - if sc.isiterable(subtarget_vals): - if len(subtarget_vals) != len(subtarget_inds): # pragma: no cover - errormsg = f'Length of subtargeting indices ({len(subtarget_inds)}) does not match length of values ({len(subtarget_vals)})' - raise ValueError(errormsg) - - return subtarget_inds, subtarget_vals - - def get_quar_inds(quar_policy, sim): ''' Helper function to return the appropriate indices for people in quarantine @@ -689,8 +725,8 @@ def initialize(self, sim): # Handle days super().initialize(sim) - self.start_day = sim.day(self.start_day) - self.end_day = sim.day(self.end_day) + self.start_day = preprocess_day(self.start_day, sim) + self.end_day = preprocess_day(self.end_day, sim) self.days = [self.start_day, self.end_day] # Process daily data @@ -703,13 +739,15 @@ def initialize(self, sim): def apply(self, sim): t = sim.t - if t < self.start_day: + start_day = get_day(self.start_day, self, sim) + end_day = get_day(self.end_day, self, sim) + if t < start_day: return - elif self.end_day is not None and t > self.end_day: + elif end_day is not None and t > end_day: return # Check that there are still tests - rel_t = t - self.start_day + rel_t = t - start_day if rel_t < len(self.daily_tests): n_tests = sc.randround(self.daily_tests[rel_t]/sim.rescale_vec[t]) # Correct for scaling that may be applied by rounding to the nearest number of tests if not (n_tests and pl.isfinite(n_tests)): # If there are no tests today, abort early @@ -817,8 +855,8 @@ def __init__(self, symp_prob, asymp_prob=0.0, symp_quar_prob=None, asymp_quar_pr def initialize(self, sim): ''' Fix the dates ''' super().initialize(sim) - self.start_day = sim.day(self.start_day) - self.end_day = sim.day(self.end_day) + self.start_day = preprocess_day(self.start_day, sim) + self.end_day = preprocess_day(self.end_day, sim) self.days = [self.start_day, self.end_day] self.ili_prev = process_daily_data(self.ili_prev, sim, self.start_day) return @@ -826,10 +864,13 @@ def initialize(self, sim): def apply(self, sim): ''' Perform testing ''' + t = sim.t - if t < self.start_day: + start_day = get_day(self.start_day, self, sim) + end_day = get_day(self.end_day, self, sim) + if t < start_day: return - elif self.end_day is not None and t > self.end_day: + elif end_day is not None and t > end_day: return # Find probablity for symptomatics to be tested @@ -849,7 +890,7 @@ def apply(self, sim): pop_size = sim['pop_size'] ili_inds = [] if self.ili_prev is not None: - rel_t = t - self.start_day + rel_t = t - start_day if rel_t < len(self.ili_prev): n_ili = int(self.ili_prev[rel_t] * pop_size) # Number with ILI symptoms on this day ili_inds = cvu.choose(pop_size, n_ili) # Give some people some symptoms, assuming that this is independent of COVID symptomaticity... @@ -924,8 +965,8 @@ def __init__(self, trace_probs=None, trace_time=None, start_day=0, end_day=None, def initialize(self, sim): ''' Process the dates and dictionaries ''' super().initialize(sim) - self.start_day = sim.day(self.start_day) - self.end_day = sim.day(self.end_day) + self.start_day = preprocess_day(self.start_day, sim) + self.end_day = preprocess_day(self.end_day, sim) self.days = [self.start_day, self.end_day] if self.trace_probs is None: self.trace_probs = 1.0 @@ -954,9 +995,11 @@ def apply(self, sim): - Notify those contacts that they have been exposed and need to take some action ''' t = sim.t - if t < self.start_day: + start_day = get_day(self.start_day, self, sim) + end_day = get_day(self.end_day, self, sim) + if t < start_day: return - elif self.end_day is not None and t > self.end_day: + elif end_day is not None and t > end_day: return trace_inds = self.select_cases(sim) @@ -1100,7 +1143,7 @@ def apply(self, sim): ''' Perform vaccination ''' # If this day is found in the list, apply the intervention - for ind in find_day(self.days, sim.t): + for ind in find_day(self.days, sim.t, interv=self, sim=sim): # Construct the testing probabilities piece by piece -- complicated, since need to do it in the right order vacc_probs = np.full(sim.n, self.prob) # Begin by assigning equal testing probability to everyone From 3bdebad8fca021fc4ba0a7be72badfaa42d7b041 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 31 Mar 2021 18:26:46 -0700 Subject: [PATCH 316/569] update docs --- CHANGELOG.rst | 1 + FAQ.rst | 8 ++--- covasim/analysis.py | 10 +++--- covasim/interventions.py | 50 +++++++++++++++++------------ covasim/people.py | 8 +++-- covasim/sim.py | 2 +- docs/index.rst | 2 +- docs/tutorials/t02.ipynb | 34 +++++++++++++++++++- docs/tutorials/t03.ipynb | 23 +++++++++++-- docs/tutorials/t04.ipynb | 11 +++++++ docs/tutorials/t05.ipynb | 29 ++++++++++++++--- docs/tutorials/t06.ipynb | 2 +- examples/t05_custom_intervention.py | 4 +-- 13 files changed, 142 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d2b7a0b11..b8d3bb884 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -30,6 +30,7 @@ Version 2.1.2 (2021-03-31) This is (probably) the last release before the Covasim 3.0 launch (vaccines and variants). +- Interventions and analyzers now accept a function as an argument to ``days`` or e.g. ``start_day``. For example, instead of defining ``start_day=30``, you can define a function (with the intervention and the sim object as arguments) that calculates and returns a start day. This allows interventions to be dynamically triggered based on the state of the sim. - Added a ``finalize()`` method to interventions and analyzers, to replace the ``if sim.t == sim.npts-1:`` blocks in ``apply()`` that had been being used to finalize. - Changed setup instructions from ``python setup.py develop`` to ``pip install -e .``, and unpinned ``line_profiler``. - *Regression information*: If you have any scripts/workflows that have been using ``python setup.py develop``, please update them to ``pip install -e .``. Likewise, ``python setup.py develop`` is now ``pip install -e .[full]``. diff --git a/FAQ.rst b/FAQ.rst index 403c0be0b..be6e7b827 100644 --- a/FAQ.rst +++ b/FAQ.rst @@ -131,12 +131,12 @@ This example illustrates the three different ways to simulation a population of import covasim as cv - s1 = cv.Sim(pop_size=100e3, pop_infected=100, pop_scale=1, rescale=True, label='Full population') - s2 = cv.Sim(pop_size=20e3, pop_infected=100, pop_scale=5, rescale=True, label='Dynamic rescaling') - s3 = cv.Sim(pop_size=20e3, pop_infected=20, pop_scale=5, rescale=False, label='Static rescaling') + s1 = cv.Sim(n_days=120, pop_size=200e3, pop_infected=50, pop_scale=1, rescale=True, label='Full population') + s2 = cv.Sim(n_days=120, pop_size=20e3, pop_infected=50, pop_scale=10, rescale=True, label='Dynamic rescaling') + s3 = cv.Sim(n_days=120, pop_size=20e3, pop_infected=5, pop_scale=10, rescale=False, label='Static rescaling') msim = cv.MultiSim([s1, s2, s3]) - msim.run() + msim.run(verbose=-1) msim.plot() Note that using the full population and using dynamic rescaling give virtually identical results, whereas static scaling gives slightly different results. diff --git a/covasim/analysis.py b/covasim/analysis.py index f4a750bf6..bcd3613cd 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -39,7 +39,7 @@ def __init__(self, label=None): return - def initialize(self, sim): + def initialize(self, sim=None): ''' Initialize the analyzer, e.g. convert date strings to integers. ''' @@ -47,7 +47,8 @@ def initialize(self, sim): self.finalized = False return - def finalize(self, sim): + + def finalize(self, sim=None): ''' Finalize analyzer @@ -59,6 +60,7 @@ def finalize(self, sim): self.finalized = True return + def apply(self, sim): ''' Apply analyzer at each time point. The analyzer has full access to the @@ -175,7 +177,7 @@ def apply(self, sim): self.snapshots[date] = sc.dcp(sim.people) # Take snapshot! def finalize(self, sim): - super().finalize(sim) + super().finalize() validate_recorded_dates(sim, requested_dates=self.dates, recorded_dates=self.snapshots.keys(), die=self.die) return @@ -298,7 +300,7 @@ def apply(self, sim): self.hists[date][state] = np.histogram(age[inds], bins=self.edges)[0]*scale # Actually count the people def finalize(self, sim): - super().finalize(sim) + super().finalize() validate_recorded_dates(sim, requested_dates=self.dates, recorded_dates=self.hists.keys(), die=self.die) return diff --git a/covasim/interventions.py b/covasim/interventions.py index 03e22aabf..606a15bd6 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -281,7 +281,7 @@ def _store_args(self): return - def initialize(self, sim): + def initialize(self, sim=None): ''' Initialize intervention -- this is used to make modifications to the intervention that can't be done until after the sim is created. @@ -290,7 +290,8 @@ def initialize(self, sim): self.finalized = False return - def finalize(self, sim): + + def finalize(self, sim=None): ''' Finalize intervention @@ -298,7 +299,7 @@ def finalize(self, sim): final operations after the simulation is complete (e.g. rescaling) ''' if self.finalized: - raise Exception('Intervention already finalized') # Raise an error because finalizing multiple times has a high probability of producing incorrect results e.g. applying rescale factors twice + raise RuntimeError('Intervention already finalized') # Raise an error because finalizing multiple times has a high probability of producing incorrect results e.g. applying rescale factors twice self.finalized = True return @@ -340,7 +341,7 @@ def plot_intervention(self, sim, ax=None, **kwargs): ax = pl.gca() if sc.isiterable(self.days): for day in self.days: - if day is not None: + if sc.isnumber(day): if self.show_label: # Choose whether to include the label in the legend label = self.label else: @@ -455,13 +456,12 @@ class sequence(Intervention): cv.test_num(n_tests=[100]*npts), cv.test_prob(symptomatic_prob=0.2, asymptomatic_prob=0.002), ]) - - Note: callable days are not supported. ''' def __init__(self, days, interventions, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object - assert len(days) == len(interventions) + if sc.isiterable(days): + assert len(days) == len(interventions) self.days = days self.interventions = interventions return @@ -469,21 +469,28 @@ def __init__(self, days, interventions, **kwargs): def initialize(self, sim): ''' Fix the dates ''' - super().initialize(sim) - self.days = [preprocess_day(day, sim) for day in self.days] - self.days_arr = np.array(self.days + [sim.npts]) + super().initialize() + self.days = process_days(sim, self.days) + if isinstance(self.days, list): # Normal use case + self.days_arr = np.array(self.days + [sim.npts]) + else: # If a function is supplied + self.days_arr = self.days for intervention in self.interventions: intervention.initialize(sim) return def apply(self, sim): - inds = find_day(self.days_arr <= sim.t, which='last') + ''' Find the matching day, and see which intervention to activate ''' + if isinstance(self.days_arr, list): # Normal use case + days_arr = np.array([get_day(d, interv=self, sim=sim) for d in self.days_arr]) <= sim.t + else: + days_arr = self.days + inds = find_day(days_arr, interv=self, sim=sim, which='last') if len(inds): return self.interventions[inds[0]].apply(sim) - #%% Beta interventions __all__+= ['change_beta', 'clip_edges'] @@ -520,7 +527,7 @@ def __init__(self, days, changes, layers=None, **kwargs): def initialize(self, sim): ''' Fix days and store beta ''' - super().initialize(sim) + super().initialize() self.days = process_days(sim, self.days) self.changes = process_changes(sim, self.changes, self.days) self.layers = sc.promotetolist(self.layers, keepnone=True) @@ -585,7 +592,7 @@ def __init__(self, days, changes, layers=None, **kwargs): def initialize(self, sim): - super().initialize(sim) + super().initialize() self.days = process_days(sim, self.days) self.changes = process_changes(sim, self.changes, self.days) if self.layers is None: @@ -624,11 +631,14 @@ def apply(self, sim): s_layer.append(to_move) else: # pragma: no cover print(f'Warning: clip_edges() was applied to layer "{lkey}", but no edges were found; please check sim.people.contacts["{lkey}"]') + return + - # Ensure the edges get deleted at the end + def finalize(self, sim): + ''' Ensure the edges get deleted at the end ''' + super().finalize() if sim.t == sim.tvec[-1]: self.contacts = None # Reset to save memory - return @@ -723,7 +733,7 @@ def initialize(self, sim): ''' Fix the dates and number of tests ''' # Handle days - super().initialize(sim) + super().initialize() self.start_day = preprocess_day(self.start_day, sim) self.end_day = preprocess_day(self.end_day, sim) @@ -854,7 +864,7 @@ def __init__(self, symp_prob, asymp_prob=0.0, symp_quar_prob=None, asymp_quar_pr def initialize(self, sim): ''' Fix the dates ''' - super().initialize(sim) + super().initialize() self.start_day = preprocess_day(self.start_day, sim) self.end_day = preprocess_day(self.end_day, sim) self.days = [self.start_day, self.end_day] @@ -964,7 +974,7 @@ def __init__(self, trace_probs=None, trace_time=None, start_day=0, end_day=None, def initialize(self, sim): ''' Process the dates and dictionaries ''' - super().initialize(sim) + super().initialize() self.start_day = preprocess_day(self.start_day, sim) self.end_day = preprocess_day(self.end_day, sim) self.days = [self.start_day, self.end_day] @@ -1128,7 +1138,7 @@ def __init__(self, days, prob=1.0, rel_sus=0.0, rel_symp=0.0, subtarget=None, cu def initialize(self, sim): ''' Fix the dates and store the vaccinations ''' - super().initialize(sim) + super().initialize() self.days = process_days(sim, self.days) self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person self.vaccination_dates = [[] for p in range(sim.n)] # Store the dates when people are vaccinated diff --git a/covasim/people.py b/covasim/people.py index 91211696c..583f670c4 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -23,6 +23,10 @@ class People(cvb.BasePeople): parameters dictionary will get passed instead since it will be needed before the People object is initialized. + Note that this class handles the mechanics of updating the actual people, while + BasePeople takes care of housekeeping (saving, loading, exporting, etc.). Please + see the BasePeople class for additional methods. + Args: pars (dict): the sim parameters, e.g. sim.pars -- alternatively, if a number, interpreted as pop_size strict (bool): whether or not to only create keys that are already in self.meta.person; otherwise, let any key be set @@ -311,8 +315,8 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None): * Infected people that develop symptoms are disaggregated into mild vs. severe (=requires hospitalization) vs. critical (=requires ICU) * Every asymptomatic, mildly symptomatic, and severely symptomatic person recovers * Critical cases either recover or die - - Method also deduplicates input arrays in case one agent is infected many times + + Method also deduplicates input arrays in case one agent is infected many times and stores who infected whom in infection_log list. Args: diff --git a/covasim/sim.py b/covasim/sim.py index 1d262bcf6..d8717b4d4 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -25,7 +25,7 @@ class Sim(cvb.BaseSim): The Sim class handles the running of the simulation: the creation of the population and the dynamics of the epidemic. This class handles the mechanics of the actual simulation, while BaseSim takes care of housekeeping (saving, - loading, exporting, etc.). + loading, exporting, etc.). Please see the BaseSim class for additional methods. Args: pars (dict): parameters to modify from their default values diff --git a/docs/index.rst b/docs/index.rst index e2d4b834c..99475bb00 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,10 +14,10 @@ Full contents overview tutorials + faq whatsnew parameters data - faq glossary contributing modules \ No newline at end of file diff --git a/docs/tutorials/t02.ipynb b/docs/tutorials/t02.ipynb index eb55882b7..1fc152980 100644 --- a/docs/tutorials/t02.ipynb +++ b/docs/tutorials/t02.ipynb @@ -151,13 +151,45 @@ "sim.plot(to_plot='overview', fig_args=dict(figsize=(30,15)))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "While we can save this figure using Matplotlib's built-in `savefig()`, if we use Covasim's `cv.savefig()` we get a couple advantages:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cv.savefig('my-fig.png')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, it saves the figure at higher resolution by default (which you can adjust with the `dpi` argument). But second, it stores information about the code that was used to generate the figure as metadata, which can be loaded later. Made an awesome plot but can't remember even what script you ran to generate it, much less what version of the code? You'll never have to worry about that again. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cv.get_png_metadata('my-fig.png')" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Saving options\n", "\n", - "Saving is pretty simple. The simplest way to save is simply" + "Saving sims is also pretty simple. The simplest way to save is simply" ] }, { diff --git a/docs/tutorials/t03.ipynb b/docs/tutorials/t03.ipynb index 9955ec896..229eea406 100644 --- a/docs/tutorials/t03.ipynb +++ b/docs/tutorials/t03.ipynb @@ -135,7 +135,26 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As you would expect, higher beta values have more infections." + "As you would expect, higher beta values have more infections.\n", + "\n", + "Finally, note that you can use multisims to do very compact scenario explorations:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def protect_elderly(sim):\n", + " if sim.t == sim.day('2021-04-01'):\n", + " elderly = sim.people.age>70\n", + " sim.people.rel_sus[elderly] = 0.0\n", + "\n", + "pars = {'start_day':'2021-03-01', 'n_days':120}\n", + "s1 = cv.Sim(pars, label='Default')\n", + "s2 = cv.Sim(pars, label='Protect the elderly', interventions=protect_elderly)\n", + "cv.MultiSim([s1, s2]).run().plot(to_plot=['cum_deaths', 'cum_infections'])" ] }, { @@ -144,7 +163,7 @@ "source": [ "
\n", "\n", - "**Gotcha:** Because `multiprocess` pickles the sims when running them, `sims[0]` (before being run by the multisim) and `msim.sims[0]` are `not` the same object. After calling `msim.run()`, always use sims from the multisim object, not from before. In contrast, if you *don't* run the multisim (e.g. if you make a multisim from already-run sims), then `sims[0]` and `msim.sims[0]` are indeed exactly the same object.\n", + "**Gotcha:** Because `multiprocess` pickles the sims when running them, `sims[0]` (before being run by the multisim) and `msim.sims[0]` are **not** the same object. After calling `msim.run()`, always use sims from the multisim object, not from before. In contrast, if you *don't* run the multisim (e.g. if you make a multisim from already-run sims), then `sims[0]` and `msim.sims[0]` are indeed exactly the same object.\n", "\n", "
" ] diff --git a/docs/tutorials/t04.ipynb b/docs/tutorials/t04.ipynb index 2948bd0d0..e6f174a53 100644 --- a/docs/tutorials/t04.ipynb +++ b/docs/tutorials/t04.ipynb @@ -48,6 +48,17 @@ "fig = sim.people.plot() # Show statistics of the people" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "\n", + "**Note:** For an explanation of population size, total population, and dynamic rescaling, please see the [FAQ](https://docs.idmod.org/projects/covasim/en/latest/faq.html#what-are-the-relationships-between-population-size-number-of-agents-population-scaling-and-total-population).\n", + " \n", + "
" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/docs/tutorials/t05.ipynb b/docs/tutorials/t05.ipynb index beb97361c..04a98d5c4 100644 --- a/docs/tutorials/t05.ipynb +++ b/docs/tutorials/t05.ipynb @@ -340,7 +340,28 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "However, function-based interventions only take you so far. We saw in Tutorial 1 how you could define a simple \"protect the elderly\" intervention with just a few lines of code. This example explains how to create an intervention object that does much the same thing, but is more fully-featured because it uses the `Intervention` class." + "However, function-based interventions only take you so far.\n", + "\n", + "We saw in Tutorial 1 how you could define a simple \"protect the elderly\" intervention with just a few lines of code:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def protect_elderly(sim):\n", + " if sim.t == sim.day('2020-04-01'):\n", + " elderly = sim.people.age>70\n", + " sim.people.rel_sus[elderly] = 0.0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This example explains how to create an intervention object that does much the same thing, but is more fully-featured because it uses the `Intervention` class." ] }, { @@ -351,7 +372,7 @@ "source": [ "
\n", "\n", - "You must include the line `super().__init__(**kwargs)` in the `self.__init__()` method, or else the intervention won't work. You must also include `self.initialized = True` in the `self.initialize()` method.\n", + "You must include the line `super().__init__(**kwargs)` in the `self.__init__()` method, or else the intervention won't work. You must also include `super().initialize()` in the `self.initialize()` method.\n", "\n", "
" ] @@ -377,13 +398,13 @@ " return\n", "\n", " def initialize(self, sim):\n", - " self.start_day = sim.day(self.start_day)\n", + " super().initialize() # NB: This line must also be included\n", + " self.start_day = sim.day(self.start_day) # Convert string or dateobject dates into an integer number of days\n", " self.end_day = sim.day(self.end_day)\n", " self.days = [self.start_day, self.end_day]\n", " self.elderly = sim.people.age > self.age_cutoff # Find the elderly people here\n", " self.exposed = np.zeros(sim.npts) # Initialize results\n", " self.tvec = sim.tvec # Copy the time vector into this intervention\n", - " self.initialized = True # NB: This line must also be included\n", " return\n", "\n", " def apply(self, sim):\n", diff --git a/docs/tutorials/t06.ipynb b/docs/tutorials/t06.ipynb index ebb4f0599..6e24f6be2 100644 --- a/docs/tutorials/t06.ipynb +++ b/docs/tutorials/t06.ipynb @@ -149,7 +149,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" }, "pycharm": { "stem_cell": { diff --git a/examples/t05_custom_intervention.py b/examples/t05_custom_intervention.py index 15fc8de14..8bfdcf365 100644 --- a/examples/t05_custom_intervention.py +++ b/examples/t05_custom_intervention.py @@ -17,13 +17,13 @@ def __init__(self, start_day=None, end_day=None, age_cutoff=70, rel_sus=0.0, *ar return def initialize(self, sim): - self.start_day = sim.day(self.start_day) + super().initialize() # NB: This line must also be included + self.start_day = sim.day(self.start_day) # Convert string or dateobject dates into an integer number of days self.end_day = sim.day(self.end_day) self.days = [self.start_day, self.end_day] self.elderly = sim.people.age > self.age_cutoff # Find the elderly people here self.exposed = np.zeros(sim.npts) # Initialize results self.tvec = sim.tvec # Copy the time vector into this intervention - self.initialized = True return def apply(self, sim): From 198e2dd535d0a023a17d1b154b2fe78f394f2815 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 31 Mar 2021 19:45:29 -0700 Subject: [PATCH 317/569] update tutorials and tests --- CHANGELOG.rst | 2 +- covasim/interventions.py | 31 ++++++++++----- docs/tutorials/t05.ipynb | 60 +++++++++++++++++++++++++++++- examples/t05_dynamic_pars.py | 4 +- examples/t05_dynamic_triggering.py | 42 +++++++++++++++++++++ tests/test_interventions.py | 33 +++++++++------- 6 files changed, 146 insertions(+), 26 deletions(-) create mode 100644 examples/t05_dynamic_triggering.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b8d3bb884..41e1c47b8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -30,7 +30,7 @@ Version 2.1.2 (2021-03-31) This is (probably) the last release before the Covasim 3.0 launch (vaccines and variants). -- Interventions and analyzers now accept a function as an argument to ``days`` or e.g. ``start_day``. For example, instead of defining ``start_day=30``, you can define a function (with the intervention and the sim object as arguments) that calculates and returns a start day. This allows interventions to be dynamically triggered based on the state of the sim. +- Interventions and analyzers now accept a function as an argument to ``days`` or e.g. ``start_day``. For example, instead of defining ``start_day=30``, you can define a function (with the intervention and the sim object as arguments) that calculates and returns a start day. This allows interventions to be dynamically triggered based on the state of the sim. See [Tutorial 5](https://docs.idmod.org/projects/covasim/en/latest/tutorials/t05.html) for a new section on how to use this feature. - Added a ``finalize()`` method to interventions and analyzers, to replace the ``if sim.t == sim.npts-1:`` blocks in ``apply()`` that had been being used to finalize. - Changed setup instructions from ``python setup.py develop`` to ``pip install -e .``, and unpinned ``line_profiler``. - *Regression information*: If you have any scripts/workflows that have been using ``python setup.py develop``, please update them to ``pip install -e .``. Likewise, ``python setup.py develop`` is now ``pip install -e .[full]``. diff --git a/covasim/interventions.py b/covasim/interventions.py index 606a15bd6..7f67abec6 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -225,15 +225,16 @@ class Intervention: To retrieve a particular intervention from a sim, use sim.get_intervention(). Args: - label (str): a label for the intervention (used for plotting, and for ease of identification) - show_label (bool): whether or not to include the label, if provided, in the legend - do_plot (bool): whether or not to plot the intervention - line_args (dict): arguments passed to pl.axvline() when plotting + label (str): a label for the intervention (used for plotting, and for ease of identification) + show_label (bool): whether or not to include the label in the legend + do_plot (bool): whether or not to plot the intervention + line_args (dict): arguments passed to pl.axvline() when plotting ''' - def __init__(self, label=None, show_label=True, do_plot=None, line_args=None): + def __init__(self, label=None, show_label=False, do_plot=None, line_args=None): self._store_args() # Store the input arguments so the intervention can be recreated + if label is None: label = self.__class__.__name__ # Use the class name if no label is supplied self.label = label # e.g. "Close schools" - self.show_label = show_label # Show the label by default + self.show_label = show_label # Do not show the label by default self.do_plot = do_plot if do_plot is not None else True # Plot the intervention, including if None self.line_args = sc.mergedicts(dict(linestyle='--', c='#aaa', lw=1.0), line_args) # Do not set alpha by default due to the issue of overlapping interventions self.days = [] # The start and end days of the intervention @@ -327,6 +328,12 @@ def plot_intervention(self, sim, ax=None, **kwargs): This can be used to do things like add vertical lines on days when interventions take place. Can be disabled by setting self.do_plot=False. + Note 1: you can modify the plotting style via the ``line_args`` argument when + creating the intervention. + + Note 2: By default, the intervention is plotted at the days stored in self.days. + However, if there is a self.plot_days attribute, this will be used instead. + Args: sim: the Sim instance ax: the axis instance @@ -339,11 +346,17 @@ def plot_intervention(self, sim, ax=None, **kwargs): if self.do_plot or self.do_plot is None: if ax is None: ax = pl.gca() - if sc.isiterable(self.days): - for day in self.days: + if hasattr(self, 'plot_days'): + days = self.plot_days + else: + days = self.days + if sc.isiterable(days): + label_shown = False # Don't show the label more than once + for day in days: if sc.isnumber(day): - if self.show_label: # Choose whether to include the label in the legend + if self.show_label and not label_shown: # Choose whether to include the label in the legend label = self.label + label_shown = True else: label = None ax.axvline(day, label=label, **line_args) diff --git a/docs/tutorials/t05.ipynb b/docs/tutorials/t05.ipynb index 04a98d5c4..88eec0537 100644 --- a/docs/tutorials/t05.ipynb +++ b/docs/tutorials/t05.ipynb @@ -309,13 +309,71 @@ "You can see the sudden jump in new infections when importations are turned on." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dynamic triggering\n", + "\n", + "Another option is to replace the ``days`` arguments with custom functions defining additional criteria. For example, perhaps you only want your beta change intervention to take effect once infections get to a sufficiently high level. Here's a fairly complex example (feel free to skip the details) that toggles the intervention on and off depending on the current number of people who are infectious.\n", + "\n", + "This example illustrates a few different features:\n", + "- The simplest change is just that we're supplying `days=inf_thresh` instead of a number or list. If we had `def inf_thresh(interv, sim): return [20,30]` this would be the same as just setting `days=[20,30]`.\n", + "- Because the first argument this function gets is the intervention itself, we can stretch the rules a little bit and name this variable `self` -- as if we're defining a new method for the intervention, even though it's actually just a function.\n", + "- We want to keep track of a few things with this intervention -- namely when it toggles on and off, and whether or not it's active. Since the intervention is just an object, we can add these attributes directly to it.\n", + "- Finally, this example shows some of the flexibility in how interventions are plotted -- i.e. shown in the legend with a label and with a custom color." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def inf_thresh(self, sim, thresh=500):\n", + " ''' Dynamically define on and off days for a beta change -- use self like it's a method '''\n", + "\n", + " # Meets threshold, activate\n", + " if sim.people.infectious.sum() > thresh:\n", + " if not self.active:\n", + " self.active = True\n", + " self.t_on = sim.t\n", + " self.plot_days.append(self.t_on)\n", + "\n", + " # Does not meet threshold, deactivate\n", + " else:\n", + " if self.active:\n", + " self.active = False\n", + " self.t_off = sim.t\n", + " self.plot_days.append(self.t_off)\n", + "\n", + " return [self.t_on, self.t_off]\n", + "\n", + "# Set up the intervention\n", + "on = 0.2 # Beta less than 1 -- intervention is on\n", + "off = 1.0 # Beta is 1, i.e. normal -- intervention is off\n", + "changes = [on, off]\n", + "plot_args = dict(label='Dynamic beta', show_label=True, line_args={'c':'blue'})\n", + "cb = cv.change_beta(days=inf_thresh, changes=changes, **plot_args)\n", + "\n", + "# Set custom properties\n", + "cb.t_on = np.nan\n", + "cb.t_off = np.nan\n", + "cb.active = False\n", + "cb.plot_days = []\n", + "\n", + "# Run the simulation and plot\n", + "sim = cv.Sim(interventions=cb)\n", + "sim.run().plot()" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Custom interventions\n", "\n", - "Covasim also lets you define an arbitrary function or class to act as an intervention instead. If a custom intervention is supplied as a function (as it was in Tutorial 1 as well), then it receives the `sim` object as its only argument, and is called on each timestep. It can perform arbitrary manipulations to the sim object, such as changing parameters, modifying state, etc.\n", + "If you're still standing after the previous example, Covasim also lets you do things that are even *more* complicated, namely define an arbitrary function or class to act as an intervention instead. If a custom intervention is supplied as a function (as it was in Tutorial 1 as well), then it receives the `sim` object as its only argument, and is called on each timestep. It can perform arbitrary manipulations to the sim object, such as changing parameters, modifying state, etc.\n", "\n", "This example reimplements the dynamic parameters example above, except using a hard-coded custom intervention:" ] diff --git a/examples/t05_dynamic_pars.py b/examples/t05_dynamic_pars.py index a5e1910e5..5734236bc 100644 --- a/examples/t05_dynamic_pars.py +++ b/examples/t05_dynamic_pars.py @@ -1,4 +1,6 @@ -''' Demonstrate dynamic parameters ''' +''' +Demonstrate dynamic parameters +''' import covasim as cv diff --git a/examples/t05_dynamic_triggering.py b/examples/t05_dynamic_triggering.py new file mode 100644 index 000000000..259f03ffd --- /dev/null +++ b/examples/t05_dynamic_triggering.py @@ -0,0 +1,42 @@ +''' +Demonstrate dynamically triggered interventions +''' + +import covasim as cv +import numpy as np + +def inf_thresh(self, sim, thresh=500): + ''' Dynamically define on and off days for a beta change -- use self like it's a method ''' + + # Meets threshold, activate + if sim.people.infectious.sum() > thresh: + if not self.active: + self.active = True + self.t_on = sim.t + self.plot_days.append(self.t_on) + + # Does not meet threshold, deactivate + else: + if self.active: + self.active = False + self.t_off = sim.t + self.plot_days.append(self.t_off) + + return [self.t_on, self.t_off] + +# Set up the intervention +on = 0.2 # Beta less than 1 -- intervention is on +off = 1.0 # Beta is 1, i.e. normal -- intervention is off +changes = [on, off] +plot_args = dict(label='Dynamic beta', show_label=True, line_args={'c':'blue'}) +cb = cv.change_beta(days=inf_thresh, changes=changes, **plot_args) + +# Set custom properties +cb.t_on = np.nan +cb.t_off = np.nan +cb.active = False +cb.plot_days = [] + +# Run the simulation and plot +sim = cv.Sim(interventions=cb) +sim.run().plot() \ No newline at end of file diff --git a/tests/test_interventions.py b/tests/test_interventions.py index 437883d8f..36ca566b0 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -6,6 +6,7 @@ import os import sciris as sc +import numpy as np import pylab as pl import covasim as cv import pytest @@ -22,9 +23,9 @@ def test_all_interventions(): pars = sc.objdict( pop_size = 1e3, pop_infected = 10, - pop_type = 'hybrid', n_days = 90, ) + hpars = sc.mergedicts(pars, {'pop_type':'hybrid'}) # Some, but not all, tests require layers #%% Define the interventions @@ -65,8 +66,12 @@ def test_all_interventions(): i7b = cv.contact_tracing(start_day=20, trace_probs=dict(h=0.9, s=0.7, w=0.7, c=0.3), trace_time=dict(h=0, s=1, w=1, c=3)) - # 8. Combination - i8a = cv.clip_edges(days=18, changes=0.0, layers='s') # Close schools + # 8. Combination, with dynamically set days + def check_inf(interv, sim, thresh=10, close_day=18): + days = close_day if sim.people.infectious.sum()>thresh else np.nan + return days + + i8a = cv.clip_edges(days=check_inf, changes=0.0, layers='s') # Close schools i8b = cv.clip_edges(days=[20, 32, 45], changes=[0.7, 0.3, 0.9], layers=['w', 'c']) # Reduce work and community i8c = cv.test_prob(start_day=38, symp_prob=0.01, asymp_prob=0.0, symp_quar_prob=1.0, asymp_quar_prob=1.0, test_delay=2) # Start testing for TTQ i8d = cv.contact_tracing(start_day=40, trace_probs=dict(h=0.9, s=0.7, w=0.7, c=0.3), trace_time=dict(h=0, s=1, w=1, c=3)) # Start tracing for TTQ @@ -77,17 +82,17 @@ def test_all_interventions(): #%% Create the simulations sims = sc.objdict() - sims.dynamic = cv.Sim(pars=pars, interventions=[i1a, i1b]) - sims.sequence = cv.Sim(pars=pars, interventions=i2) - sims.change_beta1 = cv.Sim(pars=pars, interventions=i3a) - sims.clip_edges1 = cv.Sim(pars=pars, interventions=i4a) # Roughly equivalent to change_beta1 - sims.change_beta2 = cv.Sim(pars=pars, interventions=i3b) - sims.clip_edges2 = cv.Sim(pars=pars, interventions=i4b) # Roughly equivalent to change_beta2 - sims.test_num = cv.Sim(pars=pars, interventions=i5) - sims.test_prob = cv.Sim(pars=pars, interventions=i6) - sims.tracing = cv.Sim(pars=pars, interventions=[i7a, i7b]) - sims.combo = cv.Sim(pars=pars, interventions=[i8a, i8b, i8c, i8d]) - sims.vaccine = cv.Sim(pars=pars, interventions=[i9a, i9b]) + sims.dynamic = cv.Sim(pars=pars, interventions=[i1a, i1b]) + sims.sequence = cv.Sim(pars=pars, interventions=i2) + sims.change_beta1 = cv.Sim(pars=hpars, interventions=i3a) + sims.clip_edges1 = cv.Sim(pars=hpars, interventions=i4a) # Roughly equivalent to change_beta1 + sims.change_beta2 = cv.Sim(pars=pars, interventions=i3b) + sims.clip_edges2 = cv.Sim(pars=pars, interventions=i4b) # Roughly equivalent to change_beta2 + sims.test_num = cv.Sim(pars=pars, interventions=i5) + sims.test_prob = cv.Sim(pars=pars, interventions=i6) + sims.tracing = cv.Sim(pars=hpars, interventions=[i7a, i7b]) + sims.combo = cv.Sim(pars=hpars, interventions=[i8a, i8b, i8c, i8d]) + sims.vaccine = cv.Sim(pars=pars, interventions=[i9a, i9b]) # Run the simualations for key,sim in sims.items(): From c08586109f4cc1ab0b69167acd4a998b4d0ade85 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 31 Mar 2021 19:52:36 -0700 Subject: [PATCH 318/569] tidying --- covasim/analysis.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/covasim/analysis.py b/covasim/analysis.py index bcd3613cd..de80c52bf 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -176,6 +176,7 @@ def apply(self, sim): date = self.dates[ind] self.snapshots[date] = sc.dcp(sim.people) # Take snapshot! + def finalize(self, sim): super().finalize() validate_recorded_dates(sim, requested_dates=self.dates, recorded_dates=self.snapshots.keys(), die=self.die) @@ -299,6 +300,7 @@ def apply(self, sim): inds = sim.people.defined(f'date_{state}') # Pull out people for which this state is defined self.hists[date][state] = np.histogram(age[inds], bins=self.edges)[0]*scale # Actually count the people + def finalize(self, sim): super().finalize() validate_recorded_dates(sim, requested_dates=self.dates, recorded_dates=self.hists.keys(), die=self.die) From cd801ee16d0f89d6ac04c54f8f8a2bfd244b17a4 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 31 Mar 2021 19:59:21 -0700 Subject: [PATCH 319/569] updating tests --- covasim/version.py | 4 ++-- tests/check_coverage | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/covasim/version.py b/covasim/version.py index a297f93bc..61b15c9e5 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,6 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '2.1.1' -__versiondate__ = '2021-03-29' +__version__ = '2.1.2' +__versiondate__ = '2021-03-31' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' diff --git a/tests/check_coverage b/tests/check_coverage index 54105af49..36d1f266a 100755 --- a/tests/check_coverage +++ b/tests/check_coverage @@ -2,13 +2,10 @@ # Note that although the script runs when parallelized, the coverage results are wrong. echo 'Running tests...' -pytest test_*.py --cov-config=.coveragerc --cov=../covasim --workers auto --durations=0 +pytest -v test_*.py --cov-config=.coveragerc --cov=../covasim --workers auto --durations=20 echo 'Creating HTML report...' coverage html -echo 'Running report...' -coverage report - echo 'Report location:' echo "`pwd`/htmlcov/index.html" \ No newline at end of file From 67faaa71ad4729f4ae0c19723c38a316fe54b1cd Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 31 Mar 2021 20:44:24 -0700 Subject: [PATCH 320/569] update tutorial --- docs/tutorials/t05.ipynb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/t05.ipynb b/docs/tutorials/t05.ipynb index 88eec0537..97a128d3e 100644 --- a/docs/tutorials/t05.ipynb +++ b/docs/tutorials/t05.ipynb @@ -311,17 +311,20 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "tags": [] + }, "source": [ "## Dynamic triggering\n", "\n", "Another option is to replace the ``days`` arguments with custom functions defining additional criteria. For example, perhaps you only want your beta change intervention to take effect once infections get to a sufficiently high level. Here's a fairly complex example (feel free to skip the details) that toggles the intervention on and off depending on the current number of people who are infectious.\n", "\n", "This example illustrates a few different features:\n", - "- The simplest change is just that we're supplying `days=inf_thresh` instead of a number or list. If we had `def inf_thresh(interv, sim): return [20,30]` this would be the same as just setting `days=[20,30]`.\n", - "- Because the first argument this function gets is the intervention itself, we can stretch the rules a little bit and name this variable `self` -- as if we're defining a new method for the intervention, even though it's actually just a function.\n", - "- We want to keep track of a few things with this intervention -- namely when it toggles on and off, and whether or not it's active. Since the intervention is just an object, we can add these attributes directly to it.\n", - "- Finally, this example shows some of the flexibility in how interventions are plotted -- i.e. shown in the legend with a label and with a custom color." + "\n", + "* The simplest change is just that we're supplying `days=inf_thresh` instead of a number or list. If we had `def inf_thresh(interv, sim): return [20,30]` this would be the same as just setting `days=[20,30]`.\n", + "* Because the first argument this function gets is the intervention itself, we can stretch the rules a little bit and name this variable `self` -- as if we're defining a new method for the intervention, even though it's actually just a function.\n", + "* We want to keep track of a few things with this intervention -- namely when it toggles on and off, and whether or not it's active. Since the intervention is just an object, we can add these attributes directly to it.\n", + "* Finally, this example shows some of the flexibility in how interventions are plotted -- i.e. shown in the legend with a label and with a custom color." ] }, { From 930cd659f467569a8d81eb9e9c1b62f4e14fd2a7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 31 Mar 2021 20:45:09 -0700 Subject: [PATCH 321/569] update changelog --- CHANGELOG.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 41e1c47b8..3c5453677 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -28,8 +28,6 @@ Latest versions (2.x) Version 2.1.2 (2021-03-31) -------------------------- -This is (probably) the last release before the Covasim 3.0 launch (vaccines and variants). - - Interventions and analyzers now accept a function as an argument to ``days`` or e.g. ``start_day``. For example, instead of defining ``start_day=30``, you can define a function (with the intervention and the sim object as arguments) that calculates and returns a start day. This allows interventions to be dynamically triggered based on the state of the sim. See [Tutorial 5](https://docs.idmod.org/projects/covasim/en/latest/tutorials/t05.html) for a new section on how to use this feature. - Added a ``finalize()`` method to interventions and analyzers, to replace the ``if sim.t == sim.npts-1:`` blocks in ``apply()`` that had been being used to finalize. - Changed setup instructions from ``python setup.py develop`` to ``pip install -e .``, and unpinned ``line_profiler``. From d4bd9c952c63281c1f03bb58fd10c6a73e4aeca0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 00:21:31 -0700 Subject: [PATCH 322/569] change test order --- tests/test_baselines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_baselines.py b/tests/test_baselines.py index 9403eb514..6b53ba0bc 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -186,9 +186,9 @@ def normalize_performance(): cv.options.set(interactive=do_plot) T = sc.tic() - make_sim(do_plot=do_plot) json = test_benchmark(do_save=do_save) # Run this first so benchmarking is available even if results are different new = test_baseline() + make_sim(do_plot=do_plot) print('\n'*2) sc.toc(T) From 1c31d2a56efe3c52fee5b09ccf13a9451223e634 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 00:54:53 -0700 Subject: [PATCH 323/569] change strain_pars implementation for backwards compatibility --- covasim/misc.py | 1 + covasim/parameters.py | 4 ++-- covasim/people.py | 3 ++- covasim/sim.py | 6 +++--- tests/benchmark.py | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index de7f15891..1ff95f203 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -503,6 +503,7 @@ def get_version_pars(version, verbose=True): '1.5.0': [f'1.5.{i}' for i in range(4)] + [f'1.6.{i}' for i in range(2)] + [f'1.7.{i}' for i in range(7)], '2.0.0': [f'2.0.{i}' for i in range(5)] + ['2.1.0'], '2.1.1': [f'2.1.{i}' for i in range(1,3)], + '3.0.0': ['3.0.0'], } # Find and check the match diff --git a/covasim/parameters.py b/covasim/parameters.py index 765115972..22459ab06 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -7,7 +7,6 @@ from .settings import options as cvo # For setting global options from . import misc as cvm from . import defaults as cvd -from . import utils as cvu __all__ = ['make_pars', 'reset_layer_pars', 'get_prognoses'] @@ -128,6 +127,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): reset_layer_pars(pars) if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses + pars['strain_pars'] = {} # Populated just below pars = listify_strain_pars(pars) # Turn strain parameters into lists # If version is specified, load old parameters @@ -331,7 +331,7 @@ def listify_strain_pars(pars): ''' Helper function to turn strain parameters into lists ''' for sp in cvd.strain_pars: if sp in pars.keys(): - pars[sp] = sc.promotetolist(pars[sp]) + pars['strain_pars'][sp] = sc.promotetolist(pars[sp]) return pars diff --git a/covasim/people.py b/covasim/people.py index 933396daf..6343e0bda 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -373,6 +373,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str icu_max (bool): whether or not there is an ICU bed available for this person source (array): source indices of the people who transmitted this infection (None if an importation or seed infection) layer (str): contact layer this infection was transmitted on + strain (int): the strain people are being infected by Returns: count (int): number of people infected @@ -396,7 +397,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str infect_parkeys = ['dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob'] infect_pars = dict() for key in infect_parkeys: - infect_pars[key] = self.pars[key][strain] + infect_pars[key] = self.pars['strain_pars'][key][strain] n_infections = len(inds) durpars = infect_pars['dur'] diff --git a/covasim/sim.py b/covasim/sim.py index 81e3246f2..7f79fbf40 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -577,7 +577,7 @@ def step(self): quar = people.quarantined # Initialize temp storage for strain parameters - strain_parkeys = ['rel_beta', 'asymp_factor'] + strain_keys = ['rel_beta', 'asymp_factor'] strain_pars = dict() ns = self['n_strains'] # Shorten number of strains @@ -592,8 +592,8 @@ def step(self): cvimm.check_immunity(people, strain, sus=True) # Deal with strain parameters - for key in strain_parkeys: - strain_pars[key] = self[key][strain] + for key in strain_keys: + strain_pars[key] = self['strain_pars'][key][strain] beta = cvd.default_float(self['beta'] * strain_pars['rel_beta']) asymp_factor = cvd.default_float(strain_pars['asymp_factor']) diff --git a/tests/benchmark.py b/tests/benchmark.py index 5f134666e..e5c3fd849 100644 --- a/tests/benchmark.py +++ b/tests/benchmark.py @@ -7,7 +7,7 @@ from test_baselines import make_sim sim = make_sim(use_defaults=False, do_plot=False) # Use the same sim as from the regression/benchmarking tests -to_profile = 'run' # Must be one of the options listed below +to_profile = 'step' # Must be one of the options listed below func_options = { 'make_contacts': cv.make_random_contacts, From 3b968827df4e0da6fccfc55c463d5d8c6f1bd955 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 01:39:11 -0700 Subject: [PATCH 324/569] refactoring to make strains a bit more separable --- covasim/base.py | 9 +++--- covasim/defaults.py | 61 +++++++++++++++++++++----------------- covasim/people.py | 16 +++++----- covasim/plotting.py | 6 ++-- covasim/run.py | 2 +- covasim/sim.py | 71 +++++++++++++++++++++++++-------------------- 6 files changed, 90 insertions(+), 75 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index b0071445a..f0e271e82 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -122,6 +122,7 @@ class Result(object): npts (int): if values is None, precreate it to be of this length scale (bool): whether or not the value scales by population scale factor color (str/arr): default color for plotting (hex or RGB notation) + n_strains (int): the number of strains the result is for (0 for results not by strain) **Example**:: @@ -131,18 +132,16 @@ class Result(object): print(r1.values) ''' - def __init__(self, name=None, npts=None, scale=True, color=None, strain_color=None, total_strains=1): + def __init__(self, name=None, npts=None, scale=True, color=None, n_strains=0): self.name = name # Name of this result self.scale = scale # Whether or not to scale the result by the scale factor if color is None: color = cvd.get_colors()['default'] - strain_color = cvd.get_strain_colors() self.color = color # Default color - self.strain_color = strain_color if npts is None: npts = 0 - if 'by_strain' in self.name or 'by strain' in self.name: - self.values = np.full((total_strains, npts), 0, dtype=cvd.result_float, order='F') + if n_strains>0: + self.values = np.full((n_strains, npts), 0, dtype=cvd.result_float, order='F') else: self.values = np.array(np.zeros(int(npts)), dtype=cvd.result_float) self.low = None diff --git a/covasim/defaults.py b/covasim/defaults.py index f25f8a055..79ce39cc4 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -106,35 +106,42 @@ class PeopleMeta(sc.prettyobj): # A subset of the above states are used for results result_stocks = { - 'susceptible': 'Number susceptible', - 'exposed': 'Number exposed', - 'exposed_by_strain': 'Number exposed by strain', - 'infectious': 'Number infectious', - 'infectious_by_strain': 'Number infectious by strain', - 'symptomatic': 'Number symptomatic', - 'severe': 'Number of severe cases', - 'critical': 'Number of critical cases', - 'diagnosed': 'Number of confirmed cases', - 'quarantined': 'Number in quarantine', - 'vaccinated': 'Number of people vaccinated', + 'susceptible': 'Number susceptible', + 'exposed': 'Number exposed', + 'infectious': 'Number infectious', + 'symptomatic': 'Number symptomatic', + 'severe': 'Number of severe cases', + 'critical': 'Number of critical cases', + 'diagnosed': 'Number of confirmed cases', + 'quarantined': 'Number in quarantine', + 'vaccinated': 'Number of people vaccinated', +} + +result_stocks_by_strain = { + 'exposed_by_strain': 'Number exposed by strain', + 'infectious_by_strain': 'Number infectious by strain', } # The types of result that are counted as flows -- used in sim.py; value is the label suffix -result_flows = {'infections': 'infections', - 'reinfections': 'reinfections', - 'infections_by_strain': 'infections_by_strain', - 'infectious': 'infectious', - 'infectious_by_strain': 'infectious_by_strain', - 'tests': 'tests', - 'diagnoses': 'diagnoses', - 'recoveries': 'recoveries', - 'symptomatic': 'symptomatic cases', - 'severe': 'severe cases', - 'critical': 'critical cases', - 'deaths': 'deaths', - 'quarantined': 'quarantined people', - 'vaccinations': 'vaccinations', - 'vaccinated': 'vaccinated people' +result_flows = { + 'infections': 'infections', + 'reinfections': 'reinfections', + 'infectious': 'infectious', + 'tests': 'tests', + 'diagnoses': 'diagnoses', + 'recoveries': 'recoveries', + 'symptomatic': 'symptomatic cases', + 'severe': 'severe cases', + 'critical': 'critical cases', + 'deaths': 'deaths', + 'quarantined': 'quarantined people', + 'vaccinations': 'vaccinations', + 'vaccinated': 'vaccinated people' +} + +result_flows_by_strain = { + 'infections_by_strain': 'infections_by_strain', + 'infectious_by_strain': 'infectious_by_strain', } result_imm = { @@ -145,6 +152,8 @@ class PeopleMeta(sc.prettyobj): # Define these here as well new_result_flows = [f'new_{key}' for key in result_flows.keys()] cum_result_flows = [f'cum_{key}' for key in result_flows.keys()] +new_result_flows_by_strain = [f'new_{key}' for key in result_flows_by_strain.keys()] +cum_result_flows_by_strain = [f'cum_{key}' for key in result_flows_by_strain.keys()] # Parameters that can vary by strain (should be in list format) strain_pars = ['rel_beta', diff --git a/covasim/people.py b/covasim/people.py index 6343e0bda..15249f836 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -94,9 +94,9 @@ def __init__(self, pars, strict=True, **kwargs): # Store flows to be computed during simulation self.flows = {key:0 for key in cvd.new_result_flows} - for key in cvd.new_result_flows: - if 'by_strain' in key: - self.flows[key] = np.full(self.pars['n_strains'], 0, dtype=cvd.default_float) + self.flows_strain = {} + for key in cvd.new_result_flows_by_strain: + self.flows_strain[key] = np.full(self.pars['total_strains'], 0, dtype=cvd.default_float) # Although we have called init(), we still need to call initialize() self.initialized = False @@ -167,9 +167,9 @@ def update_states_pre(self, t): # Perform updates self.flows = {key:0 for key in cvd.new_result_flows} - for key in cvd.new_result_flows: - if 'by_strain' in key: - self.flows[key] = np.full(self.pars['total_strains'], 0, dtype=cvd.default_float) + self.flows_strain = {} + for key in cvd.new_result_flows_by_strain: + self.flows_strain[key] = np.full(self.pars['total_strains'], 0, dtype=cvd.default_float) self.flows['new_infectious'] += self.check_infectious() # For people who are exposed and not infectious, check if they begin being infectious self.flows['new_symptomatic'] += self.check_symptomatic() self.flows['new_severe'] += self.check_severe() @@ -218,7 +218,7 @@ def check_infectious(self): self.infectious_strain[inds] = self.exposed_strain[inds] for strain in range(self.pars['n_strains']): this_strain_inds = cvu.itrue(self.infectious_strain[inds] == strain, inds) - self.flows['new_infectious_by_strain'][strain] += len(this_strain_inds) + self.flows_strain['new_infectious_by_strain'][strain] += len(this_strain_inds) return len(inds) @@ -409,7 +409,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.exposed_by_strain[strain, inds] = True self.date_exposed[inds] = self.t self.flows['new_infections'] += len(inds) - self.flows['new_infections_by_strain'][strain] += len(inds) + self.flows_strain['new_infections_by_strain'][strain] += len(inds) self.flows['new_reinfections'] += len(cvu.defined(self.date_recovered[inds])) # Record reinfections self.date_recovered[inds] = np.nan # Reset date they recovered - we only store the last recovery diff --git a/covasim/plotting.py b/covasim/plotting.py index 130c3072e..5d23abdf5 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -394,11 +394,11 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot res_t = sim.results['t'] if 'by_strain' in reskey: for strain in range(sim['total_strains']): - color = res.strain_color[strain] # Choose the color - if strain ==0: + color = cvd.get_strain_colors()[strain] # Choose the color + if strain == 0: label = 'wild type' else: - label =sim['strains'][strain-1].strain_label + label = sim['strains'][strain-1].strain_label if res.low is not None and res.high is not None: ax.fill_between(res_t, res.low[strain,:], res.high[strain,:], color=color, **args.fill) # Create the uncertainty bound diff --git a/covasim/run.py b/covasim/run.py index c362d5d09..297ce2171 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -971,7 +971,7 @@ def print_heading(string): scen_sims = multi_run(scen_sim, **run_args, **kwargs) # This is where the sims actually get run # Get number of strains - ns = scen_sims[0].results['cum_infections_by_strain'].values.shape[0] + ns = scen_sims[0].results['strain']['cum_infections_by_strain'].values.shape[0] # Process the simulations print_heading(f'Processing {scenkey}') diff --git a/covasim/sim.py b/covasim/sim.py index 7f79fbf40..652cf1956 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -276,36 +276,44 @@ def init_res(*args, **kwargs): return output dcols = cvd.get_colors() # Get default colors - strain_cols = cvd.get_strain_colors() # Flows and cumulative flows for key,label in cvd.result_flows.items(): - self.results[f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key], strain_color=strain_cols, total_strains=self['total_strains']) # Cumulative variables -- e.g. "Cumulative infections" + self.results[f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key]) # Cumulative variables -- e.g. "Cumulative infections" for key,label in cvd.result_flows.items(): # Repeat to keep all the cumulative keys together - self.results[f'new_{key}'] = init_res(f'Number of new {label}', color=dcols[key], strain_color=strain_cols, total_strains=self['total_strains']) # Flow variables -- e.g. "Number of new infections" + self.results[f'new_{key}'] = init_res(f'Number of new {label}', color=dcols[key]) # Flow variables -- e.g. "Number of new infections" # Stock variables for key,label in cvd.result_stocks.items(): - self.results[f'n_{key}'] = init_res(label, color=dcols[key], strain_color=strain_cols, total_strains=self['total_strains']) + self.results[f'n_{key}'] = init_res(label, color=dcols[key]) # Other variables - self.results['n_alive'] = init_res('Number of people alive', scale=False) - self.results['n_preinfectious'] = init_res('Number preinfectious', scale=False, color=dcols.exposed) - #self.results['n_removed'] = init_res('Number removed', scale=False, color=dcols.recovered) - self.results['prevalence'] = init_res('Prevalence', scale=False) - self.results['prevalence_by_strain'] = init_res('Prevalence by strain', scale=False, total_strains=self['total_strains']) - self.results['incidence'] = init_res('Incidence', scale=False) - self.results['incidence_by_strain'] = init_res('Incidence by strain', scale=False, total_strains=self['total_strains']) - self.results['r_eff'] = init_res('Effective reproduction number', scale=False) - self.results['doubling_time'] = init_res('Doubling time', scale=False) - self.results['test_yield'] = init_res('Testing yield', scale=False) - self.results['rel_test_yield'] = init_res('Relative testing yield', scale=False) - self.results['share_vaccinated'] = init_res('Share Vaccinated', scale=False) - self.results['pop_nabs'] = init_res('Population average NAb levels', scale=False, color=dcols.pop_nabs) - self.results['pop_protection'] = init_res('Population average immunity protection', scale=False, color=dcols.pop_protection) - self.results['pop_symp_protection'] = init_res('Population average symptomatic immunity protection', scale=False, - color=dcols.pop_symp_protection) + self.results['n_alive'] = init_res('Number of people alive', scale=False) + self.results['n_preinfectious'] = init_res('Number preinfectious', scale=False, color=dcols.exposed) + # self.results['n_removed'] = init_res('Number removed', scale=False, color=dcols.recovered) + self.results['prevalence'] = init_res('Prevalence', scale=False) + self.results['incidence'] = init_res('Incidence', scale=False) + self.results['r_eff'] = init_res('Effective reproduction number', scale=False) + self.results['doubling_time'] = init_res('Doubling time', scale=False) + self.results['test_yield'] = init_res('Testing yield', scale=False) + self.results['rel_test_yield'] = init_res('Relative testing yield', scale=False) + self.results['share_vaccinated'] = init_res('Proportion vaccinated', scale=False) + self.results['pop_nabs'] = init_res('Population NAb levels', scale=False, color=dcols.pop_nabs) + self.results['pop_protection'] = init_res('Population immunity protection', scale=False, color=dcols.pop_protection) + self.results['pop_symp_protection'] = init_res('Population symptomatic protection', scale=False, color=dcols.pop_symp_protection) + + # Handle strains + ns = self['total_strains'] + self.results['strain'] = {} + self.results['strain']['prevalence_by_strain'] = init_res('Prevalence by strain', scale=False, n_strains=ns) + self.results['strain']['incidence_by_strain'] = init_res('Incidence by strain', scale=False, n_strains=ns) + for key,label in cvd.result_flows_by_strain.items(): + self.results['strain'][f'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key], n_strains=ns) # Cumulative variables -- e.g. "Cumulative infections" + for key,label in cvd.result_flows_by_strain.items(): + self.results['strain'][f'new_{key}'] = init_res(f'Number of new {label}', color=dcols[key], n_strains=ns) # Flow variables -- e.g. "Number of new infections" + for key,label in cvd.result_stocks_by_strain.items(): + self.results['strain'][f'n_{key}'] = init_res(label, color=dcols[key], n_strains=ns) # Populate the rest of the results if self['rescale']: @@ -628,21 +636,20 @@ def step(self): for key in cvd.result_stocks.keys(): if 'by_strain' in key or 'by strain' in key: for strain in range(ns): - self.results[f'n_{key}'][strain][t] = people.count_by_strain(key, strain) + self.results['strain'][f'n_{key}'][strain][t] = people.count_by_strain(key, strain) else: self.results[f'n_{key}'][t] = people.count(key) # Update counts for this time step: flows for key,count in people.flows.items(): - if 'by_strain' in key or 'by strain' in key: - for strain in range(ns): - self.results[key][strain][t] += count[strain] - else: - self.results[key][t] += count + self.results[key][t] += count + for key,count in people.flows_strain.items(): + for strain in range(ns): + self.results['strain'][key][strain][t] += count[strain] # Update NAb and immunity for this time step - self.results['pop_nabs'][t] = np.sum(people.NAb[cvu.defined(people.NAb)])/len(people) - self.results['pop_protection'][t] = np.nanmean(people.sus_imm) + self.results['pop_nabs'][t] = np.sum(people.NAb[cvu.defined(people.NAb)])/len(people) + self.results['pop_protection'][t] = np.nanmean(people.sus_imm) self.results['pop_symp_protection'][t] = np.nanmean(people.symp_imm) # Apply analyzers -- same syntax as interventions @@ -760,8 +767,8 @@ def finalize(self, verbose=None, restore_pars=True): # Calculate cumulative results for key in cvd.result_flows.keys(): self.results[f'cum_{key}'][:] = np.cumsum(self.results[f'new_{key}'][:],axis=0) - for key in ['cum_infections','cum_infections_by_strain']: - self.results[key].values += self['pop_infected']*self.rescale_vec[0] # Include initially infected people + self.results['cum_infections'].values += self['pop_infected']*self.rescale_vec[0] # Include initially infected people + self.results['strain']['cum_infections_by_strain'].values += self['pop_infected']*self.rescale_vec[0] # Finalize interventions and analyzers self.finalize_interventions() @@ -817,8 +824,8 @@ def compute_states(self): # self.results['n_removed'][:] = res['cum_recoveries'][:] + res['cum_deaths'][:] # Calculate the number removed: recovered + dead self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence - self.results['incidence_by_strain'][:] = np.einsum('ji,i->ji',res['new_infections_by_strain'][:], 1/res['n_susceptible'][:]) # Calculate the incidence - self.results['prevalence_by_strain'][:] = np.einsum('ji,i->ji',res['new_infections_by_strain'][:], 1/res['n_alive'][:]) # Calculate the prevalence + self.results['strain']['incidence_by_strain'][:] = np.einsum('ji,i->ji',res['strain']['new_infections_by_strain'][:], 1/res['n_susceptible'][:]) # Calculate the incidence + self.results['strain']['prevalence_by_strain'][:] = np.einsum('ji,i->ji',res['strain']['new_infections_by_strain'][:], 1/res['n_alive'][:]) # Calculate the prevalence self.results['share_vaccinated'][:] = res['n_vaccinated'][:]/res['n_alive'][:] # Calculate the share vaccinated return From 0c8b9953beceeab5b747f140f8be3b9e3b9a7b76 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 02:03:50 -0700 Subject: [PATCH 325/569] exploring why addition is failing --- covasim/analysis.py | 2 +- covasim/base.py | 2 ++ covasim/interventions.py | 4 ++-- tests/test_resume.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index de80c52bf..29343ae96 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -596,7 +596,7 @@ def initialize(self, sim): else: self.days = sim.day(self.days) - self.keys = ['exposed', 'infectious', 'symptomatic', 'severe', 'critical', 'known_contact', 'quarantined', 'diagnosed', 'recovered', 'dead'] + self.keys = ['exposed', 'infectious', 'symptomatic', 'severe', 'critical', 'known_contact', 'quarantined', 'diagnosed', 'dead'] # Extra: 'recovered' self.basekeys = ['stocks', 'trans', 'source', 'test', 'quar'] # Categories of things to plot self.extrakeys = ['layer_counts', 'extra'] self.initialized = True diff --git a/covasim/base.py b/covasim/base.py index f0e271e82..aca8bee1f 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -839,7 +839,9 @@ def __add__(self, people2): ''' Combine two people arrays ''' newpeople = sc.dcp(self) for key in self.keys(): + print(key, len(newpeople[key])) newpeople.set(key, np.concatenate([newpeople[key], people2[key]]), die=False) # Allow size mismatch + print(key, len(newpeople[key])) # Validate newpeople.pop_size += people2.pop_size diff --git a/covasim/interventions.py b/covasim/interventions.py index 3db1ff0e8..7b90ab024 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1192,11 +1192,11 @@ def apply(self, sim): self.mod_symp_prob[vacc_inds] *= rel_symp_eff self.vaccinations[vacc_inds] += 1 for v_ind in vacc_inds: - self.vaccination_dates[v_ind].append(sim.t) + self.date_vaccinated[v_ind].append(sim.t) # Update vaccine attributes in sim sim.people.vaccinations = self.vaccinations - sim.people.vaccination_dates = self.vaccination_dates + sim.people.vaccination_dates = self.date_vaccinated return diff --git a/tests/test_resume.py b/tests/test_resume.py index eade18cad..aa870e87a 100644 --- a/tests/test_resume.py +++ b/tests/test_resume.py @@ -86,7 +86,7 @@ def test_reproducibility(): assert r1 == r2 # If you run a sim and save it, you should be able to re-run it on load - s3 = cv.Sim(pars, n_imports=1) + s3 = cv.Sim(pars, pop_infected=44) s3.run() s3.save(fn) s4 = cv.load(fn) From c955ed4c0a07d1a71a8faaeb6e997ab57e9c9eb2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 02:18:25 -0700 Subject: [PATCH 326/569] closer --- covasim/base.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index aca8bee1f..1bb5d3466 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -840,8 +840,15 @@ def __add__(self, people2): newpeople = sc.dcp(self) for key in self.keys(): print(key, len(newpeople[key])) - newpeople.set(key, np.concatenate([newpeople[key], people2[key]]), die=False) # Allow size mismatch - print(key, len(newpeople[key])) + npval = newpeople[key] + p2val = people2[key] + if npval.ndim == 1: + newpeople.set(key, np.concatenate([npval, p2val], axis=0), die=False) # Allow size mismatch + elif npval.ndim == 2: + print('Does not work, sorry') + newpeople.set(key, np.concatenate([npval, p2val], axis=1), die=False) + errormsg = f'Not sure how to combine arrays of {npval.ndim} dimensions for {key}' + raise NotImplementedError(errormsg) # Validate newpeople.pop_size += people2.pop_size From 29a3c563d2a7a88ec41b8be46340adca84b3776a Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 1 Apr 2021 15:06:22 -0400 Subject: [PATCH 327/569] making sure n_imports is an int --- covasim/immunity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 949aba99a..2bcb2dc16 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -41,7 +41,7 @@ def __init__(self, strain=None, strain_label=None, days=None, n_imports=1, **kwa # Handle inputs self.days = days - self.n_imports = n_imports + self.n_imports = cvd.default_int(n_imports) # Strains can be defined in different ways: process these here self.strain_pars = self.parse_strain_pars(strain=strain, strain_label=strain_label) From 50dc3a1ba506580b297b612a25da9dc412a94483 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 25 Mar 2021 18:01:39 -0400 Subject: [PATCH 328/569] manaus repo --- covasim/immunity.py | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 2bcb2dc16..0ad4eb392 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -41,7 +41,7 @@ def __init__(self, strain=None, strain_label=None, days=None, n_imports=1, **kwa # Handle inputs self.days = days - self.n_imports = cvd.default_int(n_imports) + self.n_imports = n_imports # Strains can be defined in different ways: process these here self.strain_pars = self.parse_strain_pars(strain=strain, strain_label=strain_label) @@ -88,11 +88,6 @@ def parse_strain_pars(self, strain=None, strain_label=None): # Known parameters on Brazil variant elif strain in choices['p1']: strain_pars = dict() - strain_pars['rel_beta'] = 1.4 - strain_pars['rel_severe_prob'] = 1.7 - strain_pars['rel_death_prob'] = 2.5 - strain_pars['dur'] = dict() - strain_pars['dur']['crit2die'] = dict(dist='lognormal_int', par1=3, par2=2) strain_pars['rel_imm'] = 0.5 self.strain_label = strain @@ -317,7 +312,7 @@ def init_nab(people, inds, prior_inf=True): no_prior_NAb_inds = np.setdiff1d(inds, prior_NAb_inds) # Find people without prior NAbs prior_NAb = people.NAb[prior_NAb_inds] # Array of NAb levels on this timestep for people with some NAbs - peak_NAb = people.init_NAb[prior_NAb_inds] + # NAbs from infection if prior_inf: @@ -331,7 +326,7 @@ def init_nab(people, inds, prior_inf=True): # 2) Prior NAb: multiply existing NAb by boost factor if len(prior_NAb_inds): - init_NAb = peak_NAb * NAb_boost + init_NAb = prior_NAb * NAb_boost people.init_NAb[prior_NAb_inds] = init_NAb # NAbs from a vaccine @@ -344,7 +339,7 @@ def init_nab(people, inds, prior_inf=True): # 2) Prior NAb (from natural or vaccine dose 1): multiply existing NAb by boost factor if len(prior_NAb_inds): - init_NAb = peak_NAb * NAb_boost + init_NAb = prior_NAb * NAb_boost people.NAb[prior_NAb_inds] = init_NAb return @@ -370,7 +365,7 @@ def check_nab(t, people, inds=None): return -def nab_to_efficacy(nab, ax, slope, n_50): +def nab_to_efficacy(nab, ax): ''' Convert NAb levels to immunity protection factors, using the functional form given in this paper: https://doi.org/10.1101/2021.03.09.21252641 @@ -381,14 +376,17 @@ def nab_to_efficacy(nab, ax, slope, n_50): an array the same size as nab, containing the immunity protection factors for the specified axis ''' - if ax not in ['sus', 'symp', 'sev']: + choices = {'sus': -0.4, 'symp': 0, 'sev': 0.4} + if ax not in choices.keys(): errormsg = f'Choice provided not in list of choices' raise ValueError(errormsg) - n_50 = n_50[ax] + # Temporary parameter values, pending confirmation + n_50 = 0.2 + slope = 2 + # put in here nab to efficacy mapping (logistic regression from fig 1a from https://doi.org/10.1101/2021.03.09.21252641) - efficacy = 1/(1+np.exp(-slope*(np.log10(nab) - np.log10(n_50)))) # from logistic regression in Khoury et al - # efficacy = 1/(1+np.exp(-(slope*np.log10(2))*(nab - np.log2(n_50)))) # from logistic regression in Khoury et al + efficacy = 1/(1+np.exp(-slope*(nab - n_50 + choices[ax]))) # from logistic regression computed in R using data from Khoury et al return efficacy @@ -469,7 +467,6 @@ def check_immunity(people, strain, sus=True, inds=None): is_vacc = cvu.true(people.vaccinated) # Vaccinated date_rec = people.date_recovered # Date recovered immunity = people.pars['immunity'] # cross-immunity/own-immunity scalars to be applied to NAb level before computing efficacy - nab_eff_pars = people.pars['NAb_eff'] # If vaccines are present, extract relevant information about them vacc_present = len(is_vacc) @@ -490,11 +487,11 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] current_NAbs = people.NAb[is_sus_vacc] - people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus', **nab_eff_pars) + people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus') if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain current_NAbs = people.NAb[is_sus_was_inf_same] - people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, strain], 'sus', **nab_eff_pars) + people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, strain], 'sus') if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain prior_strains = people.recovered_strain[is_sus_was_inf_diff] @@ -502,7 +499,7 @@ def check_immunity(people, strain, sus=True, inds=None): for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] current_NAbs = people.NAb[unique_inds] - people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus', **nab_eff_pars) + people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus') else: ### PART 2: @@ -514,13 +511,13 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] current_NAbs = people.NAb[is_inf_vacc] - people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['symp'][strain], 'symp', **nab_eff_pars) - people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['sev'][strain], 'sev', **nab_eff_pars) + people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['symp'][strain], 'symp') + people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['sev'][strain], 'sev') if len(was_inf): # Immunity for reinfected people current_NAbs = people.NAb[was_inf] - people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp', **nab_eff_pars) - people.sev_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['sev'][strain], 'sev', **nab_eff_pars) + people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp') + people.sev_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['sev'][strain], 'sev') return @@ -581,9 +578,9 @@ def pre_compute_waning(length, form='nab_decay', pars=None): def nab_decay(length, init_decay_rate, init_decay_time, decay_decay_rate): ''' - Returns an array of length 'length' containing the evaluated NAb decay + Returns an array of length 'length' containing the evaluated function NAb decay function at each point - Uses exponential decay, with the rate of exponential decay also set to exponentilly decay (!) after init_decay_time days + Uses exponential decay, with the rate of exponential decay also set to exponentilly decay (!) after 250 days ''' f1 = lambda t, init_decay_rate: np.exp(-t*init_decay_rate) From 5e5ca569b72726509108f85054771194e1048369 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 1 Apr 2021 15:11:16 -0400 Subject: [PATCH 329/569] p1 updates --- covasim/immunity.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/covasim/immunity.py b/covasim/immunity.py index 0ad4eb392..9b499ebcd 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -89,6 +89,9 @@ def parse_strain_pars(self, strain=None, strain_label=None): elif strain in choices['p1']: strain_pars = dict() strain_pars['rel_imm'] = 0.5 + strain_pars['rel_beta'] = 1.4 + strain_pars['rel_severe_prob'] = 1.4 + strain_pars['rel_death_prob'] = 2 self.strain_label = strain else: From 0b34b5dcc5beeb410219fd949a5c9bb605b89655 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 1 Apr 2021 15:16:09 -0400 Subject: [PATCH 330/569] default in for n imports --- covasim/immunity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 9b499ebcd..f8f6df6ce 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -41,7 +41,7 @@ def __init__(self, strain=None, strain_label=None, days=None, n_imports=1, **kwa # Handle inputs self.days = days - self.n_imports = n_imports + self.n_imports = cvd.default_int(n_imports) # Strains can be defined in different ways: process these here self.strain_pars = self.parse_strain_pars(strain=strain, strain_label=strain_label) From be5c250fdec303761b921070b0a3335f23e0ac52 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 1 Apr 2021 21:27:56 -0400 Subject: [PATCH 331/569] somehow the nab_eff pars didn't make it into immunity.py --- covasim/immunity.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index f8f6df6ce..1eb828842 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -368,7 +368,7 @@ def check_nab(t, people, inds=None): return -def nab_to_efficacy(nab, ax): +def nab_to_efficacy(nab, ax, slope, n_50, factors): ''' Convert NAb levels to immunity protection factors, using the functional form given in this paper: https://doi.org/10.1101/2021.03.09.21252641 @@ -379,17 +379,12 @@ def nab_to_efficacy(nab, ax): an array the same size as nab, containing the immunity protection factors for the specified axis ''' - choices = {'sus': -0.4, 'symp': 0, 'sev': 0.4} - if ax not in choices.keys(): + if ax not in ['sus', 'symp', 'sev']: errormsg = f'Choice provided not in list of choices' raise ValueError(errormsg) - # Temporary parameter values, pending confirmation - n_50 = 0.2 - slope = 2 - # put in here nab to efficacy mapping (logistic regression from fig 1a from https://doi.org/10.1101/2021.03.09.21252641) - efficacy = 1/(1+np.exp(-slope*(nab - n_50 + choices[ax]))) # from logistic regression computed in R using data from Khoury et al + efficacy = 1 / (1 + np.exp(-slope * (nab - n_50 + factors[ax]))) # from logistic regression computed in R using data from Khoury et al return efficacy @@ -470,6 +465,7 @@ def check_immunity(people, strain, sus=True, inds=None): is_vacc = cvu.true(people.vaccinated) # Vaccinated date_rec = people.date_recovered # Date recovered immunity = people.pars['immunity'] # cross-immunity/own-immunity scalars to be applied to NAb level before computing efficacy + nab_eff_pars = people.pars['NAb_eff'] # If vaccines are present, extract relevant information about them vacc_present = len(is_vacc) @@ -490,11 +486,11 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] current_NAbs = people.NAb[is_sus_vacc] - people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus') + people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus', **nab_eff_pars) if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain current_NAbs = people.NAb[is_sus_was_inf_same] - people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, strain], 'sus') + people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, strain], 'sus', **nab_eff_pars) if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain prior_strains = people.recovered_strain[is_sus_was_inf_diff] @@ -502,7 +498,7 @@ def check_immunity(people, strain, sus=True, inds=None): for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] current_NAbs = people.NAb[unique_inds] - people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus') + people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus', **nab_eff_pars) else: ### PART 2: @@ -514,13 +510,13 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] current_NAbs = people.NAb[is_inf_vacc] - people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['symp'][strain], 'symp') - people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['sev'][strain], 'sev') + people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['symp'][strain], 'symp', **nab_eff_pars) + people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['sev'][strain], 'sev', **nab_eff_pars) if len(was_inf): # Immunity for reinfected people current_NAbs = people.NAb[was_inf] - people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp') - people.sev_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['sev'][strain], 'sev') + people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp', **nab_eff_pars) + people.sev_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['sev'][strain], 'sev', **nab_eff_pars) return From 2ecccf1d9cb046966765fbfa006900cdcad76e32 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 19:09:37 -0700 Subject: [PATCH 332/569] fixed array ordering --- covasim/base.py | 13 ++-- covasim/defaults.py | 142 ++++++++++++++++++++++++-------------------- covasim/immunity.py | 29 ++++++--- covasim/utils.py | 16 ----- 4 files changed, 107 insertions(+), 93 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 1bb5d3466..8b3899356 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -140,10 +140,13 @@ def __init__(self, name=None, npts=None, scale=True, color=None, n_strains=0): self.color = color # Default color if npts is None: npts = 0 + npts = int(npts) + if n_strains>0: - self.values = np.full((n_strains, npts), 0, dtype=cvd.result_float, order='F') + self.values = np.zeros((n_strains, npts), dtype=cvd.result_float) else: - self.values = np.array(np.zeros(int(npts)), dtype=cvd.result_float) + self.values = np.zeros(npts, dtype=cvd.result_float) + self.low = None self.high = None return @@ -838,15 +841,15 @@ def __iter__(self): def __add__(self, people2): ''' Combine two people arrays ''' newpeople = sc.dcp(self) - for key in self.keys(): - print(key, len(newpeople[key])) + keys = list(self.keys()) + for key in keys: npval = newpeople[key] p2val = people2[key] if npval.ndim == 1: newpeople.set(key, np.concatenate([npval, p2val], axis=0), die=False) # Allow size mismatch elif npval.ndim == 2: - print('Does not work, sorry') newpeople.set(key, np.concatenate([npval, p2val], axis=1), die=False) + else: errormsg = f'Not sure how to combine arrays of {npval.ndim} dimensions for {key}' raise NotImplementedError(errormsg) diff --git a/covasim/defaults.py b/covasim/defaults.py index 79ce39cc4..8ca7d610c 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -37,69 +37,85 @@ class PeopleMeta(sc.prettyobj): ''' For storing all the keys relating to a person and people ''' - # Set the properties of a person - person = [ - 'uid', # Int - 'age', # Float - 'sex', # Float - 'symp_prob', # Float - 'severe_prob', # Float - 'crit_prob', # Float - 'death_prob', # Float - 'rel_trans', # Float - 'rel_sus', # Float - 'prior_symptoms', # Float - 'sus_imm', # Float - 'symp_imm', # Float - 'sev_imm', # Float - 'prior_symptoms', # Float - 'vaccinations', # Number of doses given per person - 'vaccine_source', # index of vaccine that individual received - 'init_NAb', # Initial neutralization titre relative to convalescent plasma - 'NAb', # Current neutralization titre relative to convalescent plasma - ] - - # Set the states that a person can be in: these are all booleans per person -- used in people.py - states = [ - 'susceptible', - 'exposed', - 'infectious', - 'symptomatic', - 'severe', - 'critical', - 'tested', - 'diagnosed', - 'dead', - 'known_contact', - 'quarantined', - 'vaccinated' - ] - - strain_states = [ - 'exposed_strain', - 'exposed_by_strain', - 'infectious_strain', - 'infectious_by_strain', - 'recovered_strain', - ] - - # Set the dates various events took place: these are floats per person -- used in people.py - dates = [f'date_{state}' for state in states] # Convert each state into a date - dates.append('date_pos_test') # Store the date when a person tested which will come back positive - dates.append('date_end_quarantine') # Store the date when a person comes out of quarantine - dates.append('date_recovered') # Store the date when a person recovers - dates.append('date_vaccinated') # Store the date when a person is vaccinated - - # Duration of different states: these are floats per person -- used in people.py - durs = [ - 'dur_exp2inf', - 'dur_inf2sym', - 'dur_sym2sev', - 'dur_sev2crit', - 'dur_disease', - ] - - all_states = person + states + strain_states + dates + durs + def __init__(self): + + # Set the properties of a person + self.person = [ + 'uid', # Int + 'age', # Float + 'sex', # Float + 'symp_prob', # Float + 'severe_prob', # Float + 'crit_prob', # Float + 'death_prob', # Float + 'rel_trans', # Float + 'rel_sus', # Float + 'sus_imm', # Float + 'symp_imm', # Float + 'sev_imm', # Float + 'prior_symptoms', # Float + 'vaccinations', # Number of doses given per person + 'vaccine_source', # index of vaccine that individual received + 'init_NAb', # Initial neutralization titre relative to convalescent plasma + 'NAb', # Current neutralization titre relative to convalescent plasma + ] + + # Set the states that a person can be in: these are all booleans per person -- used in people.py + self.states = [ + 'susceptible', + 'exposed', + 'infectious', + 'symptomatic', + 'severe', + 'critical', + 'tested', + 'diagnosed', + 'dead', + 'known_contact', + 'quarantined', + 'vaccinated' + ] + + self.strain_states = [ + 'exposed_strain', + 'exposed_by_strain', + 'infectious_strain', + 'infectious_by_strain', + 'recovered_strain', + ] + + # Set the dates various events took place: these are floats per person -- used in people.py + self.dates = [f'date_{state}' for state in self.states] # Convert each state into a date + self.dates.append('date_pos_test') # Store the date when a person tested which will come back positive + self.dates.append('date_end_quarantine') # Store the date when a person comes out of quarantine + self.dates.append('date_recovered') # Store the date when a person recovers + # self.dates.append('date_vaccinated') # Store the date when a person is vaccinated + + # Duration of different states: these are floats per person -- used in people.py + self.durs = [ + 'dur_exp2inf', + 'dur_inf2sym', + 'dur_sym2sev', + 'dur_sev2crit', + 'dur_disease', + ] + + self.all_states = self.person + self.states + self.strain_states + self.dates + self.durs + + # Validate + self.state_types = ['person', 'states', 'strain_states', 'dates', 'durs', 'all_states'] + for state_type in self.state_types: + states = getattr(self, state_type) + n_states = len(states) + n_unique_states = len(set(states)) + if n_states != n_unique_states: + errormsg = f'In {state_type}, only {n_unique_states} of {n_states} state names are unique' + raise ValueError(errormsg) + + return + + + #%% Define other defaults diff --git a/covasim/immunity.py b/covasim/immunity.py index f8f6df6ce..af1225ac8 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -3,21 +3,14 @@ ''' import numpy as np -import pylab as pl import sciris as sc -import datetime as dt from . import utils as cvu from . import defaults as cvd -from . import base as cvb -from . import parameters as cvpar -from . import people as cvppl -from collections import defaultdict -__all__ = [] # %% Define strain class -__all__ += ['Strain', 'Vaccine'] +__all__ = ['Strain', 'Vaccine'] class Strain(): @@ -381,7 +374,7 @@ def nab_to_efficacy(nab, ax): choices = {'sus': -0.4, 'symp': 0, 'sev': 0.4} if ax not in choices.keys(): - errormsg = f'Choice provided not in list of choices' + errormsg = f'Choice {ax} not provided in list of choices' raise ValueError(errormsg) # Temporary parameter values, pending confirmation @@ -398,6 +391,24 @@ def nab_to_efficacy(nab, ax): __all__ += ['init_immunity', 'check_immunity'] +def update_strain_attributes(people): + for key in people.meta.person: + if 'imm' in key: # everyone starts out with no immunity to either strain. # TODO: refactor + rows,cols = people[key].shape + people[key].resize(rows+1, cols, refcheck=False) + + # Set strain states, which store info about which strain a person is exposed to + for key in people.meta.strain_states: + if 'by' in key: # TODO: refactor + rows,cols = people[key].shape + people[key].resize(rows+1, cols, refcheck=False) + + for key in cvd.new_result_flows_by_strain: + rows, = people[key].shape + people.flows_strain[key].reshape(rows+1, refcheck=False) + return + + def init_immunity(sim, create=False): ''' Initialize immunity matrices with all strains that will eventually be in the sim''' ts = sim['total_strains'] diff --git a/covasim/utils.py b/covasim/utils.py index 4aac0beb9..77cd98bad 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -129,22 +129,6 @@ def find_contacts(p1, p2, inds): # pragma: no cover return pairing_partners -def update_strain_attributes(people): - for key in people.meta.person: - if 'imm' in key: # everyone starts out with no immunity to either strain. - people[key] = np.append(people[key], np.full((1, people.pop_size), 0, dtype=cvd.default_float, order='F'), axis=0) - - # Set strain states, which store info about which strain a person is exposed to - for key in people.meta.strain_states: - if 'by' in key: - people[key] = np.append(people[key], np.full((1, people.pop_size), False, dtype=bool, order='F'), axis=0) - - for key in cvd.new_result_flows: - if 'by_strain' in key: - people.flows[key] = np.append(people.flows[key], np.full(1, 0, dtype=cvd.default_float), axis=0) - return - - #%% Sampling and seed methods From a1e8254a34fe4db0084218dd8a48548355491068 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Thu, 1 Apr 2021 22:19:44 -0400 Subject: [PATCH 333/569] immunity based on conditionals instead of absolutes --- covasim/immunity.py | 30 ++++++++++++++++++++---------- covasim/parameters.py | 4 +++- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 1eb828842..56b37a0d5 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -368,7 +368,7 @@ def check_nab(t, people, inds=None): return -def nab_to_efficacy(nab, ax, slope, n_50, factors): +def nab_to_efficacy(nab, ax, args): ''' Convert NAb levels to immunity protection factors, using the functional form given in this paper: https://doi.org/10.1101/2021.03.09.21252641 @@ -382,9 +382,19 @@ def nab_to_efficacy(nab, ax, slope, n_50, factors): if ax not in ['sus', 'symp', 'sev']: errormsg = f'Choice provided not in list of choices' raise ValueError(errormsg) + args = args[ax] - # put in here nab to efficacy mapping (logistic regression from fig 1a from https://doi.org/10.1101/2021.03.09.21252641) - efficacy = 1 / (1 + np.exp(-slope * (nab - n_50 + factors[ax]))) # from logistic regression computed in R using data from Khoury et al + if ax == 'sus': + slope = args['slope'] + n_50 = args['n_50'] + efficacy = 1 / (1 + np.exp(-slope * (np.log10(nab) - np.log10(n_50)))) # from logistic regression computed in R using data from Khoury et al + else: + threshold = np.full(len(nab), fill_value=args['threshold']) + lower = args['lower'] + upper = args['upper'] + efficacy = nab>threshold + efficacy = np.where(efficacy == False, upper, efficacy) + efficacy = np.where(efficacy == True, lower, efficacy) return efficacy @@ -486,11 +496,11 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] current_NAbs = people.NAb[is_sus_vacc] - people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus', **nab_eff_pars) + people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus', nab_eff_pars) if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain current_NAbs = people.NAb[is_sus_was_inf_same] - people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, strain], 'sus', **nab_eff_pars) + people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, strain], 'sus', nab_eff_pars) if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain prior_strains = people.recovered_strain[is_sus_was_inf_diff] @@ -498,7 +508,7 @@ def check_immunity(people, strain, sus=True, inds=None): for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] current_NAbs = people.NAb[unique_inds] - people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus', **nab_eff_pars) + people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus', nab_eff_pars) else: ### PART 2: @@ -510,13 +520,13 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] current_NAbs = people.NAb[is_inf_vacc] - people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['symp'][strain], 'symp', **nab_eff_pars) - people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['sev'][strain], 'sev', **nab_eff_pars) + people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['symp'][strain], 'symp', nab_eff_pars) + people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['sev'][strain], 'sev', nab_eff_pars) if len(was_inf): # Immunity for reinfected people current_NAbs = people.NAb[was_inf] - people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp', **nab_eff_pars) - people.sev_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['sev'][strain], 'sev', **nab_eff_pars) + people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp', nab_eff_pars) + people.sev_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['sev'][strain], 'sev', nab_eff_pars) return diff --git a/covasim/parameters.py b/covasim/parameters.py index 765115972..89ae7ecfc 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -68,7 +68,9 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source - pars['NAb_eff'] = {'slope': 3.43297265, 'n_50': {'sus': 0.5, 'symp': 0.19869944, 'sev': 0.031}} # Parameters to map NAbs to efficacy + pars['NAb_eff'] = {'sus': {'slope': 2, 'n_50': 0.4}, + 'symp': {'threshold': 1.2, 'lower': 0.2, 'upper': 0.1}, + 'sev': {'threshold': 1.2, 'lower': 0.9, 'upper': 0.52}} # Parameters to map NAbs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms pars['rel_imm']['asymptomatic'] = 0.7 From da0c85f013afeb62e62a5c84123d149879db9c74 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 19:39:35 -0700 Subject: [PATCH 334/569] starting work on use_immunity --- tests/check_coverage | 5 ++++- tests/test_utils.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/check_coverage b/tests/check_coverage index 36d1f266a..36a68925b 100755 --- a/tests/check_coverage +++ b/tests/check_coverage @@ -2,10 +2,13 @@ # Note that although the script runs when parallelized, the coverage results are wrong. echo 'Running tests...' -pytest -v test_*.py --cov-config=.coveragerc --cov=../covasim --workers auto --durations=20 +pytest -v test_*.py --cov-config=.coveragerc --cov=../covasim --workers auto --durations=0 echo 'Creating HTML report...' coverage html +echo 'Printing report...' +coverage report + echo 'Report location:' echo "`pwd`/htmlcov/index.html" \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py index 1d4bdbd0a..377261262 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -200,7 +200,7 @@ def test_choose_w(): def test_doubling_time(): - sim = cv.Sim() + sim = cv.Sim(pop_size=1000) sim.run(verbose=0) d = sc.objdict() From f3bf055f84e5ef00c38dfb82e6621d81d49b61a2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 19:44:31 -0700 Subject: [PATCH 335/569] decided on parameter rather than argument --- covasim/parameters.py | 3 ++- covasim/sim.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 22459ab06..c7f7f1f2e 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -63,11 +63,12 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['total_strains'] = None # Set during sim initialization, once strains have been specified and processed # Parameters used to calculate immunity + pars['use_immunity'] = False # Whether to use dynamically calculated immunity pars['NAb_init'] = dict(dist='normal', par1= 0, par2= 2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source - pars['NAb_eff'] = {'slope': 3.43297265, 'n_50': {'sus': 0.5, 'symp': 0.19869944, 'sev': 0.031}} # Parameters to map NAbs to efficacy + pars['NAb_eff'] = {'slope': 3.433, 'n_50': {'sus': 0.5, 'symp': 0.1987, 'sev': 0.031}} # Parameters to map NAbs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms pars['rel_imm']['asymptomatic'] = 0.7 diff --git a/covasim/sim.py b/covasim/sim.py index 652cf1956..062c30db7 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -29,16 +29,16 @@ class Sim(cvb.BaseSim): loading, exporting, etc.). Please see the BaseSim class for additional methods. Args: - pars (dict): parameters to modify from their default values - datafile (str/df): filename of (Excel, CSV) data file to load, or a pandas dataframe of the data - datacols (list): list of column names of the data to load - label (str): the name of the simulation (useful to distinguish in batch runs) - simfile (str): the filename for this simulation, if it's saved (default: creation date) - popfile (str): the filename to load/save the population for this simulation - load_pop (bool): whether to load the population from the named file - save_pop (bool): whether to save the population to the named file - version (str): if supplied, use default parameters from this version of Covasim instead of the latest - kwargs (dict): passed to make_pars() + pars (dict): parameters to modify from their default values + datafile (str/df): filename of (Excel, CSV) data file to load, or a pandas dataframe of the data + datacols (list): list of column names of the data to load + label (str): the name of the simulation (useful to distinguish in batch runs) + simfile (str): the filename for this simulation, if it's saved (default: creation date) + popfile (str): the filename to load/save the population for this simulation + load_pop (bool): whether to load the population from the named file + save_pop (bool): whether to save the population to the named file + version (str): if supplied, use default parameters from this version of Covasim instead of the latest + kwargs (dict): passed to make_pars() **Examples**:: From 0bbabf505161e6fb002cf1972d7bef930dc54e04 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 20:06:33 -0700 Subject: [PATCH 336/569] runs, but results differ --- covasim/parameters.py | 4 ++-- covasim/people.py | 3 ++- covasim/sim.py | 13 ++++++++----- tests/test_baselines.py | 4 ++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index c7f7f1f2e..8f746640e 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -59,8 +59,8 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated # Parameters that control settings and defaults for multi-strain runs - pars['n_strains'] = 1 # The number of strains currently circulating in the population - pars['total_strains'] = None # Set during sim initialization, once strains have been specified and processed + pars['n_strains'] = 1 # The number of strains currently circulating in the population + pars['total_strains'] = 1 # Set during sim initialization, once strains have been specified and processed # Parameters used to calculate immunity pars['use_immunity'] = False # Whether to use dynamically calculated immunity diff --git a/covasim/people.py b/covasim/people.py index 15249f836..4d1e65529 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -391,7 +391,8 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str if source is not None: source = source[keep] - cvi.check_immunity(self, strain, sus=False, inds=inds) + if self.pars['use_immunity']: + cvi.check_immunity(self, strain, sus=False, inds=inds) # Deal with strain parameters infect_parkeys = ['dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob'] diff --git a/covasim/sim.py b/covasim/sim.py index 062c30db7..abc05653e 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -108,8 +108,9 @@ def initialize(self, reset=False, **kwargs): self.t = 0 # The current time index self.validate_pars() # Ensure parameters have valid values self.set_seed() # Reset the random seed before the population is created - self.init_strains() # ...and the strains.... - self.init_immunity() # ... and information about immunity/cross-immunity. + if self['use_immunity']: + self.init_strains() # ...and the strains.... + self.init_immunity() # ... and information about immunity/cross-immunity. self.init_results() # After initializing the strain, create the results structure self.init_people(save_pop=self.save_pop, load_pop=self.load_pop, popfile=self.popfile, reset=reset, **kwargs) # Create all the people (slow) self.init_interventions() # Initialize the interventions... @@ -590,14 +591,16 @@ def step(self): ns = self['n_strains'] # Shorten number of strains # Check NAbs. Take set difference so we don't compute NAbs for anyone currently infected - has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.false(sus)) - if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) + if self['use_immunity']: + has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.false(sus)) + if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) # Iterate through n_strains to calculate infections for strain in range(ns): # Check immunity - cvimm.check_immunity(people, strain, sus=True) + if self['use_immunity']: + cvimm.check_immunity(people, strain, sus=True) # Deal with strain parameters for key in strain_keys: diff --git a/tests/test_baselines.py b/tests/test_baselines.py index 6b53ba0bc..d6e741f8f 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -186,9 +186,9 @@ def normalize_performance(): cv.options.set(interactive=do_plot) T = sc.tic() - json = test_benchmark(do_save=do_save) # Run this first so benchmarking is available even if results are different + # json = test_benchmark(do_save=do_save) # Run this first so benchmarking is available even if results are different new = test_baseline() - make_sim(do_plot=do_plot) + # make_sim(do_plot=do_plot) print('\n'*2) sc.toc(T) From b9e9056e4188c34a4589771289dda39ce119fbbf Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 20:24:28 -0700 Subject: [PATCH 337/569] some progress, no match --- covasim/sim.py | 6 +++--- covasim/utils.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index abc05653e..e3530e8bd 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -609,8 +609,9 @@ def step(self): asymp_factor = cvd.default_float(strain_pars['asymp_factor']) # Define indices for this strain - inf_by_this_strain = sc.dcp(inf) - inf_by_this_strain[cvu.false(people.infectious_strain == strain)] = False + # inf_by_this_strain = np.zeros(len(inf), dtype=bool) + # inf_by_this_strain[cvu.true(people.infectious_strain == strain)] = True + inf_by_this_strain = people.infectious_strain == strain for lkey, layer in contacts.items(): p1 = layer['p1'] @@ -627,7 +628,6 @@ def step(self): beta_layer = cvd.default_float(self['beta_layer'][lkey]) rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, sus_imm) - rel_sus = np.float32(rel_sus) # TODO: why doesn't this get returned in this format already? # Calculate actual transmission for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 diff --git a/covasim/utils.py b/covasim/utils.py index 77cd98bad..804847dd4 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -76,14 +76,14 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -#@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=safe_parallel) +@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=safe_parallel) def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] f_iso = ~diag + diag * iso_factor # Isolation factor, changes e.g. [0,1] with a factor of 0.2 to [1,0.2] f_quar = ~quar + quar * quar_factor # Quarantine, changes e.g. [0,1] with a factor of 0.5 to [1,0.5] rel_trans = beta_layer * rel_trans * inf * f_quar * f_asymp * f_iso * viral_load # Recalculate transmissibility - rel_sus = rel_sus * sus * f_quar * (1.-immunity_factors) # Recalculate susceptibility + rel_sus = rel_sus * sus * f_quar * (1-immunity_factors) # Recalculate susceptibility return rel_trans, rel_sus From 2277e01e3bf1c1b9918f9af3505a81728b3532db Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 20:36:29 -0700 Subject: [PATCH 338/569] closer, still not matching --- covasim/defaults.py | 3 ++- covasim/parameters.py | 24 ++++++++++++------------ covasim/people.py | 38 ++++++++++++++++++++++---------------- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 8ca7d610c..4107a7237 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -70,6 +70,7 @@ def __init__(self): 'critical', 'tested', 'diagnosed', + 'recovered', 'dead', 'known_contact', 'quarantined', @@ -88,7 +89,7 @@ def __init__(self): self.dates = [f'date_{state}' for state in self.states] # Convert each state into a date self.dates.append('date_pos_test') # Store the date when a person tested which will come back positive self.dates.append('date_end_quarantine') # Store the date when a person comes out of quarantine - self.dates.append('date_recovered') # Store the date when a person recovers + # self.dates.append('date_recovered') # Store the date when a person recovers # self.dates.append('date_vaccinated') # Store the date when a person is vaccinated # Duration of different states: these are floats per person -- used in people.py diff --git a/covasim/parameters.py b/covasim/parameters.py index 8f746640e..1a2fd314b 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -64,18 +64,18 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Parameters used to calculate immunity pars['use_immunity'] = False # Whether to use dynamically calculated immunity - pars['NAb_init'] = dict(dist='normal', par1= 0, par2= 2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 - pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 - pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters - pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source - pars['NAb_eff'] = {'slope': 3.433, 'n_50': {'sus': 0.5, 'symp': 0.1987, 'sev': 0.031}} # Parameters to map NAbs to efficacy - pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains - pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms - pars['rel_imm']['asymptomatic'] = 0.7 - pars['rel_imm']['mild'] = 0.9 - pars['rel_imm']['severe'] = 1 - pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py - pars['vaccine_info'] = None # Vaccine info in a more easily accessible format + # pars['NAb_init'] = dict(dist='normal', par1= 0, par2= 2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 + # pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 + # pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters + # pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source + # pars['NAb_eff'] = {'slope': 3.433, 'n_50': {'sus': 0.5, 'symp': 0.1987, 'sev': 0.031}} # Parameters to map NAbs to efficacy + # pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains + # pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms + # pars['rel_imm']['asymptomatic'] = 0.7 + # pars['rel_imm']['mild'] = 0.9 + # pars['rel_imm']['severe'] = 1 + # pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py + # pars['vaccine_info'] = None # Vaccine info in a more easily accessible format # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 diff --git a/covasim/people.py b/covasim/people.py index 4d1e65529..91dde1b93 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -217,8 +217,8 @@ def check_infectious(self): self.infectious[inds] = True self.infectious_strain[inds] = self.exposed_strain[inds] for strain in range(self.pars['n_strains']): - this_strain_inds = cvu.itrue(self.infectious_strain[inds] == strain, inds) - self.flows_strain['new_infectious_by_strain'][strain] += len(this_strain_inds) + n_this_strain_inds = (self.infectious_strain[inds] == strain).sum() + self.flows_strain['new_infectious_by_strain'][strain] += n_this_strain_inds return len(inds) @@ -247,25 +247,31 @@ def check_recovery(self): ''' Check for recovery ''' inds = self.check_inds(self.susceptible, self.date_recovered, filter_inds=self.is_exp) - # Before letting them recover, store information about the strain they had, store symptoms and pre-compute NAbs array - self.recovered_strain[inds] = self.exposed_strain[inds] - mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=inds) - severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=inds) - self.prior_symptoms[inds] = self.pars['rel_imm']['asymptomatic'] # - self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] # - self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] # - if len(inds): - cvi.init_nab(self, inds, prior_inf=True) - # Now reset all disease states self.exposed[inds] = False self.infectious[inds] = False self.symptomatic[inds] = False self.severe[inds] = False self.critical[inds] = False - self.susceptible[inds] = True - self.infectious_strain[inds]= np.nan - self.exposed_strain[inds] = np.nan + self.recovered[inds] = True + + # Handle immunity aspects + if self.pars['use_immunity']: + + # Before letting them recover, store information about the strain they had, store symptoms and pre-compute NAbs array + self.recovered_strain[inds] = self.exposed_strain[inds] + mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=inds) + severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=inds) + + # Reset additional states + self.susceptible[inds] = True + self.infectious_strain[inds] = np.nan + self.exposed_strain[inds] = np.nan + self.prior_symptoms[inds] = self.pars['rel_imm']['asymptomatic'] # + self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] # + self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] # + if len(inds): + cvi.init_nab(self, inds, prior_inf=True) return len(inds) @@ -652,7 +658,7 @@ def label_lkey(lkey): if lkey: events.append((infection['date'], f'was infected with COVID by {infection["source"]} via the {llabel} layer')) else: - events.append((infection['date'], f'was infected with COVID as a seed infection')) + events.append((infection['date'], 'was infected with COVID as a seed infection')) if infection['source'] == uid: x = len([a for a in self.infection_log if a['source'] == infection['target']]) From d3b8ab568b8813a5c814c4c52b5e869dc4a28c96 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 20:38:05 -0700 Subject: [PATCH 339/569] reinstate parameters, now that it's known they're not used --- covasim/parameters.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 1a2fd314b..8f746640e 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -64,18 +64,18 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Parameters used to calculate immunity pars['use_immunity'] = False # Whether to use dynamically calculated immunity - # pars['NAb_init'] = dict(dist='normal', par1= 0, par2= 2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 - # pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 - # pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters - # pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source - # pars['NAb_eff'] = {'slope': 3.433, 'n_50': {'sus': 0.5, 'symp': 0.1987, 'sev': 0.031}} # Parameters to map NAbs to efficacy - # pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains - # pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms - # pars['rel_imm']['asymptomatic'] = 0.7 - # pars['rel_imm']['mild'] = 0.9 - # pars['rel_imm']['severe'] = 1 - # pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py - # pars['vaccine_info'] = None # Vaccine info in a more easily accessible format + pars['NAb_init'] = dict(dist='normal', par1= 0, par2= 2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 + pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 + pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters + pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source + pars['NAb_eff'] = {'slope': 3.433, 'n_50': {'sus': 0.5, 'symp': 0.1987, 'sev': 0.031}} # Parameters to map NAbs to efficacy + pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains + pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms + pars['rel_imm']['asymptomatic'] = 0.7 + pars['rel_imm']['mild'] = 0.9 + pars['rel_imm']['severe'] = 1 + pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py + pars['vaccine_info'] = None # Vaccine info in a more easily accessible format # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 From 3aecf5ca62955321566d2c60565a488922ba0cdb Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 21:46:06 -0700 Subject: [PATCH 340/569] debugging, happens on day of first severe case --- covasim/base.py | 10 +++++++++- covasim/defaults.py | 6 +++--- covasim/people.py | 10 ++++++---- covasim/utils.py | 4 +++- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 8b3899356..cfb66fa2c 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1055,7 +1055,15 @@ def person(self, ind): ''' Method to create person from the people ''' p = Person() for key in self.meta.all_states: - setattr(p, key, self[key][ind]) + data = self[key] + if data.ndim == 1: + val = data[ind] + elif data.ndim == 2: + val = data[:,ind] + else: + errormsg = f'Cannot extract data from {key}: unexpected dimensionality ({data.ndim})' + raise ValueError(errormsg) + setattr(p, key, val) contacts = {} for lkey, layer in self.contacts.items(): diff --git a/covasim/defaults.py b/covasim/defaults.py index 4107a7237..d1d7b559d 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -264,19 +264,19 @@ def get_strain_colors(): # Define the 'overview plots', i.e. the most useful set of plots to explore different aspects of a simulation overview_plots = [ 'cum_infections', - 'cum_infections_by_strain', + # 'cum_infections_by_strain', 'cum_severe', 'cum_critical', 'cum_deaths', 'cum_diagnoses', 'new_infections', - 'new_infections_by_strain', + # 'new_infections_by_strain', 'new_severe', 'new_critical', 'new_deaths', 'new_diagnoses', 'n_infectious', - 'n_infectious_by_strain', + # 'n_infectious_by_strain', 'n_severe', 'n_critical', 'n_susceptible', diff --git a/covasim/people.py b/covasim/people.py index 91dde1b93..ffdce9984 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -245,7 +245,7 @@ def check_critical(self): def check_recovery(self): ''' Check for recovery ''' - inds = self.check_inds(self.susceptible, self.date_recovered, filter_inds=self.is_exp) + inds = self.check_inds(self.recovered, self.date_recovered, filter_inds=self.is_exp) # TODO TEMP!!!! # Now reset all disease states self.exposed[inds] = False @@ -283,7 +283,7 @@ def check_death(self): self.symptomatic[inds] = False self.severe[inds] = False self.critical[inds] = False - self.susceptible[inds] = False + self.recovered[inds] = False self.dead[inds] = True self.infectious_strain[inds]= np.nan self.exposed_strain[inds] = np.nan @@ -321,7 +321,7 @@ def check_quar(self): for ind,end_day in self._pending_quarantine[self.t]: if self.quarantined[ind]: self.date_end_quarantine[ind] = max(self.date_end_quarantine[ind], end_day) # Extend quarantine if required - elif not (self.dead[ind] or self.diagnosed[ind]): # Unclear whether recovered should be included here + elif not (self.dead[ind] or self.recovered[ind] or self.diagnosed[ind]): # Unclear whether recovered should be included here # elif not (self.dead[ind] or self.diagnosed[ind]): self.quarantined[ind] = True self.date_quarantined[ind] = self.t self.date_end_quarantine[ind] = end_day @@ -418,7 +418,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.flows['new_infections'] += len(inds) self.flows_strain['new_infections_by_strain'][strain] += len(inds) self.flows['new_reinfections'] += len(cvu.defined(self.date_recovered[inds])) # Record reinfections - self.date_recovered[inds] = np.nan # Reset date they recovered - we only store the last recovery + #self.date_recovered[inds] = np.nan # Reset date they recovered - we only store the last recovery # TODO CK # Record transmissions for i, target in enumerate(inds): @@ -430,6 +430,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str # Use prognosis probabilities to determine what happens to them symp_probs = infect_pars['rel_symp_prob']*self.symp_prob[inds]*(1-self.symp_imm[strain, inds]) # Calculate their actual probability of being symptomatic + # print(self.symp_imm[strain, inds]) is_symp = cvu.binomial_arr(symp_probs) # Determine if they develop symptoms symp_inds = inds[is_symp] asymp_inds = inds[~is_symp] # Asymptomatic @@ -444,6 +445,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.dur_inf2sym[symp_inds] = cvu.sample(**durpars['inf2sym'], size=n_symp_inds) # Store how long this person took to develop symptoms self.date_symptomatic[symp_inds] = self.date_infectious[symp_inds] + self.dur_inf2sym[symp_inds] # Date they become symptomatic sev_probs = infect_pars['rel_severe_prob'] * self.severe_prob[symp_inds]*(1-self.sev_imm[strain, symp_inds]) # Probability of these people being severe + # print(self.sev_imm[strain, inds]) is_sev = cvu.binomial_arr(sev_probs) # See if they're a severe or mild case sev_inds = symp_inds[is_sev] mild_inds = symp_inds[~is_sev] # Not severe diff --git a/covasim/utils.py b/covasim/utils.py index 804847dd4..382daec26 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -87,7 +87,7 @@ def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, return rel_trans, rel_sus -@nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) +# @nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): # pragma: no cover ''' Compute who infects whom @@ -107,6 +107,8 @@ def compute_infections(beta, sources, targets, layer_betas, rel_trans, r transmissions = (np.random.random(len(nonzero_betas)) < nonzero_betas).nonzero()[0] # Compute the actual infections! source_inds = nonzero_sources[transmissions] target_inds = nonzero_targets[transmissions] # Filter the targets on the actual infections + print(np.random.random()) + print(target_inds) return source_inds, target_inds From af79ca1d1b22ed2f9c9be6fd5826ede2b09010f9 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 22:27:52 -0700 Subject: [PATCH 341/569] wait...it matches now? --- covasim/people.py | 2 ++ covasim/sim.py | 26 ++++++++++++++++++-------- covasim/utils.py | 14 ++++++++++---- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index ffdce9984..a0a2f84f7 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -486,6 +486,8 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.date_dead[dead_inds] = self.date_critical[dead_inds] + dur_crit2die # Date of death self.dur_disease[dead_inds] = self.dur_exp2inf[dead_inds] + self.dur_inf2sym[dead_inds] + self.dur_sym2sev[dead_inds] + self.dur_sev2crit[dead_inds] + dur_crit2die # Store how long this person had COVID-19 + print('moshi', np.random.random()) + return n_infections # For incrementing counters diff --git a/covasim/sim.py b/covasim/sim.py index e3530e8bd..b13872341 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -535,6 +535,8 @@ def step(self): rather than calling sim.step() directly. ''' + print('s1', np.random.random()) + # Set the time and if we have reached the end of the simulation, then do nothing if self.complete: raise AlreadyRunError('Simulation already complete (call sim.initialize() to re-run)') @@ -569,6 +571,8 @@ def step(self): people.update_states_post() # Check for state changes after interventions + print('s2', np.random.random()) + # Compute viral loads frac_time = cvd.default_float(self['viral_dist']['frac_time']) load_ratio = cvd.default_float(self['viral_dist']['load_ratio']) @@ -578,13 +582,6 @@ def step(self): date_dead = people.date_dead viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) - # Shorten additional useful parameters and indicators that aren't by strain - sus = people.susceptible - inf = people.infectious - symp = people.symptomatic - diag = people.diagnosed - quar = people.quarantined - # Initialize temp storage for strain parameters strain_keys = ['rel_beta', 'asymp_factor'] strain_pars = dict() @@ -595,6 +592,8 @@ def step(self): has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.false(sus)) if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) + print('s3', np.random.random()) + # Iterate through n_strains to calculate infections for strain in range(ns): @@ -611,14 +610,21 @@ def step(self): # Define indices for this strain # inf_by_this_strain = np.zeros(len(inf), dtype=bool) # inf_by_this_strain[cvu.true(people.infectious_strain == strain)] = True - inf_by_this_strain = people.infectious_strain == strain + # inf_by_this_strain = people.infectious * (people.infectious_strain == strain) for lkey, layer in contacts.items(): p1 = layer['p1'] p2 = layer['p2'] betas = layer['beta'] + print('hi!', np.mean(betas)) # Compute relative transmission and susceptibility + # Shorten additional useful parameters and indicators that aren't by strain + sus = people.susceptible + symp = people.symptomatic + diag = people.diagnosed + quar = people.quarantined + inf_by_this_strain = people.infectious rel_trans = people.rel_trans[:] rel_sus = people.rel_sus[:] sus_imm = people.sus_imm[strain,:] @@ -635,6 +641,8 @@ def step(self): people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey, strain=strain) # Actually infect people + print('s4', np.random.random()) + # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): if 'by_strain' in key or 'by strain' in key: @@ -655,6 +663,8 @@ def step(self): self.results['pop_protection'][t] = np.nanmean(people.sus_imm) self.results['pop_symp_protection'][t] = np.nanmean(people.symp_imm) + print('s5', np.random.random()) + # Apply analyzers -- same syntax as interventions for i,analyzer in enumerate(self['analyzers']): if isinstance(analyzer, cva.Analyzer): diff --git a/covasim/utils.py b/covasim/utils.py index 382daec26..91588fa63 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -76,14 +76,17 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=safe_parallel) +# @nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=safe_parallel) def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] f_iso = ~diag + diag * iso_factor # Isolation factor, changes e.g. [0,1] with a factor of 0.2 to [1,0.2] f_quar = ~quar + quar * quar_factor # Quarantine, changes e.g. [0,1] with a factor of 0.5 to [1,0.5] - rel_trans = beta_layer * rel_trans * inf * f_quar * f_asymp * f_iso * viral_load # Recalculate transmissibility + rel_trans = rel_trans * inf * f_quar * f_asymp * f_iso * beta_layer * viral_load # Recalculate transmissibility rel_sus = rel_sus * sus * f_quar * (1-immunity_factors) # Recalculate susceptibility + # print('kochia', len(rel_sus), rel_sus.mean()) + # print('odfiud', len(rel_trans), rel_trans.mean()) + # print(rel_trans.tolist()) return rel_trans, rel_sus @@ -96,6 +99,9 @@ def compute_infections(beta, sources, targets, layer_betas, rel_trans, r out who gets infected on this timestep. Cannot be easily parallelized since random numbers are used. ''' + # print('ci1', np.random.random()) + # for i,val in enumerate([beta, sources, targets, layer_betas, rel_trans, rel_sus]): + # print('var', i, np.mean(val)) source_trans = rel_trans[sources] # Pull out the transmissibility of the sources (0 for non-infectious people) inf_inds = source_trans.nonzero()[0] # Infectious indices -- remove noninfectious people betas = beta * layer_betas[inf_inds] * source_trans[inf_inds] * rel_sus[targets[inf_inds]] # Calculate the raw transmission probabilities @@ -107,8 +113,8 @@ def compute_infections(beta, sources, targets, layer_betas, rel_trans, r transmissions = (np.random.random(len(nonzero_betas)) < nonzero_betas).nonzero()[0] # Compute the actual infections! source_inds = nonzero_sources[transmissions] target_inds = nonzero_targets[transmissions] # Filter the targets on the actual infections - print(np.random.random()) - print(target_inds) + # print('ci2', np.random.random()) + # print(target_inds) return source_inds, target_inds From e5cbeca0b70d4bf6da94aa9cc18bd6963d6308b9 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 22:30:01 -0700 Subject: [PATCH 342/569] matches with numba --- covasim/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/utils.py b/covasim/utils.py index 91588fa63..3c77ebff1 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -76,7 +76,7 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -# @nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=safe_parallel) +@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=safe_parallel) def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] @@ -90,7 +90,7 @@ def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, return rel_trans, rel_sus -# @nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) +@nb.njit( (nbfloat, nbint[:], nbint[:], nbfloat[:], nbfloat[:], nbfloat[:]), cache=cache, parallel=rand_parallel) def compute_infections(beta, sources, targets, layer_betas, rel_trans, rel_sus): # pragma: no cover ''' Compute who infects whom From 6f507379900781d3d30a87626366b1de8a8f7d57 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 22:30:55 -0700 Subject: [PATCH 343/569] looks like it still matches --- covasim/sim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/sim.py b/covasim/sim.py index b13872341..41ef42972 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -624,7 +624,7 @@ def step(self): symp = people.symptomatic diag = people.diagnosed quar = people.quarantined - inf_by_this_strain = people.infectious + inf_by_this_strain = people.infectious * (people.infectious_strain == strain)#people.infectious rel_trans = people.rel_trans[:] rel_sus = people.rel_sus[:] sus_imm = people.sus_imm[strain,:] From a2be9b19aaff8468ec9c73f416271148561e47bd Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 22:36:32 -0700 Subject: [PATCH 344/569] still working --- covasim/sim.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 41ef42972..9699744a1 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -583,10 +583,16 @@ def step(self): viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) # Initialize temp storage for strain parameters - strain_keys = ['rel_beta', 'asymp_factor'] - strain_pars = dict() ns = self['n_strains'] # Shorten number of strains + # Shorten additional useful parameters and indicators that aren't by strain + sus = people.susceptible + symp = people.symptomatic + diag = people.diagnosed + quar = people.quarantined + prel_trans = people.rel_trans + prel_sus = people.rel_sus + # Check NAbs. Take set difference so we don't compute NAbs for anyone currently infected if self['use_immunity']: has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.false(sus)) @@ -602,10 +608,8 @@ def step(self): cvimm.check_immunity(people, strain, sus=True) # Deal with strain parameters - for key in strain_keys: - strain_pars[key] = self['strain_pars'][key][strain] - beta = cvd.default_float(self['beta'] * strain_pars['rel_beta']) - asymp_factor = cvd.default_float(strain_pars['asymp_factor']) + beta = cvd.default_float(self['beta'] * self['strain_pars']['rel_beta'][strain]) + asymp_factor = cvd.default_float(self['strain_pars']['asymp_factor'][strain]) # Define indices for this strain # inf_by_this_strain = np.zeros(len(inf), dtype=bool) @@ -619,20 +623,12 @@ def step(self): print('hi!', np.mean(betas)) # Compute relative transmission and susceptibility - # Shorten additional useful parameters and indicators that aren't by strain - sus = people.susceptible - symp = people.symptomatic - diag = people.diagnosed - quar = people.quarantined inf_by_this_strain = people.infectious * (people.infectious_strain == strain)#people.infectious - rel_trans = people.rel_trans[:] - rel_sus = people.rel_sus[:] sus_imm = people.sus_imm[strain,:] - iso_factor = cvd.default_float(self['iso_factor'][lkey]) quar_factor = cvd.default_float(self['quar_factor'][lkey]) beta_layer = cvd.default_float(self['beta_layer'][lkey]) - rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, + rel_trans, rel_sus = cvu.compute_trans_sus(prel_trans, prel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, sus_imm) # Calculate actual transmission From 0693bd24c32f85da5485db8a660978323689161f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 22:40:12 -0700 Subject: [PATCH 345/569] still matching develop --- covasim/people.py | 2 +- covasim/sim.py | 29 +++++------------------------ 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index a0a2f84f7..be9e5e444 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -486,7 +486,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.date_dead[dead_inds] = self.date_critical[dead_inds] + dur_crit2die # Date of death self.dur_disease[dead_inds] = self.dur_exp2inf[dead_inds] + self.dur_inf2sym[dead_inds] + self.dur_sym2sev[dead_inds] + self.dur_sev2crit[dead_inds] + dur_crit2die # Store how long this person had COVID-19 - print('moshi', np.random.random()) + # print('moshi', np.random.random()) return n_infections # For incrementing counters diff --git a/covasim/sim.py b/covasim/sim.py index 9699744a1..361095592 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -535,8 +535,6 @@ def step(self): rather than calling sim.step() directly. ''' - print('s1', np.random.random()) - # Set the time and if we have reached the end of the simulation, then do nothing if self.complete: raise AlreadyRunError('Simulation already complete (call sim.initialize() to re-run)') @@ -571,8 +569,6 @@ def step(self): people.update_states_post() # Check for state changes after interventions - print('s2', np.random.random()) - # Compute viral loads frac_time = cvd.default_float(self['viral_dist']['frac_time']) load_ratio = cvd.default_float(self['viral_dist']['load_ratio']) @@ -582,10 +578,8 @@ def step(self): date_dead = people.date_dead viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) - # Initialize temp storage for strain parameters + # Shorten useful parameters ns = self['n_strains'] # Shorten number of strains - - # Shorten additional useful parameters and indicators that aren't by strain sus = people.susceptible symp = people.symptomatic diag = people.diagnosed @@ -598,8 +592,6 @@ def step(self): has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.false(sus)) if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) - print('s3', np.random.random()) - # Iterate through n_strains to calculate infections for strain in range(ns): @@ -611,16 +603,10 @@ def step(self): beta = cvd.default_float(self['beta'] * self['strain_pars']['rel_beta'][strain]) asymp_factor = cvd.default_float(self['strain_pars']['asymp_factor'][strain]) - # Define indices for this strain - # inf_by_this_strain = np.zeros(len(inf), dtype=bool) - # inf_by_this_strain[cvu.true(people.infectious_strain == strain)] = True - # inf_by_this_strain = people.infectious * (people.infectious_strain == strain) - for lkey, layer in contacts.items(): p1 = layer['p1'] p2 = layer['p2'] betas = layer['beta'] - print('hi!', np.mean(betas)) # Compute relative transmission and susceptibility inf_by_this_strain = people.infectious * (people.infectious_strain == strain)#people.infectious @@ -637,15 +623,12 @@ def step(self): people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey, strain=strain) # Actually infect people - print('s4', np.random.random()) - # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): - if 'by_strain' in key or 'by strain' in key: - for strain in range(ns): - self.results['strain'][f'n_{key}'][strain][t] = people.count_by_strain(key, strain) - else: - self.results[f'n_{key}'][t] = people.count(key) + self.results[f'n_{key}'][t] = people.count(key) + for key in cvd.result_stocks_by_strain.keys(): + for strain in range(ns): + self.results['strain'][f'n_{key}'][strain][t] = people.count_by_strain(key, strain) # Update counts for this time step: flows for key,count in people.flows.items(): @@ -659,8 +642,6 @@ def step(self): self.results['pop_protection'][t] = np.nanmean(people.sus_imm) self.results['pop_symp_protection'][t] = np.nanmean(people.symp_imm) - print('s5', np.random.random()) - # Apply analyzers -- same syntax as interventions for i,analyzer in enumerate(self['analyzers']): if isinstance(analyzer, cva.Analyzer): From a303ba293eff3eddbf3ffe05d1a38536b3c2a8df Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 22:41:44 -0700 Subject: [PATCH 346/569] tidying --- covasim/people.py | 2 -- covasim/utils.py | 8 -------- 2 files changed, 10 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index be9e5e444..ffdce9984 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -486,8 +486,6 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.date_dead[dead_inds] = self.date_critical[dead_inds] + dur_crit2die # Date of death self.dur_disease[dead_inds] = self.dur_exp2inf[dead_inds] + self.dur_inf2sym[dead_inds] + self.dur_sym2sev[dead_inds] + self.dur_sev2crit[dead_inds] + dur_crit2die # Store how long this person had COVID-19 - # print('moshi', np.random.random()) - return n_infections # For incrementing counters diff --git a/covasim/utils.py b/covasim/utils.py index 3c77ebff1..22e4f4225 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -84,9 +84,6 @@ def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, f_quar = ~quar + quar * quar_factor # Quarantine, changes e.g. [0,1] with a factor of 0.5 to [1,0.5] rel_trans = rel_trans * inf * f_quar * f_asymp * f_iso * beta_layer * viral_load # Recalculate transmissibility rel_sus = rel_sus * sus * f_quar * (1-immunity_factors) # Recalculate susceptibility - # print('kochia', len(rel_sus), rel_sus.mean()) - # print('odfiud', len(rel_trans), rel_trans.mean()) - # print(rel_trans.tolist()) return rel_trans, rel_sus @@ -99,9 +96,6 @@ def compute_infections(beta, sources, targets, layer_betas, rel_trans, r out who gets infected on this timestep. Cannot be easily parallelized since random numbers are used. ''' - # print('ci1', np.random.random()) - # for i,val in enumerate([beta, sources, targets, layer_betas, rel_trans, rel_sus]): - # print('var', i, np.mean(val)) source_trans = rel_trans[sources] # Pull out the transmissibility of the sources (0 for non-infectious people) inf_inds = source_trans.nonzero()[0] # Infectious indices -- remove noninfectious people betas = beta * layer_betas[inf_inds] * source_trans[inf_inds] * rel_sus[targets[inf_inds]] # Calculate the raw transmission probabilities @@ -113,8 +107,6 @@ def compute_infections(beta, sources, targets, layer_betas, rel_trans, r transmissions = (np.random.random(len(nonzero_betas)) < nonzero_betas).nonzero()[0] # Compute the actual infections! source_inds = nonzero_sources[transmissions] target_inds = nonzero_targets[transmissions] # Filter the targets on the actual infections - # print('ci2', np.random.random()) - # print(target_inds) return source_inds, target_inds From a90521167fe67ba706eb5ae1d714574ddc2d1171 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 22:44:39 -0700 Subject: [PATCH 347/569] tests still fail, but closer --- tests/test_baselines.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_baselines.py b/tests/test_baselines.py index d6e741f8f..6b53ba0bc 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -186,9 +186,9 @@ def normalize_performance(): cv.options.set(interactive=do_plot) T = sc.tic() - # json = test_benchmark(do_save=do_save) # Run this first so benchmarking is available even if results are different + json = test_benchmark(do_save=do_save) # Run this first so benchmarking is available even if results are different new = test_baseline() - # make_sim(do_plot=do_plot) + make_sim(do_plot=do_plot) print('\n'*2) sc.toc(T) From 2ffe5beca1b2c4fbe637f62da7f85eac346bdb45 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 23:33:24 -0700 Subject: [PATCH 348/569] all tests except baseline pass --- covasim/defaults.py | 1 + covasim/immunity.py | 5 +++++ covasim/misc.py | 23 ++++++++++++++++++++++- covasim/parameters.py | 7 +++++-- covasim/people.py | 3 +-- covasim/sim.py | 11 ++++++----- 6 files changed, 40 insertions(+), 10 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index d1d7b559d..6d7dbcb66 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -238,6 +238,7 @@ def get_colors(): c.vaccinations = '#5c399c' c.vaccinated = '#5c399c' c.recoveries = '#9e1149' + c.recovered = c.recoveries c.symptomatic = '#c1ad71' c.severe = '#c1981d' c.critical = '#b86113' diff --git a/covasim/immunity.py b/covasim/immunity.py index af1225ac8..d3f667ff7 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -411,6 +411,11 @@ def update_strain_attributes(people): def init_immunity(sim, create=False): ''' Initialize immunity matrices with all strains that will eventually be in the sim''' + + # Don't use this function if immunity is turned off + if not sim['use_immunity']: + return + ts = sim['total_strains'] immunity = {} diff --git a/covasim/misc.py b/covasim/misc.py index 1ff95f203..d2c2d9ae8 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -191,6 +191,20 @@ def migrate_lognormal(pars, revert=False, verbose=True): return +def migrate_strains(pars, verbose=True): + ''' + Small helper function to add necessary strain parameters. + ''' + from . import parameters as cvp + pars['use_immunity'] = False + pars['n_strains'] = 1 + pars['total_strains'] = 1 + pars['strains'] = [] + pars['strain_pars'] = {} + pars = cvp.listify_strain_pars(pars) + return + + def migrate(obj, update=True, verbose=True, die=False): ''' Define migrations allowing compatibility between different versions of saved @@ -244,12 +258,19 @@ def migrate(obj, update=True, verbose=True, die=False): pass # Migration from <2.1.0 to 2.1.0 - if sc.compareversions(sim.version, '2.1.0') == -1: # Migrate from <2.0 to 2.0 + if sc.compareversions(sim.version, '2.1.0') == -1: if verbose: print(f'Migrating sim from version {sim.version} to version {cvv.__version__}') print('Note: updating lognormal stds to restore previous behavior; see v2.1.0 changelog for details') migrate_lognormal(sim.pars, verbose=verbose) + # Migration from <3.0.0 to 3.0.0 + if sc.compareversions(sim.version, '3.0.0') == -1: + if verbose: + print(f'Migrating sim from version {sim.version} to version {cvv.__version__}') + print('Adding strain parameters') + migrate_strains(sim.pars, verbose=verbose) + # Migrations for People elif isinstance(obj, cvb.BasePeople): # pragma: no cover ppl = obj diff --git a/covasim/parameters.py b/covasim/parameters.py index 8f746640e..350d900dc 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -128,8 +128,6 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): reset_layer_pars(pars) if set_prognoses: # If not set here, gets set when the population is initialized pars['prognoses'] = get_prognoses(pars['prog_by_age'], version=version) # Default to age-specific prognoses - pars['strain_pars'] = {} # Populated just below - pars = listify_strain_pars(pars) # Turn strain parameters into lists # If version is specified, load old parameters if version is not None: @@ -142,6 +140,11 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): if sc.compareversions(version, '2.1.0') == -1 and 'migrate_lognormal' not in pars: cvm.migrate_lognormal(pars, verbose=pars['verbose']) + # Handle strain pars + if 'strain_pars' not in pars: + pars['strain_pars'] = {} # Populated just below + pars = listify_strain_pars(pars) # Turn strain parameters into lists + return pars diff --git a/covasim/people.py b/covasim/people.py index ffdce9984..c030adf9b 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -386,8 +386,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str ''' # Remove duplicates - unique = np.unique(inds, return_index=True)[1] - inds = inds[unique] + inds, unique = np.unique(inds, return_index=True) if source is not None: source = source[unique] diff --git a/covasim/sim.py b/covasim/sim.py index 361095592..51e4cb203 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -290,9 +290,9 @@ def init_res(*args, **kwargs): self.results[f'n_{key}'] = init_res(label, color=dcols[key]) # Other variables - self.results['n_alive'] = init_res('Number of people alive', scale=False) + self.results['n_alive'] = init_res('Number alive', scale=False) self.results['n_preinfectious'] = init_res('Number preinfectious', scale=False, color=dcols.exposed) - # self.results['n_removed'] = init_res('Number removed', scale=False, color=dcols.recovered) + self.results['n_removed'] = init_res('Number removed', scale=False, color=dcols.recovered) self.results['prevalence'] = init_res('Prevalence', scale=False) self.results['incidence'] = init_res('Incidence', scale=False) self.results['r_eff'] = init_res('Effective reproduction number', scale=False) @@ -809,11 +809,12 @@ def compute_states(self): ''' res = self.results self.results['n_alive'][:] = self.scaled_pop_size - res['cum_deaths'][:] # Number of people still alive - self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] # Recalculate the number of susceptible people, not agents + self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents self.results['n_preinfectious'][:] = res['n_exposed'][:] - res['n_infectious'][:] # Calculate the number not yet infectious: exposed minus infectious -# self.results['n_removed'][:] = res['cum_recoveries'][:] + res['cum_deaths'][:] # Calculate the number removed: recovered + dead + self.results['n_removed'][:] = res['cum_recoveries'][:] + res['cum_deaths'][:] # Calculate the number removed: recovered + dead self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence + self.results['strain']['incidence_by_strain'][:] = np.einsum('ji,i->ji',res['strain']['new_infections_by_strain'][:], 1/res['n_susceptible'][:]) # Calculate the incidence self.results['strain']['prevalence_by_strain'][:] = np.einsum('ji,i->ji',res['strain']['new_infections_by_strain'][:], 1/res['n_alive'][:]) # Calculate the prevalence self.results['share_vaccinated'][:] = res['n_vaccinated'][:]/res['n_alive'][:] # Calculate the share vaccinated @@ -865,7 +866,7 @@ def compute_doubling(self, window=3, max_doubling_time=30): return self.results['doubling_time'].values - def compute_r_eff(self, method='infectious', smoothing=2, window=7): + def compute_r_eff(self, method='daily', smoothing=2, window=7): ''' Effective reproduction number based on number of people each person infected. From 14ccaee95b8e5c82ef82954460f5ebce08809ed2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 23:36:04 -0700 Subject: [PATCH 349/569] update baseline --- tests/baseline.json | 13 ++++++++++++- tests/benchmark.json | 6 +++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/baseline.json b/tests/baseline.json index 2c7e9b581..1ea2d06c9 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -1,6 +1,7 @@ { "summary": { "cum_infections": 9829.0, + "cum_reinfections": 0.0, "cum_infectious": 9688.0, "cum_tests": 10783.0, "cum_diagnoses": 3867.0, @@ -10,7 +11,10 @@ "cum_critical": 129.0, "cum_deaths": 30.0, "cum_quarantined": 4092.0, + "cum_vaccinations": 0.0, + "cum_vaccinated": 0.0, "new_infections": 14.0, + "new_reinfections": 0.0, "new_infectious": 47.0, "new_tests": 195.0, "new_diagnoses": 45.0, @@ -20,6 +24,8 @@ "new_critical": 2.0, "new_deaths": 3.0, "new_quarantined": 153.0, + "new_vaccinations": 0.0, + "new_vaccinated": 0.0, "n_susceptible": 10171.0, "n_exposed": 1248.0, "n_infectious": 1107.0, @@ -28,6 +34,7 @@ "n_critical": 64.0, "n_diagnosed": 3867.0, "n_quarantined": 3938.0, + "n_vaccinated": 0.0, "n_alive": 19970.0, "n_preinfectious": 141.0, "n_removed": 8581.0, @@ -36,6 +43,10 @@ "r_eff": 0.12219744828926875, "doubling_time": 30.0, "test_yield": 0.23076923076923078, - "rel_test_yield": 3.356889722743382 + "rel_test_yield": 3.356889722743382, + "share_vaccinated": 0.0, + "pop_nabs": 0.0, + "pop_protection": 0.0, + "pop_symp_protection": 0.0 } } \ No newline at end of file diff --git a/tests/benchmark.json b/tests/benchmark.json index 1ea181e6a..5bcf9be38 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,12 +1,12 @@ { "time": { - "initialize": 0.408, - "run": 0.446 + "initialize": 0.403, + "run": 0.479 }, "parameters": { "pop_size": 20000, "pop_type": "hybrid", "n_days": 60 }, - "cpu_performance": 1.01777919829902 + "cpu_performance": 0.963544593007991 } \ No newline at end of file From 573cad01c20ab443ea082c75b88aebabff18f144 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 23:36:39 -0700 Subject: [PATCH 350/569] added regression parameters --- covasim/regression/pars_v3.0.0.json | 308 ++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 covasim/regression/pars_v3.0.0.json diff --git a/covasim/regression/pars_v3.0.0.json b/covasim/regression/pars_v3.0.0.json new file mode 100644 index 000000000..eeb0d23db --- /dev/null +++ b/covasim/regression/pars_v3.0.0.json @@ -0,0 +1,308 @@ +{ + "pop_size": 20000.0, + "pop_infected": 20, + "pop_type": "random", + "location": null, + "start_day": "2020-03-01", + "end_day": null, + "n_days": 60, + "rand_seed": 1, + "verbose": 0.1, + "pop_scale": 1, + "rescale": true, + "rescale_threshold": 0.05, + "rescale_factor": 1.2, + "contacts": { + "a": 20 + }, + "dynam_layer": { + "a": 0 + }, + "beta_layer": { + "a": 1.0 + }, + "beta_dist": { + "dist": "neg_binomial", + "par1": 1.0, + "par2": 0.45, + "step": 0.01 + }, + "viral_dist": { + "frac_time": 0.3, + "load_ratio": 2, + "high_cap": 4 + }, + "beta": 0.016, + "n_strains": 1, + "total_strains": 1, + "use_immunity": false, + "NAb_init": { + "dist": "normal", + "par1": 0, + "par2": 2 + }, + "NAb_decay": { + "form": "nab_decay", + "pars": { + "init_decay_rate": 0.007701635339554948, + "init_decay_time": 250, + "decay_decay_rate": 0.001 + } + }, + "NAb_kin": null, + "NAb_boost": 2, + "NAb_eff": { + "slope": 3.433, + "n_50": { + "sus": 0.5, + "symp": 0.1987, + "sev": 0.031 + } + }, + "cross_immunity": 0.5, + "rel_imm": { + "asymptomatic": 0.7, + "mild": 0.9, + "severe": 1 + }, + "immunity": null, + "vaccine_info": null, + "rel_beta": 1.0, + "asymp_factor": 1.0, + "dur": { + "exp2inf": { + "dist": "lognormal_int", + "par1": 4.5, + "par2": 1.5 + }, + "inf2sym": { + "dist": "lognormal_int", + "par1": 1.1, + "par2": 0.9 + }, + "sym2sev": { + "dist": "lognormal_int", + "par1": 6.6, + "par2": 4.9 + }, + "sev2crit": { + "dist": "lognormal_int", + "par1": 1.5, + "par2": 2.0 + }, + "asym2rec": { + "dist": "lognormal_int", + "par1": 8.0, + "par2": 2.0 + }, + "mild2rec": { + "dist": "lognormal_int", + "par1": 8.0, + "par2": 2.0 + }, + "sev2rec": { + "dist": "lognormal_int", + "par1": 18.1, + "par2": 6.3 + }, + "crit2rec": { + "dist": "lognormal_int", + "par1": 18.1, + "par2": 6.3 + }, + "crit2die": { + "dist": "lognormal_int", + "par1": 10.7, + "par2": 4.8 + } + }, + "rel_symp_prob": 1.0, + "rel_severe_prob": 1.0, + "rel_crit_prob": 1.0, + "rel_death_prob": 1.0, + "prog_by_age": true, + "prognoses": { + "age_cutoffs": [ + 0, + 10, + 20, + 30, + 40, + 50, + 60, + 70, + 80, + 90 + ], + "sus_ORs": [ + 0.34, + 0.67, + 1.0, + 1.0, + 1.0, + 1.0, + 1.24, + 1.47, + 1.47, + 1.47 + ], + "trans_ORs": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "comorbidities": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "symp_probs": [ + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.9 + ], + "severe_probs": [ + 0.001, + 0.0029999999999999996, + 0.012, + 0.032, + 0.049, + 0.102, + 0.16599999999999998, + 0.24300000000000002, + 0.273, + 0.273 + ], + "crit_probs": [ + 0.06, + 0.04848484848484849, + 0.05, + 0.049999999999999996, + 0.06297376093294461, + 0.12196078431372549, + 0.2740210843373494, + 0.43200193657709995, + 0.708994708994709, + 0.708994708994709 + ], + "death_probs": [ + 0.6666666666666667, + 0.25, + 0.2777777777777778, + 0.30769230769230776, + 0.45370370370370366, + 0.2840300107181136, + 0.2104973893926903, + 0.2733385632634764, + 0.47600459242250287, + 0.9293915040183697 + ] + }, + "iso_factor": { + "a": 0.2 + }, + "quar_factor": { + "a": 0.3 + }, + "quar_period": 14, + "interventions": [], + "analyzers": [], + "strains": [], + "vaccines": [], + "timelimit": null, + "stopping_func": null, + "n_beds_hosp": null, + "n_beds_icu": null, + "no_hosp_factor": 2.0, + "no_icu_factor": 2.0, + "strain_pars": { + "rel_beta": [ + 1.0 + ], + "asymp_factor": [ + 1.0 + ], + "dur": [ + { + "exp2inf": { + "dist": "lognormal_int", + "par1": 4.5, + "par2": 1.5 + }, + "inf2sym": { + "dist": "lognormal_int", + "par1": 1.1, + "par2": 0.9 + }, + "sym2sev": { + "dist": "lognormal_int", + "par1": 6.6, + "par2": 4.9 + }, + "sev2crit": { + "dist": "lognormal_int", + "par1": 1.5, + "par2": 2.0 + }, + "asym2rec": { + "dist": "lognormal_int", + "par1": 8.0, + "par2": 2.0 + }, + "mild2rec": { + "dist": "lognormal_int", + "par1": 8.0, + "par2": 2.0 + }, + "sev2rec": { + "dist": "lognormal_int", + "par1": 18.1, + "par2": 6.3 + }, + "crit2rec": { + "dist": "lognormal_int", + "par1": 18.1, + "par2": 6.3 + }, + "crit2die": { + "dist": "lognormal_int", + "par1": 10.7, + "par2": 4.8 + } + } + ], + "rel_symp_prob": [ + 1.0 + ], + "rel_severe_prob": [ + 1.0 + ], + "rel_crit_prob": [ + 1.0 + ], + "rel_death_prob": [ + 1.0 + ] + } +} \ No newline at end of file From 6a33cada2c895b2b4f8453148adbcef2a48c24f0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 23:39:56 -0700 Subject: [PATCH 351/569] rename tests --- tests/unittests/check_coverage | 14 +++ .../experiment_test_disease_mortality.py | 113 ------------------ tests/unittests/get_unittest_coverage.py | 37 ------ ...interventions.py => test_interventions.py} | 0 ...miscellaneous_features.py => test_misc.py} | 0 ...disease_mortality.py => test_mortality.py} | 0 ...t_simulation_parameter.py => test_pars.py} | 0 ...{test_population_types.py => test_pops.py} | 0 ...ase_progression.py => test_progression.py} | 0 ...e_transmission.py => test_transmission.py} | 0 10 files changed, 14 insertions(+), 150 deletions(-) create mode 100755 tests/unittests/check_coverage delete mode 100644 tests/unittests/experiment_test_disease_mortality.py delete mode 100644 tests/unittests/get_unittest_coverage.py rename tests/unittests/{test_specific_interventions.py => test_interventions.py} (100%) rename tests/unittests/{test_miscellaneous_features.py => test_misc.py} (100%) rename tests/unittests/{test_disease_mortality.py => test_mortality.py} (100%) rename tests/unittests/{test_simulation_parameter.py => test_pars.py} (100%) rename tests/unittests/{test_population_types.py => test_pops.py} (100%) rename tests/unittests/{test_disease_progression.py => test_progression.py} (100%) rename tests/unittests/{test_disease_transmission.py => test_transmission.py} (100%) diff --git a/tests/unittests/check_coverage b/tests/unittests/check_coverage new file mode 100755 index 000000000..d1b1de7c3 --- /dev/null +++ b/tests/unittests/check_coverage @@ -0,0 +1,14 @@ +#!/bin/bash +# Note that although the script runs when parallelized, the coverage results are wrong. + +echo 'Running tests...' +pytest -v test_*.py --cov-config=../.coveragerc --cov=../../covasim --workers auto --durations=0 + +echo 'Creating HTML report...' +coverage html + +echo 'Printing report...' +coverage report + +echo 'Report location:' +echo "`pwd`/htmlcov/index.html" \ No newline at end of file diff --git a/tests/unittests/experiment_test_disease_mortality.py b/tests/unittests/experiment_test_disease_mortality.py deleted file mode 100644 index 6d35933b7..000000000 --- a/tests/unittests/experiment_test_disease_mortality.py +++ /dev/null @@ -1,113 +0,0 @@ -import covasim as cv -from unittest_support_classes import CovaTest - - -class SimKeys: - ''' Define mapping to simulation keys ''' - number_agents = 'pop_size' - initial_infected_count = 'pop_infected' - start_day = 'start_day' - number_simulated_days = 'n_days' - random_seed = 'rand_seed' - pass - - -class DiseaseKeys: - ''' Define mapping to keys associated with disease progression ''' - modify_progression_by_age = 'prog_by_age' - scale_probability_of_infected_developing_symptoms = 'rel_symp_prob' - scale_probability_of_symptoms_developing_severe = 'rel_severe_prob' - scale_probability_of_severe_developing_critical = 'rel_crit_prob' - scale_probability_of_critical_developing_death = 'rel_death_prob' - pass - - -class ResultsKeys: - ''' Define keys for results ''' - cumulative_number_of_deaths = 'cum_deaths' - pass - - -def define_base_parameters(): - ''' Define the basic parameters for a simulation -- these will sometimes, but rarely, change between tests ''' - base_parameters_dict = { - SimKeys.number_agents: 1000, # Keep it small so they run faster - SimKeys.initial_infected_count: 100, # Use a relatively large number to avoid stochastic effects - SimKeys.random_seed: 1, # Ensure it's reproducible - SimKeys.number_simulated_days: 60, # Don't run for too long for speed, but run for long enough - } - return base_parameters_dict - - -def BaseSim(): - ''' Create a base simulation to run tests on ''' - base_parameters_dict = define_base_parameters() - base_sim = cv.Sim(pars=base_parameters_dict) - return base_sim - - -class ExperimentalDiseaseMortalityTests(CovaTest): - ''' Define the actual tests ''' - - def test_zero_deaths(self): - ''' Confirm that if mortality is set to zero, there are zero deaths ''' - - # Create the sim - sim = BaseSim() - - # Define test-secific configurations - test_parameters = { - DiseaseKeys.modify_progression_by_age: False, # Otherwise these parameters have no effect - DiseaseKeys.scale_probability_of_critical_developing_death: 0 # Change mortality rate to 0 - } - - # Run the simulation - sim.update_pars(test_parameters) - sim.run() - - # Check results - total_deaths = sim.results[ResultsKeys.cumulative_number_of_deaths][:][-1] # Get the total number of deaths (last value of the cumulative number) - self.assertEqual(0, total_deaths, - msg=f"There should be no deaths given parameters {test_parameters}. " - f"Channel {ResultsKeys.cumulative_number_of_deaths} had " - f"bad data: {total_deaths}") - - pass - - - def test_full_deaths(self): - ''' Confirm that if all progression parameters are set to 1, everyone dies''' - - # Create the sim - sim = BaseSim() - - # reminder: these are the defaults for when "no_age" is used - # symp_probs = np.array([0.75]), - # severe_probs = np.array([0.2]), - # crit_probs = np.array([0.08]), - # death_probs = np.array([0.02]), - - # Define test-secific configurations - test_parameters = { - SimKeys.initial_infected_count: sim[SimKeys.number_agents], # Ensure everyone is infected - DiseaseKeys.modify_progression_by_age: False, # Otherwise use age-specific values, but we want simple - DiseaseKeys.scale_probability_of_infected_developing_symptoms: 1.0/0.75, # Scale factor for proportion of symptomatic cases - DiseaseKeys.scale_probability_of_symptoms_developing_severe: 1.0/0.2, # Scale factor for proportion of symptomatic cases that become severe - DiseaseKeys.scale_probability_of_severe_developing_critical: 1.0/0.08, # Scale factor for proportion of severe cases that become critical - DiseaseKeys.scale_probability_of_critical_developing_death: 1.0/0.02 #Scale factor for proportion of critical cases that result in death - } - - # Run the simulation - sim.update_pars(test_parameters) - sim.run() - - # Check results - total_deaths = sim.results[ResultsKeys.cumulative_number_of_deaths][:][-1] # Get the total number of deaths (last value of the cumulative number) - self.assertEqual(sim[SimKeys.number_agents], total_deaths, - msg=f"Everyone should die with parameters {test_parameters}. " - f"Channel {ResultsKeys.cumulative_number_of_deaths} had " - f"bad data: {total_deaths} deaths vs. {sim[SimKeys.number_agents]} people.") - - pass - - diff --git a/tests/unittests/get_unittest_coverage.py b/tests/unittests/get_unittest_coverage.py deleted file mode 100644 index c49eadc52..000000000 --- a/tests/unittests/get_unittest_coverage.py +++ /dev/null @@ -1,37 +0,0 @@ -import coverage -import unittest -loader = unittest.TestLoader() -cov = coverage.Coverage(source=["covasim.base","covasim.interventions", - "covasim.parameters","covasim.people", - "covasim.population","covasim.misc"]) -cov.start() - -# First, load and run the unittest tests -from unittest_support_classes import TestSupportTests -from test_miscellaneous_features import MiscellaneousFeatureTests -from test_simulation_parameter import SimulationParameterTests -from test_disease_transmission import DiseaseTransmissionTests -from test_disease_progression import DiseaseProgressionTests -from test_disease_mortality import DiseaseMortalityTests -# from test_diagnostic_testing import DiagnosticTestingTests - -test_classes_to_run = [TestSupportTests, - SimulationParameterTests, - DiseaseTransmissionTests, - DiseaseProgressionTests, - DiseaseMortalityTests, - MiscellaneousFeatureTests] - -suites_list = [] -for tc in test_classes_to_run: - suite = loader.loadTestsFromTestCase(tc) - suites_list.append(suite) - pass - -big_suite = unittest.TestSuite(suites_list) -runner = unittest.TextTestRunner() -results = runner.run(big_suite) - -cov.stop() -cov.save() -cov.html_report() \ No newline at end of file diff --git a/tests/unittests/test_specific_interventions.py b/tests/unittests/test_interventions.py similarity index 100% rename from tests/unittests/test_specific_interventions.py rename to tests/unittests/test_interventions.py diff --git a/tests/unittests/test_miscellaneous_features.py b/tests/unittests/test_misc.py similarity index 100% rename from tests/unittests/test_miscellaneous_features.py rename to tests/unittests/test_misc.py diff --git a/tests/unittests/test_disease_mortality.py b/tests/unittests/test_mortality.py similarity index 100% rename from tests/unittests/test_disease_mortality.py rename to tests/unittests/test_mortality.py diff --git a/tests/unittests/test_simulation_parameter.py b/tests/unittests/test_pars.py similarity index 100% rename from tests/unittests/test_simulation_parameter.py rename to tests/unittests/test_pars.py diff --git a/tests/unittests/test_population_types.py b/tests/unittests/test_pops.py similarity index 100% rename from tests/unittests/test_population_types.py rename to tests/unittests/test_pops.py diff --git a/tests/unittests/test_disease_progression.py b/tests/unittests/test_progression.py similarity index 100% rename from tests/unittests/test_disease_progression.py rename to tests/unittests/test_progression.py diff --git a/tests/unittests/test_disease_transmission.py b/tests/unittests/test_transmission.py similarity index 100% rename from tests/unittests/test_disease_transmission.py rename to tests/unittests/test_transmission.py From c759ecf89a50080dbdc0a65e64552445f3bcfcaf Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 23:41:06 -0700 Subject: [PATCH 352/569] one more --- .../{unittest_support_classes.py => unittest_support.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unittests/{unittest_support_classes.py => unittest_support.py} (100%) diff --git a/tests/unittests/unittest_support_classes.py b/tests/unittests/unittest_support.py similarity index 100% rename from tests/unittests/unittest_support_classes.py rename to tests/unittests/unittest_support.py From 7c49a520f5838e50c320ebbd5c762a72f167a717 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 23:41:42 -0700 Subject: [PATCH 353/569] rename imports --- tests/unittests/test_interventions.py | 4 ++-- tests/unittests/test_misc.py | 2 +- tests/unittests/test_mortality.py | 2 +- tests/unittests/test_pars.py | 2 +- tests/unittests/test_pops.py | 2 +- tests/unittests/test_progression.py | 2 +- tests/unittests/test_transmission.py | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unittests/test_interventions.py b/tests/unittests/test_interventions.py index 3a3a2e76b..41a3f0892 100644 --- a/tests/unittests/test_interventions.py +++ b/tests/unittests/test_interventions.py @@ -1,5 +1,5 @@ -from unittest_support_classes import CovaTest -from unittest_support_classes import TProps +from unittest_support import CovaTest +from unittest_support import TProps from math import sqrt import json import numpy as np diff --git a/tests/unittests/test_misc.py b/tests/unittests/test_misc.py index 1c885a415..4b038a0ec 100644 --- a/tests/unittests/test_misc.py +++ b/tests/unittests/test_misc.py @@ -4,7 +4,7 @@ import unittest import pandas as pd -from unittest_support_classes import CovaTest +from unittest_support import CovaTest from covasim import Sim, parameters import os diff --git a/tests/unittests/test_mortality.py b/tests/unittests/test_mortality.py index cc18ff442..6f9e62f5c 100644 --- a/tests/unittests/test_mortality.py +++ b/tests/unittests/test_mortality.py @@ -5,7 +5,7 @@ import covasim as cv import unittest -from unittest_support_classes import CovaTest, TProps +from unittest_support import CovaTest, TProps DProgKeys = TProps.ParKeys.ProgKeys TransKeys = TProps.ParKeys.TransKeys diff --git a/tests/unittests/test_pars.py b/tests/unittests/test_pars.py index ba3f871b7..1489d7354 100644 --- a/tests/unittests/test_pars.py +++ b/tests/unittests/test_pars.py @@ -4,7 +4,7 @@ """ import unittest -from unittest_support_classes import CovaTest, TProps +from unittest_support import CovaTest, TProps TPKeys = TProps.ParKeys.SimKeys ResKeys = TProps.ResKeys diff --git a/tests/unittests/test_pops.py b/tests/unittests/test_pops.py index 5d00712a4..4ca1a5ecd 100644 --- a/tests/unittests/test_pops.py +++ b/tests/unittests/test_pops.py @@ -1,4 +1,4 @@ -from unittest_support_classes import CovaTest, TProps +from unittest_support import CovaTest, TProps TPKeys = TProps.ParKeys.SimKeys diff --git a/tests/unittests/test_progression.py b/tests/unittests/test_progression.py index e6ce9fbae..90c0753e7 100644 --- a/tests/unittests/test_progression.py +++ b/tests/unittests/test_progression.py @@ -4,7 +4,7 @@ """ import unittest -from unittest_support_classes import CovaTest, TProps +from unittest_support import CovaTest, TProps ResKeys = TProps.ResKeys ParamKeys = TProps.ParKeys diff --git a/tests/unittests/test_transmission.py b/tests/unittests/test_transmission.py index c2f3c13fa..9f70a68e6 100644 --- a/tests/unittests/test_transmission.py +++ b/tests/unittests/test_transmission.py @@ -3,7 +3,7 @@ ../../covasim/README.md """ -from unittest_support_classes import CovaTest, TProps +from unittest_support import CovaTest, TProps TKeys = TProps.ParKeys.TransKeys Hightrans = TProps.SpecialSims.Hightransmission From 7787bc6ecb6ff79b78268af5358df8d7f87ef86d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 23:47:56 -0700 Subject: [PATCH 354/569] starting rename --- tests/unittests/test_pops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unittests/test_pops.py b/tests/unittests/test_pops.py index 4ca1a5ecd..5a4a094e9 100644 --- a/tests/unittests/test_pops.py +++ b/tests/unittests/test_pops.py @@ -33,8 +33,8 @@ def test_different_pop_types(self): self.assertEqual(day_0_susceptible + day_0_exposed, short_sample[TPKeys.number_agents], msg=f"Day 0 population should be as specified in params. Poptype {k} was different.") - self.assertGreater(these_results[TProps.ResKeys.infections_cumulative][-1], - these_results[TProps.ResKeys.infections_cumulative][0], + self.assertGreater(these_results['cum_infections'][-1], + these_results['cum_infections'][0], msg=f"Should see infections increase. Pop type {k} didn't do that.") self.assertGreater(these_results[TProps.ResKeys.symptomatic_cumulative][-1], these_results[TProps.ResKeys.symptomatic_cumulative][0], From 1bd338b382ae797d5ee7b826ad57f0ed2084f91b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 23:48:33 -0700 Subject: [PATCH 355/569] starting rename --- tests/unittests/test_interventions.py | 70 +++++++++++++-------------- tests/unittests/test_progression.py | 2 +- tests/unittests/unittest_support.py | 2 +- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/tests/unittests/test_interventions.py b/tests/unittests/test_interventions.py index 41a3f0892..14c06db43 100644 --- a/tests/unittests/test_interventions.py +++ b/tests/unittests/test_interventions.py @@ -23,8 +23,8 @@ def tearDown(self): # region change beta def test_brutal_change_beta_intervention(self): params = { - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 60 + 'pop_size': AGENT_COUNT, + 'n_days': 60 } self.set_sim_pars(params_dict=params) day_of_change = 30 @@ -54,8 +54,8 @@ def test_brutal_change_beta_intervention(self): def test_change_beta_days(self): params = { - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 60 + 'pop_size': AGENT_COUNT, + 'n_days': 60 } self.set_sim_pars(params_dict=params) # Do a 0.0 intervention / 1.0 intervention on different days @@ -102,8 +102,8 @@ def test_change_beta_days(self): def test_change_beta_multipliers(self): params = { - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 40 + 'pop_size': AGENT_COUNT, + 'n_days': 40 } self.set_sim_pars(params_dict=params) day_of_change = 20 @@ -155,10 +155,10 @@ def test_change_beta_layers_clustered(self): seed_list = range(0) for seed in seed_list: params = { - SimKeys.random_seed: seed, - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 60, - SimKeys.initial_infected_count: initial_infected + 'rand_seed': seed, + 'pop_size': AGENT_COUNT, + 'n_days': 60, + 'pop_infected': initial_infected } if len(seed_list) > 1: self.expected_result_filename = f"DEBUG_{self.id()}_{seed}.json" @@ -226,10 +226,10 @@ def test_change_beta_layers_random(self): seed_list = range(0) for seed in seed_list: params = { - SimKeys.random_seed: seed, - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 60, - SimKeys.initial_infected_count: initial_infected + 'rand_seed': seed, + 'pop_size': AGENT_COUNT, + 'n_days': 60, + 'pop_infected': initial_infected } self.set_sim_pars(params_dict=params) if len(seed_list) > 1: @@ -284,10 +284,10 @@ def test_change_beta_layers_hybrid(self): seed_list = range(0) for seed in seed_list: params = { - SimKeys.random_seed: seed, - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 60, - SimKeys.initial_infected_count: initial_infected + 'rand_seed': seed, + 'pop_size': AGENT_COUNT, + 'n_days': 60, + 'pop_infected': initial_infected } if len(seed_list) > 1: self.expected_result_filename = f"DEBUG_{self.id()}_{seed}.json" @@ -415,8 +415,8 @@ def test_test_prob_perfect_asymptomatic(self): self.is_debugging = False agent_count = AGENT_COUNT params = { - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 60 + 'pop_size': AGENT_COUNT, + 'n_days': 60 } self.set_sim_pars(params_dict=params) @@ -452,8 +452,8 @@ def test_test_prob_perfect_asymptomatic(self): def test_test_prob_perfect_symptomatic(self): self.is_debugging = False params = { - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 60 + 'pop_size': AGENT_COUNT, + 'n_days': 60 } self.set_sim_pars(params_dict=params) @@ -486,8 +486,8 @@ def test_test_prob_perfect_not_quarantined(self): self.is_debugging = False agent_count = AGENT_COUNT params = { - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 60 + 'pop_size': AGENT_COUNT, + 'n_days': 60 } self.set_sim_pars(params_dict=params) @@ -522,9 +522,9 @@ def test_test_prob_sensitivity(self, subtract_today_recoveries=False): error_seeds = {} for seed in seed_list: params = { - SimKeys.random_seed: seed, - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 31 + 'rand_seed': seed, + 'pop_size': AGENT_COUNT, + 'n_days': 31 } self.set_sim_pars(params_dict=params) @@ -605,8 +605,8 @@ def test_test_prob_sensitivity(self, subtract_today_recoveries=False): def test_test_prob_symptomatic_prob_of_test(self): params = { - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 31 + 'pop_size': AGENT_COUNT, + 'n_days': 31 } self.set_sim_pars(params_dict=params) @@ -656,8 +656,8 @@ def test_test_prob_symptomatic_prob_of_test(self): # region contact tracing def test_brutal_contact_tracing(self): params = { - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 55 + 'pop_size': AGENT_COUNT, + 'n_days': 55 } self.set_sim_pars(params_dict=params) @@ -717,11 +717,11 @@ def test_contact_tracing_perfect_school_layer(self): self.is_debugging = False initial_infected = 10 params = { - SimKeys.number_agents: AGENT_COUNT, - SimKeys.number_simulated_days: 60, - SimKeys.quarantine_effectiveness: {'c':0.0, 'h':0.0, 'w':0.0, 's':0.0}, + 'pop_size': AGENT_COUNT, + 'n_days': 60, + 'quar_factor': {'c':0.0, 'h':0.0, 'w':0.0, 's':0.0}, 'quar_period': 10, - SimKeys.initial_infected_count: initial_infected + 'pop_infected': initial_infected } self.set_sim_pars(params_dict=params) sequence_days = [30, 40] diff --git a/tests/unittests/test_progression.py b/tests/unittests/test_progression.py index 90c0753e7..a97b04299 100644 --- a/tests/unittests/test_progression.py +++ b/tests/unittests/test_progression.py @@ -42,7 +42,7 @@ def test_exposure_to_infectiousness_delay_scaling(self): } self.set_simulation_prognosis_probability(prob_dict) serial_delay = { - TProps.ParKeys.SimKeys.number_simulated_days: sim_dur + 'n_days': sim_dur } self.run_sim(serial_delay) infectious_channel = self.get_full_result_channel( diff --git a/tests/unittests/unittest_support.py b/tests/unittests/unittest_support.py index 1bc6a7692..4235cac90 100644 --- a/tests/unittests/unittest_support.py +++ b/tests/unittests/unittest_support.py @@ -363,7 +363,7 @@ def set_everyone_infectious_same_day(self, num_agents, days_to_infectious=1, num } self.set_simulation_prognosis_probability(prob_dict) test_config = { - TProps.ParKeys.SimKeys.number_simulated_days: num_days + 'n_days': num_days } self.set_duration_distribution_parameters( duration_in_question=DurKeys.exposed_to_infectious, From 22b806cd75d31a967909be79185d600846041cee Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 23:50:27 -0700 Subject: [PATCH 356/569] more renames --- tests/unittests/test_transmission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unittests/test_transmission.py b/tests/unittests/test_transmission.py index 9f70a68e6..97dd732be 100644 --- a/tests/unittests/test_transmission.py +++ b/tests/unittests/test_transmission.py @@ -29,7 +29,7 @@ def test_beta_zero(self): """ self.set_smallpop_hightransmission() beta_zero = { - TKeys.beta: 0 + 'beta': 0 } self.run_sim(beta_zero) exposed_today_channel = self.get_full_result_channel( From 58060acd7c210875e9ff7b81ccdcc094926a2114 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 23:52:17 -0700 Subject: [PATCH 357/569] more renaming --- tests/unittests/test_pops.py | 4 ++-- tests/unittests/unittest_support.py | 27 --------------------------- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/tests/unittests/test_pops.py b/tests/unittests/test_pops.py index 5a4a094e9..7145d9813 100644 --- a/tests/unittests/test_pops.py +++ b/tests/unittests/test_pops.py @@ -36,7 +36,7 @@ def test_different_pop_types(self): self.assertGreater(these_results['cum_infections'][-1], these_results['cum_infections'][0], msg=f"Should see infections increase. Pop type {k} didn't do that.") - self.assertGreater(these_results[TProps.ResKeys.symptomatic_cumulative][-1], - these_results[TProps.ResKeys.symptomatic_cumulative][0], + self.assertGreater(these_results['cum_symptomatic'][-1], + these_results['cum_symptomatic'][0], msg=f"Should see symptomatic counts increase. Pop type {k} didn't do that.") diff --git a/tests/unittests/unittest_support.py b/tests/unittests/unittest_support.py index 4235cac90..32b49646a 100644 --- a/tests/unittests/unittest_support.py +++ b/tests/unittests/unittest_support.py @@ -17,33 +17,6 @@ class TProps: class ParKeys: - class SimKeys: - number_agents = 'pop_size' - number_contacts = 'contacts' - population_scaling_factor = 'pop_scale' - population_rescaling = 'rescale' - population_type = 'pop_type' - initial_infected_count = 'pop_infected' - start_day = 'start_day' - number_simulated_days = 'n_days' - random_seed = 'rand_seed' - verbose = 'verbose' - enable_synthpops = 'usepopdata' - time_limit = 'timelimit' - quarantine_effectiveness = 'quar_factor' - # stopping_function = 'stop_func' - pass - - class TransKeys: - beta = 'beta' - asymptomatic_fraction = 'asym_prop' - asymptomatic_transmission_multiplier = 'asym_factor' - diagnosis_transmission_factor = 'iso_factor' - contact_transmission_factor = 'cont_factor' - contacts_per_agent = 'contacts' - beta_population_specific = 'beta_pop' - contacts_population_specific = 'contacts_pop' - pass class ProgKeys: durations = "dur" From d7aa909e6fc602c7c810f856b51d546eedba975b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 1 Apr 2021 23:55:16 -0700 Subject: [PATCH 358/569] more renames --- tests/unittests/test_progression.py | 2 +- tests/unittests/unittest_support.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unittests/test_progression.py b/tests/unittests/test_progression.py index a97b04299..968358432 100644 --- a/tests/unittests/test_progression.py +++ b/tests/unittests/test_progression.py @@ -38,7 +38,7 @@ def test_exposure_to_infectiousness_delay_scaling(self): par2=std_dev ) prob_dict = { - TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.inf_to_symptomatic_probability: 0 + 'rel_symp_prob': 0 } self.set_simulation_prognosis_probability(prob_dict) serial_delay = { diff --git a/tests/unittests/unittest_support.py b/tests/unittests/unittest_support.py index 32b49646a..b52780966 100644 --- a/tests/unittests/unittest_support.py +++ b/tests/unittests/unittest_support.py @@ -332,7 +332,7 @@ def set_everyone_infectious_same_day(self, num_agents, days_to_infectious=1, num """ self.set_everyone_infected(agent_count=num_agents) prob_dict = { - TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.inf_to_symptomatic_probability: 0 + 'rel_symp_prob': 0 } self.set_simulation_prognosis_probability(prob_dict) test_config = { @@ -356,8 +356,8 @@ def set_everyone_symptomatic(self, num_agents, constant_delay:int=None): self.set_everyone_infectious_same_day(num_agents=num_agents, days_to_infectious=0) prob_dict = { - TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.inf_to_symptomatic_probability: 1.0, - TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.sym_to_severe_probability: 0 + 'rel_symp_prob': 1.0, + 'rel_severe_prob': 0 } self.set_simulation_prognosis_probability(prob_dict) if constant_delay is not None: @@ -388,8 +388,8 @@ def set_everyone_is_going_to_die(self, num_agents): def set_everyone_severe(self, num_agents, constant_delay:int=None): self.set_everyone_symptomatic(num_agents=num_agents, constant_delay=constant_delay) prob_dict = { - TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.sym_to_severe_probability: 1.0, - TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.sev_to_critical_probability: 0.0 + 'rel_severe_prob': 1.0, + 'rel_crit_prob': 0.0 } self.set_simulation_prognosis_probability(prob_dict) if constant_delay is not None: @@ -406,8 +406,8 @@ def set_everyone_critical(self, num_agents, constant_delay:int=None): """ self.set_everyone_severe(num_agents=num_agents, constant_delay=constant_delay) prob_dict = { - TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.sev_to_critical_probability: 1.0, - TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.crt_to_death_probability: 0.0 + 'rel_crit_prob': 1.0, + 'rel_death_prob': 0.0 } self.set_simulation_prognosis_probability(prob_dict) if constant_delay is not None: From 3320b15cd2e59abb4cd8144cc0a666580138300a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:02:48 -0700 Subject: [PATCH 359/569] more updates --- tests/unittests/test_mortality.py | 4 +- tests/unittests/test_progression.py | 4 +- tests/unittests/unittest_support.py | 210 +++++++++++++--------------- 3 files changed, 105 insertions(+), 113 deletions(-) diff --git a/tests/unittests/test_mortality.py b/tests/unittests/test_mortality.py index 6f9e62f5c..6a968df4d 100644 --- a/tests/unittests/test_mortality.py +++ b/tests/unittests/test_mortality.py @@ -44,7 +44,7 @@ def test_default_death_prob_zero(self): total_agents = 500 self.set_everyone_is_going_to_die(num_agents=total_agents) prob_dict = { - DProgKeys.ProbKeys.RelProbKeys.crt_to_death_probability: 0.0 + D'rel_death_prob': 0.0 } self.set_simulation_prognosis_probability(prob_dict) self.run_sim() @@ -84,7 +84,7 @@ def test_default_death_prob_scaling(self): death_probs = [0.01, 0.05, 0.10, 0.15] old_cumulative_deaths = 0 for death_prob in death_probs: - prob_dict = {DProgKeys.ProbKeys.RelProbKeys.crt_to_death_probability: death_prob} + prob_dict = {D'rel_death_prob': death_prob} self.set_simulation_prognosis_probability(prob_dict) self.run_sim() cumulative_deaths = self.get_day_final_channel_value(ResKeys.deaths_cumulative) diff --git a/tests/unittests/test_progression.py b/tests/unittests/test_progression.py index 968358432..bcf7c7c6a 100644 --- a/tests/unittests/test_progression.py +++ b/tests/unittests/test_progression.py @@ -76,7 +76,7 @@ def test_mild_infection_duration_scaling(self): self.set_everyone_infectious_same_day(num_agents=total_agents, days_to_infectious=exposed_delay) prob_dict = { - ParamKeys.ProgKeys.ProbKeys.RelProbKeys.inf_to_symptomatic_probability: 0.0 + ParamKeys.'rel_symp_prob': 0.0 } self.set_simulation_prognosis_probability(prob_dict) infectious_durations = [1, 2, 5, 10, 20] # Keep values in order @@ -107,7 +107,7 @@ def test_time_to_die_duration_scaling(self): total_agents = 500 self.set_everyone_critical(num_agents=500, constant_delay=0) prob_dict = { - ParamKeys.ProgKeys.ProbKeys.RelProbKeys.crt_to_death_probability: 1.0 + ParamKeys.'rel_death_prob': 1.0 } self.set_simulation_prognosis_probability(prob_dict) diff --git a/tests/unittests/unittest_support.py b/tests/unittests/unittest_support.py index b52780966..2a14774f9 100644 --- a/tests/unittests/unittest_support.py +++ b/tests/unittests/unittest_support.py @@ -11,53 +11,52 @@ import json import os import numpy as np - -from covasim import Sim, parameters, change_beta, test_prob, contact_tracing, sequence +import covasim as cv class TProps: - class ParKeys: - - class ProgKeys: - durations = "dur" - param_1 = "par1" - param_2 = "par2" - - class DurKeys: - exposed_to_infectious = 'exp2inf' - infectious_to_symptomatic = 'inf2sym' - infectious_asymptomatic_to_recovered = 'asym2rec' - infectious_symptomatic_to_recovered = 'mild2rec' - symptomatic_to_severe = 'sym2sev' - severe_to_critical = 'sev2crit' - aymptomatic_to_recovered = 'asym2rec' - severe_to_recovered = 'sev2rec' - critical_to_recovered = 'crit2rec' - critical_to_death = 'crit2die' - pass - - class ProbKeys: - progression_by_age = 'prog_by_age' - class RelProbKeys: - inf_to_symptomatic_probability = 'rel_symp_prob' - sym_to_severe_probability = 'rel_severe_prob' - sev_to_critical_probability = 'rel_crit_prob' - crt_to_death_probability = 'rel_death_prob' - pass - class PrognosesListKeys: - symptomatic_probabilities = 'symp_probs' - severe_probabilities = 'severe_probs' - critical_probabilities = 'crit_probs' - death_probs = 'death_probs' - pass - - class DiagnosticTestingKeys: - number_daily_tests = 'daily_tests' - daily_test_sensitivity = 'sensitivity' - symptomatic_testing_multiplier = 'sympt_test' - contacttrace_testing_multiplier = 'trace_test' - pass - pass +# class ParKeys: + +# class ProgKeys: +# durations = "dur" +# param_1 = "par1" +# param_2 = "par2" + +# class DurKeys: +# exposed_to_infectious = 'exp2inf' +# infectious_to_symptomatic = 'inf2sym' +# infectious_asymptomatic_to_recovered = 'asym2rec' +# infectious_symptomatic_to_recovered = 'mild2rec' +# symptomatic_to_severe = 'sym2sev' +# severe_to_critical = 'sev2crit' +# aymptomatic_to_recovered = 'asym2rec' +# severe_to_recovered = 'sev2rec' +# critical_to_recovered = 'crit2rec' +# critical_to_death = 'crit2die' +# pass + +# class ProbKeys: +# progression_by_age = 'prog_by_age' +# class RelProbKeys: +# inf_to_symptomatic_probability = 'rel_symp_prob' +# sym_to_severe_probability = 'rel_severe_prob' +# sev_to_critical_probability = 'rel_crit_prob' +# crt_to_death_probability = 'rel_death_prob' +# pass +# class PrognosesListKeys: +# symptomatic_probabilities = 'symp_probs' +# severe_probabilities = 'severe_probs' +# critical_probabilities = 'crit_probs' +# death_probs = 'death_probs' +# pass + +# class DiagnosticTestingKeys: +# number_daily_tests = 'daily_tests' +# daily_test_sensitivity = 'sensitivity' +# symptomatic_testing_multiplier = 'sympt_test' +# contacttrace_testing_multiplier = 'trace_test' +# pass +# pass class SpecialSims: class Microsim: @@ -84,39 +83,39 @@ class HighMortality: # timetodie_std = 2 pass - class ResKeys: - deaths_cumulative = 'cum_deaths' - deaths_daily = 'new_deaths' - diagnoses_cumulative = 'cum_diagnoses' - diagnoses_at_timestep = 'new_diagnoses' - exposed_at_timestep = 'n_exposed' - susceptible_at_timestep = 'n_susceptible' - infectious_at_timestep = 'n_infectious' - symptomatic_at_timestep = 'n_symptomatic' - symptomatic_cumulative = 'cum_symptomatic' - symptomatic_new_timestep = 'new_symptomatic' - recovered_at_timestep = 'new_recoveries' - recovered_cumulative = 'cum_recoveries' - infections_at_timestep = 'new_infections' - infections_cumulative = 'cum_infections' - tests_at_timestep = 'new_tests' - tests_cumulative = 'cum_tests' - quarantined_new = 'new_quarantined' - GUESS_doubling_time_at_timestep = 'doubling_time' - GUESS_r_effective_at_timestep = 'r_eff' + # class ResKeys: + # deaths_cumulative = 'cum_deaths' + # deaths_daily = 'new_deaths' + # diagnoses_cumulative = 'cum_diagnoses' + # diagnoses_at_timestep = 'new_diagnoses' + # exposed_at_timestep = 'n_exposed' + # susceptible_at_timestep = 'n_susceptible' + # infectious_at_timestep = 'n_infectious' + # symptomatic_at_timestep = 'n_symptomatic' + # symptomatic_cumulative = 'cum_symptomatic' + # symptomatic_new_timestep = 'new_symptomatic' + # recovered_at_timestep = 'new_recoveries' + # recovered_cumulative = 'cum_recoveries' + # infections_at_timestep = 'new_infections' + # infections_cumulative = 'cum_infections' + # tests_at_timestep = 'new_tests' + # tests_cumulative = 'cum_tests' + # quarantined_new = 'new_quarantined' + # GUESS_doubling_time_at_timestep = 'doubling_time' + # GUESS_r_effective_at_timestep = 'r_eff' pass -DurKeys = TProps.ParKeys.ProgKeys.DurKeys +# DurKeys = TProps.ParKeys.ProgKeys.DurKeys class CovaTest(unittest.TestCase): def setUp(self): self.is_debugging = False - self.simulation_parameters = None - self.simulation_prognoses = None + self.sim_pars = None + self.sim_progs = None self.sim = None self.simulation_result = None self.interventions = None @@ -143,10 +142,10 @@ def set_sim_pars(self, params_dict=None): None, sets self.simulation_params """ - if not self.simulation_parameters: - self.simulation_parameters = parameters.make_pars(set_prognoses=True, prog_by_age=True) + if not self.sim_pars: + self.sim_pars = cv.make_pars(set_prognoses=True, prog_by_age=True) if params_dict: - self.simulation_parameters.update(params_dict) + self.sim_pars.update(params_dict) pass def set_simulation_prognosis_probability(self, params_dict): @@ -154,46 +153,39 @@ def set_simulation_prognosis_probability(self, params_dict): Allows for testing prognoses probability as absolute rather than relative. NOTE: You can only call this once per test or you will overwrite your stuff. """ - ProbKeys = TProps.ParKeys.ProgKeys.ProbKeys - RelProbKeys = ProbKeys.RelProbKeys supported_probabilities = [ - RelProbKeys.inf_to_symptomatic_probability, - RelProbKeys.sym_to_severe_probability, - RelProbKeys.sev_to_critical_probability, - RelProbKeys.crt_to_death_probability + 'rel_symp_prob', + 'rel_severe_prob', + 'rel_crit_prob', + 'rel_death_prob' ] - if not self.simulation_parameters: + if not self.sim_pars: self.set_sim_pars() pass - if not self.simulation_prognoses: - self.simulation_prognoses = parameters.get_prognoses(self.simulation_parameters[ProbKeys.progression_by_age]) + if not self.sim_progs: + self.sim_progs = cv.get_prognoses(self.sim_pars['prog_by_age']) - PrognosisKeys = ProbKeys.PrognosesListKeys for k in params_dict: prognosis_in_question = None expected_prob = params_dict[k] - if k == RelProbKeys.inf_to_symptomatic_probability: - prognosis_in_question = PrognosisKeys.symptomatic_probabilities - elif k == RelProbKeys.sym_to_severe_probability: - prognosis_in_question = PrognosisKeys.severe_probabilities - elif k == RelProbKeys.sev_to_critical_probability: - prognosis_in_question = PrognosisKeys.critical_probabilities - elif k == RelProbKeys.crt_to_death_probability: - prognosis_in_question = PrognosisKeys.death_probs + if k == 'rel_symp_prob': prognosis_in_question = 'symp_probs' + elif k == 'rel_severe_prob': prognosis_in_question = 'severe_probs' + elif k == 'rel_crit_prob': prognosis_in_question = 'crit_probs' + elif k == 'rel_death_prob': prognosis_in_question = 'death_probs' else: raise KeyError(f"Key {k} not found in {supported_probabilities}.") - old_probs = self.simulation_prognoses[prognosis_in_question] - self.simulation_prognoses[prognosis_in_question] = np.array([expected_prob] * len(old_probs)) + old_probs = self.sim_progs[prognosis_in_question] + self.sim_progs[prognosis_in_question] = np.array([expected_prob] * len(old_probs)) pass pass def set_duration_distribution_parameters(self, duration_in_question, par1, par2): - if not self.simulation_parameters: + if not self.sim_pars: self.set_sim_pars() pass - duration_node = self.simulation_parameters["dur"] + duration_node = self.sim_pars["dur"] duration_node[duration_in_question] = { "dist": "normal", "par1": par1, @@ -206,21 +198,21 @@ def set_duration_distribution_parameters(self, duration_in_question, def run_sim(self, params_dict=None, write_results_json=False, population_type=None): - if not self.simulation_parameters or params_dict: # If we need one, or have one here + if not self.sim_pars or params_dict: # If we need one, or have one here self.set_sim_pars(params_dict=params_dict) pass - self.simulation_parameters['interventions'] = self.interventions + self.sim_pars['interventions'] = self.interventions - self.sim = Sim(pars=self.simulation_parameters, + self.sim = cv.Sim(pars=self.sim_pars, datafile=None) - if not self.simulation_prognoses: - self.simulation_prognoses = parameters.get_prognoses( - self.simulation_parameters[TProps.ParKeys.ProgKeys.ProbKeys.progression_by_age] + if not self.sim_progs: + self.sim_progs = cv.get_prognoses( + self.sim_pars[TProps.ParKeys.ProgKeys.ProbKeys.progression_by_age] ) pass - self.sim['prognoses'] = self.simulation_prognoses + self.sim['prognoses'] = self.sim_progs if population_type: self.sim.update_pars(pop_type=population_type) self.sim.run(verbose=0) @@ -258,7 +250,7 @@ def intervention_set_changebeta(self, days_array, multiplier_array, layers = None): - self.interventions = change_beta(days=days_array, + self.interventions = cv.change_beta(days=days_array, changes=multiplier_array, layers=layers) pass @@ -267,7 +259,7 @@ def intervention_set_test_prob(self, symptomatic_prob=0, asymptomatic_prob=0, asymptomatic_quarantine_prob=0, symp_quar_prob=0, test_sensitivity=1.0, loss_prob=0.0, test_delay=1, start_day=0): - self.interventions = test_prob(symp_prob=symptomatic_prob, + self.interventions = cv.test_prob(symp_prob=symptomatic_prob, asymp_prob=asymptomatic_prob, asymp_quar_prob=asymptomatic_quarantine_prob, symp_quar_prob=symp_quar_prob, @@ -287,7 +279,7 @@ def intervention_set_contact_tracing(self, pass if not trace_times: trace_times = {'h': 1, 's': 1, 'w': 1, 'c': 1} - self.interventions = contact_tracing(trace_probs=trace_probabilities, + self.interventions = cv.contact_tracing(trace_probs=trace_probabilities, trace_time=trace_times, start_day=start_day) pass @@ -295,7 +287,7 @@ def intervention_set_contact_tracing(self, def intervention_build_sequence(self, day_list, intervention_list): - my_sequence = sequence(days=day_list, + my_sequence = cv.sequence(days=day_list, interventions=intervention_list) self.interventions = my_sequence # endregion @@ -339,7 +331,7 @@ def set_everyone_infectious_same_day(self, num_agents, days_to_infectious=1, num 'n_days': num_days } self.set_duration_distribution_parameters( - duration_in_question=DurKeys.exposed_to_infectious, + duration_in_question='exp2inf', par1=days_to_infectious, par2=0 ) @@ -362,7 +354,7 @@ def set_everyone_symptomatic(self, num_agents, constant_delay:int=None): self.set_simulation_prognosis_probability(prob_dict) if constant_delay is not None: self.set_duration_distribution_parameters( - duration_in_question=DurKeys.infectious_to_symptomatic, + duration_in_question='inf2sym', par1=constant_delay, par2=0 ) @@ -394,7 +386,7 @@ def set_everyone_severe(self, num_agents, constant_delay:int=None): self.set_simulation_prognosis_probability(prob_dict) if constant_delay is not None: self.set_duration_distribution_parameters( - duration_in_question=DurKeys.symptomatic_to_severe, + duration_in_question='sym2sev', par1=constant_delay, par2=0 ) @@ -412,7 +404,7 @@ def set_everyone_critical(self, num_agents, constant_delay:int=None): self.set_simulation_prognosis_probability(prob_dict) if constant_delay is not None: self.set_duration_distribution_parameters( - duration_in_question=DurKeys.severe_to_critical, + duration_in_question='sev2crit', par1=constant_delay, par2=0 ) @@ -471,13 +463,13 @@ def test_run_small_hightransmission_sim(self): Verifies that there are lots of infections in a short time. """ - self.assertIsNone(self.simulation_parameters) + self.assertIsNone(self.sim_pars) self.assertIsNone(self.sim) self.set_smallpop_hightransmission() self.run_sim() self.assertIsNotNone(self.sim) - self.assertIsNotNone(self.simulation_parameters) + self.assertIsNotNone(self.sim_pars) exposed_today_channel = self.get_full_result_channel( TProps.ResKeys.exposed_at_timestep ) From eb26957405b255a948a872a5518934a3d19b54b4 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:05:34 -0700 Subject: [PATCH 360/569] more renames --- tests/unittests/test_mortality.py | 15 ++++----------- tests/unittests/test_progression.py | 2 +- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/tests/unittests/test_mortality.py b/tests/unittests/test_mortality.py index 6a968df4d..5e70ed9fd 100644 --- a/tests/unittests/test_mortality.py +++ b/tests/unittests/test_mortality.py @@ -7,11 +7,6 @@ import unittest from unittest_support import CovaTest, TProps -DProgKeys = TProps.ParKeys.ProgKeys -TransKeys = TProps.ParKeys.TransKeys -TSimKeys = TProps.ParKeys.SimKeys -ResKeys = TProps.ResKeys - class DiseaseMortalityTests(CovaTest): def setUp(self): @@ -43,16 +38,14 @@ def test_default_death_prob_zero(self): """ total_agents = 500 self.set_everyone_is_going_to_die(num_agents=total_agents) - prob_dict = { - D'rel_death_prob': 0.0 - } + prob_dict = {'rel_death_prob': 0.0} self.set_simulation_prognosis_probability(prob_dict) self.run_sim() deaths_at_timestep_channel = self.get_full_result_channel( - ResKeys.deaths_daily + 'new_deaths' ) deaths_cumulative_channel = self.get_full_result_channel( - ResKeys.deaths_cumulative + 'new_deaths' ) death_channels = [ deaths_at_timestep_channel, @@ -87,7 +80,7 @@ def test_default_death_prob_scaling(self): prob_dict = {D'rel_death_prob': death_prob} self.set_simulation_prognosis_probability(prob_dict) self.run_sim() - cumulative_deaths = self.get_day_final_channel_value(ResKeys.deaths_cumulative) + cumulative_deaths = self.get_day_final_channel_value('new_deaths') self.assertGreaterEqual(cumulative_deaths, old_cumulative_deaths, msg="Should be more deaths with higer ratio") old_cumulative_deaths = cumulative_deaths pass diff --git a/tests/unittests/test_progression.py b/tests/unittests/test_progression.py index bcf7c7c6a..a29755ccf 100644 --- a/tests/unittests/test_progression.py +++ b/tests/unittests/test_progression.py @@ -122,7 +122,7 @@ def test_time_to_die_duration_scaling(self): ) self.run_sim() deaths_today_channel = self.get_full_result_channel( - TProps.ResKeys.deaths_daily + TProps.'new_deaths' ) for t in range(len(deaths_today_channel)): curr_deaths = deaths_today_channel[t] From b6c70c14099b8076ef14dd4a4a99c103ea3a62bb Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:06:00 -0700 Subject: [PATCH 361/569] more renames --- tests/unittests/test_mortality.py | 4 ++-- tests/unittests/test_progression.py | 6 +++--- tests/unittests/unittest_support.py | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/unittests/test_mortality.py b/tests/unittests/test_mortality.py index 5e70ed9fd..2204afb26 100644 --- a/tests/unittests/test_mortality.py +++ b/tests/unittests/test_mortality.py @@ -39,7 +39,7 @@ def test_default_death_prob_zero(self): total_agents = 500 self.set_everyone_is_going_to_die(num_agents=total_agents) prob_dict = {'rel_death_prob': 0.0} - self.set_simulation_prognosis_probability(prob_dict) + self.set_sim_prog_prob(prob_dict) self.run_sim() deaths_at_timestep_channel = self.get_full_result_channel( 'new_deaths' @@ -78,7 +78,7 @@ def test_default_death_prob_scaling(self): old_cumulative_deaths = 0 for death_prob in death_probs: prob_dict = {D'rel_death_prob': death_prob} - self.set_simulation_prognosis_probability(prob_dict) + self.set_sim_prog_prob(prob_dict) self.run_sim() cumulative_deaths = self.get_day_final_channel_value('new_deaths') self.assertGreaterEqual(cumulative_deaths, old_cumulative_deaths, msg="Should be more deaths with higer ratio") diff --git a/tests/unittests/test_progression.py b/tests/unittests/test_progression.py index a29755ccf..9ba1dc4ed 100644 --- a/tests/unittests/test_progression.py +++ b/tests/unittests/test_progression.py @@ -40,7 +40,7 @@ def test_exposure_to_infectiousness_delay_scaling(self): prob_dict = { 'rel_symp_prob': 0 } - self.set_simulation_prognosis_probability(prob_dict) + self.set_sim_prog_prob(prob_dict) serial_delay = { 'n_days': sim_dur } @@ -78,7 +78,7 @@ def test_mild_infection_duration_scaling(self): prob_dict = { ParamKeys.'rel_symp_prob': 0.0 } - self.set_simulation_prognosis_probability(prob_dict) + self.set_sim_prog_prob(prob_dict) infectious_durations = [1, 2, 5, 10, 20] # Keep values in order infectious_duration_stddev = 0 for TEST_dur in infectious_durations: @@ -109,7 +109,7 @@ def test_time_to_die_duration_scaling(self): prob_dict = { ParamKeys.'rel_death_prob': 1.0 } - self.set_simulation_prognosis_probability(prob_dict) + self.set_sim_prog_prob(prob_dict) time_to_die_durations = [1, 2, 5, 10, 20] time_to_die_stddev = 0 diff --git a/tests/unittests/unittest_support.py b/tests/unittests/unittest_support.py index 2a14774f9..03f96bc05 100644 --- a/tests/unittests/unittest_support.py +++ b/tests/unittests/unittest_support.py @@ -148,7 +148,7 @@ def set_sim_pars(self, params_dict=None): self.sim_pars.update(params_dict) pass - def set_simulation_prognosis_probability(self, params_dict): + def set_sim_prog_prob(self, params_dict): """ Allows for testing prognoses probability as absolute rather than relative. NOTE: You can only call this once per test or you will overwrite your stuff. @@ -326,7 +326,7 @@ def set_everyone_infectious_same_day(self, num_agents, days_to_infectious=1, num prob_dict = { 'rel_symp_prob': 0 } - self.set_simulation_prognosis_probability(prob_dict) + self.set_sim_prog_prob(prob_dict) test_config = { 'n_days': num_days } @@ -351,7 +351,7 @@ def set_everyone_symptomatic(self, num_agents, constant_delay:int=None): 'rel_symp_prob': 1.0, 'rel_severe_prob': 0 } - self.set_simulation_prognosis_probability(prob_dict) + self.set_sim_prog_prob(prob_dict) if constant_delay is not None: self.set_duration_distribution_parameters( duration_in_question='inf2sym', @@ -374,7 +374,7 @@ def set_everyone_is_going_to_die(self, num_agents): ProbKeys.sev_to_critical_probability: 1, ProbKeys.crt_to_death_probability: 1 } - self.set_simulation_prognosis_probability(prob_dict) + self.set_sim_prog_prob(prob_dict) pass def set_everyone_severe(self, num_agents, constant_delay:int=None): @@ -383,7 +383,7 @@ def set_everyone_severe(self, num_agents, constant_delay:int=None): 'rel_severe_prob': 1.0, 'rel_crit_prob': 0.0 } - self.set_simulation_prognosis_probability(prob_dict) + self.set_sim_prog_prob(prob_dict) if constant_delay is not None: self.set_duration_distribution_parameters( duration_in_question='sym2sev', @@ -401,7 +401,7 @@ def set_everyone_critical(self, num_agents, constant_delay:int=None): 'rel_crit_prob': 1.0, 'rel_death_prob': 0.0 } - self.set_simulation_prognosis_probability(prob_dict) + self.set_sim_prog_prob(prob_dict) if constant_delay is not None: self.set_duration_distribution_parameters( duration_in_question='sev2crit', From 51d5ec47b768e746ef39df94db309d6ca1624193 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:06:27 -0700 Subject: [PATCH 362/569] more renames --- tests/unittests/test_mortality.py | 4 ++-- tests/unittests/unittest_support.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unittests/test_mortality.py b/tests/unittests/test_mortality.py index 2204afb26..e59b737e5 100644 --- a/tests/unittests/test_mortality.py +++ b/tests/unittests/test_mortality.py @@ -37,7 +37,7 @@ def test_default_death_prob_zero(self): Depends on default_cfr_one """ total_agents = 500 - self.set_everyone_is_going_to_die(num_agents=total_agents) + self.everyone_dies(num_agents=total_agents) prob_dict = {'rel_death_prob': 0.0} self.set_sim_prog_prob(prob_dict) self.run_sim() @@ -73,7 +73,7 @@ def test_default_death_prob_scaling(self): Depends on default_cfr_one """ total_agents = 500 - self.set_everyone_is_going_to_die(num_agents=total_agents) + self.everyone_dies(num_agents=total_agents) death_probs = [0.01, 0.05, 0.10, 0.15] old_cumulative_deaths = 0 for death_prob in death_probs: diff --git a/tests/unittests/unittest_support.py b/tests/unittests/unittest_support.py index 03f96bc05..0c6828a4e 100644 --- a/tests/unittests/unittest_support.py +++ b/tests/unittests/unittest_support.py @@ -360,7 +360,7 @@ def set_everyone_symptomatic(self, num_agents, constant_delay:int=None): ) pass - def set_everyone_is_going_to_die(self, num_agents): + def everyone_dies(self, num_agents): """ Cause all agents in the simulation to begin infected and die. Args: From b912c2e225fbaf74756d38e576a5a7783c109273 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:07:37 -0700 Subject: [PATCH 363/569] more updates --- tests/unittests/test_mortality.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unittests/test_mortality.py b/tests/unittests/test_mortality.py index e59b737e5..b681314fd 100644 --- a/tests/unittests/test_mortality.py +++ b/tests/unittests/test_mortality.py @@ -45,7 +45,7 @@ def test_default_death_prob_zero(self): 'new_deaths' ) deaths_cumulative_channel = self.get_full_result_channel( - 'new_deaths' + 'cum_deaths' ) death_channels = [ deaths_at_timestep_channel, @@ -60,7 +60,7 @@ def test_default_death_prob_zero(self): pass pass cumulative_recoveries = self.get_day_final_channel_value( - ResKeys.recovered_cumulative + 'cum_recovered' ) self.assertGreaterEqual(cumulative_recoveries, 200, msg="Should be lots of recoveries") @@ -77,10 +77,10 @@ def test_default_death_prob_scaling(self): death_probs = [0.01, 0.05, 0.10, 0.15] old_cumulative_deaths = 0 for death_prob in death_probs: - prob_dict = {D'rel_death_prob': death_prob} + prob_dict = {'rel_death_prob': death_prob} self.set_sim_prog_prob(prob_dict) self.run_sim() - cumulative_deaths = self.get_day_final_channel_value('new_deaths') + cumulative_deaths = self.get_day_final_channel_value('cum_deaths') self.assertGreaterEqual(cumulative_deaths, old_cumulative_deaths, msg="Should be more deaths with higer ratio") old_cumulative_deaths = cumulative_deaths pass From e56aba50c6c7d7100ea255cdc04a0680d43cb962 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:11:11 -0700 Subject: [PATCH 364/569] more replacements --- tests/unittests/test_mortality.py | 18 +++++------------- tests/unittests/unittest_support.py | 12 +++++------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/tests/unittests/test_mortality.py b/tests/unittests/test_mortality.py index b681314fd..6067bb4a5 100644 --- a/tests/unittests/test_mortality.py +++ b/tests/unittests/test_mortality.py @@ -5,7 +5,7 @@ import covasim as cv import unittest -from unittest_support import CovaTest, TProps +from unittest_support import CovaTest class DiseaseMortalityTests(CovaTest): @@ -53,17 +53,10 @@ def test_default_death_prob_zero(self): ] for c in death_channels: for t in range(len(c)): - self.assertEqual(c[t], 0, - msg=f"There should be no deaths" - f" with critical to death probability 0.0. Channel {c} had" - f" bad data at t: {t}") - pass - pass - cumulative_recoveries = self.get_day_final_channel_value( - 'cum_recovered' - ) - self.assertGreaterEqual(cumulative_recoveries, 200, - msg="Should be lots of recoveries") + self.assertEqual(c[t], 0, msg=f"There should be no deaths with critical to death probability 0.0. Channel {c} had bad data at t: {t}") + + cumulative_recoveries = self.get_day_final_channel_value('cum_recovered') + self.assertGreaterEqual(cumulative_recoveries, 200, sg="Should be lots of recoveries") pass def test_default_death_prob_scaling(self): @@ -83,7 +76,6 @@ def test_default_death_prob_scaling(self): cumulative_deaths = self.get_day_final_channel_value('cum_deaths') self.assertGreaterEqual(cumulative_deaths, old_cumulative_deaths, msg="Should be more deaths with higer ratio") old_cumulative_deaths = cumulative_deaths - pass # Run unit tests if called as a script if __name__ == '__main__': diff --git a/tests/unittests/unittest_support.py b/tests/unittests/unittest_support.py index 0c6828a4e..8202224db 100644 --- a/tests/unittests/unittest_support.py +++ b/tests/unittests/unittest_support.py @@ -228,7 +228,7 @@ def get_full_result_channel(self, channel): result_data = self.simulation_result["results"][channel] return result_data - def get_day_zero_channel_value(self, channel=TProps.ResKeys.susceptible_at_timestep): + def get_day_zero_channel_value(self, channel='n_susceptible'): """ Args: @@ -313,8 +313,6 @@ def set_everyone_infected(self, agent_count=1000): self.set_sim_pars(params_dict=everyone_infected) pass - DurKeys = TProps.ParKeys.ProgKeys.DurKeys - def set_everyone_infectious_same_day(self, num_agents, days_to_infectious=1, num_days=60): """ Args: @@ -369,10 +367,10 @@ def everyone_dies(self, num_agents): ProbKeys = TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys self.set_everyone_infectious_same_day(num_agents=num_agents) prob_dict = { - ProbKeys.inf_to_symptomatic_probability: 1, - ProbKeys.sym_to_severe_probability: 1, - ProbKeys.sev_to_critical_probability: 1, - ProbKeys.crt_to_death_probability: 1 + 'rel_symp_prob': 1, + 'rel_severe_prob': 1, + 'rel_crit_prob': 1, + 'rel_death_prob': 1 } self.set_sim_prog_prob(prob_dict) pass From 518638f7361632a5a4e5efadd93214a4c3c47c21 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:12:25 -0700 Subject: [PATCH 365/569] updates --- tests/unittests/unittest_support.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/unittests/unittest_support.py b/tests/unittests/unittest_support.py index 8202224db..48b9fbd05 100644 --- a/tests/unittests/unittest_support.py +++ b/tests/unittests/unittest_support.py @@ -297,8 +297,8 @@ def set_microsim(self): Simkeys = TProps.ParKeys.SimKeys Micro = TProps.SpecialSims.Microsim microsim_parameters = { - Simkeys.number_agents : Micro.n, - Simkeys.initial_infected_count: Micro.pop_infected, + 'pop_size' : Micro.n, + 'pop_infected': Micro.pop_infected, Simkeys.number_simulated_days: Micro.n_days } self.set_sim_pars(microsim_parameters) @@ -307,8 +307,8 @@ def set_microsim(self): def set_everyone_infected(self, agent_count=1000): Simkeys = TProps.ParKeys.SimKeys everyone_infected = { - Simkeys.number_agents: agent_count, - Simkeys.initial_infected_count: agent_count + 'pop_size': agent_count, + 'pop_infected': agent_count } self.set_sim_pars(params_dict=everyone_infected) pass @@ -364,7 +364,6 @@ def everyone_dies(self, num_agents): Args: num_agents: Number of agents to simulate """ - ProbKeys = TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys self.set_everyone_infectious_same_day(num_agents=num_agents) prob_dict = { 'rel_symp_prob': 1, @@ -417,8 +416,8 @@ def set_smallpop_hightransmission(self): Transkeys = TProps.ParKeys.TransKeys Hightrans = TProps.SpecialSims.Hightransmission hightrans_parameters = { - Simkeys.number_agents : Hightrans.n, - Simkeys.initial_infected_count: Hightrans.pop_infected, + 'pop_size' : Hightrans.n, + 'pop_infected': Hightrans.pop_infected, Simkeys.number_simulated_days: Hightrans.n_days, Transkeys.beta : Hightrans.beta } From c5f3166f0b5498f4d45b92591ac416a1865c0f01 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:15:44 -0700 Subject: [PATCH 366/569] more updates --- covasim/base.py | 3 ++- tests/unittests/unittest_support.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index cfb66fa2c..df0b45acd 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -94,6 +94,7 @@ def __setitem__(self, key, value): def update_pars(self, pars=None, create=False): ''' Update internal dict with new pars. + Args: pars (dict): the parameters to update (if None, do nothing) create (bool): if create is False, then raise a KeyNotFoundError if the key does not already exist @@ -255,7 +256,7 @@ def update_pars(self, pars=None, create=False, defaults=None, **kwargs): if defaults is not None: # Defaults have been provided: we are now doing updates pars = cvpar.listify_strain_pars(pars) # Strain pars need to be lists pars = cvpar.update_sub_key_pars(pars, defaults) # Update dict parameters by sub-key - combined_pars = sc.mergedicts(defaults, pars) # Now that subkeys have been updated, can merge the dicts together + pars = sc.mergedicts(defaults, pars) # Now that subkeys have been updated, can merge the dicts together super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj return diff --git a/tests/unittests/unittest_support.py b/tests/unittests/unittest_support.py index 48b9fbd05..1326d7993 100644 --- a/tests/unittests/unittest_support.py +++ b/tests/unittests/unittest_support.py @@ -305,7 +305,6 @@ def set_microsim(self): pass def set_everyone_infected(self, agent_count=1000): - Simkeys = TProps.ParKeys.SimKeys everyone_infected = { 'pop_size': agent_count, 'pop_infected': agent_count From 408f7943c8a88936a0e3d7525d55c750d2155d2c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:41:31 -0700 Subject: [PATCH 367/569] use default parameters --- covasim/people.py | 9 ++++++--- covasim/sim.py | 9 +++++++-- tests/unittests/test_mortality.py | 1 + 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index c030adf9b..1e19b5a8e 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -401,9 +401,12 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str # Deal with strain parameters infect_parkeys = ['dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob'] - infect_pars = dict() - for key in infect_parkeys: - infect_pars[key] = self.pars['strain_pars'][key][strain] + if not strain: # Use defaults for wild type + infect_pars = self.pars + else: + infect_pars = dict() + for key in infect_parkeys: + infect_pars[key] = self.pars['strain_pars'][key][strain] n_infections = len(inds) durpars = infect_pars['dur'] diff --git a/covasim/sim.py b/covasim/sim.py index 51e4cb203..967d76fbf 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -600,8 +600,13 @@ def step(self): cvimm.check_immunity(people, strain, sus=True) # Deal with strain parameters - beta = cvd.default_float(self['beta'] * self['strain_pars']['rel_beta'][strain]) - asymp_factor = cvd.default_float(self['strain_pars']['asymp_factor'][strain]) + if not strain: + rel_beta = self['rel_beta'] + asymp_factor = self['asymp_factor'] + else: + rel_beta = self['strain_pars']['rel_beta'][strain] + asymp_factor = cvd.default_float(self['strain_pars']['asymp_factor'][strain]) + beta = cvd.default_float(self['beta'] * rel_beta) for lkey, layer in contacts.items(): p1 = layer['p1'] diff --git a/tests/unittests/test_mortality.py b/tests/unittests/test_mortality.py index 6067bb4a5..e02049b89 100644 --- a/tests/unittests/test_mortality.py +++ b/tests/unittests/test_mortality.py @@ -79,4 +79,5 @@ def test_default_death_prob_scaling(self): # Run unit tests if called as a script if __name__ == '__main__': + unittest.TestCase.run = lambda self,*args,**kw: unittest.TestCase.debug(self) unittest.main() \ No newline at end of file From ad99e48b7a21b53b4da1d652de097d4f96193f12 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:46:50 -0700 Subject: [PATCH 368/569] tidying parameters --- covasim/base.py | 9 +-------- covasim/parameters.py | 20 -------------------- covasim/sim.py | 2 +- 3 files changed, 2 insertions(+), 29 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index df0b45acd..b80e13025 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -243,22 +243,15 @@ def _brief(self): return string - def update_pars(self, pars=None, create=False, defaults=None, **kwargs): + def update_pars(self, pars=None, create=False, **kwargs): ''' Ensure that metaparameters get used properly before being updated ''' pars = sc.mergedicts(pars, kwargs) - if pars: if pars.get('pop_type'): cvpar.reset_layer_pars(pars, force=False) if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses - - if defaults is not None: # Defaults have been provided: we are now doing updates - pars = cvpar.listify_strain_pars(pars) # Strain pars need to be lists - pars = cvpar.update_sub_key_pars(pars, defaults) # Update dict parameters by sub-key - pars = sc.mergedicts(defaults, pars) # Now that subkeys have been updated, can merge the dicts together super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj - return diff --git a/covasim/parameters.py b/covasim/parameters.py index 350d900dc..f768c049e 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -311,26 +311,6 @@ def absolute_prognoses(prognoses): return out -def update_sub_key_pars(pars, default_pars): - ''' Helper function to update sub-keys of dict parameters ''' - for par,val in pars.items(): - if par in cvd.strain_pars: # It will be stored as a list - newval = val[0] - oldval = sc.promotetolist(default_pars[par])[0] # Might be a list or not! - if isinstance(newval, dict): # Update the dictionary, don't just overwrite it - if par == 'imm_pars': - for type, valoftype in newval.items(): - if valoftype['form'] == oldval[type]['form']: - pars[par][0][type] = sc.mergenested(oldval[type], valoftype) - else: - pars[par][0][type] = valoftype - else: - pars[par] = sc.promotetolist(sc.mergenested(oldval, newval)) - else: - pars[par] = val - return pars - - def listify_strain_pars(pars): ''' Helper function to turn strain parameters into lists ''' for sp in cvd.strain_pars: diff --git a/covasim/sim.py b/covasim/sim.py index 967d76fbf..340ac2138 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -75,7 +75,7 @@ def __init__(self, pars=None, datafile=None, datacols=None, label=None, simfile= # Now update everything self.set_metadata(simfile) # Set the simulation date and filename - self.update_pars(pars, defaults=default_pars, **kwargs) # Update the parameters, if provided + self.update_pars(pars, **kwargs) # Update the parameters, if provided self.load_data(datafile, datacols) # Load the data, if provided if self.load_pop: self.load_population(popfile) # Load the population, if provided From 931b39bad76721b71a1921973194e5f9ee14c98f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:47:23 -0700 Subject: [PATCH 369/569] update --- tests/unittests/test_mortality.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unittests/test_mortality.py b/tests/unittests/test_mortality.py index e02049b89..0dd28cbe8 100644 --- a/tests/unittests/test_mortality.py +++ b/tests/unittests/test_mortality.py @@ -68,14 +68,14 @@ def test_default_death_prob_scaling(self): total_agents = 500 self.everyone_dies(num_agents=total_agents) death_probs = [0.01, 0.05, 0.10, 0.15] - old_cumulative_deaths = 0 + old_cum_deaths = 0 for death_prob in death_probs: prob_dict = {'rel_death_prob': death_prob} self.set_sim_prog_prob(prob_dict) self.run_sim() - cumulative_deaths = self.get_day_final_channel_value('cum_deaths') - self.assertGreaterEqual(cumulative_deaths, old_cumulative_deaths, msg="Should be more deaths with higer ratio") - old_cumulative_deaths = cumulative_deaths + cum_deaths = self.get_day_final_channel_value('cum_deaths') + self.assertGreaterEqual(cum_deaths, old_cum_deaths, msg="Should be more deaths with higer ratio") + old_cum_deaths = cum_deaths # Run unit tests if called as a script if __name__ == '__main__': From d51981c2d4468e1cf46dd1944dc14527e8cd6dae Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:48:09 -0700 Subject: [PATCH 370/569] more shortening --- tests/unittests/test_interventions.py | 160 +++++++++++++------------- tests/unittests/test_mortality.py | 16 +-- tests/unittests/test_pars.py | 26 ++--- tests/unittests/test_progression.py | 26 ++--- tests/unittests/test_transmission.py | 14 +-- tests/unittests/unittest_support.py | 24 ++-- 6 files changed, 133 insertions(+), 133 deletions(-) diff --git a/tests/unittests/test_interventions.py b/tests/unittests/test_interventions.py index 14c06db43..941668531 100644 --- a/tests/unittests/test_interventions.py +++ b/tests/unittests/test_interventions.py @@ -36,21 +36,21 @@ def test_brutal_change_beta_intervention(self): multiplier_array=change_multipliers ) self.run_sim() - new_infections_channel = self.get_full_result_channel( + new_infections_ch = self.get_full_result_ch( channel=ResultsKeys.infections_at_timestep ) five_previous_days = range(day_of_change-5, day_of_change) for d in five_previous_days: - self.assertGreater(new_infections_channel[d], + self.assertGreater(new_infections_ch[d], 0, msg=f"Need to have infections before change day {day_of_change}") pass - happy_days = range(day_of_change + 1, len(new_infections_channel)) + happy_days = range(day_of_change + 1, len(new_infections_ch)) for d in happy_days: - self.assertEqual(new_infections_channel[d], + self.assertEqual(new_infections_ch[d], 0, - msg=f"expected 0 infections on day {d}, got {new_infections_channel[d]}.") + msg=f"expected 0 infections on day {d}, got {new_infections_ch[d]}.") def test_change_beta_days(self): params = { @@ -64,12 +64,12 @@ def test_change_beta_days(self): self.intervention_set_changebeta(days_array=days, multiplier_array=multipliers) self.run_sim() - new_infections_channel = self.get_full_result_channel( + new_infections_ch = self.get_full_result_ch( channel=ResultsKeys.infections_at_timestep ) five_previous_days = range(days[0] -5, days[0]) for d in five_previous_days: - self.assertGreater(new_infections_channel[d], + self.assertGreater(new_infections_ch[d], 0, msg=f"Need to have infections before first change day {days[0]}") pass @@ -79,22 +79,22 @@ def test_change_beta_days(self): happy_days = range(days[b], days[b + 1]) for d in happy_days: # print(f"DEBUG: looking at happy day {d}") - self.assertEqual(new_infections_channel[d], + self.assertEqual(new_infections_ch[d], 0, - msg=f"expected 0 infections on day {d}, got {new_infections_channel[d]}.") + msg=f"expected 0 infections on day {d}, got {new_infections_ch[d]}.") infection_days = range(days[b+1], days[b+2]) for d in infection_days: # print(f"DEBUG: looking at infection day {d}") - self.assertGreater(new_infections_channel[d], + self.assertGreater(new_infections_ch[d], 0, - msg=f"Expected some infections on day {d}, got {new_infections_channel[d]}") + msg=f"Expected some infections on day {d}, got {new_infections_ch[d]}") pass pass - for d in range (days[-1] + 1, len(new_infections_channel)): - self.assertEqual(new_infections_channel[d], + for d in range (days[-1] + 1, len(new_infections_ch)): + self.assertEqual(new_infections_ch[d], 0, msg=f"After day {days[-1]} should have no infections." - f" Got {new_infections_channel[d]} on day {d}.") + f" Got {new_infections_ch[d]} on day {d}.") # verify that every infection day after days[0] is in a 1.0 block # verify no infections after 60 @@ -118,7 +118,7 @@ def test_change_beta_multipliers(self): multiplier_array=[multiplier] ) self.run_sim(params) - these_infections = self.get_day_final_channel_value( + these_infections = self.get_day_final_ch_value( channel=ResultsKeys.infections_cumulative ) total_infections[multiplier] = these_infections @@ -185,18 +185,18 @@ def test_change_beta_layers_clustered(self): self.run_sim(population_type='clustered') last_intervention_day = intervention_days[-1] first_intervention_day = intervention_days[0] - cum_infections_channel= self.get_full_result_channel(ResultsKeys.infections_cumulative) + cum_infections_ch= self.get_full_result_ch(ResultsKeys.infections_cumulative) if len(seed_list) > 1: messages = [] - if cum_infections_channel[intervention_days[0]-1] < initial_infected: + if cum_infections_ch[intervention_days[0]-1] < initial_infected: messages.append(f"Before intervention at day {intervention_days[0]}, there should be infections happening.") pass - if cum_infections_channel[last_intervention_day] < cum_infections_channel[first_intervention_day]: + if cum_infections_ch[last_intervention_day] < cum_infections_ch[first_intervention_day]: messages.append(f"Cumulative infections should grow with only some layers enabled.") pass - if cum_infections_channel[last_intervention_day] != cum_infections_channel[-1]: + if cum_infections_ch[last_intervention_day] != cum_infections_ch[-1]: messages.append(f"The cumulative infections at {last_intervention_day} should be the same as at the end.") pass @@ -206,16 +206,16 @@ def test_change_beta_layers_clustered(self): print(f"\t{m}") pass - self.assertGreater(cum_infections_channel[intervention_days[0]-1], + self.assertGreater(cum_infections_ch[intervention_days[0]-1], initial_infected, msg=f"Before intervention at day {intervention_days[0]}, there should be infections happening.") - self.assertGreater(cum_infections_channel[last_intervention_day], - cum_infections_channel[first_intervention_day], + self.assertGreater(cum_infections_ch[last_intervention_day], + cum_infections_ch[first_intervention_day], msg=f"Cumulative infections should grow with only some layers enabled.") - self.assertEqual(cum_infections_channel[last_intervention_day], - cum_infections_channel[-1], + self.assertEqual(cum_infections_ch[last_intervention_day], + cum_infections_ch[-1], msg=f"with all layers at 0 beta, the cumulative infections at {last_intervention_day}" + f" should be the same as at the end.") pass @@ -255,14 +255,14 @@ def test_change_beta_layers_random(self): self.interventions = intervention_list self.run_sim(population_type='random') last_intervention_day = intervention_days[-1] - cum_infections_channel = self.get_full_result_channel(ResultsKeys.infections_cumulative) + cum_infections_ch = self.get_full_result_ch(ResultsKeys.infections_cumulative) if len(seed_list) > 1: messages = [] - if cum_infections_channel[intervention_days[0]-1] < initial_infected: + if cum_infections_ch[intervention_days[0]-1] < initial_infected: messages.append(f"Before intervention at day {intervention_days[0]}, there should be infections happening.") pass - if cum_infections_channel[last_intervention_day] != cum_infections_channel[-1]: + if cum_infections_ch[last_intervention_day] != cum_infections_ch[-1]: messages.append(f"The cumulative infections at {last_intervention_day} should be the same as at the end.") pass @@ -271,11 +271,11 @@ def test_change_beta_layers_random(self): for m in messages: print(f"\t{m}") pass - self.assertGreater(cum_infections_channel[intervention_days[0]-1], + self.assertGreater(cum_infections_ch[intervention_days[0]-1], initial_infected, msg=f"Before intervention at day {intervention_days[0]}, there should be infections happening.") - self.assertEqual(cum_infections_channel[last_intervention_day], - cum_infections_channel[intervention_days[0] - 1], + self.assertEqual(cum_infections_ch[last_intervention_day], + cum_infections_ch[intervention_days[0] - 1], msg=f"With all layers at 0 beta, should be 0 infections at {last_intervention_day}.") def test_change_beta_layers_hybrid(self): @@ -314,18 +314,18 @@ def test_change_beta_layers_hybrid(self): self.run_sim(population_type='hybrid') last_intervention_day = intervention_days[-1] first_intervention_day = intervention_days[0] - cum_infections_channel = self.get_full_result_channel(ResultsKeys.infections_cumulative) + cum_infections_ch = self.get_full_result_ch(ResultsKeys.infections_cumulative) if len(seed_list) > 1: messages = [] - if cum_infections_channel[intervention_days[0]-1] < initial_infected: + if cum_infections_ch[intervention_days[0]-1] < initial_infected: messages.append(f"Before intervention at day {intervention_days[0]}, there should be infections happening.") pass - if cum_infections_channel[last_intervention_day] < cum_infections_channel[first_intervention_day]: + if cum_infections_ch[last_intervention_day] < cum_infections_ch[first_intervention_day]: messages.append(f"Cumulative infections should grow with only some layers enabled.") pass - if cum_infections_channel[last_intervention_day] != cum_infections_channel[-1]: + if cum_infections_ch[last_intervention_day] != cum_infections_ch[-1]: messages.append(f"The cumulative infections at {last_intervention_day} should be the same as at the end.") pass @@ -334,32 +334,32 @@ def test_change_beta_layers_hybrid(self): for m in messages: print(f"\t{m}") pass - self.assertGreater(cum_infections_channel[intervention_days[0]-1], + self.assertGreater(cum_infections_ch[intervention_days[0]-1], initial_infected, msg=f"Before intervention at day {intervention_days[0]}, there should be infections happening.") - self.assertGreater(cum_infections_channel[last_intervention_day], - cum_infections_channel[first_intervention_day], + self.assertGreater(cum_infections_ch[last_intervention_day], + cum_infections_ch[first_intervention_day], msg=f"Cumulative infections should grow with only some layers enabled.") - self.assertEqual(cum_infections_channel[last_intervention_day], - cum_infections_channel[-1], + self.assertEqual(cum_infections_ch[last_intervention_day], + cum_infections_ch[-1], msg=f"With all layers at 0 beta, the cumulative infections at {last_intervention_day}" f" should be the same as at the end.") def verify_perfect_test_prob(self, start_day, test_delay, test_sensitivity, - target_pop_count_channel, - target_pop_new_channel, - target_test_count_channel=None): + target_pop_count_ch, + target_pop_new_ch, + target_test_count_ch=None): if test_sensitivity < 1.0: raise ValueError("This test method only works with perfect test " f"sensitivity. {test_sensitivity} won't cut it.") - new_tests = self.get_full_result_channel( + new_tests = self.get_full_result_ch( channel=ResultsKeys.tests_at_timestep ) - new_diagnoses = self.get_full_result_channel( + new_diagnoses = self.get_full_result_ch( channel=ResultsKeys.diagnoses_at_timestep ) - target_count = target_pop_count_channel - target_new = target_pop_new_channel + target_count = target_pop_count_ch + target_new = target_pop_new_ch pre_test_days = range(0, start_day) for d in pre_test_days: self.assertEqual(new_tests[d], @@ -378,8 +378,8 @@ def verify_perfect_test_prob(self, start_day, test_delay, test_sensitivity, pass self.assertEqual(new_tests[start_day], - target_test_count_channel[start_day], - msg=f"Should have each of the {target_test_count_channel[start_day]} targets" + target_test_count_ch[start_day], + msg=f"Should have each of the {target_test_count_ch[start_day]} targets" f" get tested at day {start_day}. Got {new_tests[start_day]} instead.") self.assertEqual(new_diagnoses[start_day + test_delay], target_count[start_day], @@ -387,7 +387,7 @@ def verify_perfect_test_prob(self, start_day, test_delay, test_sensitivity, f"get diagnosed at day {start_day + test_delay} with sensitivity {test_sensitivity} " f"and delay {test_delay}. Got {new_diagnoses[start_day + test_delay]} instead.") post_test_days = range(start_day + 1, len(new_tests)) - if target_pop_new_channel: + if target_pop_new_ch: for d in post_test_days[:test_delay]: symp_today = target_new[d] diag_today = new_diagnoses[d + test_delay] @@ -430,24 +430,24 @@ def test_test_prob_perfect_asymptomatic(self): test_delay=test_delay, start_day=start_day) self.run_sim() - symptomatic_count_channel = self.get_full_result_channel( + symptomatic_count_ch = self.get_full_result_ch( ResultsKeys.symptomatic_at_timestep ) - infectious_count_channel = self.get_full_result_channel( + infectious_count_ch = self.get_full_result_ch( ResultsKeys.infectious_at_timestep ) - population_channel = [agent_count] * len(symptomatic_count_channel) - asymptomatic_infectious_count_channel = list(np.subtract(np.array(infectious_count_channel), - np.array(symptomatic_count_channel))) - asymptomatic_population_count_channel = list(np.subtract(np.array(population_channel), - np.array(symptomatic_count_channel))) + population_ch = [agent_count] * len(symptomatic_count_ch) + asymptomatic_infectious_count_ch = list(np.subtract(np.array(infectious_count_ch), + np.array(symptomatic_count_ch))) + asymptomatic_population_count_ch = list(np.subtract(np.array(population_ch), + np.array(symptomatic_count_ch))) self.verify_perfect_test_prob(start_day=start_day, test_delay=test_delay, test_sensitivity=test_sensitivity, - target_pop_count_channel=asymptomatic_infectious_count_channel, - target_test_count_channel=asymptomatic_population_count_channel, - target_pop_new_channel=None) + target_pop_count_ch=asymptomatic_infectious_count_ch, + target_test_count_ch=asymptomatic_population_count_ch, + target_pop_new_ch=None) def test_test_prob_perfect_symptomatic(self): self.is_debugging = False @@ -467,18 +467,18 @@ def test_test_prob_perfect_symptomatic(self): test_delay=test_delay, start_day=start_day) self.run_sim() - symptomatic_count_channel = self.get_full_result_channel( + symptomatic_count_ch = self.get_full_result_ch( ResultsKeys.symptomatic_at_timestep ) - symptomatic_new_channel = self.get_full_result_channel( + symptomatic_new_ch = self.get_full_result_ch( ResultsKeys.symptomatic_new_timestep ) self.verify_perfect_test_prob(start_day=start_day, test_delay=test_delay, test_sensitivity=test_sensitivity, - target_pop_count_channel=symptomatic_count_channel, - target_pop_new_channel=symptomatic_new_channel, - target_test_count_channel=symptomatic_count_channel + target_pop_count_ch=symptomatic_count_ch, + target_pop_new_ch=symptomatic_new_ch, + target_test_count_ch=symptomatic_count_ch ) pass @@ -503,17 +503,17 @@ def test_test_prob_perfect_not_quarantined(self): test_delay=test_delay, start_day=start_day) self.run_sim() - infectious_count_channel = self.get_full_result_channel( + infectious_count_ch = self.get_full_result_ch( ResultsKeys.infectious_at_timestep ) - population_channel = [agent_count] * len(infectious_count_channel) + population_ch = [agent_count] * len(infectious_count_ch) self.verify_perfect_test_prob(start_day=start_day, test_delay=test_delay, test_sensitivity=test_sensitivity, - target_pop_count_channel=infectious_count_channel, - target_test_count_channel=population_channel, - target_pop_new_channel=None) + target_pop_count_ch=infectious_count_ch, + target_test_count_ch=population_ch, + target_pop_new_ch=None) pass def test_test_prob_sensitivity(self, subtract_today_recoveries=False): @@ -539,14 +539,14 @@ def test_test_prob_sensitivity(self, subtract_today_recoveries=False): test_delay=test_delay, start_day=start_day) self.run_sim() - first_day_diagnoses = self.get_full_result_channel( + first_day_diagnoses = self.get_full_result_ch( channel=ResultsKeys.diagnoses_at_timestep )[start_day] - target_count = self.get_full_result_channel( + target_count = self.get_full_result_ch( channel=ResultsKeys.symptomatic_at_timestep )[start_day] if subtract_today_recoveries: - recoveries_today = self.get_full_result_channel( + recoveries_today = self.get_full_result_ch( channel=ResultsKeys.recovered_at_timestep )[start_day] target_count = target_count - recoveries_today @@ -561,7 +561,7 @@ def test_test_prob_sensitivity(self, subtract_today_recoveries=False): print(f"\tMax: {max_tolerable_diagnoses} \n" f"\tMin: {min_tolerable_diagnoses} \n" f"\tTarget: {target_count} \n" - f"\tPrevious day Target: {self.get_full_result_channel(channel=ResultsKeys.symptomatic_at_timestep)[start_day -1 ]} \n" + f"\tPrevious day Target: {self.get_full_result_ch(channel=ResultsKeys.symptomatic_at_timestep)[start_day -1 ]} \n" f"\tSensitivity: {sensitivity} \n" f"\tIdeal: {ideal_diagnoses} \n" f"\tActual diagnoses: {first_day_diagnoses}\n") @@ -621,10 +621,10 @@ def test_test_prob_symptomatic_prob_of_test(self): test_delay=test_delay, start_day=start_day) self.run_sim() - first_day_tests = self.get_full_result_channel( + first_day_tests = self.get_full_result_ch( channel=ResultsKeys.tests_at_timestep )[start_day] - target_count = self.get_full_result_channel( + target_count = self.get_full_result_ch( channel=ResultsKeys.symptomatic_at_timestep )[start_day] ideal_test_count = target_count * s_p_o_t @@ -697,7 +697,7 @@ def test_brutal_contact_tracing(self): intervention_list.append(self.interventions) self.interventions = intervention_list self.run_sim(population_type='hybrid') - channel_new_quarantines = self.get_full_result_channel( + channel_new_quarantines = self.get_full_result_ch( ResultsKeys.quarantined_new ) quarantines_before_tracing = sum(channel_new_quarantines[:trace_start_day]) @@ -749,16 +749,16 @@ def test_contact_tracing_perfect_school_layer(self): self.interventions = sequence_interventions self.run_sim(population_type='hybrid') - channel_new_infections = self.get_full_result_channel( + channel_new_infections = self.get_full_result_ch( ResultsKeys.infections_at_timestep ) - channel_new_tests = self.get_full_result_channel( + channel_new_tests = self.get_full_result_ch( ResultsKeys.tests_at_timestep ) - channel_new_diagnoses = self.get_full_result_channel( + channel_new_diagnoses = self.get_full_result_ch( ResultsKeys.diagnoses_at_timestep ) - channel_new_quarantine = self.get_full_result_channel( + channel_new_quarantine = self.get_full_result_ch( ResultsKeys.quarantined_new ) diff --git a/tests/unittests/test_mortality.py b/tests/unittests/test_mortality.py index 0dd28cbe8..50688da1c 100644 --- a/tests/unittests/test_mortality.py +++ b/tests/unittests/test_mortality.py @@ -41,21 +41,21 @@ def test_default_death_prob_zero(self): prob_dict = {'rel_death_prob': 0.0} self.set_sim_prog_prob(prob_dict) self.run_sim() - deaths_at_timestep_channel = self.get_full_result_channel( + deaths_at_timestep_ch = self.get_full_result_ch( 'new_deaths' ) - deaths_cumulative_channel = self.get_full_result_channel( + deaths_cumulative_ch = self.get_full_result_ch( 'cum_deaths' ) - death_channels = [ - deaths_at_timestep_channel, - deaths_cumulative_channel + death_chs = [ + deaths_at_timestep_ch, + deaths_cumulative_ch ] - for c in death_channels: + for c in death_chs: for t in range(len(c)): self.assertEqual(c[t], 0, msg=f"There should be no deaths with critical to death probability 0.0. Channel {c} had bad data at t: {t}") - cumulative_recoveries = self.get_day_final_channel_value('cum_recovered') + cumulative_recoveries = self.get_day_final_ch_value('cum_recovered') self.assertGreaterEqual(cumulative_recoveries, 200, sg="Should be lots of recoveries") pass @@ -73,7 +73,7 @@ def test_default_death_prob_scaling(self): prob_dict = {'rel_death_prob': death_prob} self.set_sim_prog_prob(prob_dict) self.run_sim() - cum_deaths = self.get_day_final_channel_value('cum_deaths') + cum_deaths = self.get_day_final_ch_value('cum_deaths') self.assertGreaterEqual(cum_deaths, old_cum_deaths, msg="Should be more deaths with higer ratio") old_cum_deaths = cum_deaths diff --git a/tests/unittests/test_pars.py b/tests/unittests/test_pars.py index 1489d7354..e777415ed 100644 --- a/tests/unittests/test_pars.py +++ b/tests/unittests/test_pars.py @@ -53,13 +53,13 @@ def test_population_size(self): TPKeys.initial_infected_count: 0 } self.run_sim(pop_2_one_day) - pop_2_pop = self.get_day_zero_channel_value() + pop_2_pop = self.get_day_zero_ch_value() self.run_sim(pop_10_one_day) - pop_10_pop = self.get_day_zero_channel_value() + pop_10_pop = self.get_day_zero_ch_value() self.run_sim(pop_123_one_day) - pop_123_pop = self.get_day_zero_channel_value() + pop_123_pop = self.get_day_zero_ch_value() self.run_sim(pop_1234_one_day) - pop_1234_pop = self.get_day_zero_channel_value() + pop_1234_pop = self.get_day_zero_ch_value() self.assertEqual(pop_2_pop, pop_2_one_day[TPKeys.number_agents]) self.assertEqual(pop_10_pop, pop_10_one_day[TPKeys.number_agents]) @@ -120,11 +120,11 @@ def test_population_scaling(self): TPKeys.number_simulated_days: 1 } self.run_sim(scale_1_one_day) - scale_1_pop = self.get_day_zero_channel_value() + scale_1_pop = self.get_day_zero_ch_value() self.run_sim(scale_2_one_day) - scale_2_pop = self.get_day_zero_channel_value() + scale_2_pop = self.get_day_zero_ch_value() self.run_sim(scale_10_one_day) - scale_10_pop = self.get_day_zero_channel_value() + scale_10_pop = self.get_day_zero_ch_value() self.assertEqual(scale_2_pop, 2 * scale_1_pop) self.assertEqual(scale_10_pop, 10 * scale_1_pop) pass @@ -146,17 +146,17 @@ def test_random_seed(self): TPKeys.random_seed: 2 } self.run_sim(seed_1_params) - infectious_seed_1_v1 = self.get_full_result_channel( + infectious_seed_1_v1 = self.get_full_result_ch( ResKeys.infectious_at_timestep ) - exposures_seed_1_v1 = self.get_full_result_channel( + exposures_seed_1_v1 = self.get_full_result_ch( ResKeys.exposed_at_timestep ) self.run_sim(seed_1_params) - infectious_seed_1_v2 = self.get_full_result_channel( + infectious_seed_1_v2 = self.get_full_result_ch( ResKeys.infectious_at_timestep ) - exposures_seed_1_v2 = self.get_full_result_channel( + exposures_seed_1_v2 = self.get_full_result_ch( ResKeys.exposed_at_timestep ) self.assertEqual(infectious_seed_1_v1, infectious_seed_1_v2, @@ -166,10 +166,10 @@ def test_random_seed(self): msg=f"With random seed the same, these channels should" f"be identical.") self.run_sim(seed_2_params) - infectious_seed_2 = self.get_full_result_channel( + infectious_seed_2 = self.get_full_result_ch( ResKeys.infectious_at_timestep ) - exposures_seed_2 = self.get_full_result_channel( + exposures_seed_2 = self.get_full_result_ch( ResKeys.exposed_at_timestep ) self.assertNotEqual(infectious_seed_1_v1, infectious_seed_2, diff --git a/tests/unittests/test_progression.py b/tests/unittests/test_progression.py index 9ba1dc4ed..9fbbfaf71 100644 --- a/tests/unittests/test_progression.py +++ b/tests/unittests/test_progression.py @@ -45,23 +45,23 @@ def test_exposure_to_infectiousness_delay_scaling(self): 'n_days': sim_dur } self.run_sim(serial_delay) - infectious_channel = self.get_full_result_channel( + infectious_ch = self.get_full_result_ch( ResKeys.infectious_at_timestep ) - agents_on_infectious_day = infectious_channel[exposed_delay] + agents_on_infectious_day = infectious_ch[exposed_delay] if self.is_debugging: print(f"Delay: {exposed_delay}") print(f"Agents turned: {agents_on_infectious_day}") - print(f"Infectious channel {infectious_channel}") + print(f"Infectious channel {infectious_ch}") pass - for t in range(len(infectious_channel)): - current_infectious = infectious_channel[t] + for t in range(len(infectious_ch)): + current_infectious = infectious_ch[t] if t < exposed_delay: self.assertEqual(current_infectious, 0, msg=f"All {total_agents} should turn infectious at t: {exposed_delay}" f" instead got {current_infectious} at t: {t}") elif t == exposed_delay: - self.assertEqual(infectious_channel[exposed_delay], total_agents, + self.assertEqual(infectious_ch[exposed_delay], total_agents, msg=f"With stddev 0, all {total_agents} agents should turn infectious " f"on day {exposed_delay}, instead got {agents_on_infectious_day}. ") pass @@ -89,15 +89,15 @@ def test_mild_infection_duration_scaling(self): par2=infectious_duration_stddev ) self.run_sim() - recoveries_channel = self.get_full_result_channel( + recoveries_ch = self.get_full_result_ch( TProps.ResKeys.recovered_at_timestep ) - recoveries_on_recovery_day = recoveries_channel[recovery_day] + recoveries_on_recovery_day = recoveries_ch[recovery_day] if self.is_debugging: print(f"Delay: {recovery_day}") print(f"Agents turned: {recoveries_on_recovery_day}") - print(f"Recoveries channel {recoveries_channel}") - self.assertEqual(recoveries_channel[recovery_day], total_agents, + print(f"Recoveries channel {recoveries_ch}") + self.assertEqual(recoveries_ch[recovery_day], total_agents, msg=f"With stddev 0, all {total_agents} agents should turn infectious " f"on day {recovery_day}, instead got {recoveries_on_recovery_day}. ") @@ -121,11 +121,11 @@ def test_time_to_die_duration_scaling(self): par2=time_to_die_stddev ) self.run_sim() - deaths_today_channel = self.get_full_result_channel( + deaths_today_ch = self.get_full_result_ch( TProps.'new_deaths' ) - for t in range(len(deaths_today_channel)): - curr_deaths = deaths_today_channel[t] + for t in range(len(deaths_today_ch)): + curr_deaths = deaths_today_ch[t] if t < TEST_dur: self.assertEqual(curr_deaths, 0, msg=f"With std 0, all {total_agents} agents should die on " diff --git a/tests/unittests/test_transmission.py b/tests/unittests/test_transmission.py index 97dd732be..a524d837d 100644 --- a/tests/unittests/test_transmission.py +++ b/tests/unittests/test_transmission.py @@ -32,25 +32,25 @@ def test_beta_zero(self): 'beta': 0 } self.run_sim(beta_zero) - exposed_today_channel = self.get_full_result_channel( + exposed_today_ch = self.get_full_result_ch( TProps.ResKeys.exposed_at_timestep ) - prev_exposed = exposed_today_channel[0] + prev_exposed = exposed_today_ch[0] self.assertEqual(prev_exposed, Hightrans.pop_infected, msg="Make sure we have some initial infections") - for t in range(1, len(exposed_today_channel)): - today_exposed = exposed_today_channel[t] + for t in range(1, len(exposed_today_ch)): + today_exposed = exposed_today_ch[t] self.assertLessEqual(today_exposed, prev_exposed, msg=f"The exposure counts should do nothing but decline." f" At time {t}: {today_exposed} at {t-1}: {prev_exposed}.") prev_exposed = today_exposed pass - infections_channel = self.get_full_result_channel( + infections_ch = self.get_full_result_ch( TProps.ResKeys.infections_at_timestep ) - for t in range(len(infections_channel)): - today_infectious = infections_channel[t] + for t in range(len(infections_ch)): + today_infectious = infections_ch[t] self.assertEqual(today_infectious, 0, msg=f"With beta 0, there should be no infections." f" At ts: {t} got {today_infectious}.") diff --git a/tests/unittests/unittest_support.py b/tests/unittests/unittest_support.py index 1326d7993..2e0cfd0ee 100644 --- a/tests/unittests/unittest_support.py +++ b/tests/unittests/unittest_support.py @@ -224,11 +224,11 @@ def run_sim(self, params_dict=None, write_results_json=False, population_type=No # endregion # region simulation results support - def get_full_result_channel(self, channel): + def get_full_result_ch(self, channel): result_data = self.simulation_result["results"][channel] return result_data - def get_day_zero_channel_value(self, channel='n_susceptible'): + def get_day_zero_ch_value(self, channel='n_susceptible'): """ Args: @@ -237,11 +237,11 @@ def get_day_zero_channel_value(self, channel='n_susceptible'): Returns: day zero value for channel """ - result_data = self.get_full_result_channel(channel=channel) + result_data = self.get_full_result_ch(channel=channel) return result_data[0] - def get_day_final_channel_value(self, channel): - channel = self.get_full_result_channel(channel=channel) + def get_day_final_ch_value(self, channel): + channel = self.get_full_result_ch(channel=channel) return channel[-1] # endregion @@ -448,8 +448,8 @@ def test_everyone_infected(self): total_agents = 500 self.set_everyone_infected(agent_count=total_agents) self.run_sim() - exposed_channel = TProps.ResKeys.exposed_at_timestep - day_0_exposed = self.get_day_zero_channel_value(exposed_channel) + exposed_ch = TProps.ResKeys.exposed_at_timestep + day_0_exposed = self.get_day_zero_ch_value(exposed_ch) self.assertEqual(day_0_exposed, total_agents) pass @@ -466,22 +466,22 @@ def test_run_small_hightransmission_sim(self): self.assertIsNotNone(self.sim) self.assertIsNotNone(self.sim_pars) - exposed_today_channel = self.get_full_result_channel( + exposed_today_ch = self.get_full_result_ch( TProps.ResKeys.exposed_at_timestep ) - prev_exposed = exposed_today_channel[0] + prev_exposed = exposed_today_ch[0] for t in range(1, 10): - today_exposed = exposed_today_channel[t] + today_exposed = exposed_today_ch[t] self.assertGreaterEqual(today_exposed, prev_exposed, msg=f"The first 10 days should have increasing" f" exposure counts. At time {t}: {today_exposed} at" f" {t-1}: {prev_exposed}.") prev_exposed = today_exposed pass - infections_channel = self.get_full_result_channel( + infections_ch = self.get_full_result_ch( TProps.ResKeys.infections_at_timestep ) - self.assertGreaterEqual(sum(infections_channel), 150, + self.assertGreaterEqual(sum(infections_ch), 150, msg="Should have at least 150 infections") pass pass From ecb1a28d734326acd852dd4f665f6faafcce497b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 00:51:01 -0700 Subject: [PATCH 371/569] fixed one test --- tests/unittests/test_mortality.py | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/tests/unittests/test_mortality.py b/tests/unittests/test_mortality.py index 50688da1c..3d570c514 100644 --- a/tests/unittests/test_mortality.py +++ b/tests/unittests/test_mortality.py @@ -9,13 +9,6 @@ class DiseaseMortalityTests(CovaTest): - def setUp(self): - super().setUp() - pass - - def tearDown(self): - super().tearDown() - pass def test_default_death_prob_one(self): """ @@ -41,22 +34,14 @@ def test_default_death_prob_zero(self): prob_dict = {'rel_death_prob': 0.0} self.set_sim_prog_prob(prob_dict) self.run_sim() - deaths_at_timestep_ch = self.get_full_result_ch( - 'new_deaths' - ) - deaths_cumulative_ch = self.get_full_result_ch( - 'cum_deaths' - ) - death_chs = [ - deaths_at_timestep_ch, - deaths_cumulative_ch - ] + deaths_at_timestep_ch = self.get_full_result_ch('new_deaths') + deaths_cumulative_ch = self.get_full_result_ch('cum_deaths') + death_chs = [deaths_at_timestep_ch,deaths_cumulative_ch] for c in death_chs: for t in range(len(c)): self.assertEqual(c[t], 0, msg=f"There should be no deaths with critical to death probability 0.0. Channel {c} had bad data at t: {t}") - - cumulative_recoveries = self.get_day_final_ch_value('cum_recovered') - self.assertGreaterEqual(cumulative_recoveries, 200, sg="Should be lots of recoveries") + cumulative_recoveries = self.get_day_final_ch_value('cum_recoveries') + self.assertGreaterEqual(cumulative_recoveries, 200, msg="Should be lots of recoveries") pass def test_default_death_prob_scaling(self): From 56bfb0b32640c221a2e3e5986b76b99041138cf0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 01:27:49 -0700 Subject: [PATCH 372/569] all but one working --- tests/unittests/test_data_loaders.py | 1 + tests/unittests/test_interventions.py | 402 ++++++-------------------- tests/unittests/test_misc.py | 13 +- tests/unittests/test_pars.py | 159 ++++------ tests/unittests/test_pops.py | 39 +-- tests/unittests/test_progression.py | 72 ++--- tests/unittests/test_transmission.py | 46 +-- tests/unittests/unittest_support.py | 260 +++++------------ 8 files changed, 261 insertions(+), 731 deletions(-) diff --git a/tests/unittests/test_data_loaders.py b/tests/unittests/test_data_loaders.py index c7621b089..a7b7fd520 100644 --- a/tests/unittests/test_data_loaders.py +++ b/tests/unittests/test_data_loaders.py @@ -19,4 +19,5 @@ def test_country_households(self): if __name__ == '__main__': + unittest.TestCase.run = lambda self,*args,**kw: unittest.TestCase.debug(self) unittest.main() diff --git a/tests/unittests/test_interventions.py b/tests/unittests/test_interventions.py index 941668531..e284ce77e 100644 --- a/tests/unittests/test_interventions.py +++ b/tests/unittests/test_interventions.py @@ -1,24 +1,12 @@ -from unittest_support import CovaTest -from unittest_support import TProps -from math import sqrt import json import numpy as np - +import covasim as cv +from unittest_support import CovaTest import unittest -AGENT_COUNT = 1000 - +AGENT_COUNT = 500 -ResultsKeys = TProps.ResKeys -SimKeys = TProps.ParKeys.SimKeys class InterventionTests(CovaTest): - def setUp(self): - super().setUp() - pass - - def tearDown(self): - super().tearDown() - pass # region change beta def test_brutal_change_beta_intervention(self): @@ -31,26 +19,16 @@ def test_brutal_change_beta_intervention(self): change_days = [day_of_change] change_multipliers = [0.0] - self.intervention_set_changebeta( - days_array=change_days, - multiplier_array=change_multipliers - ) + self.intervention_set_changebeta(days_array=change_days, multiplier_array=change_multipliers) self.run_sim() - new_infections_ch = self.get_full_result_ch( - channel=ResultsKeys.infections_at_timestep - ) + new_infections_ch = self.get_full_result_ch(channel='new_infections') five_previous_days = range(day_of_change-5, day_of_change) for d in five_previous_days: - self.assertGreater(new_infections_ch[d], - 0, - msg=f"Need to have infections before change day {day_of_change}") - pass + self.assertGreater(new_infections_ch[d], 0, msg=f"Need to have infections before change day {day_of_change}") happy_days = range(day_of_change + 1, len(new_infections_ch)) for d in happy_days: - self.assertEqual(new_infections_ch[d], - 0, - msg=f"expected 0 infections on day {d}, got {new_infections_ch[d]}.") + self.assertEqual(new_infections_ch[d], 0, msg=f"expected 0 infections on day {d}, got {new_infections_ch[d]}.") def test_change_beta_days(self): params = { @@ -61,44 +39,23 @@ def test_change_beta_days(self): # Do a 0.0 intervention / 1.0 intervention on different days days = [ 30, 32, 40, 42, 50] multipliers = [0.0, 1.0, 0.0, 1.0, 0.0] - self.intervention_set_changebeta(days_array=days, - multiplier_array=multipliers) + self.intervention_set_changebeta(days_array=days, multiplier_array=multipliers) self.run_sim() - new_infections_ch = self.get_full_result_ch( - channel=ResultsKeys.infections_at_timestep - ) + new_infections_ch = self.get_full_result_ch('new_infections') five_previous_days = range(days[0] -5, days[0]) for d in five_previous_days: - self.assertGreater(new_infections_ch[d], - 0, - msg=f"Need to have infections before first change day {days[0]}") - pass + self.assertGreater(new_infections_ch[d], 0, msg=f"Need to have infections before first change day {days[0]}") break_days = [0, 2] # index of "beta to zero" periods for b in break_days: happy_days = range(days[b], days[b + 1]) for d in happy_days: - # print(f"DEBUG: looking at happy day {d}") - self.assertEqual(new_infections_ch[d], - 0, - msg=f"expected 0 infections on day {d}, got {new_infections_ch[d]}.") + self.assertEqual(new_infections_ch[d], 0, msg=f"expected 0 infections on day {d}, got {new_infections_ch[d]}.") infection_days = range(days[b+1], days[b+2]) for d in infection_days: - # print(f"DEBUG: looking at infection day {d}") - self.assertGreater(new_infections_ch[d], - 0, - msg=f"Expected some infections on day {d}, got {new_infections_ch[d]}") - pass - pass + self.assertGreater(new_infections_ch[d], 0, msg=f"Expected some infections on day {d}, got {new_infections_ch[d]}") for d in range (days[-1] + 1, len(new_infections_ch)): - self.assertEqual(new_infections_ch[d], - 0, - msg=f"After day {days[-1]} should have no infections." - f" Got {new_infections_ch[d]} on day {d}.") - - # verify that every infection day after days[0] is in a 1.0 block - # verify no infections after 60 - pass + self.assertEqual(new_infections_ch[d], 0, msg=f"After day {days[-1]} should have no infections. Got {new_infections_ch[d]} on day {d}.") def test_change_beta_multipliers(self): params = { @@ -112,17 +69,11 @@ def test_change_beta_multipliers(self): total_infections = {} for multiplier in change_multipliers: self.interventions = None - - self.intervention_set_changebeta( - days_array=change_days, - multiplier_array=[multiplier] - ) + self.intervention_set_changebeta( days_array=change_days, multiplier_array=[multiplier]) self.run_sim(params) - these_infections = self.get_day_final_ch_value( - channel=ResultsKeys.infections_cumulative - ) + these_infections = self.get_day_final_ch_value(channel='cum_infections') total_infections[multiplier] = these_infections - pass + for result_index in range(0, len(change_multipliers) - 1): my_multiplier = change_multipliers[result_index] next_multiplier = change_multipliers[result_index + 1] @@ -133,92 +84,17 @@ def test_change_beta_multipliers(self): f"(with {total_infections[next_multiplier]} infections)") def test_change_beta_layers_clustered(self): - ''' - Suggested alternative implementation: - - import covasim as cv - - # Define the interventions - days = dict(h=30, s=35, w=40, c=45) - interventions = [] - for key,day in days.items(): - interventions.append(cv.change_beta(days=day, changes=0, layers=key)) - - # Create and run the sim - sim = cv.Sim(pop_type='hybrid', n_days=60, interventions=interventions) - sim.run() - assert sim.results['new_infections'].values[days['c']:].sum() == 0 - sim.plot() - ''' - self.is_debugging = False - initial_infected = 10 - seed_list = range(0) - for seed in seed_list: - params = { - 'rand_seed': seed, - 'pop_size': AGENT_COUNT, - 'n_days': 60, - 'pop_infected': initial_infected - } - if len(seed_list) > 1: - self.expected_result_filename = f"DEBUG_{self.id()}_{seed}.json" - self.set_sim_pars(params_dict=params) - day_of_change = 25 - change_multipliers = [0.0] - layer_keys = ['c','h','s','w'] - - intervention_days = [] - intervention_list = [] - - for k in layer_keys: # Zero out one layer at a time - day_of_change += 5 - self.intervention_set_changebeta( - days_array=[day_of_change], - multiplier_array=change_multipliers, - layers=[k] - ) - intervention_days.append(day_of_change) - intervention_list.append(self.interventions) - self.interventions = None - pass - self.interventions = intervention_list - self.run_sim(population_type='clustered') - last_intervention_day = intervention_days[-1] - first_intervention_day = intervention_days[0] - cum_infections_ch= self.get_full_result_ch(ResultsKeys.infections_cumulative) - if len(seed_list) > 1: - messages = [] - if cum_infections_ch[intervention_days[0]-1] < initial_infected: - messages.append(f"Before intervention at day {intervention_days[0]}, there should be infections happening.") - pass - - if cum_infections_ch[last_intervention_day] < cum_infections_ch[first_intervention_day]: - messages.append(f"Cumulative infections should grow with only some layers enabled.") - pass - - if cum_infections_ch[last_intervention_day] != cum_infections_ch[-1]: - messages.append(f"The cumulative infections at {last_intervention_day} should be the same as at the end.") - pass - - if len(messages) > 0: - print(f"ERROR: seed {seed}") - for m in messages: - print(f"\t{m}") - pass - - self.assertGreater(cum_infections_ch[intervention_days[0]-1], - initial_infected, - msg=f"Before intervention at day {intervention_days[0]}, there should be infections happening.") - - self.assertGreater(cum_infections_ch[last_intervention_day], - cum_infections_ch[first_intervention_day], - msg=f"Cumulative infections should grow with only some layers enabled.") - - self.assertEqual(cum_infections_ch[last_intervention_day], - cum_infections_ch[-1], - msg=f"with all layers at 0 beta, the cumulative infections at {last_intervention_day}" + - f" should be the same as at the end.") - pass + # Define the interventions + days = dict(h=30, s=35, w=40, c=45) + interventions = [] + for key,day in days.items(): + interventions.append(cv.change_beta(days=day, changes=0, layers=key)) + + # Create and run the sim + sim = cv.Sim(pop_size=AGENT_COUNT, pop_type='hybrid', n_days=60, interventions=interventions) + sim.run() + assert sim.results['new_infections'].values[days['c']:].sum() == 0 + return def test_change_beta_layers_random(self): self.is_debugging = False @@ -237,7 +113,6 @@ def test_change_beta_layers_random(self): day_of_change = 25 change_multipliers = [0.0] layer_keys = ['a'] - intervention_days = [] intervention_list = [] @@ -251,32 +126,22 @@ def test_change_beta_layers_random(self): intervention_days.append(day_of_change) intervention_list.append(self.interventions) self.interventions = None - pass self.interventions = intervention_list self.run_sim(population_type='random') last_intervention_day = intervention_days[-1] - cum_infections_ch = self.get_full_result_ch(ResultsKeys.infections_cumulative) + cum_infections_ch = self.get_full_result_ch('cum_infections') if len(seed_list) > 1: messages = [] if cum_infections_ch[intervention_days[0]-1] < initial_infected: messages.append(f"Before intervention at day {intervention_days[0]}, there should be infections happening.") - pass - if cum_infections_ch[last_intervention_day] != cum_infections_ch[-1]: messages.append(f"The cumulative infections at {last_intervention_day} should be the same as at the end.") - pass - if len(messages) > 0: print(f"ERROR: seed {seed}") for m in messages: print(f"\t{m}") - pass - self.assertGreater(cum_infections_ch[intervention_days[0]-1], - initial_infected, - msg=f"Before intervention at day {intervention_days[0]}, there should be infections happening.") - self.assertEqual(cum_infections_ch[last_intervention_day], - cum_infections_ch[intervention_days[0] - 1], - msg=f"With all layers at 0 beta, should be 0 infections at {last_intervention_day}.") + self.assertGreater(cum_infections_ch[intervention_days[0]-1], initial_infected, msg=f"Before intervention at day {intervention_days[0]}, there should be infections happening.") + self.assertEqual(cum_infections_ch[last_intervention_day], cum_infections_ch[intervention_days[0] - 1], msg=f"With all layers at 0 beta, should be 0 infections at {last_intervention_day}.") def test_change_beta_layers_hybrid(self): self.is_debugging = False @@ -314,78 +179,43 @@ def test_change_beta_layers_hybrid(self): self.run_sim(population_type='hybrid') last_intervention_day = intervention_days[-1] first_intervention_day = intervention_days[0] - cum_infections_ch = self.get_full_result_ch(ResultsKeys.infections_cumulative) + cum_infections_ch = self.get_full_result_ch('cum_infections') if len(seed_list) > 1: messages = [] if cum_infections_ch[intervention_days[0]-1] < initial_infected: messages.append(f"Before intervention at day {intervention_days[0]}, there should be infections happening.") - pass - if cum_infections_ch[last_intervention_day] < cum_infections_ch[first_intervention_day]: - messages.append(f"Cumulative infections should grow with only some layers enabled.") - pass - + messages.append("Cumulative infections should grow with only some layers enabled.") if cum_infections_ch[last_intervention_day] != cum_infections_ch[-1]: messages.append(f"The cumulative infections at {last_intervention_day} should be the same as at the end.") - pass - if len(messages) > 0: print(f"ERROR: seed {seed}") for m in messages: print(f"\t{m}") - pass - self.assertGreater(cum_infections_ch[intervention_days[0]-1], - initial_infected, - msg=f"Before intervention at day {intervention_days[0]}, there should be infections happening.") - self.assertGreater(cum_infections_ch[last_intervention_day], - cum_infections_ch[first_intervention_day], - msg=f"Cumulative infections should grow with only some layers enabled.") - self.assertEqual(cum_infections_ch[last_intervention_day], - cum_infections_ch[-1], - msg=f"With all layers at 0 beta, the cumulative infections at {last_intervention_day}" - f" should be the same as at the end.") - - def verify_perfect_test_prob(self, start_day, test_delay, test_sensitivity, - target_pop_count_ch, - target_pop_new_ch, - target_test_count_ch=None): + self.assertGreater(cum_infections_ch[intervention_days[0]-1], initial_infected, msg=f"Before intervention at day {intervention_days[0]}, there should be infections happening.") + self.assertGreater(cum_infections_ch[last_intervention_day], cum_infections_ch[first_intervention_day], msg="Cumulative infections should grow with only some layers enabled.") + self.assertEqual(cum_infections_ch[last_intervention_day], cum_infections_ch[-1], msg=f"With all layers at 0 beta, the cumulative infections at {last_intervention_day} should be the same as at the end.") + + def verify_perfect_test_prob(self, start_day, test_delay, test_sensitivity, target_pop_count_ch, target_pop_new_ch, target_test_count_ch=None): if test_sensitivity < 1.0: - raise ValueError("This test method only works with perfect test " - f"sensitivity. {test_sensitivity} won't cut it.") - new_tests = self.get_full_result_ch( - channel=ResultsKeys.tests_at_timestep - ) - new_diagnoses = self.get_full_result_ch( - channel=ResultsKeys.diagnoses_at_timestep - ) + raise ValueError("This test method only works with perfect test sensitivity. {test_sensitivity} won't cut it.") + new_tests = self.get_full_result_ch(channel='new_tests') + new_diagnoses = self.get_full_result_ch(channel='new_diagnoses') target_count = target_pop_count_ch target_new = target_pop_new_ch pre_test_days = range(0, start_day) for d in pre_test_days: - self.assertEqual(new_tests[d], - 0, - msg=f"Should be no testing before day {start_day}. Got some at {d}") - self.assertEqual(new_diagnoses[d], - 0, - msg=f"Should be no diagnoses before day {start_day}. Got some at {d}") - pass + self.assertEqual(new_tests[d], 0, msg=f"Should be no testing before day {start_day}. Got some at {d}") + self.assertEqual(new_diagnoses[d], 0, msg=f"Should be no diagnoses before day {start_day}. Got some at {d}") if self.is_debugging: print("DEBUGGING") print(f"Start day is {start_day}") print(f"new tests before, on, and after start day: {new_tests[start_day-1:start_day+2]}") print(f"new diagnoses before, on, after start day: {new_diagnoses[start_day-1:start_day+2]}") print(f"target count before, on, after start day: {target_count[start_day-1:start_day+2]}") - pass - self.assertEqual(new_tests[start_day], - target_test_count_ch[start_day], - msg=f"Should have each of the {target_test_count_ch[start_day]} targets" - f" get tested at day {start_day}. Got {new_tests[start_day]} instead.") - self.assertEqual(new_diagnoses[start_day + test_delay], - target_count[start_day], - msg=f"Should have each of the {target_count[start_day]} targets " - f"get diagnosed at day {start_day + test_delay} with sensitivity {test_sensitivity} " - f"and delay {test_delay}. Got {new_diagnoses[start_day + test_delay]} instead.") + self.assertEqual(new_tests[start_day], target_test_count_ch[start_day], msg=f"Should have each of the {target_test_count_ch[start_day]} targets get tested at day {start_day}. Got {new_tests[start_day]} instead.") + self.assertEqual(new_diagnoses[start_day + test_delay], target_count[start_day], msg=f"Should have each of the {target_count[start_day]} targets get diagnosed at day {start_day + test_delay} with sensitivity {test_sensitivity} and delay {test_delay}. Got {new_diagnoses[start_day + test_delay]} instead.") post_test_days = range(start_day + 1, len(new_tests)) if target_pop_new_ch: for d in post_test_days[:test_delay]: @@ -393,17 +223,8 @@ def verify_perfect_test_prob(self, start_day, test_delay, test_sensitivity, diag_today = new_diagnoses[d + test_delay] test_today = new_tests[d] - self.assertEqual(symp_today, - test_today, - msg=f"Should have each of the {symp_today} newly symptomatics get" - f" tested on day {d}. Got {test_today} instead.") - self.assertEqual(symp_today, - diag_today, - msg=f"Should have each of the {symp_today} newly symptomatics get" - f" diagnosed on day {d + test_delay} with sensitivity {test_sensitivity}." - f" Got {test_today} instead.") - pass - pass + self.assertEqual(symp_today, test_today, msg=f"Should have each of the {symp_today} newly symptomatics get tested on day {d}. Got {test_today} instead.") + self.assertEqual(symp_today, diag_today, msg=f"Should have each of the {symp_today} newly symptomatics get diagnosed on day {d + test_delay} with sensitivity {test_sensitivity}. Got {test_today} instead.") def test_test_prob_perfect_asymptomatic(self): @@ -431,10 +252,10 @@ def test_test_prob_perfect_asymptomatic(self): start_day=start_day) self.run_sim() symptomatic_count_ch = self.get_full_result_ch( - ResultsKeys.symptomatic_at_timestep + 'cum_symptomatic' ) infectious_count_ch = self.get_full_result_ch( - ResultsKeys.infectious_at_timestep + 'cum_infectious' ) population_ch = [agent_count] * len(symptomatic_count_ch) asymptomatic_infectious_count_ch = list(np.subtract(np.array(infectious_count_ch), @@ -467,12 +288,8 @@ def test_test_prob_perfect_symptomatic(self): test_delay=test_delay, start_day=start_day) self.run_sim() - symptomatic_count_ch = self.get_full_result_ch( - ResultsKeys.symptomatic_at_timestep - ) - symptomatic_new_ch = self.get_full_result_ch( - ResultsKeys.symptomatic_new_timestep - ) + symptomatic_count_ch = self.get_full_result_ch('cum_symptomatic') + symptomatic_new_ch = self.get_full_result_ch('new_symptomatic') self.verify_perfect_test_prob(start_day=start_day, test_delay=test_delay, test_sensitivity=test_sensitivity, @@ -480,7 +297,6 @@ def test_test_prob_perfect_symptomatic(self): target_pop_new_ch=symptomatic_new_ch, target_test_count_ch=symptomatic_count_ch ) - pass def test_test_prob_perfect_not_quarantined(self): self.is_debugging = False @@ -504,7 +320,7 @@ def test_test_prob_perfect_not_quarantined(self): start_day=start_day) self.run_sim() infectious_count_ch = self.get_full_result_ch( - ResultsKeys.infectious_at_timestep + 'cum_infectious' ) population_ch = [agent_count] * len(infectious_count_ch) @@ -514,7 +330,6 @@ def test_test_prob_perfect_not_quarantined(self): target_pop_count_ch=infectious_count_ch, target_test_count_ch=population_ch, target_pop_new_ch=None) - pass def test_test_prob_sensitivity(self, subtract_today_recoveries=False): self.is_debugging = False @@ -539,20 +354,14 @@ def test_test_prob_sensitivity(self, subtract_today_recoveries=False): test_delay=test_delay, start_day=start_day) self.run_sim() - first_day_diagnoses = self.get_full_result_ch( - channel=ResultsKeys.diagnoses_at_timestep - )[start_day] - target_count = self.get_full_result_ch( - channel=ResultsKeys.symptomatic_at_timestep - )[start_day] + first_day_diagnoses = self.get_full_result_ch(channel='new_diagnoses')[start_day] + target_count = self.get_full_result_ch('new_symptomatic')[start_day] if subtract_today_recoveries: - recoveries_today = self.get_full_result_ch( - channel=ResultsKeys.recovered_at_timestep - )[start_day] + recoveries_today = self.get_full_result_ch(channel='new_recoveries')[start_day] target_count = target_count - recoveries_today ideal_diagnoses = target_count * sensitivity - standard_deviation = sqrt(sensitivity * (1 - sensitivity) * target_count) + standard_deviation = np.sqrt(sensitivity * (1 - sensitivity) * target_count) # 99.7% confidence interval min_tolerable_diagnoses = ideal_diagnoses - 3 * standard_deviation max_tolerable_diagnoses = ideal_diagnoses + 3 * standard_deviation @@ -561,7 +370,7 @@ def test_test_prob_sensitivity(self, subtract_today_recoveries=False): print(f"\tMax: {max_tolerable_diagnoses} \n" f"\tMin: {min_tolerable_diagnoses} \n" f"\tTarget: {target_count} \n" - f"\tPrevious day Target: {self.get_full_result_ch(channel=ResultsKeys.symptomatic_at_timestep)[start_day -1 ]} \n" + f"\tPrevious day Target: {self.get_full_result_ch('new_symptomatic')[start_day -1 ]} \n" f"\tSensitivity: {sensitivity} \n" f"\tIdeal: {ideal_diagnoses} \n" f"\tActual diagnoses: {first_day_diagnoses}\n") @@ -595,13 +404,10 @@ def test_test_prob_sensitivity(self, subtract_today_recoveries=False): pass pass if len(seed_list) > 1: - with open(f"DEBUG_test_prob_sensitivity_sweep.json",'w') as outfile: + with open("DEBUG_test_prob_sensitivity_sweep.json",'w') as outfile: json.dump(error_seeds, outfile, indent=4) - pass acceptable_losses = len(seed_list) // 10 - self.assertLessEqual(len(error_seeds), - acceptable_losses, - msg=error_seeds) + self.assertLessEqual(len(error_seeds), acceptable_losses, msg=error_seeds) def test_test_prob_symptomatic_prob_of_test(self): params = { @@ -622,13 +428,13 @@ def test_test_prob_symptomatic_prob_of_test(self): start_day=start_day) self.run_sim() first_day_tests = self.get_full_result_ch( - channel=ResultsKeys.tests_at_timestep + channel='new_tests' )[start_day] target_count = self.get_full_result_ch( - channel=ResultsKeys.symptomatic_at_timestep + 'new_symptomatic' )[start_day] ideal_test_count = target_count * s_p_o_t - standard_deviation = sqrt(s_p_o_t * (1 - s_p_o_t) * target_count) + standard_deviation = np.sqrt(s_p_o_t * (1 - s_p_o_t) * target_count) # 99.7% confidence interval min_tolerable_tests = ideal_test_count - 3 * standard_deviation max_tolerable_tests = ideal_test_count + 3 * standard_deviation @@ -648,12 +454,7 @@ def test_test_prob_symptomatic_prob_of_test(self): msg=f"Expected no more than {max_tolerable_tests} tests with {target_count}" f" symptomatic and {s_p_o_t} sensitivity. Got {first_day_tests}" f" diagnoses, which is too high.") - pass - pass - # endregion - - # region contact tracing def test_brutal_contact_tracing(self): params = { 'pop_size': AGENT_COUNT, @@ -662,17 +463,12 @@ def test_brutal_contact_tracing(self): self.set_sim_pars(params_dict=params) intervention_list = [] - symptomatic_probability_of_test = 1.0 test_sensitivity = 1.0 test_delay = 0 tests_start_day = 30 trace_start_day = 40 - - self.intervention_set_test_prob(symptomatic_prob=symptomatic_probability_of_test, - test_sensitivity=test_sensitivity, - test_delay=test_delay, - start_day=tests_start_day) + self.intervention_set_test_prob(symptomatic_prob=symptomatic_probability_of_test, test_sensitivity=test_sensitivity, test_delay=test_delay, start_day=tests_start_day) intervention_list.append(self.interventions) trace_probability = 1.0 @@ -691,25 +487,18 @@ def test_brutal_contact_tracing(self): 'c': trace_delay } - self.intervention_set_contact_tracing(start_day=trace_start_day, - trace_probabilities=trace_probabilities, - trace_times=trace_delays) + self.intervention_set_contact_tracing(start_day=trace_start_day, trace_probabilities=trace_probabilities, trace_times=trace_delays) intervention_list.append(self.interventions) self.interventions = intervention_list self.run_sim(population_type='hybrid') - channel_new_quarantines = self.get_full_result_ch( - ResultsKeys.quarantined_new - ) + channel_new_quarantines = self.get_full_result_ch('new_quarantined') quarantines_before_tracing = sum(channel_new_quarantines[:trace_start_day]) quarantines_before_delay_completed = sum(channel_new_quarantines[trace_start_day:trace_start_day + trace_delay]) quarantines_after_delay = sum(channel_new_quarantines[trace_start_day+trace_delay:]) - self.assertEqual(quarantines_before_tracing, 0, - msg="There should be no quarantines until tracing begins.") - self.assertEqual(quarantines_before_delay_completed, 0, - msg="There should be no quarantines until delay expires") - self.assertGreater(quarantines_after_delay, 0, - msg="There should be quarantines after tracing begins") + self.assertEqual(quarantines_before_tracing, 0, msg="There should be no quarantines until tracing begins.") + self.assertEqual(quarantines_before_delay_completed, 0, msg="There should be no quarantines until delay expires") + self.assertGreater(quarantines_after_delay, 0, msg="There should be quarantines after tracing begins") pass @@ -729,57 +518,34 @@ def test_contact_tracing_perfect_school_layer(self): layers_to_zero_beta = ['c','h','w'] - self.intervention_set_test_prob(symptomatic_prob=1.0, - asymptomatic_prob=1.0, - test_sensitivity=1.0, - start_day=sequence_days[1]) + self.intervention_set_test_prob(symptomatic_prob=1.0, asymptomatic_prob=1.0, test_sensitivity=1.0, start_day=sequence_days[1]) sequence_interventions.append(self.interventions) - - self.intervention_set_changebeta(days_array=[sequence_days[0]], - multiplier_array=[0.0], - layers=layers_to_zero_beta) + self.intervention_set_changebeta(days_array=[sequence_days[0]], multiplier_array=[0.0], layers=layers_to_zero_beta) sequence_interventions.append(self.interventions) trace_probabilities = {'c': 0, 'h': 0, 'w': 0, 's': 1} trace_times = {'c': 0, 'h': 0, 'w': 0, 's': 0} - self.intervention_set_contact_tracing(start_day=sequence_days[1], - trace_probabilities=trace_probabilities, - trace_times=trace_times) + self.intervention_set_contact_tracing(start_day=sequence_days[1], trace_probabilities=trace_probabilities, trace_times=trace_times) sequence_interventions.append(self.interventions) self.interventions = sequence_interventions self.run_sim(population_type='hybrid') - channel_new_infections = self.get_full_result_ch( - ResultsKeys.infections_at_timestep - ) - channel_new_tests = self.get_full_result_ch( - ResultsKeys.tests_at_timestep - ) - channel_new_diagnoses = self.get_full_result_ch( - ResultsKeys.diagnoses_at_timestep - ) - channel_new_quarantine = self.get_full_result_ch( - ResultsKeys.quarantined_new - ) + channel_new_infections = self.get_full_result_ch('new_infections') + channel_new_tests = self.get_full_result_ch('new_tests') + channel_new_diagnoses = self.get_full_result_ch('new_diagnoses') + channel_new_quarantine = self.get_full_result_ch('new_quarantined') infections_before_quarantine = sum(channel_new_infections[sequence_days[0]:sequence_days[1]]) infections_after_quarantine = sum(channel_new_infections[sequence_days[1]:sequence_days[1] + 10]) if self.is_debugging: - print(f"Quarantined before, during, three days past sequence:" - f" {channel_new_quarantine[sequence_days[0] -1: sequence_days[-1] + 10]}") - print(f"Tested before, during, three days past sequence:" - f" {channel_new_tests[sequence_days[0] -1: sequence_days[-1] + 10]}") - print(f"Diagnosed before, during, three days past sequence:" - f" {channel_new_diagnoses[sequence_days[0] -1: sequence_days[-1] + 10]}") - print(f"Infections before, during, three days past sequence:" - f" {channel_new_infections[sequence_days[0] -1: sequence_days[-1] + 10]}") - print(f"10 Days after change beta but before quarantine: {infections_before_quarantine} " - f"should be less than 10 days after: {infections_after_quarantine}") - - self.assertLess(infections_after_quarantine, infections_before_quarantine, - msg=f"10 Days after change beta but before quarantine: {infections_before_quarantine} " - f"should be less than 10 days after: {infections_after_quarantine}") - - + print(f"Quarantined before, during, three days past sequence: {channel_new_quarantine[sequence_days[0] -1: sequence_days[-1] + 10]}") + print(f"Tested before, during, three days past sequence: {channel_new_tests[sequence_days[0] -1: sequence_days[-1] + 10]}") + print(f"Diagnosed before, during, three days past sequence: {channel_new_diagnoses[sequence_days[0] -1: sequence_days[-1] + 10]}") + print(f"Infections before, during, three days past sequence: {channel_new_infections[sequence_days[0] -1: sequence_days[-1] + 10]}") + print(f"10 Days after change beta but before quarantine: {infections_before_quarantine} should be less than 10 days after: {infections_after_quarantine}") + self.assertLess(infections_after_quarantine, infections_before_quarantine, msg=f"10 Days after change beta but before quarantine: {infections_before_quarantine} should be less than 10 days after: {infections_after_quarantine}") + +# Run unit tests if called as a script if __name__ == '__main__': - unittest.main() + unittest.TestCase.run = lambda self,*args,**kw: unittest.TestCase.debug(self) + unittest.main() \ No newline at end of file diff --git a/tests/unittests/test_misc.py b/tests/unittests/test_misc.py index 4b038a0ec..3cdbe27e5 100644 --- a/tests/unittests/test_misc.py +++ b/tests/unittests/test_misc.py @@ -11,7 +11,7 @@ class MiscellaneousFeatureTests(CovaTest): def setUp(self): super().setUp() - self.sim = Sim() + self.sim = Sim(pop_size=500) self.pars = parameters.make_pars() self.is_debugging = False @@ -22,9 +22,9 @@ def test_xslx_generation(self): excel_filename = f"{root_filename}.xlsx" if os.path.isfile(excel_filename): os.unlink(excel_filename) - pass test_infected_value = 31 params_dict = { + 'pop_size': 500, 'pop_infected': test_infected_value } self.run_sim(params_dict) @@ -33,7 +33,6 @@ def test_xslx_generation(self): expected_sheets = ['Results','Parameters'] for sheet in expected_sheets: self.assertIn(sheet, simulation_df.sheet_names) - pass params_df = simulation_df.parse('Parameters') observed_infected_param = params_df.loc[params_df['Parameter'] == 'pop_infected', 'Value'].values[0] self.assertEqual(observed_infected_param, test_infected_value, @@ -44,16 +43,13 @@ def test_xslx_generation(self): msg="Should be able to parse the day 0 n_exposed value from the results sheet.") if not self.is_debugging: os.unlink(excel_filename) - pass def test_set_pars_invalid_key(self): with self.assertRaises(KeyError) as context: self.sim['n_infectey'] = 10 - pass error_message = str(context.exception) self.assertIn('n_infectey', error_message) self.assertIn('pop_infected', error_message) - pass def test_update_pars_invalid_key(self): invalid_key = { @@ -61,11 +57,10 @@ def test_update_pars_invalid_key(self): } with self.assertRaises(KeyError) as context: self.sim.update_pars(invalid_key) - pass error_message = str(context.exception) self.assertIn('dooty_doo', error_message) - pass - +# Run unit tests if called as a script if __name__ == '__main__': + unittest.TestCase.run = lambda self,*args,**kw: unittest.TestCase.debug(self) unittest.main() \ No newline at end of file diff --git a/tests/unittests/test_pars.py b/tests/unittests/test_pars.py index e777415ed..b3f99f073 100644 --- a/tests/unittests/test_pars.py +++ b/tests/unittests/test_pars.py @@ -3,20 +3,10 @@ ../../covasim/README.md """ import unittest +from unittest_support import CovaTest -from unittest_support import CovaTest, TProps - -TPKeys = TProps.ParKeys.SimKeys -ResKeys = TProps.ResKeys class SimulationParameterTests(CovaTest): - def setUp(self): - super().setUp() - pass - - def tearDown(self): - super().tearDown() - pass def test_population_size(self): """ @@ -25,32 +15,31 @@ def test_population_size(self): Depends on run default simulation """ - TPKeys = TProps.ParKeys.SimKeys pop_2_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 2, - TPKeys.number_contacts: {'a': 1}, - TPKeys.initial_infected_count: 0 + 'pop_scale': 1, + 'n_days': 1, + 'pop_size': 2, + 'contacts': {'a': 1}, + 'pop_infected': 0 } pop_10_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 10, - TPKeys.number_contacts: {'a': 4}, - TPKeys.initial_infected_count: 0 + 'pop_scale': 1, + 'n_days': 1, + 'pop_size': 10, + 'contacts': {'a': 4}, + 'pop_infected': 0 } pop_123_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 123, - TPKeys.initial_infected_count: 0 + 'pop_scale': 1, + 'n_days': 1, + 'pop_size': 123, + 'pop_infected': 0 } pop_1234_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 1234, - TPKeys.initial_infected_count: 0 + 'pop_scale': 1, + 'n_days': 1, + 'pop_size': 1234, + 'pop_infected': 0 } self.run_sim(pop_2_one_day) pop_2_pop = self.get_day_zero_ch_value() @@ -61,22 +50,20 @@ def test_population_size(self): self.run_sim(pop_1234_one_day) pop_1234_pop = self.get_day_zero_ch_value() - self.assertEqual(pop_2_pop, pop_2_one_day[TPKeys.number_agents]) - self.assertEqual(pop_10_pop, pop_10_one_day[TPKeys.number_agents]) - self.assertEqual(pop_123_pop, pop_123_one_day[TPKeys.number_agents]) - self.assertEqual(pop_1234_pop, pop_1234_one_day[TPKeys.number_agents]) - - pass + self.assertEqual(pop_2_pop, pop_2_one_day['pop_size']) + self.assertEqual(pop_10_pop, pop_10_one_day['pop_size']) + self.assertEqual(pop_123_pop, pop_123_one_day['pop_size']) + self.assertEqual(pop_1234_pop, pop_1234_one_day['pop_size']) def test_population_size_ranges(self): """ Intent is to test zero, negative, and excessively large pop sizes """ pop_neg_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: -10, - TPKeys.initial_infected_count: 0 + 'pop_scale': 1, + 'n_days': 1, + 'pop_size': -10, + 'pop_infected': 0 } with self.assertRaises(ValueError) as context: self.run_sim(pop_neg_one_day) @@ -84,16 +71,14 @@ def test_population_size_ranges(self): self.assertIn("negative", error_message) pop_zero_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 100, - TPKeys.number_agents: 0, - TPKeys.initial_infected_count: 0 + 'pop_scale': 1, + 'n_days': 100, + 'pop_size': 0, + 'pop_infected': 0 } self.run_sim(pop_zero_one_day) - self.assertEqual(self.simulation_result['results'][ResKeys.susceptible_at_timestep][-1], 0) - self.assertEqual(self.simulation_result['results'][ResKeys.susceptible_at_timestep][0], 0) - - pass + self.assertEqual(self.simulation_result['results']['n_susceptible'][-1], 0) + self.assertEqual(self.simulation_result['results']['n_susceptible'][0], 0) def test_population_scaling(self): """ @@ -103,21 +88,21 @@ def test_population_scaling(self): Depends on population_size """ scale_1_one_day = { - TPKeys.number_agents: 100, - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1 + 'pop_size': 100, + 'pop_scale': 1, + 'n_days': 1 } scale_2_one_day = { - TPKeys.number_agents: 100, - TPKeys.population_scaling_factor: 2, - TPKeys.population_rescaling: False, - TPKeys.number_simulated_days: 1 + 'pop_size': 100, + 'pop_scale': 2, + 'rescale': False, + 'n_days': 1 } scale_10_one_day = { - TPKeys.number_agents: 100, - TPKeys.population_scaling_factor: 10, - TPKeys.population_rescaling: False, - TPKeys.number_simulated_days: 1 + 'pop_size': 100, + 'pop_scale': 10, + 'rescale': False, + 'n_days': 1 } self.run_sim(scale_1_one_day) scale_1_pop = self.get_day_zero_ch_value() @@ -127,8 +112,6 @@ def test_population_scaling(self): scale_10_pop = self.get_day_zero_ch_value() self.assertEqual(scale_2_pop, 2 * scale_1_pop) self.assertEqual(scale_10_pop, 10 * scale_1_pop) - pass - def test_random_seed(self): """ @@ -139,47 +122,23 @@ def test_random_seed(self): different in the third """ self.set_smallpop_hightransmission() - seed_1_params = { - TPKeys.random_seed: 1 - } - seed_2_params = { - TPKeys.random_seed: 2 - } + seed_1_params = {'rand_seed': 1} + seed_2_params = {'rand_seed': 2} self.run_sim(seed_1_params) - infectious_seed_1_v1 = self.get_full_result_ch( - ResKeys.infectious_at_timestep - ) - exposures_seed_1_v1 = self.get_full_result_ch( - ResKeys.exposed_at_timestep - ) + infectious_seed_1_v1 = self.get_full_result_ch('new_infectious') + exposures_seed_1_v1 = self.get_full_result_ch('new_infections') self.run_sim(seed_1_params) - infectious_seed_1_v2 = self.get_full_result_ch( - ResKeys.infectious_at_timestep - ) - exposures_seed_1_v2 = self.get_full_result_ch( - ResKeys.exposed_at_timestep - ) - self.assertEqual(infectious_seed_1_v1, infectious_seed_1_v2, - msg=f"With random seed the same, these channels should" - f"be identical.") - self.assertEqual(exposures_seed_1_v1, exposures_seed_1_v2, - msg=f"With random seed the same, these channels should" - f"be identical.") + infectious_seed_1_v2 = self.get_full_result_ch('new_infectious') + exposures_seed_1_v2 = self.get_full_result_ch('new_infections') + self.assertEqual(infectious_seed_1_v1, infectious_seed_1_v2, msg="With random seed the same, these channels should be identical.") + self.assertEqual(exposures_seed_1_v1, exposures_seed_1_v2, msg="With random seed the same, these channels should be identical.") self.run_sim(seed_2_params) - infectious_seed_2 = self.get_full_result_ch( - ResKeys.infectious_at_timestep - ) - exposures_seed_2 = self.get_full_result_ch( - ResKeys.exposed_at_timestep - ) - self.assertNotEqual(infectious_seed_1_v1, infectious_seed_2, - msg=f"With random seed the different, these channels should" - f"be distinct.") - self.assertNotEqual(exposures_seed_1_v1, exposures_seed_2, - msg=f"With random seed the different, these channels should" - f"be distinct.") - pass - + infectious_seed_2 = self.get_full_result_ch('new_infectious') + exposures_seed_2 = self.get_full_result_ch('new_infections') + self.assertNotEqual(infectious_seed_1_v1, infectious_seed_2, msg="With random seed the different, these channels should be distinct.") + self.assertNotEqual(exposures_seed_1_v1, exposures_seed_2, msg="With random seed the different, these channels should be distinct.") +# Run unit tests if called as a script if __name__ == '__main__': - unittest.main() + unittest.TestCase.run = lambda self,*args,**kw: unittest.TestCase.debug(self) + unittest.main() \ No newline at end of file diff --git a/tests/unittests/test_pops.py b/tests/unittests/test_pops.py index 7145d9813..efad21257 100644 --- a/tests/unittests/test_pops.py +++ b/tests/unittests/test_pops.py @@ -1,24 +1,15 @@ -from unittest_support import CovaTest, TProps - -TPKeys = TProps.ParKeys.SimKeys - +from unittest_support import CovaTest +import unittest class PopulationTypeTests(CovaTest): - def setUp(self): - super().setUp() - pass - - def tearDown(self): - super().tearDown() - pass def test_different_pop_types(self): pop_types = ['random', 'hybrid'] #, 'synthpops'] results = {} short_sample = { - TPKeys.number_agents: 1000, - TPKeys.number_simulated_days: 10, - TPKeys.initial_infected_count: 50 + 'pop_size': 1000, + 'n_days': 10, + 'pop_infected': 50 } for poptype in pop_types: self.run_sim(short_sample, population_type=poptype) @@ -28,15 +19,13 @@ def test_different_pop_types(self): for k in results: these_results = results[k] self.assertIsNotNone(these_results) - day_0_susceptible = these_results[TProps.ResKeys.susceptible_at_timestep][0] - day_0_exposed = these_results[TProps.ResKeys.exposed_at_timestep][0] - - self.assertEqual(day_0_susceptible + day_0_exposed, short_sample[TPKeys.number_agents], - msg=f"Day 0 population should be as specified in params. Poptype {k} was different.") - self.assertGreater(these_results['cum_infections'][-1], - these_results['cum_infections'][0], - msg=f"Should see infections increase. Pop type {k} didn't do that.") - self.assertGreater(these_results['cum_symptomatic'][-1], - these_results['cum_symptomatic'][0], - msg=f"Should see symptomatic counts increase. Pop type {k} didn't do that.") + day_0_susceptible = these_results['n_susceptible'][0] + day_0_exposed = these_results['cum_infections'][0] + self.assertEqual(day_0_susceptible + day_0_exposed, short_sample['pop_size'], msg=f"Day 0 population should be as specified in params. Poptype {k} was different.") + self.assertGreater(these_results['cum_infections'][-1], these_results['cum_infections'][0], msg=f"Should see infections increase. Pop type {k} didn't do that.") + self.assertGreater(these_results['cum_symptomatic'][-1], these_results['cum_symptomatic'][0], msg=f"Should see symptomatic counts increase. Pop type {k} didn't do that.") +# Run unit tests if called as a script +if __name__ == '__main__': + unittest.TestCase.run = lambda self,*args,**kw: unittest.TestCase.debug(self) + unittest.main() \ No newline at end of file diff --git a/tests/unittests/test_progression.py b/tests/unittests/test_progression.py index 9fbbfaf71..691ae0db0 100644 --- a/tests/unittests/test_progression.py +++ b/tests/unittests/test_progression.py @@ -3,12 +3,7 @@ ../../covasim/README.md """ import unittest - -from unittest_support import CovaTest, TProps - -ResKeys = TProps.ResKeys -ParamKeys = TProps.ParKeys - +from unittest_support import CovaTest class DiseaseProgressionTests(CovaTest): def setUp(self): @@ -33,21 +28,15 @@ def test_exposure_to_infectiousness_delay_scaling(self): std_dev = 0 for exposed_delay in exposed_delays: self.set_duration_distribution_parameters( - duration_in_question=ParamKeys.ProgKeys.DurKeys.exposed_to_infectious, + duration_in_question='exp2inf', par1=exposed_delay, par2=std_dev ) - prob_dict = { - 'rel_symp_prob': 0 - } + prob_dict = {'rel_symp_prob': 0} self.set_sim_prog_prob(prob_dict) - serial_delay = { - 'n_days': sim_dur - } + serial_delay = {'n_days': sim_dur} self.run_sim(serial_delay) - infectious_ch = self.get_full_result_ch( - ResKeys.infectious_at_timestep - ) + infectious_ch = self.get_full_result_ch('new_infectious') agents_on_infectious_day = infectious_ch[exposed_delay] if self.is_debugging: print(f"Delay: {exposed_delay}") @@ -57,13 +46,9 @@ def test_exposure_to_infectiousness_delay_scaling(self): for t in range(len(infectious_ch)): current_infectious = infectious_ch[t] if t < exposed_delay: - self.assertEqual(current_infectious, 0, - msg=f"All {total_agents} should turn infectious at t: {exposed_delay}" - f" instead got {current_infectious} at t: {t}") + self.assertEqual(current_infectious, 0, msg=f"All {total_agents} should turn infectious at t: {exposed_delay} instead got {current_infectious} at t: {t}") elif t == exposed_delay: - self.assertEqual(infectious_ch[exposed_delay], total_agents, - msg=f"With stddev 0, all {total_agents} agents should turn infectious " - f"on day {exposed_delay}, instead got {agents_on_infectious_day}. ") + self.assertEqual(infectious_ch[exposed_delay], total_agents, msg=f"With stddev 0, all {total_agents} agents should turn infectious on day {exposed_delay}, instead got {agents_on_infectious_day}. ") pass def test_mild_infection_duration_scaling(self): @@ -73,42 +58,32 @@ def test_mild_infection_duration_scaling(self): """ total_agents = 500 exposed_delay = 1 - self.set_everyone_infectious_same_day(num_agents=total_agents, - days_to_infectious=exposed_delay) - prob_dict = { - ParamKeys.'rel_symp_prob': 0.0 - } + self.set_everyone_infectious_same_day(num_agents=total_agents, days_to_infectious=exposed_delay) + prob_dict = {'rel_symp_prob': 0.0} self.set_sim_prog_prob(prob_dict) infectious_durations = [1, 2, 5, 10, 20] # Keep values in order - infectious_duration_stddev = 0 for TEST_dur in infectious_durations: recovery_day = exposed_delay + TEST_dur self.set_duration_distribution_parameters( - duration_in_question=ParamKeys.ProgKeys.DurKeys.infectious_asymptomatic_to_recovered, + duration_in_question='asym2rec', par1=TEST_dur, - par2=infectious_duration_stddev + par2=0 ) self.run_sim() - recoveries_ch = self.get_full_result_ch( - TProps.ResKeys.recovered_at_timestep - ) + recoveries_ch = self.get_full_result_ch('new_recoveries') recoveries_on_recovery_day = recoveries_ch[recovery_day] if self.is_debugging: print(f"Delay: {recovery_day}") print(f"Agents turned: {recoveries_on_recovery_day}") print(f"Recoveries channel {recoveries_ch}") - self.assertEqual(recoveries_ch[recovery_day], total_agents, - msg=f"With stddev 0, all {total_agents} agents should turn infectious " - f"on day {recovery_day}, instead got {recoveries_on_recovery_day}. ") + self.assertEqual(recoveries_ch[recovery_day], total_agents, msg=f"With stddev 0, all {total_agents} agents should turn infectious on day {recovery_day}, instead got {recoveries_on_recovery_day}. ") pass def test_time_to_die_duration_scaling(self): total_agents = 500 self.set_everyone_critical(num_agents=500, constant_delay=0) - prob_dict = { - ParamKeys.'rel_death_prob': 1.0 - } + prob_dict = {'rel_death_prob': 1.0} self.set_sim_prog_prob(prob_dict) time_to_die_durations = [1, 2, 5, 10, 20] @@ -116,29 +91,22 @@ def test_time_to_die_duration_scaling(self): for TEST_dur in time_to_die_durations: self.set_duration_distribution_parameters( - duration_in_question=ParamKeys.ProgKeys.DurKeys.critical_to_death, + duration_in_question='crit2die', par1=TEST_dur, par2=time_to_die_stddev ) self.run_sim() - deaths_today_ch = self.get_full_result_ch( - TProps.'new_deaths' - ) + deaths_today_ch = self.get_full_result_ch('new_deaths') for t in range(len(deaths_today_ch)): curr_deaths = deaths_today_ch[t] if t < TEST_dur: - self.assertEqual(curr_deaths, 0, - msg=f"With std 0, all {total_agents} agents should die on " - f"t: {TEST_dur}. Got {curr_deaths} at t: {t}") + self.assertEqual(curr_deaths, 0, msg=f"With std 0, all {total_agents} agents should die on t: {TEST_dur}. Got {curr_deaths} at t: {t}") elif t == TEST_dur: - self.assertEqual(curr_deaths, total_agents, - msg=f"With std 0, all {total_agents} agents should die at t:" - f" {TEST_dur}, got {curr_deaths} instead.") + self.assertEqual(curr_deaths, total_agents, msg=f"With std 0, all {total_agents} agents should die at t: {TEST_dur}, got {curr_deaths} instead.") else: - self.assertEqual(curr_deaths, 0, - msg=f"With std 0, all {total_agents} agents should die at t:" - f" {TEST_dur}, got {curr_deaths} at t: {t}") + self.assertEqual(curr_deaths, 0, msg=f"With std 0, all {total_agents} agents should die at t: {TEST_dur}, got {curr_deaths} at t: {t}") pass if __name__ == '__main__': + unittest.TestCase.run = lambda self,*args,**kw: unittest.TestCase.debug(self) unittest.main() \ No newline at end of file diff --git a/tests/unittests/test_transmission.py b/tests/unittests/test_transmission.py index a524d837d..489f72b11 100644 --- a/tests/unittests/test_transmission.py +++ b/tests/unittests/test_transmission.py @@ -3,10 +3,9 @@ ../../covasim/README.md """ -from unittest_support import CovaTest, TProps - -TKeys = TProps.ParKeys.TransKeys -Hightrans = TProps.SpecialSims.Hightransmission +import unittest +from unittest_support import CovaTest, SpecialSims +Hightrans = SpecialSims.Hightransmission class DiseaseTransmissionTests(CovaTest): """ @@ -14,45 +13,28 @@ class DiseaseTransmissionTests(CovaTest): pre requisites simulation parameter tests """ - def setUp(self): - super().setUp() - pass - - def tearDown(self): - super().tearDown() - pass - def test_beta_zero(self): """ Test that with beta at zero, no transmission Start with high transmission sim """ self.set_smallpop_hightransmission() - beta_zero = { - 'beta': 0 - } + beta_zero = {'beta': 0} self.run_sim(beta_zero) - exposed_today_ch = self.get_full_result_ch( - TProps.ResKeys.exposed_at_timestep - ) + exposed_today_ch = self.get_full_result_ch('cum_infections') prev_exposed = exposed_today_ch[0] - self.assertEqual(prev_exposed, Hightrans.pop_infected, - msg="Make sure we have some initial infections") + self.assertEqual(prev_exposed, Hightrans.pop_infected, msg="Make sure we have some initial infections") for t in range(1, len(exposed_today_ch)): today_exposed = exposed_today_ch[t] - self.assertLessEqual(today_exposed, prev_exposed, - msg=f"The exposure counts should do nothing but decline." - f" At time {t}: {today_exposed} at {t-1}: {prev_exposed}.") + self.assertLessEqual(today_exposed, prev_exposed, msg=f"The exposure counts should do nothing but decline. At time {t}: {today_exposed} at {t-1}: {prev_exposed}.") prev_exposed = today_exposed - pass - infections_ch = self.get_full_result_ch( - TProps.ResKeys.infections_at_timestep - ) + infections_ch = self.get_full_result_ch('new_infections') for t in range(len(infections_ch)): today_infectious = infections_ch[t] - self.assertEqual(today_infectious, 0, - msg=f"With beta 0, there should be no infections." - f" At ts: {t} got {today_infectious}.") - pass - pass \ No newline at end of file + self.assertEqual(today_infectious, 0, msg=f"With beta 0, there should be no infections. At ts: {t} got {today_infectious}.") + +# Run unit tests if called as a script +if __name__ == '__main__': + unittest.TestCase.run = lambda self,*args,**kw: unittest.TestCase.debug(self) + unittest.main() \ No newline at end of file diff --git a/tests/unittests/unittest_support.py b/tests/unittests/unittest_support.py index 2e0cfd0ee..3e93c03a2 100644 --- a/tests/unittests/unittest_support.py +++ b/tests/unittests/unittest_support.py @@ -14,100 +14,27 @@ import covasim as cv -class TProps: -# class ParKeys: - -# class ProgKeys: -# durations = "dur" -# param_1 = "par1" -# param_2 = "par2" - -# class DurKeys: -# exposed_to_infectious = 'exp2inf' -# infectious_to_symptomatic = 'inf2sym' -# infectious_asymptomatic_to_recovered = 'asym2rec' -# infectious_symptomatic_to_recovered = 'mild2rec' -# symptomatic_to_severe = 'sym2sev' -# severe_to_critical = 'sev2crit' -# aymptomatic_to_recovered = 'asym2rec' -# severe_to_recovered = 'sev2rec' -# critical_to_recovered = 'crit2rec' -# critical_to_death = 'crit2die' -# pass - -# class ProbKeys: -# progression_by_age = 'prog_by_age' -# class RelProbKeys: -# inf_to_symptomatic_probability = 'rel_symp_prob' -# sym_to_severe_probability = 'rel_severe_prob' -# sev_to_critical_probability = 'rel_crit_prob' -# crt_to_death_probability = 'rel_death_prob' -# pass -# class PrognosesListKeys: -# symptomatic_probabilities = 'symp_probs' -# severe_probabilities = 'severe_probs' -# critical_probabilities = 'crit_probs' -# death_probs = 'death_probs' -# pass - -# class DiagnosticTestingKeys: -# number_daily_tests = 'daily_tests' -# daily_test_sensitivity = 'sensitivity' -# symptomatic_testing_multiplier = 'sympt_test' -# contacttrace_testing_multiplier = 'trace_test' -# pass -# pass - - class SpecialSims: - class Microsim: - n = 10 - pop_infected = 1 - contacts = 2 - n_days = 10 - pass - class Hightransmission: - n = 500 - pop_infected = 10 - n_days = 30 - contacts = 3 - beta = 0.4 - serial = 2 - # serial_std = 0.5 - dur = 3 - pass - class HighMortality: - n = 1000 - cfr_by_age = False - default_cfr = 0.2 - timetodie = 6 - # timetodie_std = 2 - pass - - # class ResKeys: - # deaths_cumulative = 'cum_deaths' - # deaths_daily = 'new_deaths' - # diagnoses_cumulative = 'cum_diagnoses' - # diagnoses_at_timestep = 'new_diagnoses' - # exposed_at_timestep = 'n_exposed' - # susceptible_at_timestep = 'n_susceptible' - # infectious_at_timestep = 'n_infectious' - # symptomatic_at_timestep = 'n_symptomatic' - # symptomatic_cumulative = 'cum_symptomatic' - # symptomatic_new_timestep = 'new_symptomatic' - # recovered_at_timestep = 'new_recoveries' - # recovered_cumulative = 'cum_recoveries' - # infections_at_timestep = 'new_infections' - # infections_cumulative = 'cum_infections' - # tests_at_timestep = 'new_tests' - # tests_cumulative = 'cum_tests' - # quarantined_new = 'new_quarantined' - # GUESS_doubling_time_at_timestep = 'doubling_time' - # GUESS_r_effective_at_timestep = 'r_eff' - - pass - - -# DurKeys = TProps.ParKeys.ProgKeys.DurKeys +class SpecialSims: + class Microsim: + n = 10 + pop_infected = 1 + contacts = 2 + n_days = 10 + + class Hightransmission: + n = 500 + pop_infected = 10 + n_days = 30 + contacts = 3 + beta = 0.4 + serial = 2 + dur = 3 + + class HighMortality: + n = 1000 + cfr_by_age = False + default_cfr = 0.2 + timetodie = 6 class CovaTest(unittest.TestCase): @@ -122,13 +49,13 @@ def setUp(self): self.expected_result_filename = f"DEBUG_{self.id()}.json" if os.path.isfile(self.expected_result_filename): os.unlink(self.expected_result_filename) - pass + def tearDown(self): if not self.is_debugging: if os.path.isfile(self.expected_result_filename): os.unlink(self.expected_result_filename) - pass + # region configuration methods def set_sim_pars(self, params_dict=None): @@ -146,7 +73,7 @@ def set_sim_pars(self, params_dict=None): self.sim_pars = cv.make_pars(set_prognoses=True, prog_by_age=True) if params_dict: self.sim_pars.update(params_dict) - pass + def set_sim_prog_prob(self, params_dict): """ @@ -161,7 +88,7 @@ def set_sim_prog_prob(self, params_dict): ] if not self.sim_pars: self.set_sim_pars() - pass + if not self.sim_progs: self.sim_progs = cv.get_prognoses(self.sim_pars['prog_by_age']) @@ -177,40 +104,30 @@ def set_sim_prog_prob(self, params_dict): raise KeyError(f"Key {k} not found in {supported_probabilities}.") old_probs = self.sim_progs[prognosis_in_question] self.sim_progs[prognosis_in_question] = np.array([expected_prob] * len(old_probs)) - pass - pass + + def set_duration_distribution_parameters(self, duration_in_question, par1, par2): if not self.sim_pars: self.set_sim_pars() - pass + duration_node = self.sim_pars["dur"] duration_node[duration_in_question] = { "dist": "normal", "par1": par1, "par2": par2 } - params_dict = { - "dur": duration_node - } + params_dict = {"dur": duration_node} self.set_sim_pars(params_dict=params_dict) - def run_sim(self, params_dict=None, write_results_json=False, population_type=None): if not self.sim_pars or params_dict: # If we need one, or have one here self.set_sim_pars(params_dict=params_dict) - pass - self.sim_pars['interventions'] = self.interventions - - self.sim = cv.Sim(pars=self.sim_pars, - datafile=None) + self.sim = cv.Sim(pars=self.sim_pars, datafile=None) if not self.sim_progs: - self.sim_progs = cv.get_prognoses( - self.sim_pars[TProps.ParKeys.ProgKeys.ProbKeys.progression_by_age] - ) - pass + self.sim_progs = cv.get_prognoses(self.sim_pars['prog_by_age']) self.sim['prognoses'] = self.sim_progs if population_type: @@ -220,10 +137,8 @@ def run_sim(self, params_dict=None, write_results_json=False, population_type=No if write_results_json or self.is_debugging: with open(self.expected_result_filename, 'w') as outfile: json.dump(self.simulation_result, outfile, indent=4, sort_keys=True) - pass - # endregion - # region simulation results support + def get_full_result_ch(self, channel): result_data = self.simulation_result["results"][channel] return result_data @@ -243,66 +158,40 @@ def get_day_zero_ch_value(self, channel='n_susceptible'): def get_day_final_ch_value(self, channel): channel = self.get_full_result_ch(channel=channel) return channel[-1] - # endregion - # region interventions support - def intervention_set_changebeta(self, - days_array, - multiplier_array, - layers = None): - self.interventions = cv.change_beta(days=days_array, - changes=multiplier_array, - layers=layers) - pass - - def intervention_set_test_prob(self, symptomatic_prob=0, asymptomatic_prob=0, - asymptomatic_quarantine_prob=0, symp_quar_prob=0, - test_sensitivity=1.0, loss_prob=0.0, test_delay=1, - start_day=0): - self.interventions = cv.test_prob(symp_prob=symptomatic_prob, - asymp_prob=asymptomatic_prob, - asymp_quar_prob=asymptomatic_quarantine_prob, - symp_quar_prob=symp_quar_prob, - sensitivity=test_sensitivity, - loss_prob=loss_prob, - test_delay=test_delay, - start_day=start_day) - pass - - def intervention_set_contact_tracing(self, - start_day, - trace_probabilities=None, - trace_times=None): + def intervention_set_changebeta(self, days_array, multiplier_array, layers = None): + self.interventions = cv.change_beta(days=days_array, changes=multiplier_array, layers=layers) + + + def intervention_set_test_prob(self, symptomatic_prob=0, asymptomatic_prob=0, asymptomatic_quarantine_prob=0, symp_quar_prob=0, test_sensitivity=1.0, loss_prob=0.0, test_delay=1, start_day=0): + self.interventions = cv.test_prob(symp_prob=symptomatic_prob, asymp_prob=asymptomatic_prob, asymp_quar_prob=asymptomatic_quarantine_prob, symp_quar_prob=symp_quar_prob, sensitivity=test_sensitivity, loss_prob=loss_prob, test_delay=test_delay, start_day=start_day) + + + def intervention_set_contact_tracing(self, start_day, trace_probabilities=None, trace_times=None): if not trace_probabilities: trace_probabilities = {'h': 1, 's': 1, 'w': 1, 'c': 1} - pass + if not trace_times: trace_times = {'h': 1, 's': 1, 'w': 1, 'c': 1} - self.interventions = cv.contact_tracing(trace_probs=trace_probabilities, - trace_time=trace_times, - start_day=start_day) - pass - - def intervention_build_sequence(self, - day_list, - intervention_list): - my_sequence = cv.sequence(days=day_list, - interventions=intervention_list) + self.interventions = cv.contact_tracing(trace_probs=trace_probabilities, trace_time=trace_times, start_day=start_day) + + + def intervention_build_sequence(self, day_list, intervention_list): + my_sequence = cv.sequence(days=day_list, interventions=intervention_list) self.interventions = my_sequence # endregion # region specialized simulation methods def set_microsim(self): - Simkeys = TProps.ParKeys.SimKeys - Micro = TProps.SpecialSims.Microsim + Micro = SpecialSims.Microsim microsim_parameters = { 'pop_size' : Micro.n, 'pop_infected': Micro.pop_infected, - Simkeys.number_simulated_days: Micro.n_days + 'n_days': Micro.n_days } self.set_sim_pars(microsim_parameters) - pass + def set_everyone_infected(self, agent_count=1000): everyone_infected = { @@ -310,7 +199,7 @@ def set_everyone_infected(self, agent_count=1000): 'pop_infected': agent_count } self.set_sim_pars(params_dict=everyone_infected) - pass + def set_everyone_infectious_same_day(self, num_agents, days_to_infectious=1, num_days=60): """ @@ -333,7 +222,7 @@ def set_everyone_infectious_same_day(self, num_agents, days_to_infectious=1, num par2=0 ) self.set_sim_pars(params_dict=test_config) - pass + def set_everyone_symptomatic(self, num_agents, constant_delay:int=None): """ @@ -355,7 +244,6 @@ def set_everyone_symptomatic(self, num_agents, constant_delay:int=None): par1=constant_delay, par2=0 ) - pass def everyone_dies(self, num_agents): """ @@ -371,7 +259,6 @@ def everyone_dies(self, num_agents): 'rel_death_prob': 1 } self.set_sim_prog_prob(prob_dict) - pass def set_everyone_severe(self, num_agents, constant_delay:int=None): self.set_everyone_symptomatic(num_agents=num_agents, constant_delay=constant_delay) @@ -386,7 +273,6 @@ def set_everyone_severe(self, num_agents, constant_delay:int=None): par1=constant_delay, par2=0 ) - pass def set_everyone_critical(self, num_agents, constant_delay:int=None): """ @@ -404,28 +290,20 @@ def set_everyone_critical(self, num_agents, constant_delay:int=None): par1=constant_delay, par2=0 ) - pass def set_smallpop_hightransmission(self): """ Creates a small population with lots of transmission """ - Simkeys = TProps.ParKeys.SimKeys - Transkeys = TProps.ParKeys.TransKeys - Hightrans = TProps.SpecialSims.Hightransmission + Hightrans = SpecialSims.Hightransmission hightrans_parameters = { 'pop_size' : Hightrans.n, 'pop_infected': Hightrans.pop_infected, - Simkeys.number_simulated_days: Hightrans.n_days, - Transkeys.beta : Hightrans.beta + 'n_days': Hightrans.n_days, + 'beta' : Hightrans.beta } self.set_sim_pars(hightrans_parameters) - pass - - # endregion - pass - class TestSupportTests(CovaTest): def test_run_vanilla_simulation(self): @@ -438,7 +316,7 @@ def test_run_vanilla_simulation(self): self.run_sim(write_results_json=True) json_file_found = os.path.isfile(self.expected_result_filename) self.assertTrue(json_file_found, msg=f"Expected {self.expected_result_filename} to be found.") - pass + def test_everyone_infected(self): """ @@ -448,10 +326,10 @@ def test_everyone_infected(self): total_agents = 500 self.set_everyone_infected(agent_count=total_agents) self.run_sim() - exposed_ch = TProps.ResKeys.exposed_at_timestep + exposed_ch = 'cum_infections' day_0_exposed = self.get_day_zero_ch_value(exposed_ch) self.assertEqual(day_0_exposed, total_agents) - pass + def test_run_small_hightransmission_sim(self): """ @@ -466,25 +344,17 @@ def test_run_small_hightransmission_sim(self): self.assertIsNotNone(self.sim) self.assertIsNotNone(self.sim_pars) - exposed_today_ch = self.get_full_result_ch( - TProps.ResKeys.exposed_at_timestep - ) + exposed_today_ch = self.get_full_result_ch('cum_infections') prev_exposed = exposed_today_ch[0] for t in range(1, 10): today_exposed = exposed_today_ch[t] - self.assertGreaterEqual(today_exposed, prev_exposed, - msg=f"The first 10 days should have increasing" - f" exposure counts. At time {t}: {today_exposed} at" - f" {t-1}: {prev_exposed}.") + self.assertGreaterEqual(today_exposed, prev_exposed, msg=f"The first 10 days should have increasing exposure counts. At time {t}: {today_exposed} at {t-1}: {prev_exposed}.") prev_exposed = today_exposed - pass - infections_ch = self.get_full_result_ch( - TProps.ResKeys.infections_at_timestep - ) - self.assertGreaterEqual(sum(infections_ch), 150, - msg="Should have at least 150 infections") - pass - pass + + infections_ch = self.get_full_result_ch('new_infections') + self.assertGreaterEqual(sum(infections_ch), 150, msg="Should have at least 150 infections") + + From fef4ae651fac4357d662a71a0238fb5020dc242d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 01:37:23 -0700 Subject: [PATCH 373/569] all tests pass --- tests/unittests/test_interventions.py | 32 +++++++++------------------ 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/tests/unittests/test_interventions.py b/tests/unittests/test_interventions.py index e284ce77e..71f3c43c1 100644 --- a/tests/unittests/test_interventions.py +++ b/tests/unittests/test_interventions.py @@ -4,7 +4,7 @@ from unittest_support import CovaTest import unittest -AGENT_COUNT = 500 +AGENT_COUNT = 1000 class InterventionTests(CovaTest): @@ -207,7 +207,7 @@ def verify_perfect_test_prob(self, start_day, test_delay, test_sensitivity, targ for d in pre_test_days: self.assertEqual(new_tests[d], 0, msg=f"Should be no testing before day {start_day}. Got some at {d}") self.assertEqual(new_diagnoses[d], 0, msg=f"Should be no diagnoses before day {start_day}. Got some at {d}") - if self.is_debugging: + if 1:#self.is_debugging: print("DEBUGGING") print(f"Start day is {start_day}") print(f"new tests before, on, and after start day: {new_tests[start_day-1:start_day+2]}") @@ -251,17 +251,11 @@ def test_test_prob_perfect_asymptomatic(self): test_delay=test_delay, start_day=start_day) self.run_sim() - symptomatic_count_ch = self.get_full_result_ch( - 'cum_symptomatic' - ) - infectious_count_ch = self.get_full_result_ch( - 'cum_infectious' - ) + symptomatic_count_ch = self.get_full_result_ch('n_symptomatic') + infectious_count_ch = self.get_full_result_ch('n_infectious') population_ch = [agent_count] * len(symptomatic_count_ch) - asymptomatic_infectious_count_ch = list(np.subtract(np.array(infectious_count_ch), - np.array(symptomatic_count_ch))) - asymptomatic_population_count_ch = list(np.subtract(np.array(population_ch), - np.array(symptomatic_count_ch))) + asymptomatic_infectious_count_ch = list(np.subtract(np.array(infectious_count_ch), np.array(symptomatic_count_ch))) + asymptomatic_population_count_ch = list(np.subtract(np.array(population_ch), np.array(symptomatic_count_ch))) self.verify_perfect_test_prob(start_day=start_day, test_delay=test_delay, @@ -288,7 +282,7 @@ def test_test_prob_perfect_symptomatic(self): test_delay=test_delay, start_day=start_day) self.run_sim() - symptomatic_count_ch = self.get_full_result_ch('cum_symptomatic') + symptomatic_count_ch = self.get_full_result_ch('n_symptomatic') symptomatic_new_ch = self.get_full_result_ch('new_symptomatic') self.verify_perfect_test_prob(start_day=start_day, test_delay=test_delay, @@ -319,9 +313,7 @@ def test_test_prob_perfect_not_quarantined(self): test_delay=test_delay, start_day=start_day) self.run_sim() - infectious_count_ch = self.get_full_result_ch( - 'cum_infectious' - ) + infectious_count_ch = self.get_full_result_ch('n_infectious') population_ch = [agent_count] * len(infectious_count_ch) self.verify_perfect_test_prob(start_day=start_day, @@ -427,12 +419,8 @@ def test_test_prob_symptomatic_prob_of_test(self): test_delay=test_delay, start_day=start_day) self.run_sim() - first_day_tests = self.get_full_result_ch( - channel='new_tests' - )[start_day] - target_count = self.get_full_result_ch( - 'new_symptomatic' - )[start_day] + first_day_tests = self.get_full_result_ch(channel='new_tests')[start_day] + target_count = self.get_full_result_ch('n_symptomatic')[start_day] ideal_test_count = target_count * s_p_o_t standard_deviation = np.sqrt(s_p_o_t * (1 - s_p_o_t) * target_count) # 99.7% confidence interval From 9a26114834567ff7e41a5d32b3e1598c53928e63 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 01:42:46 -0700 Subject: [PATCH 374/569] tests pass --- covasim/run.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index 297ce2171..cadb3441e 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -954,8 +954,7 @@ def print_heading(string): scen_sim = sc.dcp(self.base_sim) scen_sim.label = scenkey - defaults = {par: scen_sim[par] for par in cvd.strain_pars} - scen_sim.update_pars(scenpars, defaults=defaults, **kwargs) # Update the parameters, if provided + scen_sim.update_pars(scenpars, **kwargs) # Update the parameters, if provided if 'strains' in scenpars: # Process strains scen_sim.init_strains() scen_sim.init_immunity(create=True) @@ -1337,22 +1336,22 @@ def single_run(sim, ind=0, reseed=True, noise=0.0, noisepar=None, keep_people=Fa sim.set_seed() # If the noise parameter is not found, guess what it should be - # if noisepar is None: - # noisepar = 'beta' - # if noisepar not in sim.pars.keys(): - # raise sc.KeyNotFoundError(f'Noise parameter {noisepar} was not found in sim parameters') + if noisepar is None: + noisepar = 'beta' + if noisepar not in sim.pars.keys(): + raise sc.KeyNotFoundError(f'Noise parameter {noisepar} was not found in sim parameters') # Handle noise -- normally distributed fractional error - # noiseval = noise*np.random.normal() - # if noiseval > 0: - # noisefactor = 1 + noiseval - # else: - # noisefactor = 1/(1-noiseval) - # sim[noisepar] *= noisefactor - - # if verbose>=1: - # verb = 'Running' if do_run else 'Creating' - # print(f'{verb} a simulation using seed={sim["rand_seed"]} and noise={noiseval}') + noiseval = noise*np.random.normal() + if noiseval > 0: + noisefactor = 1 + noiseval + else: + noisefactor = 1/(1-noiseval) + sim[noisepar] *= noisefactor + + if verbose>=1: + verb = 'Running' if do_run else 'Creating' + print(f'{verb} a simulation using seed={sim["rand_seed"]} and noise={noiseval}') # Handle additional arguments for key,val in sim_args.items(): From 6f328fc5b6e1a9b5b93cdc9771f14af29504d3a7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 2 Apr 2021 01:52:40 -0700 Subject: [PATCH 375/569] reinstate n_imports --- covasim/immunity.py | 64 ++++++++++++++++++++++++------------------- covasim/parameters.py | 1 + covasim/sim.py | 7 +++++ 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index d3f667ff7..e3cecb9a7 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -24,6 +24,7 @@ class Strain(): kwargs (dict): **Example**:: + b117 = cv.Strain('b117', days=10) # Make strain B117 active from day 10 p1 = cv.Strain('p1', days=15) # Make strain P1 active from day 15 my_var = cv.Strain(strain={'rel_beta': 2.5}, strain_label='My strain', days=20) # Make a custom strain active from day 20 @@ -140,22 +141,23 @@ def apply(self, sim): class Vaccine(): ''' - Add a new vaccine to the sim (called by interventions.py vaccinate() + Add a new vaccine to the sim (called by interventions.py vaccinate() + + stores number of doses for vaccine and a dictionary to pass to init_immunity for each dose - stores number of doses for vaccine and a dictionary to pass to init_immunity for each dose + Args: + vaccine (dict or str): dictionary of parameters specifying information about the vaccine or label for loading pre-defined vaccine + kwargs (dict): - Args: - vaccine (dict or str): dictionary of parameters specifying information about the vaccine or label for loading pre-defined vaccine - kwargs (dict): + **Example**:: - **Example**:: - moderna = cv.Vaccine('moderna') # Create Moderna vaccine - pfizer = cv.Vaccine('pfizer) # Create Pfizer vaccine - j&j = cv.Vaccine('j&j') # Create J&J vaccine - az = cv.Vaccine('az) # Create AstraZeneca vaccine - interventions += [cv.vaccinate(vaccines=[moderna, pfizer, j&j, az], days=[1, 10, 10, 30])] # Add them all to the sim - sim = cv.Sim(interventions=interventions) - ''' + moderna = cv.Vaccine('moderna') # Create Moderna vaccine + pfizer = cv.Vaccine('pfizer) # Create Pfizer vaccine + j&j = cv.Vaccine('j&j') # Create J&J vaccine + az = cv.Vaccine('az) # Create AstraZeneca vaccine + interventions += [cv.vaccinate(vaccines=[moderna, pfizer, j&j, az], days=[1, 10, 10, 30])] # Add them all to the sim + sim = cv.Sim(interventions=interventions) + ''' def __init__(self, vaccine=None): @@ -365,9 +367,11 @@ def nab_to_efficacy(nab, ax): ''' Convert NAb levels to immunity protection factors, using the functional form given in this paper: https://doi.org/10.1101/2021.03.09.21252641 - Inputs: + + Args: nab (arr): an array of NAb levels ax (str): can be 'sus', 'symp' or 'sev', corresponding to the efficacy of protection against infection, symptoms, and severe disease respectively + Returns: an array the same size as nab, containing the immunity protection factors for the specified axis ''' @@ -474,14 +478,16 @@ def init_immunity(sim, create=False): def check_immunity(people, strain, sus=True, inds=None): ''' - Calculate people's immunity on this timestep from prior infections + vaccination - There are two fundamental sources of immunity: - (1) prior exposure: degree of protection depends on strain, prior symptoms, and time since recovery - (2) vaccination: degree of protection depends on strain, vaccine, and time since vaccination - - Gets called from sim before computing trans_sus, sus=True, inds=None - Gets called from people.infect() to calculate prog/trans, sus=False, inds= inds of people being infected - ''' + Calculate people's immunity on this timestep from prior infections + vaccination + + There are two fundamental sources of immunity: + + (1) prior exposure: degree of protection depends on strain, prior symptoms, and time since recovery + (2) vaccination: degree of protection depends on strain, vaccine, and time since vaccination + + Gets called from sim before computing trans_sus, sus=True, inds=None + Gets called from people.infect() to calculate prog/trans, sus=False, inds= inds of people being infected + ''' was_inf = cvu.true(people.t >= people.date_recovered) # Had a previous exposure, now recovered is_vacc = cvu.true(people.vaccinated) # Vaccinated date_rec = people.date_recovered # Date recovered @@ -547,17 +553,19 @@ def check_immunity(people, strain, sus=True, inds=None): def pre_compute_waning(length, form='nab_decay', pars=None): ''' - Process functional form and parameters into values - - 'nab_decay' : specific decay function taken from https://doi.org/10.1101/2021.03.09.21252641 - - 'exp_decay' : exponential decay. Parameters should be init_val and half_life (half_life can be None/nan) - - 'logistic_decay' : logistic decay (TODO fill in details) - - 'linear' : linear decay (TODO fill in details) - - others TBC! + Process functional form and parameters into values: + + - 'nab_decay' : specific decay function taken from https://doi.org/10.1101/2021.03.09.21252641 + - 'exp_decay' : exponential decay. Parameters should be init_val and half_life (half_life can be None/nan) + - 'logistic_decay' : logistic decay (TODO fill in details) + - 'linear' : linear decay (TODO fill in details) + - others TBC! Args: length (float): length of array to return, i.e., for how long waning is calculated form (str): the functional form to use pars (dict): passed to individual immunity functions + Returns: array of length 'length' of values ''' diff --git a/covasim/parameters.py b/covasim/parameters.py index f768c049e..574e55ca2 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -59,6 +59,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated # Parameters that control settings and defaults for multi-strain runs + pars['n_imports'] = 0 # Average daily number of imported cases (actual number is drawn from Poisson distribution) pars['n_strains'] = 1 # The number of strains currently circulating in the population pars['total_strains'] = 1 # Set during sim initialization, once strains have been specified and processed diff --git a/covasim/sim.py b/covasim/sim.py index 340ac2138..8a8175dae 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -549,6 +549,13 @@ def step(self): hosp_max = people.count('severe') > self['n_beds_hosp'] if self['n_beds_hosp'] else False # Check for acute bed constraint icu_max = people.count('critical') > self['n_beds_icu'] if self['n_beds_icu'] else False # Check for ICU bed constraint + # Randomly infect some people (imported infections) + if self['n_imports']: + n_imports = cvu.poisson(self['n_imports']/self.rescale_vec[self.t]) # Imported cases + if n_imports>0: + importation_inds = cvu.choose(max_n=len(people), n=n_imports) + people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation') + # Add strains for strain in self['strains']: if isinstance(strain, cvimm.Strain): From a6747e843940d123dace4ee69161ba2bd604fb03 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 2 Apr 2021 14:51:59 -0400 Subject: [PATCH 376/569] fixed calculation of cumulatives by strain --- covasim/sim.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/covasim/sim.py b/covasim/sim.py index 174631bb9..89178dbe6 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -750,7 +750,11 @@ def finalize(self, verbose=None, restore_pars=True): # Calculate cumulative results for key in cvd.result_flows.keys(): - self.results[f'cum_{key}'][:] = np.cumsum(self.results[f'new_{key}'][:],axis=0) + if 'by_strain' in key: + for strain in range(self['total_strains']): + self.results[f'cum_{key}'][strain,:] = np.cumsum(self.results[f'new_{key}'][strain,:], axis=0) + else: + self.results[f'cum_{key}'][:] = np.cumsum(self.results[f'new_{key}'][:],axis=0) for key in ['cum_infections','cum_infections_by_strain']: self.results[key].values += self['pop_infected']*self.rescale_vec[0] # Include initially infected people From 12b9c21b107e9f35d310f235f4d41ed7d1131beb Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 2 Apr 2021 19:11:10 -0400 Subject: [PATCH 377/569] separating out nab-> eff params for vacc vs natural infection --- covasim/immunity.py | 11 ++++++++--- covasim/sim.py | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 56b37a0d5..f1c23721b 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -171,6 +171,9 @@ def __init__(self, vaccine=None): self.interval = None self.NAb_init = None self.NAb_boost = None + self.NAb_eff = {'sus': {'slope': 2, 'n_50': 0.4}, + 'symp': {'threshold': 1.2, 'lower': 0.2, 'upper': 0.1}, + 'sev': {'threshold': 1.2, 'lower': 0.9, 'upper': 0.52}} # Parameters to map NAbs to efficacy self.vaccine_strain_info = self.init_strain_vaccine_info() self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine) for par, val in self.vaccine_pars.items(): @@ -368,7 +371,7 @@ def check_nab(t, people, inds=None): return -def nab_to_efficacy(nab, ax, args): +def nab_to_efficacy(nab, ax, function_args): ''' Convert NAb levels to immunity protection factors, using the functional form given in this paper: https://doi.org/10.1101/2021.03.09.21252641 @@ -382,7 +385,7 @@ def nab_to_efficacy(nab, ax, args): if ax not in ['sus', 'symp', 'sev']: errormsg = f'Choice provided not in list of choices' raise ValueError(errormsg) - args = args[ax] + args = function_args[ax] if ax == 'sus': slope = args['slope'] @@ -477,10 +480,12 @@ def check_immunity(people, strain, sus=True, inds=None): immunity = people.pars['immunity'] # cross-immunity/own-immunity scalars to be applied to NAb level before computing efficacy nab_eff_pars = people.pars['NAb_eff'] + # If vaccines are present, extract relevant information about them vacc_present = len(is_vacc) if vacc_present: vacc_info = people.pars['vaccine_info'] + vx_nab_eff_pars = vacc_info['NAb_eff'] if sus: ### PART 1: @@ -496,7 +501,7 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] current_NAbs = people.NAb[is_sus_vacc] - people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus', nab_eff_pars) + people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus', vx_nab_eff_pars) if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain current_NAbs = people.NAb[is_sus_was_inf_same] diff --git a/covasim/sim.py b/covasim/sim.py index 89178dbe6..c4a9fc453 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -485,6 +485,7 @@ def init_vaccines(self): self['vaccine_info']['doses'] = vacc.doses self['vaccine_info']['NAb_init'] = vacc.NAb_init self['vaccine_info']['NAb_boost'] = vacc.NAb_boost + self['vaccine_info']['NAb_eff'] = vacc.NAb_eff return From cec6cb443ebeeddabd0c1c79a5bee8c3b2fe9489 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Sat, 3 Apr 2021 18:43:40 -0400 Subject: [PATCH 378/569] updates to immune model --- covasim/immunity.py | 20 ++++++++++---------- covasim/parameters.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index f1c23721b..e0d6830d1 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -82,7 +82,7 @@ def parse_strain_pars(self, strain=None, strain_label=None): strain_pars['rel_beta'] = 1.4 strain_pars['rel_severe_prob'] = 1.4 strain_pars['rel_death_prob'] = 1.4 - strain_pars['rel_imm'] = 0.5 + strain_pars['rel_imm'] = 0.25 self.strain_label = strain # Known parameters on Brazil variant @@ -171,9 +171,7 @@ def __init__(self, vaccine=None): self.interval = None self.NAb_init = None self.NAb_boost = None - self.NAb_eff = {'sus': {'slope': 2, 'n_50': 0.4}, - 'symp': {'threshold': 1.2, 'lower': 0.2, 'upper': 0.1}, - 'sev': {'threshold': 1.2, 'lower': 0.9, 'upper': 0.52}} # Parameters to map NAbs to efficacy + self.NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.5}} # Parameters to map NAbs to efficacy self.vaccine_strain_info = self.init_strain_vaccine_info() self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine) for par, val in self.vaccine_pars.items(): @@ -413,8 +411,10 @@ def init_immunity(sim, create=False): # Pull out all of the circulating strains for cross-immunity circulating_strains = ['wild'] + rel_imms = dict() for strain in sim['strains']: circulating_strains.append(strain.strain_label) + rel_imms[strain.strain_label] = strain.rel_imm # If immunity values have been provided, process them if sim['immunity'] is None or create: @@ -428,7 +428,7 @@ def init_immunity(sim, create=False): immunity[ax] = np.full(ts, 1, dtype=cvd.default_float) known_strains = ['wild', 'b117', 'b1351', 'p1'] - cross_immunity = create_cross_immunity(circulating_strains) + cross_immunity = create_cross_immunity(circulating_strains, rel_imms) for i in range(ts): for j in range(ts): if i != j: @@ -439,7 +439,7 @@ def init_immunity(sim, create=False): else: # if we know all the circulating strains, then update, otherwise use defaults known_strains = ['wild', 'b117', 'b1351', 'p1'] - cross_immunity = create_cross_immunity(circulating_strains) + cross_immunity = create_cross_immunity(circulating_strains, rel_imms) if sc.checktype(sim['immunity']['sus'], 'arraylike'): correct_size = sim['immunity']['sus'].shape == (ts, ts) if not correct_size: @@ -653,7 +653,7 @@ def linear_growth(length, slope): return (slope * t) -def create_cross_immunity(circulating_strains): +def create_cross_immunity(circulating_strains, rel_imms): known_strains = ['wild', 'b117', 'b1351', 'p1'] known_cross_immunity = dict() known_cross_immunity['wild'] = {} # cross-immunity to wild @@ -661,15 +661,15 @@ def create_cross_immunity(circulating_strains): known_cross_immunity['wild']['b1351'] = .5 known_cross_immunity['wild']['p1'] = .5 known_cross_immunity['b117'] = {} # cross-immunity to b117 - known_cross_immunity['b117']['wild'] = 1 + known_cross_immunity['b117']['wild'] = rel_imms['b117'] if 'b117' in circulating_strains else 1 known_cross_immunity['b117']['b1351'] = 1 known_cross_immunity['b117']['p1'] = 1 known_cross_immunity['b1351'] = {} # cross-immunity to b1351 - known_cross_immunity['b1351']['wild'] = 0.1 + known_cross_immunity['b1351']['wild'] = rel_imms['b1351'] if 'b1351' in circulating_strains else 0.1 known_cross_immunity['b1351']['b117'] = 0.1 known_cross_immunity['b1351']['p1'] = 0.1 known_cross_immunity['p1'] = {} # cross-immunity to p1 - known_cross_immunity['p1']['wild'] = 0.2 + known_cross_immunity['p1']['wild'] = rel_imms['p1'] if 'p1' in circulating_strains else 0.2 known_cross_immunity['p1']['b117'] = 0.2 known_cross_immunity['p1']['b1351'] = 0.2 diff --git a/covasim/parameters.py b/covasim/parameters.py index 89ae7ecfc..121a9a62f 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -68,7 +68,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source - pars['NAb_eff'] = {'sus': {'slope': 2, 'n_50': 0.4}, + pars['NAb_eff'] = {'sus': {'slope': 2.5, 'n_50': 0.05}, 'symp': {'threshold': 1.2, 'lower': 0.2, 'upper': 0.1}, 'sev': {'threshold': 1.2, 'lower': 0.9, 'upper': 0.52}} # Parameters to map NAbs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains From 7128f1dfb898be51ab431e7fe0fbe51ac01b4630 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Sun, 4 Apr 2021 21:40:16 -0400 Subject: [PATCH 379/569] updates to immune model --- covasim/immunity.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index e0d6830d1..2e0df4d83 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -171,7 +171,7 @@ def __init__(self, vaccine=None): self.interval = None self.NAb_init = None self.NAb_boost = None - self.NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.5}} # Parameters to map NAbs to efficacy + self.NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}} # Parameters to map NAbs to efficacy self.vaccine_strain_info = self.init_strain_vaccine_info() self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine) for par, val in self.vaccine_pars.items(): @@ -480,7 +480,6 @@ def check_immunity(people, strain, sus=True, inds=None): immunity = people.pars['immunity'] # cross-immunity/own-immunity scalars to be applied to NAb level before computing efficacy nab_eff_pars = people.pars['NAb_eff'] - # If vaccines are present, extract relevant information about them vacc_present = len(is_vacc) if vacc_present: From 36822a27d9ef8e684599c71cbb3fe075fe5dbb19 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 5 Apr 2021 12:54:57 -0400 Subject: [PATCH 380/569] vaccinated people with prior infected access natural mapping --- covasim/immunity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/covasim/immunity.py b/covasim/immunity.py index 2e0df4d83..7f9dcea96 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -493,6 +493,7 @@ def check_immunity(people, strain, sus=True, inds=None): was_inf_same = cvu.true((people.recovered_strain == strain) & (people.t >= date_rec)) # Had a previous exposure to the same strain, now recovered was_inf_diff = np.setdiff1d(was_inf, was_inf_same) # Had a previous exposure to a different strain, now recovered is_sus_vacc = np.intersect1d(is_sus, is_vacc) # Susceptible and vaccinated + is_sus_vacc = np.setdiff1d(is_sus_vacc, was_inf) # Susceptible, vaccinated without prior infection is_sus_was_inf_same = np.intersect1d(is_sus, was_inf_same) # Susceptible and being challenged by the same strain is_sus_was_inf_diff = np.intersect1d(is_sus, was_inf_diff) # Susceptible and being challenged by a different strain From 7fb41548d358d5359b751e5733aba88ff6760e19 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 5 Apr 2021 21:44:42 -0400 Subject: [PATCH 381/569] boost peak nabs, somehow this got lost in merges?! --- covasim/immunity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 7f9dcea96..28448c75f 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -316,7 +316,7 @@ def init_nab(people, inds, prior_inf=True): no_prior_NAb_inds = np.setdiff1d(inds, prior_NAb_inds) # Find people without prior NAbs prior_NAb = people.NAb[prior_NAb_inds] # Array of NAb levels on this timestep for people with some NAbs - + peak_NAb = people.init_NAb[prior_NAb_inds] # NAbs from infection if prior_inf: @@ -330,7 +330,7 @@ def init_nab(people, inds, prior_inf=True): # 2) Prior NAb: multiply existing NAb by boost factor if len(prior_NAb_inds): - init_NAb = prior_NAb * NAb_boost + init_NAb = peak_NAb * NAb_boost people.init_NAb[prior_NAb_inds] = init_NAb # NAbs from a vaccine @@ -343,7 +343,7 @@ def init_nab(people, inds, prior_inf=True): # 2) Prior NAb (from natural or vaccine dose 1): multiply existing NAb by boost factor if len(prior_NAb_inds): - init_NAb = prior_NAb * NAb_boost + init_NAb = peak_NAb * NAb_boost people.NAb[prior_NAb_inds] = init_NAb return From d339f9bc770dbc39fe47d97630de6bb6d08a0862 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Apr 2021 18:38:10 +0000 Subject: [PATCH 382/569] Bump urllib3 from 1.26.3 to 1.26.4 in /tests Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.3 to 1.26.4. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.3...1.26.4) Signed-off-by: dependabot[bot] --- tests/requirements_frozen.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/requirements_frozen.txt b/tests/requirements_frozen.txt index 6e7672ac8..5ee142483 100644 --- a/tests/requirements_frozen.txt +++ b/tests/requirements_frozen.txt @@ -46,7 +46,7 @@ smmap==3.0.5 statsmodels==0.12.2 Topology==2.16.0 traitlets==5.0.5 -urllib3==1.26.3 +urllib3==1.26.4 wcwidth==0.2.5 xlrd==1.2.0 XlsxWriter==1.3.7 \ No newline at end of file From f0f4800d4e57f384b69550f62f7ba7ad39c829a2 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 6 Apr 2021 16:17:43 -0400 Subject: [PATCH 383/569] some updates --- covasim/immunity.py | 23 +++++++++-------------- covasim/parameters.py | 11 +++++------ 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 28448c75f..bdd87ea34 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -33,7 +33,8 @@ class Strain(): **Example**:: b117 = cv.Strain('b117', days=10) # Make strain B117 active from day 10 p1 = cv.Strain('p1', days=15) # Make strain P1 active from day 15 - my_var = cv.Strain(strain={'rel_beta': 2.5}, strain_label='My strain', days=20) # Make a custom strain active from day 20 + # Make a custom strain active from day 20 + my_var = cv.Strain(strain={'rel_beta': 2.5}, strain_label='My strain', days=20) sim = cv.Sim(strains=[b117, p1, my_var]) # Add them all to the sim ''' @@ -60,7 +61,6 @@ def parse_strain_pars(self, strain=None, strain_label=None): 'wild': ['default', 'wild', 'pre-existing'], 'b117': ['b117', 'B117', 'B.1.1.7', 'UK', 'uk', 'UK variant', 'uk variant'], 'b1351': ['b1351', 'B1351', 'B.1.351', 'SA', 'sa', 'SA variant', 'sa variant'], - # TODO: add other aliases 'p1': ['p1', 'P1', 'P.1', 'B.1.1.248', 'b11248', 'Brazil', 'Brazil variant', 'brazil variant'], } @@ -390,12 +390,7 @@ def nab_to_efficacy(nab, ax, function_args): n_50 = args['n_50'] efficacy = 1 / (1 + np.exp(-slope * (np.log10(nab) - np.log10(n_50)))) # from logistic regression computed in R using data from Khoury et al else: - threshold = np.full(len(nab), fill_value=args['threshold']) - lower = args['lower'] - upper = args['upper'] - efficacy = nab>threshold - efficacy = np.where(efficacy == False, upper, efficacy) - efficacy = np.where(efficacy == True, lower, efficacy) + efficacy = np.full(len(nab), fill_value=args) return efficacy @@ -657,13 +652,13 @@ def create_cross_immunity(circulating_strains, rel_imms): known_strains = ['wild', 'b117', 'b1351', 'p1'] known_cross_immunity = dict() known_cross_immunity['wild'] = {} # cross-immunity to wild - known_cross_immunity['wild']['b117'] = .5 - known_cross_immunity['wild']['b1351'] = .5 - known_cross_immunity['wild']['p1'] = .5 + known_cross_immunity['wild']['b117'] = 0.5 + known_cross_immunity['wild']['b1351'] = 0.5 + known_cross_immunity['wild']['p1'] = 0.5 known_cross_immunity['b117'] = {} # cross-immunity to b117 - known_cross_immunity['b117']['wild'] = rel_imms['b117'] if 'b117' in circulating_strains else 1 - known_cross_immunity['b117']['b1351'] = 1 - known_cross_immunity['b117']['p1'] = 1 + known_cross_immunity['b117']['wild'] = rel_imms['b117'] if 'b117' in circulating_strains else 0.8 + known_cross_immunity['b117']['b1351'] = 0.8 + known_cross_immunity['b117']['p1'] = 0.8 known_cross_immunity['b1351'] = {} # cross-immunity to b1351 known_cross_immunity['b1351']['wild'] = rel_imms['b1351'] if 'b1351' in circulating_strains else 0.1 known_cross_immunity['b1351']['b117'] = 0.1 diff --git a/covasim/parameters.py b/covasim/parameters.py index 121a9a62f..6cfd39589 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -68,14 +68,13 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source - pars['NAb_eff'] = {'sus': {'slope': 2.5, 'n_50': 0.05}, - 'symp': {'threshold': 1.2, 'lower': 0.2, 'upper': 0.1}, - 'sev': {'threshold': 1.2, 'lower': 0.9, 'upper': 0.52}} # Parameters to map NAbs to efficacy + pars['NAb_eff'] = {'sus': {'slope': 2.7, 'n_50': 0.03}, + 'symp': 0.1, 'sev': 0.52} # Parameters to map NAbs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms - pars['rel_imm']['asymptomatic'] = 0.7 - pars['rel_imm']['mild'] = 0.9 - pars['rel_imm']['severe'] = 1 + pars['rel_imm']['asymptomatic'] = 0.85 + pars['rel_imm']['mild'] = 1 + pars['rel_imm']['severe'] = 1.5 pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py pars['vaccine_info'] = None # Vaccine info in a more easily accessible format From fd6e9ba1c396f0a83ff14c627ebd895a9db2ff73 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 7 Apr 2021 15:49:16 +0200 Subject: [PATCH 384/569] doesnt work as is --- covasim/immunity.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index dd6a8f2e6..a544035d9 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -530,7 +530,12 @@ def check_immunity(people, strain, sus=True, inds=None): for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] current_NAbs = people.NAb[unique_inds] - people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus', nab_eff_pars) + try: people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus', nab_eff_pars) + except: + import traceback; + traceback.print_exc(); + import pdb; + pdb.set_trace() else: ### PART 2: From c45bee80024019e8e9e0d5de1cfed39d64278d13 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 7 Apr 2021 18:36:56 +0200 Subject: [PATCH 385/569] nowhere near functional yet --- tests/devtests/test_variants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index cdb1b50cf..af354568f 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -515,7 +515,7 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - if 0: + if 1: sim = cv.Sim() sim.run() From 36ab2f308fc9ab18827662685ed5d24f298995a5 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 7 Apr 2021 13:56:09 -0400 Subject: [PATCH 386/569] fixed init_nab for vaccine boosting --- covasim/immunity.py | 20 ++++++++++---------- covasim/parameters.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index bdd87ea34..4730b560e 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -198,8 +198,8 @@ def init_strain_vaccine_info(self): rel_imm['az']['b1351'] = .5 rel_imm['az']['p1'] = .5 - rel_imm['j&j']['b1351'] = .5 - rel_imm['j&j']['p1'] = .5 + rel_imm['j&j']['b1351'] = 1/6.7 + rel_imm['j&j']['p1'] = 1/8.6 return rel_imm @@ -224,7 +224,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2= 2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 - vaccine_pars['NAb_boost'] = 3 + vaccine_pars['NAb_boost'] = 2 vaccine_pars['label'] = vaccine # Known parameters on moderna @@ -233,7 +233,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 29 - vaccine_pars['NAb_boost'] = 3 + vaccine_pars['NAb_boost'] = 2 vaccine_pars['label'] = vaccine # Known parameters on az @@ -242,7 +242,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 - vaccine_pars['NAb_boost'] = 3 + vaccine_pars['NAb_boost'] = 2 vaccine_pars['label'] = vaccine # Known parameters on j&j @@ -251,7 +251,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) vaccine_pars['doses'] = 1 vaccine_pars['interval'] = None - vaccine_pars['NAb_boost'] = 3 + vaccine_pars['NAb_boost'] = 2 vaccine_pars['label'] = vaccine else: @@ -344,7 +344,7 @@ def init_nab(people, inds, prior_inf=True): # 2) Prior NAb (from natural or vaccine dose 1): multiply existing NAb by boost factor if len(prior_NAb_inds): init_NAb = peak_NAb * NAb_boost - people.NAb[prior_NAb_inds] = init_NAb + people.init_NAb[prior_NAb_inds] = init_NAb return @@ -656,15 +656,15 @@ def create_cross_immunity(circulating_strains, rel_imms): known_cross_immunity['wild']['b1351'] = 0.5 known_cross_immunity['wild']['p1'] = 0.5 known_cross_immunity['b117'] = {} # cross-immunity to b117 - known_cross_immunity['b117']['wild'] = rel_imms['b117'] if 'b117' in circulating_strains else 0.8 + known_cross_immunity['b117']['wild'] = rel_imms['b117'] if 'b117' in circulating_strains else .5 known_cross_immunity['b117']['b1351'] = 0.8 known_cross_immunity['b117']['p1'] = 0.8 known_cross_immunity['b1351'] = {} # cross-immunity to b1351 - known_cross_immunity['b1351']['wild'] = rel_imms['b1351'] if 'b1351' in circulating_strains else 0.1 + known_cross_immunity['b1351']['wild'] = rel_imms['b1351'] if 'b1351' in circulating_strains else 0.066 known_cross_immunity['b1351']['b117'] = 0.1 known_cross_immunity['b1351']['p1'] = 0.1 known_cross_immunity['p1'] = {} # cross-immunity to p1 - known_cross_immunity['p1']['wild'] = rel_imms['p1'] if 'p1' in circulating_strains else 0.2 + known_cross_immunity['p1']['wild'] = rel_imms['p1'] if 'p1' in circulating_strains else 0.17 known_cross_immunity['p1']['b117'] = 0.2 known_cross_immunity['p1']['b1351'] = 0.2 diff --git a/covasim/parameters.py b/covasim/parameters.py index 6cfd39589..4adc69c0c 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -67,7 +67,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['NAb_init'] = dict(dist='normal', par1= 0, par2= 2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters - pars['NAb_boost'] = 2 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source + pars['NAb_boost'] = 1.5 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source pars['NAb_eff'] = {'sus': {'slope': 2.7, 'n_50': 0.03}, 'symp': 0.1, 'sev': 0.52} # Parameters to map NAbs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains From e2e25f759a334666e885b69ba13b44d50ddcb9c0 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 7 Apr 2021 21:04:07 +0200 Subject: [PATCH 387/569] making changes --- covasim/immunity.py | 12 +++++++++--- covasim/sim.py | 8 +++++++- tests/devtests/test_variants.py | 8 ++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index a544035d9..26edeceb6 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -105,8 +105,8 @@ def parse_strain_pars(self, strain=None, strain_label=None): return strain_pars def initialize(self, sim): - if not hasattr(self, 'rel_imm'): - self.rel_imm = 1 +# if not hasattr(self, 'rel_imm'): +# self.rel_imm = 1 # Update strain info for strain_key in cvd.strain_pars: @@ -131,7 +131,7 @@ def apply(self, sim): sim['n_strains'] += 1 # Update strain-specific people attributes - cvu.update_strain_attributes(sim.people) # don't think we need to do this if we just create people arrays with number of total strains in sim + #cvu.update_strain_attributes(sim.people) # don't think we need to do this if we just create people arrays with number of total strains in sim susceptible_inds = cvu.true(sim.people.susceptible) importation_inds = np.random.choice(susceptible_inds, self.n_imports) sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) @@ -514,6 +514,12 @@ def check_immunity(people, strain, sus=True, inds=None): is_sus_was_inf_same = np.intersect1d(is_sus, was_inf_same) # Susceptible and being challenged by the same strain is_sus_was_inf_diff = np.intersect1d(is_sus, was_inf_diff) # Susceptible and being challenged by a different strain + # if people.t==50: + # import traceback; + # traceback.print_exc(); + # import pdb; + # pdb.set_trace() + if len(is_sus_vacc): vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] diff --git a/covasim/sim.py b/covasim/sim.py index 561c48e0f..7794c22d6 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -597,7 +597,13 @@ def step(self): # Check NAbs. Take set difference so we don't compute NAbs for anyone currently infected if self['use_immunity']: - has_nabs = np.setdiff1d(cvu.defined(people.init_NAb),cvu.false(sus)) + # if t==109: + # import traceback; + # traceback.print_exc(); + # import pdb; + # pdb.set_trace() +# has_nabs = np.setdiff1d(cvu.defined(people.init_NAb), cvu.false(sus)) + has_nabs = np.setdiff1d(cvu.defined(people.init_NAb), cvu.false(people.exposed)) if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) # Iterate through n_strains to calculate infections diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index af354568f..e358c6c18 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -108,7 +108,7 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): 'beta': 0.01 } strain = cv.Strain(strain_pars, days=1, n_imports=20) - sim = cv.Sim(pars=pars, strains=strain, analyzers=cv.snapshot(30, 60)) + sim = cv.Sim(use_immunity=True, pars=pars, strains=strain, analyzers=cv.snapshot(30, 60)) sim.run() if do_plot: @@ -516,8 +516,8 @@ def get_ind_of_min_value(list, time): # Run simplest possible test if 1: - sim = cv.Sim() - sim.run() + sim = cv.Sim().run().plot() + sim = cv.Sim(use_immunity=True).run().plot() # Run more complex single-sim tests # sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) @@ -536,7 +536,7 @@ def get_ind_of_min_value(list, time): # msim0 = test_msim() # Run immunity tests - sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) sc.toc() From fd1335c7c757d9d78ad2f77773b811a6163b722f Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 7 Apr 2021 21:06:49 +0200 Subject: [PATCH 388/569] change to use_waning --- covasim/immunity.py | 8 +------- covasim/people.py | 4 ++-- covasim/sim.py | 12 +++--------- tests/devtests/test_variants.py | 10 +++++----- 4 files changed, 11 insertions(+), 23 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 26edeceb6..d11089a2a 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -418,7 +418,7 @@ def init_immunity(sim, create=False): ''' Initialize immunity matrices with all strains that will eventually be in the sim''' # Don't use this function if immunity is turned off - if not sim['use_immunity']: + if not sim['use_waning']: return ts = sim['total_strains'] @@ -514,12 +514,6 @@ def check_immunity(people, strain, sus=True, inds=None): is_sus_was_inf_same = np.intersect1d(is_sus, was_inf_same) # Susceptible and being challenged by the same strain is_sus_was_inf_diff = np.intersect1d(is_sus, was_inf_diff) # Susceptible and being challenged by a different strain - # if people.t==50: - # import traceback; - # traceback.print_exc(); - # import pdb; - # pdb.set_trace() - if len(is_sus_vacc): vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] diff --git a/covasim/people.py b/covasim/people.py index 1e19b5a8e..5b35b3a00 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -256,7 +256,7 @@ def check_recovery(self): self.recovered[inds] = True # Handle immunity aspects - if self.pars['use_immunity']: + if self.pars['use_waning']: # Before letting them recover, store information about the strain they had, store symptoms and pre-compute NAbs array self.recovered_strain[inds] = self.exposed_strain[inds] @@ -396,7 +396,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str if source is not None: source = source[keep] - if self.pars['use_immunity']: + if self.pars['use_waning']: cvi.check_immunity(self, strain, sus=False, inds=inds) # Deal with strain parameters diff --git a/covasim/sim.py b/covasim/sim.py index 7794c22d6..2362c1196 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -108,7 +108,7 @@ def initialize(self, reset=False, **kwargs): self.t = 0 # The current time index self.validate_pars() # Ensure parameters have valid values self.set_seed() # Reset the random seed before the population is created - if self['use_immunity']: + if self['use_waning']: self.init_strains() # ...and the strains.... self.init_immunity() # ... and information about immunity/cross-immunity. self.init_results() # After initializing the strain, create the results structure @@ -596,13 +596,7 @@ def step(self): prel_sus = people.rel_sus # Check NAbs. Take set difference so we don't compute NAbs for anyone currently infected - if self['use_immunity']: - # if t==109: - # import traceback; - # traceback.print_exc(); - # import pdb; - # pdb.set_trace() -# has_nabs = np.setdiff1d(cvu.defined(people.init_NAb), cvu.false(sus)) + if self['use_waning']: has_nabs = np.setdiff1d(cvu.defined(people.init_NAb), cvu.false(people.exposed)) if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) @@ -610,7 +604,7 @@ def step(self): for strain in range(ns): # Check immunity - if self['use_immunity']: + if self['use_waning']: cvimm.check_immunity(people, strain, sus=True) # Deal with strain parameters diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index e358c6c18..7c1b0c96f 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -517,25 +517,25 @@ def get_ind_of_min_value(list, time): # Run simplest possible test if 1: sim = cv.Sim().run().plot() - sim = cv.Sim(use_immunity=True).run().plot() + sim = cv.Sim(use_waning=True).run().plot() - # Run more complex single-sim tests + # Run more complex single-sim tests: TODO, NOT WORKING CURRENTLY # sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - # Run Vaccine tests + # Run Vaccine tests: TODO, NOT WORKING CURRENTLY # sim4 = test_synthpops() # sim5 = test_vaccine_1strain() - # # # Run multisim and scenario tests + # # # Run multisim and scenario tests: TODO, NOT WORKING CURRENTLY # scens0 = test_vaccine_1strain_scen() # scens1 = test_vaccine_2strains_scen() # scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) # msim0 = test_msim() - # Run immunity tests + # Run immunity tests: TODO, NOT WORKING CURRENTLY # sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) sc.toc() From 658b99f7e807f8942fdc4760e5f9efc7eba7daf0 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 7 Apr 2021 21:36:57 +0200 Subject: [PATCH 389/569] another rename --- covasim/immunity.py | 7 +------ covasim/parameters.py | 25 +++++++++++-------------- tests/devtests/test_variants.py | 4 ++-- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 2c5a51772..2dfd5959b 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -530,12 +530,7 @@ def check_immunity(people, strain, sus=True, inds=None): for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] current_NAbs = people.NAb[unique_inds] - try: people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus', nab_eff_pars) - except: - import traceback; - traceback.print_exc(); - import pdb; - pdb.set_trace() + people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus', nab_eff_pars) else: ### PART 2: diff --git a/covasim/parameters.py b/covasim/parameters.py index a71f057ef..b0dd23ddf 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -64,20 +64,17 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['total_strains'] = 1 # Set during sim initialization, once strains have been specified and processed # Parameters used to calculate immunity - pars['use_immunity'] = False # Whether to use dynamically calculated immunity - pars['NAb_init'] = dict(dist='normal', par1= 0, par2= 2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 - pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 - pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters - pars['NAb_boost'] = 1.5 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source - pars['NAb_eff'] = {'sus': {'slope': 2.7, 'n_50': 0.03}, - 'symp': 0.1, 'sev': 0.52} # Parameters to map NAbs to efficacy - pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains - pars['rel_imm'] = {} # Relative immunity from natural infection varies by symptoms - pars['rel_imm']['asymptomatic'] = 0.85 - pars['rel_imm']['mild'] = 1 - pars['rel_imm']['severe'] = 1.5 - pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py - pars['vaccine_info'] = None # Vaccine info in a more easily accessible format + pars['use_waning'] = False # Whether to use dynamically calculated immunity + pars['NAb_init'] = dict(dist='normal', par1= 0, par2= 2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 + pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 + pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters + pars['NAb_boost'] = 1.5 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source + pars['NAb_eff'] = {'sus': {'slope': 2.7, 'n_50': 0.03}, + 'symp': 0.1, 'sev': 0.52} # Parameters to map NAbs to efficacy + pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains + pars['rel_imm'] = {'asymptomatic': 0.85, 'mild': 1, 'severe': 1.5} # Relative immunity from natural infection varies by symptoms + pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py + pars['vaccine_info'] = None # Vaccine info in a more easily accessible format # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 7c1b0c96f..7a8c10766 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -515,12 +515,12 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - if 1: + if 0: sim = cv.Sim().run().plot() sim = cv.Sim(use_waning=True).run().plot() # Run more complex single-sim tests: TODO, NOT WORKING CURRENTLY - # sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) From b56f63c47f12ac718e27366be5105026ca81479a Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 7 Apr 2021 22:15:03 +0200 Subject: [PATCH 390/569] almost all tests ok! --- covasim/immunity.py | 10 ++-- covasim/people.py | 5 +- tests/devtests/test_variants.py | 87 ++++++++++++++------------------- 3 files changed, 43 insertions(+), 59 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 2dfd5959b..8188f04d7 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -105,20 +105,20 @@ def parse_strain_pars(self, strain=None, strain_label=None): return strain_pars def initialize(self, sim): -# if not hasattr(self, 'rel_imm'): -# self.rel_imm = 1 + if not hasattr(self, 'rel_imm'): + self.rel_imm = 1 # Update strain info for strain_key in cvd.strain_pars: if hasattr(self, strain_key): newval = getattr(self, strain_key) if strain_key == 'dur': # Validate durations (make sure there are values for all durations) - newval = sc.mergenested(sim[strain_key][0], newval) - sim[strain_key].append(newval) + newval = sc.mergenested(sim['strain_pars'][strain_key][0], newval) + sim['strain_pars'][strain_key].append(newval) else: # use default print(f'{strain_key} not provided for this strain, using default value') - sim[strain_key].append(sim[strain_key][0]) + sim['strain_pars'][strain_key].append(sim['strain_pars'][strain_key][0]) self.initialized = True diff --git a/covasim/people.py b/covasim/people.py index 5b35b3a00..11f0a5a46 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -64,7 +64,7 @@ def __init__(self, pars, strict=True, **kwargs): if key == 'uid': self[key] = np.arange(self.pop_size, dtype=cvd.default_int) elif 'imm' in key: # everyone starts out with no immunity - self[key] = np.full((self.pars['n_strains'], self.pop_size), 0, dtype=cvd.default_float) + self[key] = np.full((self.pars['total_strains'], self.pop_size), 0, dtype=cvd.default_float) elif key == 'vaccinations': self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) else: @@ -80,7 +80,7 @@ def __init__(self, pars, strict=True, **kwargs): # Set strain states, which store info about which strain a person is exposed to for key in self.meta.strain_states: if 'by' in key: - self[key] = np.full((self.pars['n_strains'], self.pop_size), False, dtype=bool) + self[key] = np.full((self.pars['total_strains'], self.pop_size), False, dtype=bool) else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -432,7 +432,6 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str # Use prognosis probabilities to determine what happens to them symp_probs = infect_pars['rel_symp_prob']*self.symp_prob[inds]*(1-self.symp_imm[strain, inds]) # Calculate their actual probability of being symptomatic - # print(self.symp_imm[strain, inds]) is_symp = cvu.binomial_arr(symp_probs) # Determine if they develop symptoms symp_inds = inds[is_symp] asymp_inds = inds[~is_symp] # Asymptomatic diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 7a8c10766..b841f181a 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -23,7 +23,7 @@ def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): } n_runs = 3 - base_sim = cv.Sim(base_pars) + base_sim = cv.Sim(use_waning=True, pars=base_pars) # Define the scenarios b1351 = cv.Strain('b1351', days=100, n_imports=20) @@ -108,13 +108,9 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): 'beta': 0.01 } strain = cv.Strain(strain_pars, days=1, n_imports=20) - sim = cv.Sim(use_immunity=True, pars=pars, strains=strain, analyzers=cv.snapshot(30, 60)) + sim = cv.Sim(use_waning=True, pars=pars, strains=strain, analyzers=cv.snapshot(30, 60)) sim.run() - if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (cross immunity)', filename='test_importstrain1', labels=strain_labels, do_show=do_show, do_save=do_save) - plot_shares(sim, key='new_infections', title='Shares of new infections by strain', filename='test_importstrain1_shares', do_show=do_show, do_save=do_save) - return sim @@ -124,7 +120,7 @@ def test_import2strains(do_plot=False, do_show=True, do_save=False): b117 = cv.Strain('b117', days=1, n_imports=20) p1 = cv.Strain('sa variant', days=2, n_imports=20) - sim = cv.Sim(strains=[b117, p1], label='With imported infections') + sim = cv.Sim(use_waning=True, strains=[b117, p1], label='With imported infections') sim.run() strain_labels = [ @@ -133,10 +129,10 @@ def test_import2strains(do_plot=False, do_show=True, do_save=False): 'Strain 3: SA Variant on day 30' ] - if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strains', filename='test_importstrain2', labels=strain_labels, do_show=do_show, do_save=do_save) - plot_shares(sim, key='new_infections', title='Shares of new infections by strain', - filename='test_importstrain2_shares', do_show=do_show, do_save=do_save) + # if do_plot: + # plot_results(sim, key='incidence_by_strain', title='Imported strains', filename='test_importstrain2', labels=strain_labels, do_show=do_show, do_save=do_save) + # plot_shares(sim, key='new_infections', title='Shares of new infections by strain', + # filename='test_importstrain2_shares', do_show=do_show, do_save=do_save) return sim @@ -159,12 +155,12 @@ def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): } strain = cv.Strain(strain=strain_pars, strain_label='Custom strain', days=10, n_imports=30) - sim = cv.Sim(pars=pars, strains=strain, label='With imported infections') + sim = cv.Sim(use_waning=True, pars=pars, strains=strain, label='With imported infections') sim.run() - if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (longer duration)', filename='test_importstrain1', labels=strain_labels, do_show=do_show, do_save=do_save) - plot_shares(sim, key='new_infections', title='Shares of new infections by strain', filename='test_importstrain_longerdur_shares', do_show=do_show, do_save=do_save) + # if do_plot: + # plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (longer duration)', filename='test_importstrain1', labels=strain_labels, do_show=do_show, do_save=do_save) + # plot_shares(sim, key='new_infections', title='Shares of new infections by strain', filename='test_importstrain_longerdur_shares', do_show=do_show, do_save=do_save) return sim @@ -182,19 +178,9 @@ def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): strains = [cv.Strain(strain=strain2, days=10, n_imports=20), cv.Strain(strain=strain3, days=30, n_imports=20), ] - sim = cv.Sim(interventions=intervs, strains=strains, label='With imported infections') + sim = cv.Sim(use_waning=True, interventions=intervs, strains=strains, label='With imported infections') sim.run() - strain_labels = [ - f'Strain 1: beta {sim["beta"]}', - f'Strain 2: beta {sim["beta"]*sim["rel_beta"][1]}, 20 imported day 10', - f'Strain 3: beta {sim["beta"]*sim["rel_beta"][2]}, 20 imported day 30' - ] - - if do_plot: - plot_results(sim, key='incidence_by_strain', title='Imported strains', filename='test_import2strains_changebeta', labels=strain_labels, do_show=do_show, do_save=do_save) - plot_shares(sim, key='new_infections', title='Shares of new infections by strain', - filename='test_import2strains_changebeta_shares', do_show=do_show, do_save=do_save) return sim @@ -212,6 +198,7 @@ def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer') sim = cv.Sim( + use_waning=True, pars=pars, interventions=pfizer ) @@ -229,7 +216,7 @@ def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): def test_synthpops(): - sim = cv.Sim(pop_size=5000, pop_type='synthpops') + sim = cv.Sim(use_waning=True, pop_size=5000, pop_type='synthpops') sim.popdict = cv.make_synthpop(sim, with_facilities=True, layer_mapping={'LTCF': 'f'}) sim.reset_layer_pars() @@ -259,7 +246,7 @@ def test_vaccine_1strain_scen(do_plot=True, do_show=True, do_save=False): } n_runs = 3 - base_sim = cv.Sim(base_pars) + base_sim = cv.Sim(use_waning=True, pars=base_pars) # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 base_sim.vxsubtarg = sc.objdict() @@ -310,7 +297,7 @@ def test_vaccine_2strains_scen(do_plot=True, do_show=True, do_save=False): } n_runs = 3 - base_sim = cv.Sim(base_pars) + base_sim = cv.Sim(use_waning=True, pars=base_pars) # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 base_sim.vxsubtarg = sc.objdict() @@ -377,7 +364,7 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): } n_runs = 1 - base_sim = cv.Sim(base_pars) + base_sim = cv.Sim(use_waning=True, pars=base_pars) # Define the scenarios scenarios = { @@ -410,17 +397,15 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): def test_msim(): # basic test for vaccine b117 = cv.Strain('b117', days=0) - sim = cv.Sim(strains=[b117]) + sim = cv.Sim(use_waning=True, strains=[b117]) msim = cv.MultiSim(sim, n_runs=2) msim.run() msim.reduce() to_plot = sc.objdict({ - 'Total infections': ['cum_infections'], 'New infections per day': ['new_infections'], 'New Re-infections per day': ['new_reinfections'], - 'New infections by strain': ['new_infections_by_strain'] }) msim.plot(to_plot=to_plot, do_save=0, do_show=1, legend_args={'loc': 'upper left'}, axis_args={'hspace': 0.4}, interval=35) @@ -515,28 +500,28 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - if 0: + if 1: sim = cv.Sim().run().plot() sim = cv.Sim(use_waning=True).run().plot() - # Run more complex single-sim tests: TODO, NOT WORKING CURRENTLY + # Run more complex single-sim tests sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - - # Run Vaccine tests: TODO, NOT WORKING CURRENTLY - # sim4 = test_synthpops() - # sim5 = test_vaccine_1strain() - - # # # Run multisim and scenario tests: TODO, NOT WORKING CURRENTLY - # scens0 = test_vaccine_1strain_scen() - # scens1 = test_vaccine_2strains_scen() - # scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) - # msim0 = test_msim() - - # Run immunity tests: TODO, NOT WORKING CURRENTLY - # sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + + # Run Vaccine tests + sim4 = test_synthpops() + sim5 = test_vaccine_1strain() + + # Run multisim and scenario tests + #scens0 = test_vaccine_1strain_scen() #TODO, NOT WORKING CURRENTLY + #scens1 = test_vaccine_2strains_scen() #TODO, NOT WORKING CURRENTLY + scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) + msim0 = test_msim() + + # Run immunity tests + sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) sc.toc() From a57115ca2f129b80594a3b9a4de3c7246ae090bd Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 7 Apr 2021 22:31:53 +0200 Subject: [PATCH 391/569] new test to investigate waning vs not --- tests/devtests/test_variants.py | 95 ++++++++++++++++++++++++--------- 1 file changed, 71 insertions(+), 24 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index b841f181a..3287f4e8d 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -8,7 +8,7 @@ do_plot = 1 -do_show = 1 +do_show = 0 do_save = 1 @@ -394,6 +394,50 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): return scens +def test_waning_vs_not(do_plot=True, do_show=True, do_save=False): + + # Define baseline parameters + base_pars = { + 'pop_size': 100e3, + 'pop_scale': 50, + 'n_days': 150, + 'use_waning': False, + } + + n_runs = 3 + base_sim = cv.Sim(pars=base_pars) + + # Define the scenarios + scenarios = { + 'no_waning': { + 'name': 'No waning', + 'pars': { + } + }, + 'waning': { + 'name': 'Waning', + 'pars': { + 'use_waning': True, + } + }, + } + + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'New reinfections': ['new_reinfections'], + 'Cumulative infections': ['cum_infections'], + 'Cumulative reinfections': ['cum_reinfections'], + }) + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_waning_vs_not.png', to_plot=to_plot) + + return scens + + def test_msim(): # basic test for vaccine b117 = cv.Strain('b117', days=0) @@ -499,29 +543,32 @@ def get_ind_of_min_value(list, time): if __name__ == '__main__': sc.tic() - # Run simplest possible test - if 1: - sim = cv.Sim().run().plot() - sim = cv.Sim(use_waning=True).run().plot() - - # Run more complex single-sim tests - sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - - # Run Vaccine tests - sim4 = test_synthpops() - sim5 = test_vaccine_1strain() - - # Run multisim and scenario tests - #scens0 = test_vaccine_1strain_scen() #TODO, NOT WORKING CURRENTLY - #scens1 = test_vaccine_2strains_scen() #TODO, NOT WORKING CURRENTLY - scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) - msim0 = test_msim() - - # Run immunity tests - sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) + # # Run simplest possible test + # if 1: + # sim = cv.Sim().run().plot() + # sim = cv.Sim(use_waning=True).run().plot() + # + # # Run more complex single-sim tests + # sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + # + # # Run Vaccine tests + # sim4 = test_synthpops() + # sim5 = test_vaccine_1strain() + # + # # Run multisim and scenario tests + # #scens0 = test_vaccine_1strain_scen() #TODO, NOT WORKING CURRENTLY + # #scens1 = test_vaccine_2strains_scen() #TODO, NOT WORKING CURRENTLY + # scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) + # msim0 = test_msim() + # + # # Run immunity tests + # sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) + + # Run test to compare sims with and without waning + scens3 = test_waning_vs_not(do_plot=do_plot, do_save=do_save, do_show=do_show) sc.toc() From d5065949903f99068d89847f0610749549f9535c Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Wed, 7 Apr 2021 22:35:53 +0200 Subject: [PATCH 392/569] investigating lack of reinfections --- tests/devtests/test_variants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 3287f4e8d..25f2f6b27 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -546,7 +546,7 @@ def get_ind_of_min_value(list, time): # # Run simplest possible test # if 1: # sim = cv.Sim().run().plot() - # sim = cv.Sim(use_waning=True).run().plot() + # sim = cv.Sim(n_days=300, use_waning=True).run().plot() # # # Run more complex single-sim tests # sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) @@ -568,7 +568,7 @@ def get_ind_of_min_value(list, time): # sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # Run test to compare sims with and without waning - scens3 = test_waning_vs_not(do_plot=do_plot, do_save=do_save, do_show=do_show) + # scens3 = test_waning_vs_not(do_plot=do_plot, do_save=do_save, do_show=do_show) sc.toc() From a5ee2965bda3d9475c1ebb993d5510484880b6a1 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 7 Apr 2021 17:18:41 -0400 Subject: [PATCH 393/569] rel imms for p1 and b1351 --- covasim/immunity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 4730b560e..fa797be5b 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -82,13 +82,13 @@ def parse_strain_pars(self, strain=None, strain_label=None): strain_pars['rel_beta'] = 1.4 strain_pars['rel_severe_prob'] = 1.4 strain_pars['rel_death_prob'] = 1.4 - strain_pars['rel_imm'] = 0.25 + strain_pars['rel_imm'] = 0.066 self.strain_label = strain # Known parameters on Brazil variant elif strain in choices['p1']: strain_pars = dict() - strain_pars['rel_imm'] = 0.5 + strain_pars['rel_imm'] = 0.17 strain_pars['rel_beta'] = 1.4 strain_pars['rel_severe_prob'] = 1.4 strain_pars['rel_death_prob'] = 2 From 197f0346271bd0d5f11fb854c07ee5a773c43653 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 8 Apr 2021 00:00:52 -0700 Subject: [PATCH 394/569] update parameter name --- covasim/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/misc.py b/covasim/misc.py index d2c2d9ae8..fb9eb101c 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -196,7 +196,7 @@ def migrate_strains(pars, verbose=True): Small helper function to add necessary strain parameters. ''' from . import parameters as cvp - pars['use_immunity'] = False + pars['use_waning'] = True pars['n_strains'] = 1 pars['total_strains'] = 1 pars['strains'] = [] From f41ec582385fb66643d954671e4df3de968794f1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 8 Apr 2021 22:37:40 -0700 Subject: [PATCH 395/569] fix tests --- covasim/misc.py | 5 +- tests/devtests/test_variants.py | 112 ++++++++++++++------------------ 2 files changed, 51 insertions(+), 66 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index fb9eb101c..503648233 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -195,13 +195,10 @@ def migrate_strains(pars, verbose=True): ''' Small helper function to add necessary strain parameters. ''' - from . import parameters as cvp - pars['use_waning'] = True + pars['use_waning'] = False pars['n_strains'] = 1 pars['total_strains'] = 1 pars['strains'] = [] - pars['strain_pars'] = {} - pars = cvp.listify_strain_pars(pars) return diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 25f2f6b27..17b0f9acd 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -1,29 +1,27 @@ import covasim as cv -import covasim.defaults as cvd import sciris as sc import matplotlib.pyplot as plt import numpy as np -import pandas as pd -import seaborn as sns do_plot = 1 do_show = 0 do_save = 1 +base_pars = dict( + pop_size = 10e3, + verbose = 0.01, +) + + def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): sc.heading('Test varying properties of immunity') sc.heading('Setting up...') # Define baseline parameters - base_pars = { - 'pop_size': 100000, - 'n_days': 400, - } - n_runs = 3 - base_sim = cv.Sim(use_waning=True, pars=base_pars) + base_sim = cv.Sim(use_waning=True, n_days=400, pars=base_pars) # Define the scenarios b1351 = cv.Strain('b1351', days=100, n_imports=20) @@ -87,7 +85,7 @@ def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): 'Population Immunity': ['pop_protection'], }) if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_basic_immunity.png', to_plot=to_plot) + scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_basic_immunity.png', to_plot=to_plot) return scens @@ -108,7 +106,7 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): 'beta': 0.01 } strain = cv.Strain(strain_pars, days=1, n_imports=20) - sim = cv.Sim(use_waning=True, pars=pars, strains=strain, analyzers=cv.snapshot(30, 60)) + sim = cv.Sim(use_waning=True, pars=pars, strains=strain, analyzers=cv.snapshot(30, 60), **pars, **base_pars) sim.run() return sim @@ -120,7 +118,7 @@ def test_import2strains(do_plot=False, do_show=True, do_save=False): b117 = cv.Strain('b117', days=1, n_imports=20) p1 = cv.Strain('sa variant', days=2, n_imports=20) - sim = cv.Sim(use_waning=True, strains=[b117, p1], label='With imported infections') + sim = cv.Sim(use_waning=True, strains=[b117, p1], label='With imported infections', **base_pars) sim.run() strain_labels = [ @@ -145,9 +143,9 @@ def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): 'Strain 2: beta 0.025' ] - pars = { + pars = sc.mergedicts(base_pars, { 'n_days': 120, - } + }) strain_pars = { 'rel_beta': 1.5, @@ -178,7 +176,7 @@ def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): strains = [cv.Strain(strain=strain2, days=10, n_imports=20), cv.Strain(strain=strain3, days=30, n_imports=20), ] - sim = cv.Sim(use_waning=True, interventions=intervs, strains=strains, label='With imported infections') + sim = cv.Sim(use_waning=True, interventions=intervs, strains=strains, label='With imported infections', **base_pars) sim.run() return sim @@ -191,10 +189,10 @@ def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): sc.heading('Test vaccination with a single strain') sc.heading('Setting up...') - pars = { + pars = sc.mergedicts(base_pars, { 'beta': 0.015, 'n_days': 120, - } + }) pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer') sim = cv.Sim( @@ -216,7 +214,7 @@ def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): def test_synthpops(): - sim = cv.Sim(use_waning=True, pop_size=5000, pop_type='synthpops') + sim = cv.Sim(use_waning=True, **sc.mergedicts(base_pars, dict(pop_size=5000, pop_type='synthpops'))) sim.popdict = cv.make_synthpop(sim, with_facilities=True, layer_mapping={'LTCF': 'f'}) sim.reset_layer_pars() @@ -241,10 +239,6 @@ def test_vaccine_1strain_scen(do_plot=True, do_show=True, do_save=False): sc.heading('Setting up...') # Define baseline parameters - base_pars = { - 'n_days': 200, - } - n_runs = 3 base_sim = cv.Sim(use_waning=True, pars=base_pars) @@ -281,7 +275,7 @@ def test_vaccine_1strain_scen(do_plot=True, do_show=True, do_save=False): # 'Cumulative reinfections': ['cum_reinfections'], }) if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_basic_vaccination.png', to_plot=to_plot) + scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_basic_vaccination.png', to_plot=to_plot) return scens @@ -292,10 +286,6 @@ def test_vaccine_2strains_scen(do_plot=True, do_show=True, do_save=False): sc.heading('Setting up...') # Define baseline parameters - base_pars = { - 'n_days': 250, - } - n_runs = 3 base_sim = cv.Sim(use_waning=True, pars=base_pars) @@ -357,14 +347,13 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): strains = cv.Strain(strain=strain_pars, strain_label='10 days til symptoms', days=10, n_imports=30) tp = cv.test_prob(symp_prob=0.2) # Add an efficient testing program - base_pars = { + pars = sc.mergedicts(base_pars, { 'beta': 0.015, # Make beta higher than usual so people get infected quickly 'n_days': 120, 'interventions': tp - } - + }) n_runs = 1 - base_sim = cv.Sim(use_waning=True, pars=base_pars) + base_sim = cv.Sim(use_waning=True, pars=pars) # Define the scenarios scenarios = { @@ -389,7 +378,7 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): 'Cumulative diagnoses': ['cum_diagnoses'], }) if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_strainduration.png', to_plot=to_plot) + scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_strainduration.png', to_plot=to_plot) return scens @@ -397,15 +386,15 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): def test_waning_vs_not(do_plot=True, do_show=True, do_save=False): # Define baseline parameters - base_pars = { - 'pop_size': 100e3, + pars = sc.mergedicts(base_pars, { + 'pop_size': 10e3, 'pop_scale': 50, 'n_days': 150, 'use_waning': False, - } + }) n_runs = 3 - base_sim = cv.Sim(pars=base_pars) + base_sim = cv.Sim(pars=pars) # Define the scenarios scenarios = { @@ -433,7 +422,7 @@ def test_waning_vs_not(do_plot=True, do_show=True, do_save=False): 'Cumulative reinfections': ['cum_reinfections'], }) if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_waning_vs_not.png', to_plot=to_plot) + scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_waning_vs_not.png', to_plot=to_plot) return scens @@ -441,7 +430,7 @@ def test_waning_vs_not(do_plot=True, do_show=True, do_save=False): def test_msim(): # basic test for vaccine b117 = cv.Strain('b117', days=0) - sim = cv.Sim(use_waning=True, strains=[b117]) + sim = cv.Sim(use_waning=True, strains=[b117], **base_pars) msim = cv.MultiSim(sim, n_runs=2) msim.run() msim.reduce() @@ -543,32 +532,31 @@ def get_ind_of_min_value(list, time): if __name__ == '__main__': sc.tic() - # # Run simplest possible test - # if 1: - # sim = cv.Sim().run().plot() - # sim = cv.Sim(n_days=300, use_waning=True).run().plot() - # - # # Run more complex single-sim tests - # sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - # - # # Run Vaccine tests - # sim4 = test_synthpops() - # sim5 = test_vaccine_1strain() - # - # # Run multisim and scenario tests - # #scens0 = test_vaccine_1strain_scen() #TODO, NOT WORKING CURRENTLY - # #scens1 = test_vaccine_2strains_scen() #TODO, NOT WORKING CURRENTLY - # scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) - # msim0 = test_msim() - # - # # Run immunity tests - # sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) + # Run simplest possible test + sim = cv.Sim().run().plot() + sim = cv.Sim(n_days=300, use_waning=True).run().plot() + + # Run more complex single-sim tests + sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + + # Run Vaccine tests + sim4 = test_synthpops() + sim5 = test_vaccine_1strain() + + # Run multisim and scenario tests + #scens0 = test_vaccine_1strain_scen() #TODO, NOT WORKING CURRENTLY + #scens1 = test_vaccine_2strains_scen() #TODO, NOT WORKING CURRENTLY + scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) + msim0 = test_msim() + + # Run immunity tests + sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) # Run test to compare sims with and without waning - # scens3 = test_waning_vs_not(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens3 = test_waning_vs_not(do_plot=do_plot, do_save=do_save, do_show=do_show) sc.toc() From 26df01c6f843479d770d028eb8f18089562543c2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 8 Apr 2021 23:47:57 -0700 Subject: [PATCH 396/569] added demo --- covasim/sim.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/covasim/sim.py b/covasim/sim.py index 2362c1196..e127c463c 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -18,7 +18,7 @@ from . import analysis as cva # Almost everything in this file is contained in the Sim class -__all__ = ['Sim', 'diff_sims', 'AlreadyRunError'] +__all__ = ['Sim', 'diff_sims', 'demo', 'AlreadyRunError'] class Sim(cvb.BaseSim): @@ -1411,6 +1411,76 @@ def diff_sims(sim1, sim2, skip_key_diffs=False, output=False, die=False): return +def demo(preset=None, overview=False, scens=None, run_args=None, plot_args=None, **kwargs): + ''' + Shortcut for ``cv.Sim().run().plot()``. + + Args: + preset (str): use a preset run configuration; currently the only option is "full" + overview (bool): whether to show the overview plot (all results) + scens (dict): dictionary of scenarios to run as a multisim, if preset='full' + kwargs (dict): passed to Sim() + run_args (dict): passed to sim.run() + plot_args (dict): passed to sim.plot() + + **Examples**:: + + cv.demo() # Simplest example + cv.demo('full') # Full example + cv.demo('full', overview=True) # Plot all results + cv.demo(beta=0.020, run_args={'verbose':0}, plot_args={'to_plot':'overview'}) # Pass in custom values + ''' + from . import interventions as cvi + from . import run as cvr + + run_args = sc.mergedicts(run_args) + plot_args = sc.mergedicts(plot_args) + if overview: + plot_args = sc.mergedicts(plot_args, {'to_plot':'overview'}) + + if not preset: + sim = Sim(**kwargs) + sim.run(**run_args) + sim.plot(**plot_args) + return sim + + elif preset == 'full': + + # Define interventions + cb = cvi.change_beta(days=40, changes=0.5) + tp = cvi.test_prob(start_day=20, symp_prob=0.1, asymp_prob=0.01) + ct = cvi.contact_tracing(trace_probs=0.3, start_day=50) + + # Define the parameters + pars = dict( + pop_size = 20e3, # Population size + pop_infected = 100, # Number of initial infections -- use more for increased robustness + pop_type = 'hybrid', # Population to use -- "hybrid" is random with household, school,and work structure + n_days = 60, # Number of days to simulate + verbose = 0, # Don't print details of the run + rand_seed = 2, # Set a non-default seed + interventions = [cb, tp, ct], # Include the most common interventions + ) + pars = sc.mergedicts(pars, kwargs) + if scens is None: + scens = ('beta', {'Low beta':0.012, 'Medium beta':0.016, 'High beta':0.020}) + scenpar = scens[0] + scenval = scens[1] + + # Run the simulations + sims = [Sim(pars, **{scenpar:val}, label=label) for label,val in scenval.items()] + msim = cvr.MultiSim(sims) + msim.run(**run_args) + msim.plot(**plot_args) + msim.median() + msim.plot(**plot_args) + return msim + + else: + errormsg = f'Could not understand preset argument "{preset}"; must be None or "full"' + raise NotImplementedError(errormsg) + + class AlreadyRunError(RuntimeError): ''' This error is raised if a simulation is run in such a way that no timesteps From 493dab948bcab94debfa7ce3266712bb87990fd2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 9 Apr 2021 00:09:45 -0700 Subject: [PATCH 397/569] added extra test --- covasim/defaults.py | 6 +++--- covasim/people.py | 13 ++++++------- tests/test_other.py | 3 +++ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 6d7dbcb66..800f96383 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -50,9 +50,9 @@ def __init__(self): 'death_prob', # Float 'rel_trans', # Float 'rel_sus', # Float - 'sus_imm', # Float - 'symp_imm', # Float - 'sev_imm', # Float + 'sus_imm', # Float, by strain + 'symp_imm', # Float, by strain + 'sev_imm', # Float, by strain 'prior_symptoms', # Float 'vaccinations', # Number of doses given per person 'vaccine_source', # index of vaccine that individual received diff --git a/covasim/people.py b/covasim/people.py index 11f0a5a46..7c550238f 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -49,6 +49,7 @@ def __init__(self, pars, strict=True, **kwargs): self.pars = pars # Equivalent to self.set_pars(pars) self.pop_size = int(pars['pop_size']) self.location = pars.get('location') # Try to get location, but set to None otherwise + self.total_strains = pars.get('total_strains', 1) # Assume 1 strain if not supplied self.version = cvv.__version__ # Store version info # Other initialization @@ -61,12 +62,10 @@ def __init__(self, pars, strict=True, **kwargs): # Set person properties -- all floats except for UID for key in self.meta.person: - if key == 'uid': + if key in ['uid', 'vaccinations']: self[key] = np.arange(self.pop_size, dtype=cvd.default_int) - elif 'imm' in key: # everyone starts out with no immunity - self[key] = np.full((self.pars['total_strains'], self.pop_size), 0, dtype=cvd.default_float) - elif key == 'vaccinations': - self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) + elif key in ['sus_imm', 'symp_imm', 'sev_imm']: # everyone starts out with no immunity + self[key] = np.full((self.total_strains, self.pop_size), 0, dtype=cvd.default_float) else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -80,7 +79,7 @@ def __init__(self, pars, strict=True, **kwargs): # Set strain states, which store info about which strain a person is exposed to for key in self.meta.strain_states: if 'by' in key: - self[key] = np.full((self.pars['total_strains'], self.pop_size), False, dtype=bool) + self[key] = np.full((self.total_strains, self.pop_size), False, dtype=bool) else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) @@ -96,7 +95,7 @@ def __init__(self, pars, strict=True, **kwargs): self.flows = {key:0 for key in cvd.new_result_flows} self.flows_strain = {} for key in cvd.new_result_flows_by_strain: - self.flows_strain[key] = np.full(self.pars['total_strains'], 0, dtype=cvd.default_float) + self.flows_strain[key] = np.full(self.total_strains, 0, dtype=cvd.default_float) # Although we have called init(), we still need to call initialize() self.initialized = False diff --git a/tests/test_other.py b/tests/test_other.py index b43f21fca..53f6d9f8d 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -146,6 +146,9 @@ def test_basepeople(): s2.run() assert cv.diff_sims(s1, s2, output=True) + # Create a bare People object + ppl = cv.People(100) + return From ed8924320e8f7b64ee15081a7b837ed80a47f683 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 9 Apr 2021 00:13:43 -0700 Subject: [PATCH 398/569] update readme --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index ad0e3d2d5..5b42ecde5 100644 --- a/README.rst +++ b/README.rst @@ -40,11 +40,11 @@ Covasim has been used for analyses in over a dozen countries, both to inform pol 4. **Lessons learned from Vietnam's COVID-19 response: the role of adaptive behaviour change and testing in epidemic control**. Pham QD, Stuart RM, Nguyen TV, Luong QC, Tran DQ, Phan LT, Dang TQ, Tran DN, Mistry D, Klein DJ, Abeysuriya RG, Oron AP, Kerr CC (in press; accepted 2021-02-25). *Lancet Global Health*; doi: https://doi.org/10.1101/2020.12.18.20248454. -5. **The role of masks, testing and contact tracing in preventing COVID-19 resurgences: a case study from New South Wales, Australia**. Stuart RM, Abeysuriya RG, Kerr CC, Mistry D, Klein DJ, Gray R, Hellard M, Scott N (under review; posted 2020-09-03). *medRxiv* 2020.09.02.20186742; doi: https://doi.org/10.1101/2020.09.02.20186742. +5. **The potential contribution of face coverings to the control of SARS-CoV-2 transmission in schools and broader society in the UK: a modelling study**. Panovska-Griffiths J, Kerr CC, Waites W, Stuart RM, Mistry D, Foster D, Klein DJ, Viner R, Bonnell C (in press; accepted 2021-04-08). *Nature Scientific Reports* doi: https://doi.org/10.1101/2020.09.28.20202937. -6. **Schools are not islands: Balancing COVID-19 risk and educational benefits using structural and temporal countermeasures**. Cohen JA, Mistry D, Kerr CC, Klein DJ (under review; posted 2020-09-10). *medRxiv* 2020.09.08.20190942; doi: https://doi.org/10.1101/2020.09.08.20190942. +6. **The role of masks, testing and contact tracing in preventing COVID-19 resurgences: a case study from New South Wales, Australia**. Stuart RM, Abeysuriya RG, Kerr CC, Mistry D, Klein DJ, Gray R, Hellard M, Scott N (under review; posted 2020-09-03). *medRxiv* 2020.09.02.20186742; doi: https://doi.org/10.1101/2020.09.02.20186742. -7. **The potential contribution of face coverings to the control of SARS-CoV-2 transmission in schools and broader society in the UK: a modelling study**. Panovska-Griffiths J, Kerr CC, Waites W, Stuart RM, Mistry D, Foster D, Klein DJ, Viner R, Bonnell C (under review; posted 2020-10-08). *medRxiv* 2020.09.28.20202937; doi: https://doi.org/10.1101/2020.09.28.20202937. +7. **Schools are not islands: Balancing COVID-19 risk and educational benefits using structural and temporal countermeasures**. Cohen JA, Mistry D, Kerr CC, Klein DJ (under review; posted 2020-09-10). *medRxiv* 2020.09.08.20190942; doi: https://doi.org/10.1101/2020.09.08.20190942. 8. **COVID-19 reopening strategies at the county level in the face of uncertainty: Multiple Models for Outbreak Decision Support**. Shea K, Borchering RK, Probert WJM, et al. (under review; posted 2020-11-05). *medRxiv* 2020.11.03.20225409; doi: https://doi.org/10.1101/2020.11.03.20225409. From d9a6cdca78726913f6dd871cc93cc3c6ac4a617c Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 9 Apr 2021 22:13:14 -0400 Subject: [PATCH 399/569] manaus updates --- covasim/immunity.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index fa797be5b..ae03fb9f8 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -195,8 +195,9 @@ def init_strain_vaccine_info(self): rel_imm['moderna']['b1351'] = 1/4.5 rel_imm['moderna']['p1'] = 1/8.6 - rel_imm['az']['b1351'] = .5 - rel_imm['az']['p1'] = .5 + rel_imm['az']['b117'] = 1/2.3 + rel_imm['az']['b1351'] = 1/9 + rel_imm['az']['p1'] = 1/2.9 rel_imm['j&j']['b1351'] = 1/6.7 rel_imm['j&j']['p1'] = 1/8.6 @@ -221,7 +222,7 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on pfizer if vaccine in choices['pfizer']: vaccine_pars = dict() - vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2= 2) + vaccine_pars['NAb_init'] = dict(dist='normal', par1=2, par2= 2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['NAb_boost'] = 2 @@ -230,7 +231,7 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on moderna elif vaccine in choices['moderna']: vaccine_pars = dict() - vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) + vaccine_pars['NAb_init'] = dict(dist='normal', par1=2, par2=2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 29 vaccine_pars['NAb_boost'] = 2 @@ -239,7 +240,7 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on az elif vaccine in choices['az']: vaccine_pars = dict() - vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) + vaccine_pars['NAb_init'] = dict(dist='normal', par1=-1, par2=2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 vaccine_pars['NAb_boost'] = 2 @@ -248,7 +249,7 @@ def parse_vaccine_pars(self, vaccine=None): # Known parameters on j&j elif vaccine in choices['j&j']: vaccine_pars = dict() - vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) + vaccine_pars['NAb_init'] = dict(dist='normal', par1=-1, par2=2) vaccine_pars['doses'] = 1 vaccine_pars['interval'] = None vaccine_pars['NAb_boost'] = 2 From 480480c2ea59c14ed908a525f91c190475445831 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 14:38:07 -0700 Subject: [PATCH 400/569] fix people initialization --- covasim/people.py | 4 ++-- tests/test_other.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index 7c550238f..3515d4b41 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -130,8 +130,8 @@ def set_prognoses(self): ''' pars = self.pars # Shorten - if 'prognoses' not in pars: - errormsg = 'This people object does not have the required parameters ("prognoses"). Create a sim (or parameters), then do e.g. people.set_pars(sim.pars).' + if 'prognoses' not in pars or 'rand_seed' not in pars: + errormsg = 'This people object does not have the required parameters ("prognoses" and "rand_seed"). Create a sim (or parameters), then do e.g. people.set_pars(sim.pars).' raise sc.KeyNotFoundError(errormsg) def find_cutoff(age_cutoffs, age): diff --git a/tests/test_other.py b/tests/test_other.py index 53f6d9f8d..8ab3ba455 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -148,6 +148,8 @@ def test_basepeople(): # Create a bare People object ppl = cv.People(100) + with pytest.raises(sc.KeyNotFoundError): # Need additional parameters + ppl.initialize() return From f478bb44d939b97503732bf3921acb328c3a2ee7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 14:51:25 -0700 Subject: [PATCH 401/569] slight refactoring --- covasim/misc.py | 102 ++++++++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index 503648233..801efe29a 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -23,7 +23,7 @@ #%% Loading/saving functions -__all__ += ['load_data', 'load', 'save', 'migrate', 'savefig'] +__all__ += ['load_data', 'load', 'save', 'savefig'] def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=True, start_day=None, **kwargs): @@ -154,6 +154,60 @@ def save(*args, **kwargs): return filepath +def savefig(filename=None, comments=None, **kwargs): + ''' + Wrapper for Matplotlib's savefig() function which automatically stores Covasim + metadata in the figure. By default, saves (git) information from both the Covasim + version and the calling function. Additional comments can be added to the saved + file as well. These can be retrieved via cv.get_png_metadata(). Metadata can + also be stored for SVG and PDF formats, but cannot be automatically retrieved. + + Args: + filename (str): name of the file to save to (default, timestamp) + comments (str): additional metadata to save to the figure + kwargs (dict): passed to savefig() + + **Example**:: + + cv.Sim().run(do_plot=True) + filename = cv.savefig() + ''' + + # Handle inputs + dpi = kwargs.pop('dpi', 150) + metadata = kwargs.pop('metadata', {}) + + if filename is None: # pragma: no cover + now = sc.getdate(dateformat='%Y-%b-%d_%H.%M.%S') + filename = f'covasim_{now}.png' + + metadata = {} + metadata['Covasim version'] = cvv.__version__ + gitinfo = git_info() + for key,value in gitinfo['covasim'].items(): + metadata[f'Covasim {key}'] = value + for key,value in gitinfo['called_by'].items(): + metadata[f'Covasim caller {key}'] = value + metadata['Covasim current time'] = sc.getdate() + metadata['Covasim calling file'] = sc.getcaller() + if comments: + metadata['Covasim comments'] = comments + + # Handle different formats + lcfn = filename.lower() # Lowercase filename + if lcfn.endswith('pdf') or lcfn.endswith('svg'): + metadata = {'Keywords':str(metadata)} # PDF and SVG doesn't support storing a dict + + # Save the figure + pl.savefig(filename, dpi=dpi, metadata=metadata, **kwargs) + return filename + + + +#%% Migration functions + +__all__ += ['migrate'] + def migrate_lognormal(pars, revert=False, verbose=True): ''' Small helper function to automatically migrate the standard deviation of lognormal @@ -313,53 +367,7 @@ def migrate(obj, update=True, verbose=True, die=False): return obj -def savefig(filename=None, comments=None, **kwargs): - ''' - Wrapper for Matplotlib's savefig() function which automatically stores Covasim - metadata in the figure. By default, saves (git) information from both the Covasim - version and the calling function. Additional comments can be added to the saved - file as well. These can be retrieved via cv.get_png_metadata(). Metadata can - also be stored for SVG and PDF formats, but cannot be automatically retrieved. - - Args: - filename (str): name of the file to save to (default, timestamp) - comments (str): additional metadata to save to the figure - kwargs (dict): passed to savefig() - - **Example**:: - - cv.Sim().run(do_plot=True) - filename = cv.savefig() - ''' - - # Handle inputs - dpi = kwargs.pop('dpi', 150) - metadata = kwargs.pop('metadata', {}) - if filename is None: # pragma: no cover - now = sc.getdate(dateformat='%Y-%b-%d_%H.%M.%S') - filename = f'covasim_{now}.png' - - metadata = {} - metadata['Covasim version'] = cvv.__version__ - gitinfo = git_info() - for key,value in gitinfo['covasim'].items(): - metadata[f'Covasim {key}'] = value - for key,value in gitinfo['called_by'].items(): - metadata[f'Covasim caller {key}'] = value - metadata['Covasim current time'] = sc.getdate() - metadata['Covasim calling file'] = sc.getcaller() - if comments: - metadata['Covasim comments'] = comments - - # Handle different formats - lcfn = filename.lower() # Lowercase filename - if lcfn.endswith('pdf') or lcfn.endswith('svg'): - metadata = {'Keywords':str(metadata)} # PDF and SVG doesn't support storing a dict - - # Save the figure - pl.savefig(filename, dpi=dpi, metadata=metadata, **kwargs) - return filename From 1d7698f2e2579407c85bc0ee8822b0177de9d29c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 14:59:03 -0700 Subject: [PATCH 402/569] fix spacing --- covasim/misc.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index 801efe29a..6f2c48889 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -203,7 +203,6 @@ def savefig(filename=None, comments=None, **kwargs): return filename - #%% Migration functions __all__ += ['migrate'] @@ -368,9 +367,6 @@ def migrate(obj, update=True, verbose=True, die=False): - - - #%% Versioning functions __all__ += ['git_info', 'check_version', 'check_save_version', 'get_version_pars', 'get_png_metadata'] @@ -586,6 +582,7 @@ def get_png_metadata(filename, output=False): return + #%% Simulation/statistics functions __all__ += ['get_doubling_time', 'poisson_test', 'compute_gof'] From d364e9b258955f421c627ba2eb1d22a919e2c7ba Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 18:17:11 -0700 Subject: [PATCH 403/569] refactoring --- covasim/defaults.py | 63 +++++++------- covasim/immunity.py | 148 +++++++++++++++++--------------- covasim/people.py | 14 ++- covasim/sim.py | 21 ++--- tests/devtests/test_variants.py | 35 +------- 5 files changed, 129 insertions(+), 152 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 800f96383..1a803a212 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -166,20 +166,19 @@ def __init__(self): 'pop_protection': 'Population average protective immunity' } -# Define these here as well +# Define new and cumulative flows new_result_flows = [f'new_{key}' for key in result_flows.keys()] cum_result_flows = [f'cum_{key}' for key in result_flows.keys()] new_result_flows_by_strain = [f'new_{key}' for key in result_flows_by_strain.keys()] cum_result_flows_by_strain = [f'cum_{key}' for key in result_flows_by_strain.keys()] -# Parameters that can vary by strain (should be in list format) -strain_pars = ['rel_beta', - 'asymp_factor', - 'dur', - 'rel_symp_prob', - 'rel_severe_prob', - 'rel_crit_prob', - 'rel_death_prob', +# Parameters that can vary by strain +strain_pars = [ + 'rel_beta', + 'rel_symp_prob', + 'rel_severe_prob', + 'rel_crit_prob', + 'rel_death_prob', ] # Immunity is broken down according to 3 axes, as listed here @@ -194,26 +193,26 @@ def __init__(self): # Default age data, based on Seattle 2018 census data -- used in population.py default_age_data = np.array([ - [ 0, 4, 0.0605], - [ 5, 9, 0.0607], - [10, 14, 0.0566], - [15, 19, 0.0557], - [20, 24, 0.0612], - [25, 29, 0.0843], - [30, 34, 0.0848], - [35, 39, 0.0764], - [40, 44, 0.0697], - [45, 49, 0.0701], - [50, 54, 0.0681], - [55, 59, 0.0653], - [60, 64, 0.0591], - [65, 69, 0.0453], - [70, 74, 0.0312], - [75, 79, 0.02016], # Calculated based on 0.0504 total for >=75 - [80, 84, 0.01344], - [85, 89, 0.01008], - [90, 99, 0.00672], - ]) + [ 0, 4, 0.0605], + [ 5, 9, 0.0607], + [10, 14, 0.0566], + [15, 19, 0.0557], + [20, 24, 0.0612], + [25, 29, 0.0843], + [30, 34, 0.0848], + [35, 39, 0.0764], + [40, 44, 0.0697], + [45, 49, 0.0701], + [50, 54, 0.0681], + [55, 59, 0.0653], + [60, 64, 0.0591], + [65, 69, 0.0453], + [70, 74, 0.0312], + [75, 79, 0.02016], # Calculated based on 0.0504 total for >=75 + [80, 84, 0.01344], + [85, 89, 0.01008], + [90, 99, 0.00672], +]) def get_colors(): @@ -227,7 +226,7 @@ def get_colors(): c.exposed = '#c78f65' c.exposed_by_strain = '#c75649', c.infectious = '#e45226' - c.infectious_by_strain = '#e45226' + c.infectious_by_strain = c.infectious c.infections = '#b62413' c.reinfections = '#732e26' c.infections_by_strain = '#b62413' @@ -235,8 +234,8 @@ def get_colors(): c.diagnoses = '#5f5cd2' c.diagnosed = c.diagnoses c.quarantined = '#5c399c' - c.vaccinations = '#5c399c' - c.vaccinated = '#5c399c' + c.vaccinations = c.quarantined # TODO: new color + c.vaccinated = c.quarantined c.recoveries = '#9e1149' c.recovered = c.recoveries c.symptomatic = '#c1ad71' diff --git a/covasim/immunity.py b/covasim/immunity.py index 8188f04d7..79368d6a8 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -6,6 +6,7 @@ import sciris as sc from . import utils as cvu from . import defaults as cvd +from . import interventions as cvi # %% Define strain class @@ -13,7 +14,7 @@ __all__ = ['Strain', 'Vaccine'] -class Strain(): +class Strain(cvi.Intervention): ''' Add a new strain to the sim @@ -21,125 +22,135 @@ class Strain(): day (int): day on which new variant is introduced. n_imports (int): the number of imports of the strain to be added strain (dict): dictionary of parameters specifying information about the strain - kwargs (dict): + kwargs (dict): passed to Intervention() **Example**:: b117 = cv.Strain('b117', days=10) # Make strain B117 active from day 10 p1 = cv.Strain('p1', days=15) # Make strain P1 active from day 15 # Make a custom strain active from day 20 - my_var = cv.Strain(strain={'rel_beta': 2.5}, strain_label='My strain', days=20) + my_var = cv.Strain(strain={'rel_beta': 2.5}, label='My strain', days=20) sim = cv.Sim(strains=[b117, p1, my_var]) # Add them all to the sim ''' - def __init__(self, strain=None, strain_label=None, days=None, n_imports=1, **kwargs): + def __init__(self, strain=None, label=None, days=None, n_imports=1, rescale=True, **kwargs): + super().__init__(**kwargs) # Initialize the Intervention object # Handle inputs self.days = days self.n_imports = cvd.default_int(n_imports) # Strains can be defined in different ways: process these here - self.strain_pars = self.parse_strain_pars(strain=strain, strain_label=strain_label) + self.strain_pars = self.parse_strain_pars(strain=strain, label=label) for par, val in self.strain_pars.items(): setattr(self, par, val) return - def parse_strain_pars(self, strain=None, strain_label=None): + + def parse_strain_pars(self, strain=None, label=None): ''' Unpack strain information, which may be given in different ways''' + # List of choices currently available: new ones can be added to the list along with their aliases + choices = { + 'wild': ['wild', 'default', 'pre-existing', 'original'], + 'b117': ['b117', 'uk'], + 'b1351': ['b1351', 'sa'], + 'p1': ['p1', 'b11248', 'brazil'], + } + choicestr = sc.newlinejoin(sc.mergelists(*choices.values())) + reversemap = {name:key for key,synonyms in choices.items() for name in synonyms} # Flip from key:value to value:key + + mapping = dict( # TODO: move to parameters.py + wild = dict(), + + b117 = dict( + rel_beta = 1.5, # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 + rel_severe_prob = 1.8, # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf + ), + + b1351 = dict( + rel_imm = 0.25, + rel_beta = 1.4, + rel_severe_prob = 1.4, + rel_death_prob = 1.4, + ), + + p1 = dict( + rel_imm = 0.5, + rel_beta = 1.4, + rel_severe_prob = 1.4, + rel_death_prob = 2, + ) + ) + # Option 1: strains can be chosen from a list of pre-defined strains if isinstance(strain, str): - # List of choices currently available: new ones can be added to the list along with their aliases - choices = { - 'wild': ['default', 'wild', 'pre-existing'], - 'b117': ['b117', 'B117', 'B.1.1.7', 'UK', 'uk', 'UK variant', 'uk variant'], - 'b1351': ['b1351', 'B1351', 'B.1.351', 'SA', 'sa', 'SA variant', 'sa variant'], - 'p1': ['p1', 'P1', 'P.1', 'B.1.1.248', 'b11248', 'Brazil', 'Brazil variant', 'brazil variant'], - } - - # Empty pardict for wild strain - if strain in choices['wild']: - strain_pars = dict() - self.strain_label = strain - - # Known parameters on B117 - elif strain in choices['b117']: - strain_pars = dict() - strain_pars['rel_beta'] = 1.5 # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 - strain_pars['rel_severe_prob'] = 1.8 # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf - self.strain_label = strain - - # Known parameters on South African variant - elif strain in choices['b1351']: - strain_pars = dict() - strain_pars['rel_beta'] = 1.4 - strain_pars['rel_severe_prob'] = 1.4 - strain_pars['rel_death_prob'] = 1.4 - strain_pars['rel_imm'] = 0.25 - self.strain_label = strain - - # Known parameters on Brazil variant - elif strain in choices['p1']: - strain_pars = dict() - strain_pars['rel_imm'] = 0.5 - strain_pars['rel_beta'] = 1.4 - strain_pars['rel_severe_prob'] = 1.4 - strain_pars['rel_death_prob'] = 2 - self.strain_label = strain + # Normalize input: lowrcase and remove + normstrain = strain.lower() + for txt in ['.', ' ', 'strain', 'variant', 'voc']: + normstrain = normstrain.replace(txt, '') + if normstrain in reversemap: + strain_pars = mapping[reversemap[normstrain]] else: - choicestr = '\n'.join(choices.values()) - errormsg = f'The selected variant "{strain}" is not implemented; choices are: {choicestr}' + errormsg = f'The selected variant "{strain}" is not implemented; choices are:\n{choicestr}' raise NotImplementedError(errormsg) # Option 2: strains can be specified as a dict of pars elif isinstance(strain, dict): strain_pars = strain - self.strain_label = strain_label + if label is None: + label = 'Custom strain' else: - errormsg = f'Could not understand {type(strain)}, please specify as a string indexing a predefined strain or a dict.' + errormsg = f'Could not understand {type(strain)}, please specify as a dict or a predefined strain:\n{choicestr}' raise ValueError(errormsg) + # Set label + self.label = label if label else normstrain + return strain_pars + def initialize(self, sim): - if not hasattr(self, 'rel_imm'): + super().initialize() + + if not hasattr(self, 'rel_imm'): # TODO: refactor self.rel_imm = 1 # Update strain info for strain_key in cvd.strain_pars: - if hasattr(self, strain_key): + if hasattr(self, strain_key): # TODO: refactor newval = getattr(self, strain_key) - if strain_key == 'dur': # Validate durations (make sure there are values for all durations) - newval = sc.mergenested(sim['strain_pars'][strain_key][0], newval) sim['strain_pars'][strain_key].append(newval) else: # use default print(f'{strain_key} not provided for this strain, using default value') sim['strain_pars'][strain_key].append(sim['strain_pars'][strain_key][0]) - self.initialized = True + return + def apply(self, sim): - if sim.t == self.days: # Time to introduce strain + if sim.t == self.days: # Time to introduce strain # TODO: use find_day # Check number of strains - prev_strains = sim['n_strains'] + prev_strains = sim['n_strains'] # TODO: refactor to be explicit sim['n_strains'] += 1 # Update strain-specific people attributes #cvu.update_strain_attributes(sim.people) # don't think we need to do this if we just create people arrays with number of total strains in sim susceptible_inds = cvu.true(sim.people.susceptible) - importation_inds = np.random.choice(susceptible_inds, self.n_imports) + n_imports = sc.randround(self.n_imports/sim.rescale_vec[sim.t]) # Round stochastically to the nearest number of imports + importation_inds = np.random.choice(susceptible_inds, n_imports) sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) return -class Vaccine(): +class Vaccine(cvi.Intervention): ''' Add a new vaccine to the sim (called by interventions.py vaccinate() @@ -147,7 +158,7 @@ class Vaccine(): Args: vaccine (dict or str): dictionary of parameters specifying information about the vaccine or label for loading pre-defined vaccine - kwargs (dict): + kwargs (dict): passed to Intervention() **Example**:: @@ -159,8 +170,8 @@ class Vaccine(): sim = cv.Sim(interventions=interventions) ''' - def __init__(self, vaccine=None): - + def __init__(self, vaccine=None, **kwargs): + super().__init__(**kwargs) self.rel_imm = None # list of length total_strains with relative immunity factor self.doses = None self.interval = None @@ -265,18 +276,19 @@ def parse_vaccine_pars(self, vaccine=None): return vaccine_pars def initialize(self, sim): + super().initialize() ts = sim['total_strains'] circulating_strains = ['wild'] # assume wild is circulating for strain in range(ts-1): - circulating_strains.append(sim['strains'][strain].strain_label) + circulating_strains.append(sim['strains'][strain].label) if self.NAb_init is None : - errormsg = f'Did not provide parameters for this vaccine' + errormsg = 'Did not provide parameters for this vaccine' raise ValueError(errormsg) if self.rel_imm is None: - print(f'Did not provide rel_imm parameters for this vaccine, trying to find values') + print('Did not provide rel_imm parameters for this vaccine, trying to find values') self.rel_imm = [] for strain in circulating_strains: if strain in self.vaccine_strain_info['known_strains']: @@ -286,7 +298,7 @@ def initialize(self, sim): correct_size = len(self.rel_imm) == ts if not correct_size: - errormsg = f'Did not provide relative immunity for each strain' + errormsg = 'Did not provide relative immunity for each strain' raise ValueError(errormsg) return @@ -294,7 +306,6 @@ def initialize(self, sim): # %% NAb methods -__all__ += ['init_nab', 'check_nab', 'nab_to_efficacy'] def init_nab(people, inds, prior_inf=True): ''' @@ -310,7 +321,7 @@ def init_nab(people, inds, prior_inf=True): prior_NAb_inds = cvu.idefined(NAb_arrays, inds) # Find people with prior NAbs no_prior_NAb_inds = np.setdiff1d(inds, prior_NAb_inds) # Find people without prior NAbs - prior_NAb = people.NAb[prior_NAb_inds] # Array of NAb levels on this timestep for people with some NAbs + # prior_NAb = people.NAb[prior_NAb_inds] # Array of NAb levels on this timestep for people with some NAbs peak_NAb = people.init_NAb[prior_NAb_inds] # NAbs from infection @@ -393,7 +404,6 @@ def nab_to_efficacy(nab, ax, function_args): # %% Immunity methods -__all__ += ['init_immunity', 'check_immunity'] def update_strain_attributes(people): @@ -428,8 +438,8 @@ def init_immunity(sim, create=False): circulating_strains = ['wild'] rel_imms = dict() for strain in sim['strains']: - circulating_strains.append(strain.strain_label) - rel_imms[strain.strain_label] = strain.rel_imm + circulating_strains.append(strain.label) + rel_imms[strain.label] = strain.rel_imm # If immunity values have been provided, process them if sim['immunity'] is None or create: @@ -555,7 +565,7 @@ def check_immunity(people, strain, sus=True, inds=None): # %% Methods for computing waning -__all__ += ['pre_compute_waning'] +# __all__ += ['pre_compute_waning'] def pre_compute_waning(length, form='nab_decay', pars=None): ''' diff --git a/covasim/people.py b/covasim/people.py index 3515d4b41..988282fa4 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -399,16 +399,14 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str cvi.check_immunity(self, strain, sus=False, inds=inds) # Deal with strain parameters - infect_parkeys = ['dur', 'rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob'] - if not strain: # Use defaults for wild type - infect_pars = self.pars - else: - infect_pars = dict() - for key in infect_parkeys: - infect_pars[key] = self.pars['strain_pars'][key][strain] + strain_keys = ['rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob'] + infect_pars = {k:self.pars[k] for k in strain_keys} + if strain: + for k in strain_keys: + infect_pars[k] *= self.pars['strain_pars'][k][strain] n_infections = len(inds) - durpars = infect_pars['dur'] + durpars = self.pars['dur'] # Update states, strain info, and flows self.susceptible[inds] = False diff --git a/covasim/sim.py b/covasim/sim.py index e127c463c..05de36c14 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -412,7 +412,7 @@ def init_people(self, save_pop=False, load_pop=False, popfile=None, reset=False, self.people.initialize() # Fully initialize the people # Create the seed infections - pop_infected_per_strain = cvd.default_int(self['pop_infected']/self['n_strains']) + pop_infected_per_strain = cvd.default_int(self['pop_infected']/self['n_strains']) # TODO: refactor for strain in range(self['n_strains']): inds = cvu.choose(self['pop_size'], pop_infected_per_strain) self.people.infect(inds=inds, layer='seed_infection', strain=strain) @@ -608,12 +608,10 @@ def step(self): cvimm.check_immunity(people, strain, sus=True) # Deal with strain parameters - if not strain: - rel_beta = self['rel_beta'] - asymp_factor = self['asymp_factor'] - else: - rel_beta = self['strain_pars']['rel_beta'][strain] - asymp_factor = cvd.default_float(self['strain_pars']['asymp_factor'][strain]) + rel_beta = self['rel_beta'] + asymp_factor = self['asymp_factor'] + if strain: + rel_beta *= self['strain_pars']['rel_beta'][strain] beta = cvd.default_float(self['beta'] * rel_beta) for lkey, layer in contacts.items(): @@ -622,13 +620,12 @@ def step(self): betas = layer['beta'] # Compute relative transmission and susceptibility - inf_by_this_strain = people.infectious * (people.infectious_strain == strain)#people.infectious + inf_strain = people.infectious * (people.infectious_strain == strain) # TODO: move out of loop? sus_imm = people.sus_imm[strain,:] - iso_factor = cvd.default_float(self['iso_factor'][lkey]) + iso_factor = cvd.default_float(self['iso_factor'][lkey]) quar_factor = cvd.default_float(self['quar_factor'][lkey]) - beta_layer = cvd.default_float(self['beta_layer'][lkey]) - rel_trans, rel_sus = cvu.compute_trans_sus(prel_trans, prel_sus, inf_by_this_strain, sus, beta_layer, viral_load, symp, - diag, quar, asymp_factor, iso_factor, quar_factor, sus_imm) + beta_layer = cvd.default_float(self['beta_layer'][lkey]) + rel_trans, rel_sus = cvu.compute_trans_sus(prel_trans, prel_sus, inf_strain, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, sus_imm) # Calculate actual transmission for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 17b0f9acd..72199f95d 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -92,12 +92,6 @@ def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): def test_import1strain(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing a new strain partway through a sim') - sc.heading('Setting up...') - - strain_labels = [ - 'Strain 1', - 'Strain 2: 1.5x more transmissible' - ] strain_pars = { 'rel_beta': 1.5, @@ -105,7 +99,7 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): pars = { 'beta': 0.01 } - strain = cv.Strain(strain_pars, days=1, n_imports=20) + strain = cv.Strain(strain_pars, days=1, n_imports=20, label='Strain 2: 1.5x more transmissible') sim = cv.Sim(use_waning=True, pars=pars, strains=strain, analyzers=cv.snapshot(30, 60), **pars, **base_pars) sim.run() @@ -114,34 +108,17 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): def test_import2strains(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing 2 new strains partway through a sim') - sc.heading('Setting up...') b117 = cv.Strain('b117', days=1, n_imports=20) p1 = cv.Strain('sa variant', days=2, n_imports=20) sim = cv.Sim(use_waning=True, strains=[b117, p1], label='With imported infections', **base_pars) sim.run() - strain_labels = [ - 'Strain 1: Wild Type', - 'Strain 2: UK Variant on day 10', - 'Strain 3: SA Variant on day 30' - ] - - # if do_plot: - # plot_results(sim, key='incidence_by_strain', title='Imported strains', filename='test_importstrain2', labels=strain_labels, do_show=do_show, do_save=do_save) - # plot_shares(sim, key='new_infections', title='Shares of new infections by strain', - # filename='test_importstrain2_shares', do_show=do_show, do_save=do_save) return sim def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing a new strain with longer duration partway through a sim') - sc.heading('Setting up...') - - strain_labels = [ - 'Strain 1: beta 0.016', - 'Strain 2: beta 0.025' - ] pars = sc.mergedicts(base_pars, { 'n_days': 120, @@ -152,19 +129,15 @@ def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} } - strain = cv.Strain(strain=strain_pars, strain_label='Custom strain', days=10, n_imports=30) + strain = cv.Strain(strain=strain_pars, label='Custom strain', days=10, n_imports=30) sim = cv.Sim(use_waning=True, pars=pars, strains=strain, label='With imported infections') sim.run() - # if do_plot: - # plot_results(sim, key='incidence_by_strain', title='Imported strain on day 30 (longer duration)', filename='test_importstrain1', labels=strain_labels, do_show=do_show, do_save=do_save) - # plot_shares(sim, key='new_infections', title='Shares of new infections by strain', filename='test_importstrain_longerdur_shares', do_show=do_show, do_save=do_save) return sim def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing 2 new strains partway through a sim, with a change_beta intervention') - sc.heading('Setting up...') strain2 = {'rel_beta': 1.5, 'rel_severe_prob': 1.3} @@ -334,7 +307,7 @@ def test_vaccine_2strains_scen(do_plot=True, do_show=True, do_save=False): # 'Cumulative reinfections': ['cum_reinfections'], }) if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_vaccine_b1351.png', to_plot=to_plot) + scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_vaccine_b1351.png', to_plot=to_plot) return scens @@ -344,7 +317,7 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): sc.heading('Setting up...') strain_pars = {'dur':{'inf2sym': {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9}}} - strains = cv.Strain(strain=strain_pars, strain_label='10 days til symptoms', days=10, n_imports=30) + strains = cv.Strain(strain=strain_pars, label='10 days til symptoms', days=10, n_imports=30) tp = cv.test_prob(symp_prob=0.2) # Add an efficient testing program pars = sc.mergedicts(base_pars, { From dbced0f064e1741529deb10b65e9bb4624c21e6b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 18:36:59 -0700 Subject: [PATCH 404/569] add note --- covasim/immunity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 79368d6a8..e45437467 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -19,7 +19,7 @@ class Strain(cvi.Intervention): Add a new strain to the sim Args: - day (int): day on which new variant is introduced. + day (int): day on which new variant is introduced. # TODO: update with correct name and find_day n_imports (int): the number of imports of the strain to be added strain (dict): dictionary of parameters specifying information about the strain kwargs (dict): passed to Intervention() From e5f36c1783a867bd72307b3facbd389dd854279e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 18:44:26 -0700 Subject: [PATCH 405/569] a little confused --- covasim/sim.py | 2 ++ tests/devtests/test_variants.py | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 05de36c14..6b422a111 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -478,6 +478,8 @@ def init_strains(self): for strain in self['strains']: if isinstance(strain, cvimm.Strain): strain.initialize(self) + else: + raise Exception('Wrong type') # TODO: refactor # Calculate the total number of strains that will be active at some point in the sim self['total_strains'] = self['n_strains'] + len(self['strains']) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 72199f95d..f986e5cfe 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -256,8 +256,6 @@ def test_vaccine_1strain_scen(do_plot=True, do_show=True, do_save=False): def test_vaccine_2strains_scen(do_plot=True, do_show=True, do_save=False): sc.heading('Run a basic sim with b117 strain on day 10, pfizer vaccine day 20') - sc.heading('Setting up...') - # Define baseline parameters n_runs = 3 base_sim = cv.Sim(use_waning=True, pars=base_pars) @@ -314,7 +312,6 @@ def test_vaccine_2strains_scen(do_plot=True, do_show=True, do_save=False): def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') - sc.heading('Setting up...') strain_pars = {'dur':{'inf2sym': {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9}}} strains = cv.Strain(strain=strain_pars, label='10 days til symptoms', days=10, n_imports=30) @@ -357,6 +354,7 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): def test_waning_vs_not(do_plot=True, do_show=True, do_save=False): + sc.heading('Testing waning...') # Define baseline parameters pars = sc.mergedicts(base_pars, { @@ -401,6 +399,8 @@ def test_waning_vs_not(do_plot=True, do_show=True, do_save=False): def test_msim(): + sc.heading('Testing multisim...') + # basic test for vaccine b117 = cv.Strain('b117', days=0) sim = cv.Sim(use_waning=True, strains=[b117], **base_pars) @@ -520,8 +520,8 @@ def get_ind_of_min_value(list, time): sim5 = test_vaccine_1strain() # Run multisim and scenario tests - #scens0 = test_vaccine_1strain_scen() #TODO, NOT WORKING CURRENTLY - #scens1 = test_vaccine_2strains_scen() #TODO, NOT WORKING CURRENTLY + scens0 = test_vaccine_1strain_scen() #TODO, NOT WORKING CURRENTLY + scens1 = test_vaccine_2strains_scen() #TODO, NOT WORKING CURRENTLY scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) msim0 = test_msim() From caf2486403154452aa8e6829a01076670ceadb12 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 18:54:13 -0700 Subject: [PATCH 406/569] update docstrings --- covasim/utils.py | 5 ++++- tests/test_analysis.py | 2 +- tests/test_baselines.py | 3 ++- tests/test_interventions.py | 3 ++- tests/test_resume.py | 2 +- tests/test_run.py | 2 +- tests/test_sim.py | 2 +- tests/test_utils.py | 2 +- 8 files changed, 13 insertions(+), 8 deletions(-) diff --git a/covasim/utils.py b/covasim/utils.py index 22e4f4225..5f066bade 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -1,5 +1,8 @@ ''' -Numerical utilities for running Covasim +Numerical utilities for running Covasim. + +These include the viral load, transmissibility, and infection calculations +at the heart of the integration loop. ''' #%% Housekeeping diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 7c637c058..acf6f9d85 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -1,5 +1,5 @@ ''' -Execute analysis tools in order to broadly cover basic functionality of analysis.py +Tests for the analyzers and other analysis tools. ''' import numpy as np diff --git a/tests/test_baselines.py b/tests/test_baselines.py index 6b53ba0bc..9e79cf95d 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -1,5 +1,6 @@ """ -Compare current results to baseline +Test that the current version of Covasim exactly matches +the baseline results. """ import numpy as np diff --git a/tests/test_interventions.py b/tests/test_interventions.py index 36ca566b0..ac152de1a 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -1,5 +1,6 @@ ''' -Demonstrate all interventions, taken from intervention docstrings +Tests covering all the built-in interventions, mostly taken +from the intervention's docstrings. ''' #%% Housekeeping diff --git a/tests/test_resume.py b/tests/test_resume.py index aa870e87a..62a2ef171 100644 --- a/tests/test_resume.py +++ b/tests/test_resume.py @@ -1,5 +1,5 @@ ''' -Test resuming a simulation partway, as well as reproducing two simulations with +Tests for resuming a simulation partway, as well as reproducing two simulations with different initialization states and after saving to disk. ''' diff --git a/tests/test_run.py b/tests/test_run.py index 367da0a9f..693531fe9 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -1,5 +1,5 @@ ''' -Test run options (multisims and scenarios) +Tests for run options (multisims and scenarios) ''' #%% Imports and settings diff --git a/tests/test_sim.py b/tests/test_sim.py index da2079999..c568b0616 100644 --- a/tests/test_sim.py +++ b/tests/test_sim.py @@ -1,5 +1,5 @@ ''' -Simple example usage for the Covid-19 agent-based model +Tests for single simulations ''' #%% Imports and settings diff --git a/tests/test_utils.py b/tests/test_utils.py index 377261262..e1c68b6af 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,5 @@ ''' -Tests of the utilies for the model. +Tests of the numerical utilities for the model. ''' #%% Imports and settings From a434695dc18bafa73bbeb08fddd2b6e282262705 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 19:13:21 -0700 Subject: [PATCH 407/569] update docstrings --- covasim/immunity.py | 2 +- covasim/interventions.py | 4 ++-- covasim/people.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index e45437467..43a3b0f15 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -452,7 +452,7 @@ def init_immunity(sim, create=False): else: # Progression and transmission are matrices of scalars of size sim['n_strains'] immunity[ax] = np.full(ts, 1, dtype=cvd.default_float) - known_strains = ['wild', 'b117', 'b1351', 'p1'] + known_strains = ['wild', 'b117', 'b1351', 'p1'] # TODO: only appear once cross_immunity = create_cross_immunity(circulating_strains, rel_imms) for i in range(ts): for j in range(ts): diff --git a/covasim/interventions.py b/covasim/interventions.py index 7b90ab024..3f551f752 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -966,7 +966,7 @@ class contact_tracing(Intervention): start_day (int): intervention start day (default: 0, i.e. the start of the simulation) end_day (int): intervention end day (default: no end) presumptive (bool): whether or not to begin isolation and contact tracing on the presumption of a positive diagnosis (default: no) - quar_period (int): number of days to quarantine when notified as a known contact. Default value is pars['quar_period'] + quar_period (int): number of days to quarantine when notified as a known contact. Default value is ``pars['quar_period']`` kwargs (dict): passed to Intervention() **Example**:: @@ -982,7 +982,7 @@ def __init__(self, trace_probs=None, trace_time=None, start_day=0, end_day=None, self.start_day = start_day self.end_day = end_day self.presumptive = presumptive - self.quar_period = quar_period #: If quar_period is None, it will be drawn from sim.pars at initialization + self.quar_period = quar_period # If quar_period is None, it will be drawn from sim.pars at initialization return diff --git a/covasim/people.py b/covasim/people.py index 988282fa4..df88c1247 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -531,7 +531,7 @@ def schedule_quarantine(self, inds, start_date=None, period=None): Args: inds (int): indices of who to quarantine, specified by check_quar() start_date (int): day to begin quarantine (defaults to the current day, `sim.t`) - period (int): quarantine duration (defaults to `pars['quar_period']`) + period (int): quarantine duration (defaults to ``pars['quar_period']``) ''' start_date = self.t if start_date is None else int(start_date) From 465f3ad028d93ee6c423f6549567df944c151bb8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 19:26:02 -0700 Subject: [PATCH 408/569] fixing up tests --- covasim/immunity.py | 10 +++++----- covasim/run.py | 2 +- tests/devtests/test_variants.py | 19 +++++++++++-------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 43a3b0f15..92c8fa144 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -124,9 +124,8 @@ def initialize(self, sim): if hasattr(self, strain_key): # TODO: refactor newval = getattr(self, strain_key) sim['strain_pars'][strain_key].append(newval) - else: - # use default - print(f'{strain_key} not provided for this strain, using default value') + else: # use default + sc.printv(f'{strain_key} not provided for this strain, using default value', 1, sim['verbose']) sim['strain_pars'][strain_key].append(sim['strain_pars'][strain_key][0]) return @@ -275,6 +274,7 @@ def parse_vaccine_pars(self, vaccine=None): return vaccine_pars + def initialize(self, sim): super().initialize() @@ -287,8 +287,8 @@ def initialize(self, sim): errormsg = 'Did not provide parameters for this vaccine' raise ValueError(errormsg) - if self.rel_imm is None: - print('Did not provide rel_imm parameters for this vaccine, trying to find values') + if self.rel_imm is None: # TODO: refactor + sc.printv('Did not provide rel_imm parameters for this vaccine, trying to find values', 1, sim['verbose']) self.rel_imm = [] for strain in circulating_strains: if strain in self.vaccine_strain_info['known_strains']: diff --git a/covasim/run.py b/covasim/run.py index cadb3441e..74ad11df7 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -11,7 +11,6 @@ from . import defaults as cvd from . import base as cvb from . import sim as cvs -from . import immunity as cvimm from . import plotting as cvplt from .settings import options as cvo @@ -890,6 +889,7 @@ def __init__(self, sim=None, metapars=None, scenarios=None, basepars=None, scenf # Copy quantities from the base sim to the main object self.npts = self.base_sim.npts self.tvec = self.base_sim.tvec + self['verbose'] = self.base_sim['verbose'] # Create the results object; order is: results key, scenario, best/low/high self.sims = sc.objdict() diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index f986e5cfe..98328c200 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -4,20 +4,26 @@ import numpy as np -do_plot = 1 +do_plot = 0 do_show = 0 -do_save = 1 +do_save = 0 base_pars = dict( pop_size = 10e3, - verbose = 0.01, + verbose = -1, ) +def test_simple(do_plot=False): + s1 = cv.Sim(base_pars).run() + s2 = cv.Sim(base_pars, n_days=300, use_waning=True).run() + if do_plot: + s1.plot() + s2.plot() + return def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): sc.heading('Test varying properties of immunity') - sc.heading('Setting up...') # Define baseline parameters n_runs = 3 @@ -209,8 +215,6 @@ def test_synthpops(): def test_vaccine_1strain_scen(do_plot=True, do_show=True, do_save=False): sc.heading('Run a basic sim with 1 strain, pfizer vaccine') - sc.heading('Setting up...') - # Define baseline parameters n_runs = 3 base_sim = cv.Sim(use_waning=True, pars=base_pars) @@ -506,8 +510,7 @@ def get_ind_of_min_value(list, time): sc.tic() # Run simplest possible test - sim = cv.Sim().run().plot() - sim = cv.Sim(n_days=300, use_waning=True).run().plot() + test_simple(do_plot=do_plot) # Run more complex single-sim tests sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) From cd44b6a2c8f77b44edc006cccd6e73fc41bfad79 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 19:31:15 -0700 Subject: [PATCH 409/569] add start of test script --- tests/test_immunity.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/test_immunity.py diff --git a/tests/test_immunity.py b/tests/test_immunity.py new file mode 100644 index 000000000..4ac98408f --- /dev/null +++ b/tests/test_immunity.py @@ -0,0 +1,42 @@ +''' +Tests for immune waning, strains, and vaccine intervention. +''' + +#%% Imports and settings +# import pytest +import sciris as sc +import covasim as cv + +do_plot = 1 +do_save = 0 +cv.options.set(interactive=False) # Assume not running interactively + +base_pars = dict( + pop_size = 2e3, + verbose = -1, +) + +#%% Define the tests + +def test_waning(do_plot=False): + sc.heading('Testing with and without waning') + s1 = cv.Sim(base_pars, n_days=300, label='No waning') + s2 = cv.Sim(base_pars, n_days=300, use_waning=True, label='With waning') + msim = cv.MultiSim([s1,s2]) + msim.run() + if do_plot: + msim.plot() + return msim + + +#%% Run as a script +if __name__ == '__main__': + + # Start timing and optionally enable interactive plotting + cv.options.set(interactive=do_plot) + T = sc.tic() + + msim1 = test_waning(do_plot=do_plot) + + sc.toc(T) + print('Done.') From b629b4c68d9801c66c023fb9b52b2bd773c0926e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 21:43:21 -0700 Subject: [PATCH 410/569] almost no speed increase --- tests/test_baselines.py | 7 +++---- tests/test_interventions.py | 37 +++++++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/tests/test_baselines.py b/tests/test_baselines.py index 9e79cf95d..b5dcfd7a4 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -1,5 +1,5 @@ """ -Test that the current version of Covasim exactly matches +Test that the current version of Covasim exactly matches the baseline results. """ @@ -93,13 +93,12 @@ def test_baseline(): return new -def test_benchmark(do_save=do_save): +def test_benchmark(do_save=do_save, repeats=1): ''' Compare benchmark performance ''' print('Running benchmark...') previous = sc.loadjson(benchmark_filename) - repeats = 5 t_inits = [] t_runs = [] @@ -187,7 +186,7 @@ def normalize_performance(): cv.options.set(interactive=do_plot) T = sc.tic() - json = test_benchmark(do_save=do_save) # Run this first so benchmarking is available even if results are different + json = test_benchmark(do_save=do_save, repeats=5) # Run this first so benchmarking is available even if results are different new = test_baseline() make_sim(do_plot=do_plot) diff --git a/tests/test_interventions.py b/tests/test_interventions.py index ac152de1a..e4f0bee6d 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -1,5 +1,5 @@ ''' -Tests covering all the built-in interventions, mostly taken +Tests covering all the built-in interventions, mostly taken from the intervention's docstrings. ''' @@ -20,6 +20,7 @@ def test_all_interventions(): ''' Test all interventions supported by Covasim ''' + sc.heading('Testing default interventions') pars = sc.objdict( pop_size = 1e3, @@ -27,6 +28,18 @@ def test_all_interventions(): n_days = 90, ) hpars = sc.mergedicts(pars, {'pop_type':'hybrid'}) # Some, but not all, tests require layers + rsim = cv.Sim(pars).initialize() + hsim = cv.Sim(hpars).initialize() + + def make_sim(which='r', interventions=None): + ''' Helper function to avoid having to recreate the sim each time ''' + if which == 'r': + sim = sc.dcp(rsim) + elif which == 'h': + sim = sc.dcp(hsim) + sim['interventions'] = interventions + sim.initialize() + return sim #%% Define the interventions @@ -83,17 +96,17 @@ def check_inf(interv, sim, thresh=10, close_day=18): #%% Create the simulations sims = sc.objdict() - sims.dynamic = cv.Sim(pars=pars, interventions=[i1a, i1b]) - sims.sequence = cv.Sim(pars=pars, interventions=i2) - sims.change_beta1 = cv.Sim(pars=hpars, interventions=i3a) - sims.clip_edges1 = cv.Sim(pars=hpars, interventions=i4a) # Roughly equivalent to change_beta1 - sims.change_beta2 = cv.Sim(pars=pars, interventions=i3b) - sims.clip_edges2 = cv.Sim(pars=pars, interventions=i4b) # Roughly equivalent to change_beta2 - sims.test_num = cv.Sim(pars=pars, interventions=i5) - sims.test_prob = cv.Sim(pars=pars, interventions=i6) - sims.tracing = cv.Sim(pars=hpars, interventions=[i7a, i7b]) - sims.combo = cv.Sim(pars=hpars, interventions=[i8a, i8b, i8c, i8d]) - sims.vaccine = cv.Sim(pars=pars, interventions=[i9a, i9b]) + sims.dynamic = make_sim('r', [i1a, i1b]) + sims.sequence = make_sim('r', i2) + sims.change_beta1 = make_sim('h', i3a) + sims.clip_edges1 = make_sim('h', i4a) # Roughly equivalent to change_beta1 + sims.change_beta2 = make_sim('r', i3b) + sims.clip_edges2 = make_sim('r', i4b) # Roughly equivalent to change_beta2 + sims.test_num = make_sim('r', i5) + sims.test_prob = make_sim('r', i6) + sims.tracing = make_sim('h', [i7a, i7b]) + sims.combo = make_sim('h', [i8a, i8b, i8c, i8d]) + sims.vaccine = make_sim('r', [i9a, i9b]) # Run the simualations for key,sim in sims.items(): From f544fd31c451dfc3c64e87a750dbe05330c38a22 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 21:52:36 -0700 Subject: [PATCH 411/569] somewhat faster --- tests/test_interventions.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_interventions.py b/tests/test_interventions.py index e4f0bee6d..c7b120bd9 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -12,31 +12,31 @@ import covasim as cv import pytest -verbose = 0 -do_plot = 1 # Whether to plot when run interactively +verbose = -1 +do_plot = 0 # Whether to plot when run interactively cv.options.set(interactive=False) # Assume not running interactively csv_file = os.path.join(sc.thisdir(), 'example_data.csv') -def test_all_interventions(): +def test_all_interventions(do_plot=False): ''' Test all interventions supported by Covasim ''' sc.heading('Testing default interventions') + # Default parameters, using the random layer pars = sc.objdict( pop_size = 1e3, pop_infected = 10, n_days = 90, + verbose = verbose, ) hpars = sc.mergedicts(pars, {'pop_type':'hybrid'}) # Some, but not all, tests require layers - rsim = cv.Sim(pars).initialize() - hsim = cv.Sim(hpars).initialize() + rsim = cv.Sim(pars) + hsim = cv.Sim(hpars) def make_sim(which='r', interventions=None): ''' Helper function to avoid having to recreate the sim each time ''' - if which == 'r': - sim = sc.dcp(rsim) - elif which == 'h': - sim = sc.dcp(hsim) + if which == 'r': sim = sc.dcp(rsim) + elif which == 'h': sim = sc.dcp(hsim) sim['interventions'] = interventions sim.initialize() return sim @@ -111,7 +111,7 @@ def check_inf(interv, sim, thresh=10, close_day=18): # Run the simualations for key,sim in sims.items(): sim.label = key - sim.run(verbose=verbose) + sim.run() # Test intervention retrieval methods sim = sims.combo @@ -166,7 +166,7 @@ def test_data_interventions(): cv.options.set(interactive=do_plot) T = sc.tic() - test_all_interventions() + test_all_interventions(do_plot=do_plot) test_data_interventions() sc.toc(T) From f4d8e6ffa5e87b2e04a043695d5b6bf888d6f20e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 22:06:34 -0700 Subject: [PATCH 412/569] about 33% reduction --- tests/test_analysis.py | 4 ++-- tests/test_baselines.py | 2 +- tests/test_other.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_analysis.py b/tests/test_analysis.py index acf6f9d85..7c0362bb7 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -80,8 +80,8 @@ def test_daily_age(): def test_daily_stats(): sc.heading('Testing daily stats analyzer') - ds = cv.daily_stats(days=['2020-04-04', '2020-04-14'], save_inds=True) - sim = cv.Sim(pars, analyzers=ds) + ds = cv.daily_stats(days=['2020-04-04'], save_inds=True) + sim = cv.Sim(pars, n_days=40, analyzers=ds) sim.run() daily = sim.get_analyzer() if do_plot: diff --git a/tests/test_baselines.py b/tests/test_baselines.py index b5dcfd7a4..48f7c2ea4 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -105,7 +105,7 @@ def test_benchmark(do_save=do_save, repeats=1): def normalize_performance(): ''' Normalize performance across CPUs -- simple Numpy calculation ''' t_bls = [] - bl_repeats = 5 + bl_repeats = 3 n_outer = 10 n_inner = 1e6 for r in range(bl_repeats): diff --git a/tests/test_other.py b/tests/test_other.py index 8ab3ba455..61f7a31e4 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -11,7 +11,7 @@ import covasim as cv do_plot = 1 -verbose = 0 +verbose = -1 debug = 1 # This runs without parallelization; faster with pytest csv_file = os.path.join(sc.thisdir(), 'example_data.csv') xlsx_file = os.path.join(sc.thisdir(), 'example_data.xlsx') @@ -134,12 +134,12 @@ def test_basepeople(): assert len(layer2.keys()) == 5 # Test dynamic layers, plotting, and stories - pars = dict(pop_size=1000, n_days=50, verbose=verbose, pop_type='hybrid') + pars = dict(pop_size=100, n_days=10, verbose=verbose, pop_type='hybrid', beta=0.02) s1 = cv.Sim(pars, dynam_layer={'c':1}) s1.run() s1.people.plot() - for person in [25, 79]: - sim.people.story(person) + for person in [0, 50]: + s1.people.story(person) # Run without dynamic layers and assert that the results are different s2 = cv.Sim(pars, dynam_layer={'c':0}) From 0da360847dcbb67d98f16dd904318fad73a37f10 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 22:45:51 -0700 Subject: [PATCH 413/569] plotting mostly working, but labels a little off --- covasim/defaults.py | 97 +++++++++++++++++++++++++++++------------- covasim/parameters.py | 2 +- covasim/plotting.py | 13 ++++-- tests/test_immunity.py | 6 +-- 4 files changed, 80 insertions(+), 38 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 1a803a212..a697c417e 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -263,35 +263,42 @@ def get_strain_colors(): # Define the 'overview plots', i.e. the most useful set of plots to explore different aspects of a simulation overview_plots = [ - 'cum_infections', - # 'cum_infections_by_strain', - 'cum_severe', - 'cum_critical', - 'cum_deaths', - 'cum_diagnoses', - 'new_infections', - # 'new_infections_by_strain', - 'new_severe', - 'new_critical', - 'new_deaths', - 'new_diagnoses', - 'n_infectious', - # 'n_infectious_by_strain', - 'n_severe', - 'n_critical', - 'n_susceptible', - 'new_tests', - 'n_symptomatic', - 'new_quarantined', - 'n_quarantined', - 'new_vaccinations', - 'new_vaccinated', - 'cum_vaccinated', - 'cum_vaccinations', - 'test_yield', - 'r_eff', - ] + 'cum_infections', + 'cum_severe', + 'cum_critical', + 'cum_deaths', + 'cum_diagnoses', + 'new_infections', + 'new_severe', + 'new_critical', + 'new_deaths', + 'new_diagnoses', + 'n_infectious', + 'n_severe', + 'n_critical', + 'n_susceptible', + 'new_tests', + 'n_symptomatic', + 'new_quarantined', + 'n_quarantined', + 'new_vaccinations', + 'new_vaccinated', + 'cum_vaccinated', + 'cum_vaccinations', + 'test_yield', + 'r_eff', +] +overview_strain_plots = [ + 'cum_infections_by_strain', + 'new_infections_by_strain', + 'n_infectious_by_strain', + 'cum_reinfections', + 'new_reinfections', + 'pop_nabs', + 'pop_protection', + 'pop_symp_protection', +] def get_sim_plots(which='default'): ''' @@ -300,6 +307,8 @@ def get_sim_plots(which='default'): Args: which (str): either 'default' or 'overview' ''' + + # Default plots if which in [None, 'default']: plots = sc.odict({ 'Total counts': [ @@ -317,9 +326,36 @@ def get_sim_plots(which='default'): 'cum_deaths', ], }) + + # Show everything elif which == 'overview': plots = sc.dcp(overview_plots) - elif which == 'seir': + + # Show everything plus strains + elif 'overview' in which and 'strain' in which: + plots = sc.dcp(overview_plots) + sc.dcp(overview_strain_plots) + + # Show default but with strains + elif 'strain' in which: + plots = sc.odict({ + 'Total counts': [ + 'cum_infections_by_strain', + 'n_infectious_by_strain', + 'cum_diagnoses', + ], + 'Daily counts': [ + 'new_infections_by_strain', + 'new_diagnoses', + ], + 'Health outcomes': [ + 'cum_severe', + 'cum_critical', + 'cum_deaths', + ], + }) + + # Plot SEIR compartments + elif which.lower() == 'seir': plots = sc.odict({ 'SEIR states': [ 'n_susceptible', @@ -328,8 +364,9 @@ def get_sim_plots(which='default'): 'n_removed', ], }) + else: # pragma: no cover - errormsg = f'The choice which="{which}" is not supported' + errormsg = f'The choice which="{which}" is not supported: choices are "default", "overview", "strain", "overview-strain", or "seir"' raise ValueError(errormsg) return plots diff --git a/covasim/parameters.py b/covasim/parameters.py index b0dd23ddf..21841cea4 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -59,7 +59,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated # Parameters that control settings and defaults for multi-strain runs - pars['n_imports'] = 0 # Average daily number of imported cases (actual number is drawn from Poisson distribution) + pars['n_imports'] = 0 # Average daily number of imported cases (actual number is drawn from Poisson distribution) pars['n_strains'] = 1 # The number of strains currently circulating in the population pars['total_strains'] = 1 # Set during sim initialization, once strains have been specified and processed diff --git a/covasim/plotting.py b/covasim/plotting.py index 5d23abdf5..68399376b 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -107,7 +107,11 @@ def handle_to_plot(which, to_plot, n_cols, sim, check_ready=True): to_plot_list = to_plot # Store separately to_plot = sc.odict() # Create the dict for reskey in to_plot_list: - to_plot[sim.results[reskey].name] = [reskey] # Use the result name as the key and the reskey as the value + if 'strain' in sim.results and reskey in sim.results['strain']: + name = sim.results['strain'][reskey].name + else: + name = sim.results[reskey].name + to_plot[name] = [reskey] # Use the result name as the key and the reskey as the value to_plot = sc.odict(sc.dcp(to_plot)) # In case it's supplied as a dict @@ -390,9 +394,9 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot for pnum,title,keylabels in to_plot.enumitems(): ax = create_subplots(figs, fig, ax, n_rows, n_cols, pnum, args.fig, sep_figs, log_scale, title) for resnum,reskey in enumerate(keylabels): - res = sim.results[reskey] res_t = sim.results['t'] - if 'by_strain' in reskey: + if 'strain' in sim.results and reskey in sim.results['strain']: + res = sim.results['strain'][reskey] for strain in range(sim['total_strains']): color = cvd.get_strain_colors()[strain] # Choose the color if strain == 0: @@ -405,6 +409,7 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot ax.plot(res_t, res.values[strain,:], label=label, **args.plot, c=color) # Actually plot the sim! else: + res = sim.results[reskey] color = set_line_options(colors, reskey, resnum, res.color) # Choose the color label = set_line_options(labels, reskey, resnum, res.name) # Choose the label if res.low is not None and res.high is not None: @@ -443,7 +448,7 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, resdata = scens.results[reskey] for snum,scenkey,scendata in resdata.enumitems(): sim = scens.sims[scenkey][0] # Pull out the first sim in the list for this scenario - if 'by_strain' in reskey: + if 'by_strain' in reskey: # TODO: refactor for strain in range(sim['total_strains']): res_y = scendata.best[strain,:] color = default_colors[strain] # Choose the color diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 4ac98408f..da4edb83d 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -12,7 +12,7 @@ cv.options.set(interactive=False) # Assume not running interactively base_pars = dict( - pop_size = 2e3, + pop_size = 1e3, verbose = -1, ) @@ -20,12 +20,12 @@ def test_waning(do_plot=False): sc.heading('Testing with and without waning') - s1 = cv.Sim(base_pars, n_days=300, label='No waning') + s1 = cv.Sim(base_pars, n_days=300, use_waning=True, label='No waning') s2 = cv.Sim(base_pars, n_days=300, use_waning=True, label='With waning') msim = cv.MultiSim([s1,s2]) msim.run() if do_plot: - msim.plot() + msim.plot('strain') return msim From a2afabf01dd413794c3f55b62a06a4684fb2edaf Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 22:49:33 -0700 Subject: [PATCH 414/569] immunity seemingly not activated --- tests/test_immunity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index da4edb83d..86a49f68b 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -25,7 +25,7 @@ def test_waning(do_plot=False): msim = cv.MultiSim([s1,s2]) msim.run() if do_plot: - msim.plot('strain') + msim.plot('overview-strain') return msim From 4c7e36c89370a7d58966bf63e9aa8698c80a99a1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 22:55:36 -0700 Subject: [PATCH 415/569] working now, oddly --- tests/devtests/test_variants.py | 93 +++--- tests/test_immunity.py | 489 +++++++++++++++++++++++++++++++- 2 files changed, 535 insertions(+), 47 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 98328c200..77eb45558 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -212,7 +212,7 @@ def test_synthpops(): #%% Multisim and scenario tests -def test_vaccine_1strain_scen(do_plot=True, do_show=True, do_save=False): +def test_vaccine_1strain_scen(do_plot=False, do_show=True, do_save=False): sc.heading('Run a basic sim with 1 strain, pfizer vaccine') # Define baseline parameters @@ -257,7 +257,7 @@ def test_vaccine_1strain_scen(do_plot=True, do_show=True, do_save=False): return scens -def test_vaccine_2strains_scen(do_plot=True, do_show=True, do_save=False): +def test_vaccine_2strains_scen(do_plot=False, do_show=True, do_save=False): sc.heading('Run a basic sim with b117 strain on day 10, pfizer vaccine day 20') # Define baseline parameters @@ -357,7 +357,7 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): return scens -def test_waning_vs_not(do_plot=True, do_show=True, do_save=False): +def test_waning_vs_not(do_plot=False, do_show=True, do_save=False): sc.heading('Testing waning...') # Define baseline parameters @@ -402,7 +402,7 @@ def test_waning_vs_not(do_plot=True, do_show=True, do_save=False): return scens -def test_msim(): +def test_msim(do_plot=False): sc.heading('Testing multisim...') # basic test for vaccine @@ -418,67 +418,68 @@ def test_msim(): 'New Re-infections per day': ['new_reinfections'], }) - msim.plot(to_plot=to_plot, do_save=0, do_show=1, legend_args={'loc': 'upper left'}, axis_args={'hspace': 0.4}, interval=35) + if do_plot: + msim.plot(to_plot=to_plot, do_save=0, do_show=1, legend_args={'loc': 'upper left'}, axis_args={'hspace': 0.4}, interval=35) return msim #%% Plotting and utilities -def plot_results(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): +# def plot_results(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): - results = sim.results - results_to_plot = results[key] +# results = sim.results +# results_to_plot = results[key] - # extract data for plotting - x = sim.results['t'] - y = results_to_plot.values - y = np.transpose(y) +# # extract data for plotting +# x = sim.results['t'] +# y = results_to_plot.values +# y = np.transpose(y) - fig, ax = plt.subplots() - ax.plot(x, y) +# fig, ax = plt.subplots() +# ax.plot(x, y) - ax.set(xlabel='Day of simulation', ylabel=results_to_plot.name, title=title) +# ax.set(xlabel='Day of simulation', ylabel=results_to_plot.name, title=title) - if labels is None: - labels = [0]*len(y[0]) - for strain in range(len(y[0])): - labels[strain] = f'Strain {strain +1}' - ax.legend(labels) +# if labels is None: +# labels = [0]*len(y[0]) +# for strain in range(len(y[0])): +# labels[strain] = f'Strain {strain +1}' +# ax.legend(labels) - if do_show: - plt.show() - if do_save: - cv.savefig(f'results/{filename}.png') +# if do_show: +# plt.show() +# if do_save: +# cv.savefig(f'results/{filename}.png') - return +# return -def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): +# def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): - results = sim.results - n_strains = sim.results['new_infections_by_strain'].values.shape[0] # TODO: this should be stored in the sim somewhere more intuitive! - prop_new = {f'Strain {s}': sc.safedivide(results[key+'_by_strain'].values[s,:], results[key].values, 0) for s in range(n_strains)} - num_new = {f'Strain {s}': results[key+'_by_strain'].values[s,:] for s in range(n_strains)} +# results = sim.results +# n_strains = sim.results['new_infections_by_strain'].values.shape[0] # TODO: this should be stored in the sim somewhere more intuitive! +# prop_new = {f'Strain {s}': sc.safedivide(results[key+'_by_strain'].values[s,:], results[key].values, 0) for s in range(n_strains)} +# num_new = {f'Strain {s}': results[key+'_by_strain'].values[s,:] for s in range(n_strains)} - # extract data for plotting - x = sim.results['t'] - fig, ax = plt.subplots(2,1,sharex=True) - ax[0].stackplot(x, prop_new.values(), - labels=prop_new.keys()) - ax[0].legend(loc='upper left') - ax[0].set_title(title) - ax[1].stackplot(sim.results['t'], num_new.values(), - labels=num_new.keys()) - ax[1].legend(loc='upper left') - ax[1].set_title(title) +# # extract data for plotting +# x = sim.results['t'] +# fig, ax = plt.subplots(2,1,sharex=True) +# ax[0].stackplot(x, prop_new.values(), +# labels=prop_new.keys()) +# ax[0].legend(loc='upper left') +# ax[0].set_title(title) +# ax[1].stackplot(sim.results['t'], num_new.values(), +# labels=num_new.keys()) +# ax[1].legend(loc='upper left') +# ax[1].set_title(title) - if do_show: - plt.show() - if do_save: - cv.savefig(f'results/{filename}.png') +# if do_show: +# plt.show() +# if do_save: +# cv.savefig(f'results/{filename}.png') - return +# return def vacc_subtarg(sim): diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 86a49f68b..093edcee3 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -6,6 +6,7 @@ # import pytest import sciris as sc import covasim as cv +import numpy as np do_plot = 1 do_save = 0 @@ -20,7 +21,7 @@ def test_waning(do_plot=False): sc.heading('Testing with and without waning') - s1 = cv.Sim(base_pars, n_days=300, use_waning=True, label='No waning') + s1 = cv.Sim(base_pars, n_days=300, use_waning=False, label='No waning') s2 = cv.Sim(base_pars, n_days=300, use_waning=True, label='With waning') msim = cv.MultiSim([s1,s2]) msim.run() @@ -29,6 +30,492 @@ def test_waning(do_plot=False): return msim + +def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): + sc.heading('Test varying properties of immunity') + + # Define baseline parameters + n_runs = 3 + base_sim = cv.Sim(use_waning=True, n_days=400, pars=base_pars) + + # Define the scenarios + b1351 = cv.Strain('b1351', days=100, n_imports=20) + + scenarios = { + 'baseline': { + 'name': 'Default Immunity (decay at log(2)/90)', + 'pars': { + 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, + 'decay_decay_rate': 0.001}), + }, + }, + # 'slower_immunity': { + # 'name': 'Slower Immunity (decay at log(2)/150)', + # 'pars': { + # 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 150, 'init_decay_time': 250, + # 'decay_decay_rate': 0.001}), + # }, + # }, + 'faster_immunity': { + 'name': 'Faster Immunity (decay at log(2)/30)', + 'pars': { + 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, + 'decay_decay_rate': 0.001}), + }, + }, + 'baseline_b1351': { + 'name': 'Default Immunity (decay at log(2)/90), B1351 on day 100', + 'pars': { + 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, + 'decay_decay_rate': 0.001}), + 'strains': [b1351], + }, + }, + # 'slower_immunity_b1351': { + # 'name': 'Slower Immunity (decay at log(2)/150), B1351 on day 100', + # 'pars': { + # 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 150, 'init_decay_time': 250, + # 'decay_decay_rate': 0.001}), + # 'strains': [b1351], + # }, + # }, + 'faster_immunity_b1351': { + 'name': 'Faster Immunity (decay at log(2)/30), B1351 on day 100', + 'pars': { + 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, + 'decay_decay_rate': 0.001}), + 'strains': [b1351], + }, + }, + } + + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'New re-infections': ['new_reinfections'], + 'Population Nabs': ['pop_nabs'], + 'Population Immunity': ['pop_protection'], + }) + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_basic_immunity.png', to_plot=to_plot) + + return scens + + +def test_import1strain(do_plot=False, do_show=True, do_save=False): + sc.heading('Test introducing a new strain partway through a sim') + + strain_pars = { + 'rel_beta': 1.5, + } + pars = { + 'beta': 0.01 + } + strain = cv.Strain(strain_pars, days=1, n_imports=20, label='Strain 2: 1.5x more transmissible') + sim = cv.Sim(use_waning=True, pars=pars, strains=strain, analyzers=cv.snapshot(30, 60), **pars, **base_pars) + sim.run() + + return sim + + +def test_import2strains(do_plot=False, do_show=True, do_save=False): + sc.heading('Test introducing 2 new strains partway through a sim') + + b117 = cv.Strain('b117', days=1, n_imports=20) + p1 = cv.Strain('sa variant', days=2, n_imports=20) + sim = cv.Sim(use_waning=True, strains=[b117, p1], label='With imported infections', **base_pars) + sim.run() + + return sim + + +def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): + sc.heading('Test introducing a new strain with longer duration partway through a sim') + + pars = sc.mergedicts(base_pars, { + 'n_days': 120, + }) + + strain_pars = { + 'rel_beta': 1.5, + 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} + } + + strain = cv.Strain(strain=strain_pars, label='Custom strain', days=10, n_imports=30) + sim = cv.Sim(use_waning=True, pars=pars, strains=strain, label='With imported infections') + sim.run() + + return sim + + +def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): + sc.heading('Test introducing 2 new strains partway through a sim, with a change_beta intervention') + + strain2 = {'rel_beta': 1.5, + 'rel_severe_prob': 1.3} + + strain3 = {'rel_beta': 2, + 'rel_symp_prob': 1.6} + + intervs = cv.change_beta(days=[5, 20, 40], changes=[0.8, 0.7, 0.6]) + strains = [cv.Strain(strain=strain2, days=10, n_imports=20), + cv.Strain(strain=strain3, days=30, n_imports=20), + ] + sim = cv.Sim(use_waning=True, interventions=intervs, strains=strains, label='With imported infections', **base_pars) + sim.run() + + return sim + + + +#%% Vaccination tests + +def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): + sc.heading('Test vaccination with a single strain') + sc.heading('Setting up...') + + pars = sc.mergedicts(base_pars, { + 'beta': 0.015, + 'n_days': 120, + }) + + pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer') + sim = cv.Sim( + use_waning=True, + pars=pars, + interventions=pfizer + ) + sim.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'Cumulative infections': ['cum_infections'], + 'New reinfections': ['new_reinfections'], + }) + if do_plot: + sim.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_reinfection.png', to_plot=to_plot) + + return sim + + +def test_synthpops(): + sim = cv.Sim(use_waning=True, **sc.mergedicts(base_pars, dict(pop_size=5000, pop_type='synthpops'))) + sim.popdict = cv.make_synthpop(sim, with_facilities=True, layer_mapping={'LTCF': 'f'}) + sim.reset_layer_pars() + + # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 + sim.vxsubtarg = sc.objdict() + sim.vxsubtarg.age = [75, 65, 50, 18] + sim.vxsubtarg.prob = [.05, .05, .05, .05] + sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] + pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) + sim['interventions'] += [pfizer] + + sim.run() + return sim + + + +#%% Multisim and scenario tests + +def test_vaccine_1strain_scen(do_plot=False, do_show=True, do_save=False): + sc.heading('Run a basic sim with 1 strain, pfizer vaccine') + + # Define baseline parameters + n_runs = 3 + base_sim = cv.Sim(use_waning=True, pars=base_pars) + + # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 + base_sim.vxsubtarg = sc.objdict() + base_sim.vxsubtarg.age = [75, 65, 50, 18] + base_sim.vxsubtarg.prob = [.05, .05, .05, .05] + base_sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] + pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) + + # Define the scenarios + + scenarios = { + 'baseline': { + 'name': 'No Vaccine', + 'pars': {} + }, + 'pfizer': { + 'name': 'Pfizer starting on day 20', + 'pars': { + 'interventions': [pfizer], + } + }, + } + + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'Cumulative infections': ['cum_infections'], + 'New reinfections': ['new_reinfections'], + # 'Cumulative reinfections': ['cum_reinfections'], + }) + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_basic_vaccination.png', to_plot=to_plot) + + return scens + + +def test_vaccine_2strains_scen(do_plot=False, do_show=True, do_save=False): + sc.heading('Run a basic sim with b117 strain on day 10, pfizer vaccine day 20') + + # Define baseline parameters + n_runs = 3 + base_sim = cv.Sim(use_waning=True, pars=base_pars) + + # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 + base_sim.vxsubtarg = sc.objdict() + base_sim.vxsubtarg.age = [75, 65, 50, 18] + base_sim.vxsubtarg.prob = [.01, .01, .01, .01] + base_sim.vxsubtarg.days = subtarg_days = [60, 150, 200, 220] + jnj = cv.vaccinate(days=subtarg_days, vaccine_pars='j&j', subtarget=vacc_subtarg) + b1351 = cv.Strain('b1351', days=10, n_imports=20) + p1 = cv.Strain('p1', days=100, n_imports=100) + + # Define the scenarios + + scenarios = { + 'baseline': { + 'name': 'B1351 on day 10, No Vaccine', + 'pars': { + 'strains': [b1351] + } + }, + 'b1351': { + 'name': 'B1351 on day 10, J&J starting on day 60', + 'pars': { + 'interventions': [jnj], + 'strains': [b1351], + } + }, + 'p1': { + 'name': 'B1351 on day 10, J&J starting on day 60, p1 on day 100', + 'pars': { + 'interventions': [jnj], + 'strains': [b1351, p1], + } + }, + } + + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'Cumulative infections': ['cum_infections'], + 'New reinfections': ['new_reinfections'], + # 'Cumulative reinfections': ['cum_reinfections'], + }) + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_vaccine_b1351.png', to_plot=to_plot) + + return scens + + +def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): + sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') + + strain_pars = {'dur':{'inf2sym': {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9}}} + strains = cv.Strain(strain=strain_pars, label='10 days til symptoms', days=10, n_imports=30) + tp = cv.test_prob(symp_prob=0.2) # Add an efficient testing program + + pars = sc.mergedicts(base_pars, { + 'beta': 0.015, # Make beta higher than usual so people get infected quickly + 'n_days': 120, + 'interventions': tp + }) + n_runs = 1 + base_sim = cv.Sim(use_waning=True, pars=pars) + + # Define the scenarios + scenarios = { + 'baseline': { + 'name':'1 day to symptoms', + 'pars': {} + }, + 'slowsymp': { + 'name':'10 days to symptoms', + 'pars': {'strains': [strains]} + } + } + + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'Cumulative infections': ['cum_infections'], + 'New diagnoses': ['new_diagnoses'], + 'Cumulative diagnoses': ['cum_diagnoses'], + }) + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_strainduration.png', to_plot=to_plot) + + return scens + + +def test_waning_vs_not(do_plot=False, do_show=True, do_save=False): + sc.heading('Testing waning...') + + # Define baseline parameters + pars = sc.mergedicts(base_pars, { + 'pop_size': 10e3, + 'pop_scale': 50, + 'n_days': 150, + 'use_waning': False, + }) + + n_runs = 3 + base_sim = cv.Sim(pars=pars) + + # Define the scenarios + scenarios = { + 'no_waning': { + 'name': 'No waning', + 'pars': { + } + }, + 'waning': { + 'name': 'Waning', + 'pars': { + 'use_waning': True, + } + }, + } + + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run() + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'New reinfections': ['new_reinfections'], + 'Cumulative infections': ['cum_infections'], + 'Cumulative reinfections': ['cum_reinfections'], + }) + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_waning_vs_not.png', to_plot=to_plot) + + return scens + + +def test_msim(do_plot=False): + sc.heading('Testing multisim...') + + # basic test for vaccine + b117 = cv.Strain('b117', days=0) + sim = cv.Sim(use_waning=True, strains=[b117], **base_pars) + msim = cv.MultiSim(sim, n_runs=2) + msim.run() + msim.reduce() + + to_plot = sc.objdict({ + 'Total infections': ['cum_infections'], + 'New infections per day': ['new_infections'], + 'New Re-infections per day': ['new_reinfections'], + }) + + if do_plot: + msim.plot(to_plot=to_plot, do_save=0, do_show=1, legend_args={'loc': 'upper left'}, axis_args={'hspace': 0.4}, interval=35) + + return msim + + +#%% Plotting and utilities + +# def plot_results(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): + +# results = sim.results +# results_to_plot = results[key] + +# # extract data for plotting +# x = sim.results['t'] +# y = results_to_plot.values +# y = np.transpose(y) + +# fig, ax = plt.subplots() +# ax.plot(x, y) + +# ax.set(xlabel='Day of simulation', ylabel=results_to_plot.name, title=title) + +# if labels is None: +# labels = [0]*len(y[0]) +# for strain in range(len(y[0])): +# labels[strain] = f'Strain {strain +1}' +# ax.legend(labels) + +# if do_show: +# plt.show() +# if do_save: +# cv.savefig(f'results/{filename}.png') + +# return + + +# def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): + +# results = sim.results +# n_strains = sim.results['new_infections_by_strain'].values.shape[0] # TODO: this should be stored in the sim somewhere more intuitive! +# prop_new = {f'Strain {s}': sc.safedivide(results[key+'_by_strain'].values[s,:], results[key].values, 0) for s in range(n_strains)} +# num_new = {f'Strain {s}': results[key+'_by_strain'].values[s,:] for s in range(n_strains)} + +# # extract data for plotting +# x = sim.results['t'] +# fig, ax = plt.subplots(2,1,sharex=True) +# ax[0].stackplot(x, prop_new.values(), +# labels=prop_new.keys()) +# ax[0].legend(loc='upper left') +# ax[0].set_title(title) +# ax[1].stackplot(sim.results['t'], num_new.values(), +# labels=num_new.keys()) +# ax[1].legend(loc='upper left') +# ax[1].set_title(title) + +# if do_show: +# plt.show() +# if do_save: +# cv.savefig(f'results/{filename}.png') + +# return + + +def vacc_subtarg(sim): + ''' Subtarget by age''' + + # retrieves the first ind that is = or < sim.t + ind = get_ind_of_min_value(sim.vxsubtarg.days, sim.t) + age = sim.vxsubtarg.age[ind] + prob = sim.vxsubtarg.prob[ind] + inds = sc.findinds((sim.people.age>=age) * ~sim.people.vaccinated) + vals = prob*np.ones(len(inds)) + return {'inds':inds, 'vals':vals} + + +def get_ind_of_min_value(list, time): + ind = None + for place, t in enumerate(list): + if time >= t: + ind = place + + if ind is None: + errormsg = f'{time} is not within the list of times' + raise ValueError(errormsg) + return ind + + + #%% Run as a script if __name__ == '__main__': From 42658ae228d9d762f747ffc42efee4f6f7a6788e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 23:03:52 -0700 Subject: [PATCH 416/569] working better now --- covasim/immunity.py | 4 +- tests/immcov | 14 + tests/test_immunity.py | 813 +++++++++++++++++++---------------------- 3 files changed, 388 insertions(+), 443 deletions(-) create mode 100755 tests/immcov diff --git a/covasim/immunity.py b/covasim/immunity.py index 92c8fa144..6262dca71 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -354,10 +354,12 @@ def init_nab(people, inds, prior_inf=True): return - +# def check_nab(t, people, inds=None): ''' Determines current NAbs based on date since recovered/vaccinated.''' + # import traceback; traceback.print_exc(); import pdb; pdb.set_trace() + # Indices of people who've had some NAb event rec_inds = cvu.defined(people.date_recovered[inds]) vac_inds = cvu.defined(people.date_vaccinated[inds]) diff --git a/tests/immcov b/tests/immcov new file mode 100755 index 000000000..527f22fa6 --- /dev/null +++ b/tests/immcov @@ -0,0 +1,14 @@ +#!/bin/bash +# Note that although the script runs when parallelized, the coverage results are wrong. + +echo 'Running tests...' +pytest -v test_immunity.py --cov-config=.coveragerc --cov=../covasim --workers auto --durations=0 + +echo 'Creating HTML report...' +coverage html + +echo 'Printing report...' +coverage report + +echo 'Report location:' +echo "`pwd`/htmlcov/index.html" \ No newline at end of file diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 093edcee3..841453e00 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -21,476 +21,405 @@ def test_waning(do_plot=False): sc.heading('Testing with and without waning') - s1 = cv.Sim(base_pars, n_days=300, use_waning=False, label='No waning') - s2 = cv.Sim(base_pars, n_days=300, use_waning=True, label='With waning') + s1 = cv.Sim(base_pars, n_days=300, use_waning=False, label='No waning').run() + s2 = cv.Sim(base_pars, n_days=300, use_waning=True, label='With waning').run() msim = cv.MultiSim([s1,s2]) - msim.run() + # msim.run() if do_plot: msim.plot('overview-strain') return msim -def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): - sc.heading('Test varying properties of immunity') - - # Define baseline parameters - n_runs = 3 - base_sim = cv.Sim(use_waning=True, n_days=400, pars=base_pars) - - # Define the scenarios - b1351 = cv.Strain('b1351', days=100, n_imports=20) - - scenarios = { - 'baseline': { - 'name': 'Default Immunity (decay at log(2)/90)', - 'pars': { - 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), - }, - }, - # 'slower_immunity': { - # 'name': 'Slower Immunity (decay at log(2)/150)', - # 'pars': { - # 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 150, 'init_decay_time': 250, - # 'decay_decay_rate': 0.001}), - # }, - # }, - 'faster_immunity': { - 'name': 'Faster Immunity (decay at log(2)/30)', - 'pars': { - 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), - }, - }, - 'baseline_b1351': { - 'name': 'Default Immunity (decay at log(2)/90), B1351 on day 100', - 'pars': { - 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), - 'strains': [b1351], - }, - }, - # 'slower_immunity_b1351': { - # 'name': 'Slower Immunity (decay at log(2)/150), B1351 on day 100', - # 'pars': { - # 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 150, 'init_decay_time': 250, - # 'decay_decay_rate': 0.001}), - # 'strains': [b1351], - # }, - # }, - 'faster_immunity_b1351': { - 'name': 'Faster Immunity (decay at log(2)/30), B1351 on day 100', - 'pars': { - 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), - 'strains': [b1351], - }, - }, - } - - metapars = {'n_runs': n_runs} - scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run() - - to_plot = sc.objdict({ - 'New infections': ['new_infections'], - 'New re-infections': ['new_reinfections'], - 'Population Nabs': ['pop_nabs'], - 'Population Immunity': ['pop_protection'], - }) - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_basic_immunity.png', to_plot=to_plot) - - return scens - - -def test_import1strain(do_plot=False, do_show=True, do_save=False): - sc.heading('Test introducing a new strain partway through a sim') - - strain_pars = { - 'rel_beta': 1.5, - } - pars = { - 'beta': 0.01 - } - strain = cv.Strain(strain_pars, days=1, n_imports=20, label='Strain 2: 1.5x more transmissible') - sim = cv.Sim(use_waning=True, pars=pars, strains=strain, analyzers=cv.snapshot(30, 60), **pars, **base_pars) - sim.run() - - return sim - - -def test_import2strains(do_plot=False, do_show=True, do_save=False): - sc.heading('Test introducing 2 new strains partway through a sim') - - b117 = cv.Strain('b117', days=1, n_imports=20) - p1 = cv.Strain('sa variant', days=2, n_imports=20) - sim = cv.Sim(use_waning=True, strains=[b117, p1], label='With imported infections', **base_pars) - sim.run() - - return sim - +# def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): +# sc.heading('Test varying properties of immunity') + +# # Define baseline parameters +# n_runs = 3 +# base_sim = cv.Sim(use_waning=True, n_days=400, pars=base_pars) + +# # Define the scenarios +# b1351 = cv.Strain('b1351', days=100, n_imports=20) + +# scenarios = { +# 'baseline': { +# 'name': 'Default Immunity (decay at log(2)/90)', +# 'pars': { +# 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, +# 'decay_decay_rate': 0.001}), +# }, +# }, +# 'faster_immunity': { +# 'name': 'Faster Immunity (decay at log(2)/30)', +# 'pars': { +# 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, +# 'decay_decay_rate': 0.001}), +# }, +# }, +# 'baseline_b1351': { +# 'name': 'Default Immunity (decay at log(2)/90), B1351 on day 100', +# 'pars': { +# 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, +# 'decay_decay_rate': 0.001}), +# 'strains': [b1351], +# }, +# }, +# 'faster_immunity_b1351': { +# 'name': 'Faster Immunity (decay at log(2)/30), B1351 on day 100', +# 'pars': { +# 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, +# 'decay_decay_rate': 0.001}), +# 'strains': [b1351], +# }, +# }, +# } + +# metapars = {'n_runs': n_runs} +# scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) +# scens.run() + +# to_plot = sc.objdict({ +# 'New infections': ['new_infections'], +# 'New re-infections': ['new_reinfections'], +# 'Population Nabs': ['pop_nabs'], +# 'Population Immunity': ['pop_protection'], +# }) +# if do_plot: +# scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_basic_immunity.png', to_plot=to_plot) -def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): - sc.heading('Test introducing a new strain with longer duration partway through a sim') +# return scens + + +# def test_import1strain(do_plot=False, do_show=True, do_save=False): +# sc.heading('Test introducing a new strain partway through a sim') + +# strain_pars = { +# 'rel_beta': 1.5, +# } +# pars = { +# 'beta': 0.01 +# } +# strain = cv.Strain(strain_pars, days=1, n_imports=20, label='Strain 2: 1.5x more transmissible') +# sim = cv.Sim(use_waning=True, pars=pars, strains=strain, analyzers=cv.snapshot(30, 60), **pars, **base_pars) +# sim.run() - pars = sc.mergedicts(base_pars, { - 'n_days': 120, - }) +# return sim - strain_pars = { - 'rel_beta': 1.5, - 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} - } - strain = cv.Strain(strain=strain_pars, label='Custom strain', days=10, n_imports=30) - sim = cv.Sim(use_waning=True, pars=pars, strains=strain, label='With imported infections') - sim.run() +# def test_import2strains(do_plot=False, do_show=True, do_save=False): +# sc.heading('Test introducing 2 new strains partway through a sim') - return sim +# b117 = cv.Strain('b117', days=1, n_imports=20) +# p1 = cv.Strain('sa variant', days=2, n_imports=20) +# sim = cv.Sim(use_waning=True, strains=[b117, p1], label='With imported infections', **base_pars) +# sim.run() +# return sim -def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): - sc.heading('Test introducing 2 new strains partway through a sim, with a change_beta intervention') - strain2 = {'rel_beta': 1.5, - 'rel_severe_prob': 1.3} +# def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): +# sc.heading('Test introducing a new strain with longer duration partway through a sim') - strain3 = {'rel_beta': 2, - 'rel_symp_prob': 1.6} +# pars = sc.mergedicts(base_pars, { +# 'n_days': 120, +# }) - intervs = cv.change_beta(days=[5, 20, 40], changes=[0.8, 0.7, 0.6]) - strains = [cv.Strain(strain=strain2, days=10, n_imports=20), - cv.Strain(strain=strain3, days=30, n_imports=20), - ] - sim = cv.Sim(use_waning=True, interventions=intervs, strains=strains, label='With imported infections', **base_pars) - sim.run() +# strain_pars = { +# 'rel_beta': 1.5, +# 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} +# } + +# strain = cv.Strain(strain=strain_pars, label='Custom strain', days=10, n_imports=30) +# sim = cv.Sim(use_waning=True, pars=pars, strains=strain, label='With imported infections') +# sim.run() + +# return sim + + +# def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): +# sc.heading('Test introducing 2 new strains partway through a sim, with a change_beta intervention') + +# strain2 = {'rel_beta': 1.5, +# 'rel_severe_prob': 1.3} + +# strain3 = {'rel_beta': 2, +# 'rel_symp_prob': 1.6} + +# intervs = cv.change_beta(days=[5, 20, 40], changes=[0.8, 0.7, 0.6]) +# strains = [cv.Strain(strain=strain2, days=10, n_imports=20), +# cv.Strain(strain=strain3, days=30, n_imports=20), +# ] +# sim = cv.Sim(use_waning=True, interventions=intervs, strains=strains, label='With imported infections', **base_pars) +# sim.run() + +# return sim + + + +# #%% Vaccination tests - return sim +# def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): +# sc.heading('Test vaccination with a single strain') +# sc.heading('Setting up...') +# pars = sc.mergedicts(base_pars, { +# 'beta': 0.015, +# 'n_days': 120, +# }) +# pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer') +# sim = cv.Sim( +# use_waning=True, +# pars=pars, +# interventions=pfizer +# ) +# sim.run() -#%% Vaccination tests - -def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): - sc.heading('Test vaccination with a single strain') - sc.heading('Setting up...') - - pars = sc.mergedicts(base_pars, { - 'beta': 0.015, - 'n_days': 120, - }) - - pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer') - sim = cv.Sim( - use_waning=True, - pars=pars, - interventions=pfizer - ) - sim.run() - - to_plot = sc.objdict({ - 'New infections': ['new_infections'], - 'Cumulative infections': ['cum_infections'], - 'New reinfections': ['new_reinfections'], - }) - if do_plot: - sim.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_reinfection.png', to_plot=to_plot) - - return sim - - -def test_synthpops(): - sim = cv.Sim(use_waning=True, **sc.mergedicts(base_pars, dict(pop_size=5000, pop_type='synthpops'))) - sim.popdict = cv.make_synthpop(sim, with_facilities=True, layer_mapping={'LTCF': 'f'}) - sim.reset_layer_pars() - - # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 - sim.vxsubtarg = sc.objdict() - sim.vxsubtarg.age = [75, 65, 50, 18] - sim.vxsubtarg.prob = [.05, .05, .05, .05] - sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] - pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) - sim['interventions'] += [pfizer] - - sim.run() - return sim - - - -#%% Multisim and scenario tests - -def test_vaccine_1strain_scen(do_plot=False, do_show=True, do_save=False): - sc.heading('Run a basic sim with 1 strain, pfizer vaccine') - - # Define baseline parameters - n_runs = 3 - base_sim = cv.Sim(use_waning=True, pars=base_pars) - - # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 - base_sim.vxsubtarg = sc.objdict() - base_sim.vxsubtarg.age = [75, 65, 50, 18] - base_sim.vxsubtarg.prob = [.05, .05, .05, .05] - base_sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] - pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) - - # Define the scenarios - - scenarios = { - 'baseline': { - 'name': 'No Vaccine', - 'pars': {} - }, - 'pfizer': { - 'name': 'Pfizer starting on day 20', - 'pars': { - 'interventions': [pfizer], - } - }, - } - - metapars = {'n_runs': n_runs} - scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run() - - to_plot = sc.objdict({ - 'New infections': ['new_infections'], - 'Cumulative infections': ['cum_infections'], - 'New reinfections': ['new_reinfections'], - # 'Cumulative reinfections': ['cum_reinfections'], - }) - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_basic_vaccination.png', to_plot=to_plot) - - return scens - - -def test_vaccine_2strains_scen(do_plot=False, do_show=True, do_save=False): - sc.heading('Run a basic sim with b117 strain on day 10, pfizer vaccine day 20') - - # Define baseline parameters - n_runs = 3 - base_sim = cv.Sim(use_waning=True, pars=base_pars) - - # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 - base_sim.vxsubtarg = sc.objdict() - base_sim.vxsubtarg.age = [75, 65, 50, 18] - base_sim.vxsubtarg.prob = [.01, .01, .01, .01] - base_sim.vxsubtarg.days = subtarg_days = [60, 150, 200, 220] - jnj = cv.vaccinate(days=subtarg_days, vaccine_pars='j&j', subtarget=vacc_subtarg) - b1351 = cv.Strain('b1351', days=10, n_imports=20) - p1 = cv.Strain('p1', days=100, n_imports=100) - - # Define the scenarios - - scenarios = { - 'baseline': { - 'name': 'B1351 on day 10, No Vaccine', - 'pars': { - 'strains': [b1351] - } - }, - 'b1351': { - 'name': 'B1351 on day 10, J&J starting on day 60', - 'pars': { - 'interventions': [jnj], - 'strains': [b1351], - } - }, - 'p1': { - 'name': 'B1351 on day 10, J&J starting on day 60, p1 on day 100', - 'pars': { - 'interventions': [jnj], - 'strains': [b1351, p1], - } - }, - } - - metapars = {'n_runs': n_runs} - scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run() - - to_plot = sc.objdict({ - 'New infections': ['new_infections'], - 'Cumulative infections': ['cum_infections'], - 'New reinfections': ['new_reinfections'], - # 'Cumulative reinfections': ['cum_reinfections'], - }) - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_vaccine_b1351.png', to_plot=to_plot) - - return scens - - -def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): - sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') - - strain_pars = {'dur':{'inf2sym': {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9}}} - strains = cv.Strain(strain=strain_pars, label='10 days til symptoms', days=10, n_imports=30) - tp = cv.test_prob(symp_prob=0.2) # Add an efficient testing program - - pars = sc.mergedicts(base_pars, { - 'beta': 0.015, # Make beta higher than usual so people get infected quickly - 'n_days': 120, - 'interventions': tp - }) - n_runs = 1 - base_sim = cv.Sim(use_waning=True, pars=pars) - - # Define the scenarios - scenarios = { - 'baseline': { - 'name':'1 day to symptoms', - 'pars': {} - }, - 'slowsymp': { - 'name':'10 days to symptoms', - 'pars': {'strains': [strains]} - } - } - - metapars = {'n_runs': n_runs} - scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run() - - to_plot = sc.objdict({ - 'New infections': ['new_infections'], - 'Cumulative infections': ['cum_infections'], - 'New diagnoses': ['new_diagnoses'], - 'Cumulative diagnoses': ['cum_diagnoses'], - }) - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_strainduration.png', to_plot=to_plot) - - return scens - - -def test_waning_vs_not(do_plot=False, do_show=True, do_save=False): - sc.heading('Testing waning...') - - # Define baseline parameters - pars = sc.mergedicts(base_pars, { - 'pop_size': 10e3, - 'pop_scale': 50, - 'n_days': 150, - 'use_waning': False, - }) - - n_runs = 3 - base_sim = cv.Sim(pars=pars) - - # Define the scenarios - scenarios = { - 'no_waning': { - 'name': 'No waning', - 'pars': { - } - }, - 'waning': { - 'name': 'Waning', - 'pars': { - 'use_waning': True, - } - }, - } - - metapars = {'n_runs': n_runs} - scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run() - - to_plot = sc.objdict({ - 'New infections': ['new_infections'], - 'New reinfections': ['new_reinfections'], - 'Cumulative infections': ['cum_infections'], - 'Cumulative reinfections': ['cum_reinfections'], - }) - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_waning_vs_not.png', to_plot=to_plot) - - return scens +# to_plot = sc.objdict({ +# 'New infections': ['new_infections'], +# 'Cumulative infections': ['cum_infections'], +# 'New reinfections': ['new_reinfections'], +# }) +# if do_plot: +# sim.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_reinfection.png', to_plot=to_plot) +# return sim -def test_msim(do_plot=False): - sc.heading('Testing multisim...') - # basic test for vaccine - b117 = cv.Strain('b117', days=0) - sim = cv.Sim(use_waning=True, strains=[b117], **base_pars) - msim = cv.MultiSim(sim, n_runs=2) - msim.run() - msim.reduce() - - to_plot = sc.objdict({ - 'Total infections': ['cum_infections'], - 'New infections per day': ['new_infections'], - 'New Re-infections per day': ['new_reinfections'], - }) - - if do_plot: - msim.plot(to_plot=to_plot, do_save=0, do_show=1, legend_args={'loc': 'upper left'}, axis_args={'hspace': 0.4}, interval=35) - - return msim +# def test_synthpops(): +# sim = cv.Sim(use_waning=True, **sc.mergedicts(base_pars, dict(pop_size=5000, pop_type='synthpops'))) +# sim.popdict = cv.make_synthpop(sim, with_facilities=True, layer_mapping={'LTCF': 'f'}) +# sim.reset_layer_pars() + +# # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 +# sim.vxsubtarg = sc.objdict() +# sim.vxsubtarg.age = [75, 65, 50, 18] +# sim.vxsubtarg.prob = [.05, .05, .05, .05] +# sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] +# pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) +# sim['interventions'] += [pfizer] + +# sim.run() +# return sim + + + +# #%% Multisim and scenario tests + +# def test_vaccine_1strain_scen(do_plot=False, do_show=True, do_save=False): +# sc.heading('Run a basic sim with 1 strain, pfizer vaccine') + +# # Define baseline parameters +# n_runs = 3 +# base_sim = cv.Sim(use_waning=True, pars=base_pars) + +# # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 +# base_sim.vxsubtarg = sc.objdict() +# base_sim.vxsubtarg.age = [75, 65, 50, 18] +# base_sim.vxsubtarg.prob = [.05, .05, .05, .05] +# base_sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] +# pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) + +# # Define the scenarios + +# scenarios = { +# 'baseline': { +# 'name': 'No Vaccine', +# 'pars': {} +# }, +# 'pfizer': { +# 'name': 'Pfizer starting on day 20', +# 'pars': { +# 'interventions': [pfizer], +# } +# }, +# } + +# metapars = {'n_runs': n_runs} +# scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) +# scens.run() + +# to_plot = sc.objdict({ +# 'New infections': ['new_infections'], +# 'Cumulative infections': ['cum_infections'], +# 'New reinfections': ['new_reinfections'], +# # 'Cumulative reinfections': ['cum_reinfections'], +# }) +# if do_plot: +# scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_basic_vaccination.png', to_plot=to_plot) + +# return scens + + +# def test_vaccine_2strains_scen(do_plot=False, do_show=True, do_save=False): +# sc.heading('Run a basic sim with b117 strain on day 10, pfizer vaccine day 20') + +# # Define baseline parameters +# n_runs = 3 +# base_sim = cv.Sim(use_waning=True, pars=base_pars) + +# # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 +# base_sim.vxsubtarg = sc.objdict() +# base_sim.vxsubtarg.age = [75, 65, 50, 18] +# base_sim.vxsubtarg.prob = [.01, .01, .01, .01] +# base_sim.vxsubtarg.days = subtarg_days = [60, 150, 200, 220] +# jnj = cv.vaccinate(days=subtarg_days, vaccine_pars='j&j', subtarget=vacc_subtarg) +# b1351 = cv.Strain('b1351', days=10, n_imports=20) +# p1 = cv.Strain('p1', days=100, n_imports=100) + +# # Define the scenarios + +# scenarios = { +# 'baseline': { +# 'name': 'B1351 on day 10, No Vaccine', +# 'pars': { +# 'strains': [b1351] +# } +# }, +# 'b1351': { +# 'name': 'B1351 on day 10, J&J starting on day 60', +# 'pars': { +# 'interventions': [jnj], +# 'strains': [b1351], +# } +# }, +# 'p1': { +# 'name': 'B1351 on day 10, J&J starting on day 60, p1 on day 100', +# 'pars': { +# 'interventions': [jnj], +# 'strains': [b1351, p1], +# } +# }, +# } + +# metapars = {'n_runs': n_runs} +# scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) +# scens.run() + +# to_plot = sc.objdict({ +# 'New infections': ['new_infections'], +# 'Cumulative infections': ['cum_infections'], +# 'New reinfections': ['new_reinfections'], +# # 'Cumulative reinfections': ['cum_reinfections'], +# }) +# if do_plot: +# scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_vaccine_b1351.png', to_plot=to_plot) + +# return scens + + +# def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): +# sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') + +# strain_pars = {'dur':{'inf2sym': {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9}}} +# strains = cv.Strain(strain=strain_pars, label='10 days til symptoms', days=10, n_imports=30) +# tp = cv.test_prob(symp_prob=0.2) # Add an efficient testing program + +# pars = sc.mergedicts(base_pars, { +# 'beta': 0.015, # Make beta higher than usual so people get infected quickly +# 'n_days': 120, +# 'interventions': tp +# }) +# n_runs = 1 +# base_sim = cv.Sim(use_waning=True, pars=pars) + +# # Define the scenarios +# scenarios = { +# 'baseline': { +# 'name':'1 day to symptoms', +# 'pars': {} +# }, +# 'slowsymp': { +# 'name':'10 days to symptoms', +# 'pars': {'strains': [strains]} +# } +# } + +# metapars = {'n_runs': n_runs} +# scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) +# scens.run() + +# to_plot = sc.objdict({ +# 'New infections': ['new_infections'], +# 'Cumulative infections': ['cum_infections'], +# 'New diagnoses': ['new_diagnoses'], +# 'Cumulative diagnoses': ['cum_diagnoses'], +# }) +# if do_plot: +# scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_strainduration.png', to_plot=to_plot) + +# return scens + + +# def test_waning_vs_not(do_plot=False, do_show=True, do_save=False): +# sc.heading('Testing waning...') + +# # Define baseline parameters +# pars = sc.mergedicts(base_pars, { +# 'pop_size': 10e3, +# 'pop_scale': 50, +# 'n_days': 150, +# 'use_waning': False, +# }) + +# n_runs = 3 +# base_sim = cv.Sim(pars=pars) + +# # Define the scenarios +# scenarios = { +# 'no_waning': { +# 'name': 'No waning', +# 'pars': { +# } +# }, +# 'waning': { +# 'name': 'Waning', +# 'pars': { +# 'use_waning': True, +# } +# }, +# } + +# metapars = {'n_runs': n_runs} +# scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) +# scens.run() + +# to_plot = sc.objdict({ +# 'New infections': ['new_infections'], +# 'New reinfections': ['new_reinfections'], +# 'Cumulative infections': ['cum_infections'], +# 'Cumulative reinfections': ['cum_reinfections'], +# }) +# if do_plot: +# scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_waning_vs_not.png', to_plot=to_plot) + +# return scens + + +# def test_msim(do_plot=False): +# sc.heading('Testing multisim...') + +# # basic test for vaccine +# b117 = cv.Strain('b117', days=0) +# sim = cv.Sim(use_waning=True, strains=[b117], **base_pars) +# msim = cv.MultiSim(sim, n_runs=2) +# msim.run() +# msim.reduce() + +# to_plot = sc.objdict({ +# 'Total infections': ['cum_infections'], +# 'New infections per day': ['new_infections'], +# 'New Re-infections per day': ['new_reinfections'], +# }) + +# if do_plot: +# msim.plot(to_plot=to_plot, do_save=0, do_show=1, legend_args={'loc': 'upper left'}, axis_args={'hspace': 0.4}, interval=35) + +# return msim #%% Plotting and utilities -# def plot_results(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): - -# results = sim.results -# results_to_plot = results[key] - -# # extract data for plotting -# x = sim.results['t'] -# y = results_to_plot.values -# y = np.transpose(y) - -# fig, ax = plt.subplots() -# ax.plot(x, y) - -# ax.set(xlabel='Day of simulation', ylabel=results_to_plot.name, title=title) - -# if labels is None: -# labels = [0]*len(y[0]) -# for strain in range(len(y[0])): -# labels[strain] = f'Strain {strain +1}' -# ax.legend(labels) - -# if do_show: -# plt.show() -# if do_save: -# cv.savefig(f'results/{filename}.png') - -# return - - -# def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): - -# results = sim.results -# n_strains = sim.results['new_infections_by_strain'].values.shape[0] # TODO: this should be stored in the sim somewhere more intuitive! -# prop_new = {f'Strain {s}': sc.safedivide(results[key+'_by_strain'].values[s,:], results[key].values, 0) for s in range(n_strains)} -# num_new = {f'Strain {s}': results[key+'_by_strain'].values[s,:] for s in range(n_strains)} - -# # extract data for plotting -# x = sim.results['t'] -# fig, ax = plt.subplots(2,1,sharex=True) -# ax[0].stackplot(x, prop_new.values(), -# labels=prop_new.keys()) -# ax[0].legend(loc='upper left') -# ax[0].set_title(title) -# ax[1].stackplot(sim.results['t'], num_new.values(), -# labels=num_new.keys()) -# ax[1].legend(loc='upper left') -# ax[1].set_title(title) - -# if do_show: -# plt.show() -# if do_save: -# cv.savefig(f'results/{filename}.png') - -# return - - def vacc_subtarg(sim): ''' Subtarget by age''' From 79a32a3088c4f7cac6a61e1f559f2a55dc133168 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 23:12:51 -0700 Subject: [PATCH 417/569] decay does not seem to be working --- tests/test_immunity.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 841453e00..83d6609fa 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -13,7 +13,7 @@ cv.options.set(interactive=False) # Assume not running interactively base_pars = dict( - pop_size = 1e3, + pop_size = 10e3, verbose = -1, ) @@ -21,12 +21,16 @@ def test_waning(do_plot=False): sc.heading('Testing with and without waning') - s1 = cv.Sim(base_pars, n_days=300, use_waning=False, label='No waning').run() - s2 = cv.Sim(base_pars, n_days=300, use_waning=True, label='With waning').run() + pars = dict( + n_days = 500, + beta = 0.008, + NAb_decay = dict(form='nab_decay', pars={'init_decay_rate': 0.1, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) + ) + s1 = cv.Sim(base_pars, **pars, use_waning=False, label='No waning').run() + s2 = cv.Sim(base_pars, **pars, use_waning=True, label='With waning').run() msim = cv.MultiSim([s1,s2]) - # msim.run() if do_plot: - msim.plot('overview-strain') + msim.plot('overview-strain', rotation=30) return msim From 654124b9d2360915815a08b9fddd7e1e6aa9974a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 23:18:20 -0700 Subject: [PATCH 418/569] have theory --- covasim/people.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/covasim/people.py b/covasim/people.py index df88c1247..06f5a8352 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -256,6 +256,7 @@ def check_recovery(self): # Handle immunity aspects if self.pars['use_waning']: + print(f'DEBUG: {self.t} {len(inds)}') # Before letting them recover, store information about the strain they had, store symptoms and pre-compute NAbs array self.recovered_strain[inds] = self.exposed_strain[inds] @@ -415,8 +416,9 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.exposed_by_strain[strain, inds] = True self.date_exposed[inds] = self.t self.flows['new_infections'] += len(inds) - self.flows_strain['new_infections_by_strain'][strain] += len(inds) self.flows['new_reinfections'] += len(cvu.defined(self.date_recovered[inds])) # Record reinfections + self.flows_strain['new_infections_by_strain'][strain] += len(inds) + print('HI DEBUG', self.t, len(inds), len(cvu.defined(self.date_recovered[inds]))) #self.date_recovered[inds] = np.nan # Reset date they recovered - we only store the last recovery # TODO CK # Record transmissions From 9a71ad685b50323b10550e46d1cc53e4df840f47 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 10 Apr 2021 23:20:52 -0700 Subject: [PATCH 419/569] confused --- tests/test_immunity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 83d6609fa..eb9ca0d91 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -31,6 +31,7 @@ def test_waning(do_plot=False): msim = cv.MultiSim([s1,s2]) if do_plot: msim.plot('overview-strain', rotation=30) + sc.maximize() return msim From 2677aef488cad5dc5142ed0669f7c1e1b2ec4c69 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 02:30:38 -0700 Subject: [PATCH 420/569] getting closer --- covasim/defaults.py | 2 +- covasim/immunity.py | 6 ++-- covasim/people.py | 45 +++++++++++++++----------- covasim/plotting.py | 6 ++-- covasim/sim.py | 34 ++++++++++---------- tests/immcov | 2 +- tests/state_diagram.xlsx | Bin 0 -> 6576 bytes tests/test_immunity.py | 66 +++++++++++++++++++++++++++++++++++++-- 8 files changed, 112 insertions(+), 49 deletions(-) create mode 100644 tests/state_diagram.xlsx diff --git a/covasim/defaults.py b/covasim/defaults.py index a697c417e..6c1b3cc54 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -74,7 +74,7 @@ def __init__(self): 'dead', 'known_contact', 'quarantined', - 'vaccinated' + 'vaccinated', ] self.strain_states = [ diff --git a/covasim/immunity.py b/covasim/immunity.py index 6262dca71..4469907da 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -354,12 +354,10 @@ def init_nab(people, inds, prior_inf=True): return -# + def check_nab(t, people, inds=None): ''' Determines current NAbs based on date since recovered/vaccinated.''' - # import traceback; traceback.print_exc(); import pdb; pdb.set_trace() - # Indices of people who've had some NAb event rec_inds = cvu.defined(people.date_recovered[inds]) vac_inds = cvu.defined(people.date_vaccinated[inds]) @@ -684,7 +682,7 @@ def linear_growth(length, slope): return (slope * t) -def create_cross_immunity(circulating_strains, rel_imms): +def create_cross_immunity(circulating_strains, rel_imms): # TODO: refactor known_strains = ['wild', 'b117', 'b1351', 'p1'] known_cross_immunity = dict() known_cross_immunity['wild'] = {} # cross-immunity to wild diff --git a/covasim/people.py b/covasim/people.py index 06f5a8352..c28909f64 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -62,8 +62,10 @@ def __init__(self, pars, strict=True, **kwargs): # Set person properties -- all floats except for UID for key in self.meta.person: - if key in ['uid', 'vaccinations']: + if key in ['uid']: self[key] = np.arange(self.pop_size, dtype=cvd.default_int) + elif key in ['vaccinations']: + self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) elif key in ['sus_imm', 'symp_imm', 'sev_imm']: # everyone starts out with no immunity self[key] = np.full((self.total_strains, self.pop_size), 0, dtype=cvd.default_float) else: @@ -267,9 +269,9 @@ def check_recovery(self): self.susceptible[inds] = True self.infectious_strain[inds] = np.nan self.exposed_strain[inds] = np.nan - self.prior_symptoms[inds] = self.pars['rel_imm']['asymptomatic'] # - self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] # - self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] # + self.prior_symptoms[inds] = self.pars['rel_imm']['asymptomatic'] + self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] + self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] if len(inds): cvi.init_nab(self, inds, prior_inf=True) return len(inds) @@ -278,16 +280,19 @@ def check_recovery(self): def check_death(self): ''' Check whether or not this person died on this timestep ''' inds = self.check_inds(self.dead, self.date_dead, filter_inds=self.is_exp) - self.exposed[inds] = False - self.infectious[inds] = False - self.symptomatic[inds] = False - self.severe[inds] = False - self.critical[inds] = False - self.recovered[inds] = False - self.dead[inds] = True - self.infectious_strain[inds]= np.nan - self.exposed_strain[inds] = np.nan - self.recovered_strain[inds] = np.nan + self.susceptible[inds] = False + self.exposed[inds] = False + self.infectious[inds] = False + self.symptomatic[inds] = False + self.severe[inds] = False + self.critical[inds] = False + self.known_contact[inds] = False + self.quarantined[inds] = False + self.recovered[inds] = False + self.dead[inds] = True + self.infectious_strain[inds] = np.nan + self.exposed_strain[inds] = np.nan + self.recovered_strain[inds] = np.nan return len(inds) @@ -410,16 +415,17 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str durpars = self.pars['dur'] # Update states, strain info, and flows - self.susceptible[inds] = False - self.exposed[inds] = True + self.susceptible[inds] = False + self.recovered[inds] = False + self.exposed[inds] = True + self.date_exposed[inds] = self.t self.exposed_strain[inds] = strain self.exposed_by_strain[strain, inds] = True - self.date_exposed[inds] = self.t - self.flows['new_infections'] += len(inds) + self.flows['new_infections'] += len(inds) self.flows['new_reinfections'] += len(cvu.defined(self.date_recovered[inds])) # Record reinfections self.flows_strain['new_infections_by_strain'][strain] += len(inds) print('HI DEBUG', self.t, len(inds), len(cvu.defined(self.date_recovered[inds]))) - #self.date_recovered[inds] = np.nan # Reset date they recovered - we only store the last recovery # TODO CK + # self.date_recovered[inds] = np.nan # Reset date they recovered - we only store the last recovery # TODO CK # Record transmissions for i, target in enumerate(inds): @@ -485,6 +491,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str dur_crit2die = cvu.sample(**durpars['crit2die'], size=len(dead_inds)) self.date_dead[dead_inds] = self.date_critical[dead_inds] + dur_crit2die # Date of death self.dur_disease[dead_inds] = self.dur_exp2inf[dead_inds] + self.dur_inf2sym[dead_inds] + self.dur_sym2sev[dead_inds] + self.dur_sev2crit[dead_inds] + dur_crit2die # Store how long this person had COVID-19 + self.date_recovered[dead_inds] = np.nan # If they did die, remove them from recovered return n_infections # For incrementing counters diff --git a/covasim/plotting.py b/covasim/plotting.py index 68399376b..4a92ed9de 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -377,7 +377,7 @@ def set_line_options(input_args, reskey, resnum, default): #%% Core plotting functions -def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, +def plot_sim(to_plot=None, sim=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, date_args=None, show_args=None, mpl_args=None, n_cols=None, grid=False, commaticks=True, setylim=True, log_scale=False, colors=None, labels=None, do_show=None, sep_figs=False, @@ -427,7 +427,7 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, args) -def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, +def plot_scens(to_plot=None, scens=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, date_args=None, show_args=None, mpl_args=None, n_cols=None, grid=False, commaticks=True, setylim=True, log_scale=False, colors=None, labels=None, do_show=None, sep_figs=False, fig=None, ax=None, **kwargs): @@ -481,7 +481,7 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, args) -def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, +def plot_result(key, sim=None, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, date_args=None, mpl_args=None, grid=False, commaticks=True, setylim=True, color=None, label=None, do_show=None, do_save=False, fig_path=None, fig=None, ax=None, **kwargs): ''' Plot a single result -- see Sim.plot_result() for documentation ''' diff --git a/covasim/sim.py b/covasim/sim.py index 6b422a111..dfa852250 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -29,16 +29,16 @@ class Sim(cvb.BaseSim): loading, exporting, etc.). Please see the BaseSim class for additional methods. Args: - pars (dict): parameters to modify from their default values - datafile (str/df): filename of (Excel, CSV) data file to load, or a pandas dataframe of the data - datacols (list): list of column names of the data to load - label (str): the name of the simulation (useful to distinguish in batch runs) - simfile (str): the filename for this simulation, if it's saved (default: creation date) - popfile (str): the filename to load/save the population for this simulation - load_pop (bool): whether to load the population from the named file - save_pop (bool): whether to save the population to the named file - version (str): if supplied, use default parameters from this version of Covasim instead of the latest - kwargs (dict): passed to make_pars() + pars (dict): parameters to modify from their default values + datafile (str/df): filename of (Excel, CSV) data file to load, or a pandas dataframe of the data + datacols (list): list of column names of the data to load + label (str): the name of the simulation (useful to distinguish in batch runs) + simfile (str): the filename for this simulation, if it's saved (default: creation date) + popfile (str): the filename to load/save the population for this simulation + load_pop (bool): whether to load the population from the named file + save_pop (bool): whether to save the population to the named file + version (str): if supplied, use default parameters from this version of Covasim instead of the latest + kwargs (dict): passed to make_pars() **Examples**:: @@ -109,7 +109,7 @@ def initialize(self, reset=False, **kwargs): self.validate_pars() # Ensure parameters have valid values self.set_seed() # Reset the random seed before the population is created if self['use_waning']: - self.init_strains() # ...and the strains.... + self.init_strains() # ...and the strains.... # TODO: move out of if? self.init_immunity() # ... and information about immunity/cross-immunity. self.init_results() # After initializing the strain, create the results structure self.init_people(save_pop=self.save_pop, load_pop=self.load_pop, popfile=self.popfile, reset=reset, **kwargs) # Create all the people (slow) @@ -476,10 +476,7 @@ def init_strains(self): self['strains'] = self._orig_pars.pop('strains') # Restore for strain in self['strains']: - if isinstance(strain, cvimm.Strain): - strain.initialize(self) - else: - raise Exception('Wrong type') # TODO: refactor + strain.initialize(self) # Calculate the total number of strains that will be active at some point in the sim self['total_strains'] = self['n_strains'] + len(self['strains']) @@ -599,7 +596,7 @@ def step(self): # Check NAbs. Take set difference so we don't compute NAbs for anyone currently infected if self['use_waning']: - has_nabs = np.setdiff1d(cvu.defined(people.init_NAb), cvu.false(people.exposed)) + has_nabs = np.setdiff1d(cvu.defined(people.init_NAb), cvu.false(people.susceptible)) if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) # Iterate through n_strains to calculate infections @@ -820,10 +817,11 @@ def compute_states(self): the number removed, and recalculates susceptibles to handle scaling. ''' res = self.results + count_recov = 1-self['use_waning'] # If waning is on, don't count recovered people as removed self.results['n_alive'][:] = self.scaled_pop_size - res['cum_deaths'][:] # Number of people still alive - self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents + self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - count_recov*res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents self.results['n_preinfectious'][:] = res['n_exposed'][:] - res['n_infectious'][:] # Calculate the number not yet infectious: exposed minus infectious - self.results['n_removed'][:] = res['cum_recoveries'][:] + res['cum_deaths'][:] # Calculate the number removed: recovered + dead + self.results['n_removed'][:] = count_recov*res['cum_recoveries'][:] + res['cum_deaths'][:] # Calculate the number removed: recovered + dead self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence diff --git a/tests/immcov b/tests/immcov index 527f22fa6..d837827a1 100755 --- a/tests/immcov +++ b/tests/immcov @@ -2,7 +2,7 @@ # Note that although the script runs when parallelized, the coverage results are wrong. echo 'Running tests...' -pytest -v test_immunity.py --cov-config=.coveragerc --cov=../covasim --workers auto --durations=0 +pytest -v test_immunity.py --cov-config=.coveragerc --cov=../covasim --durations=0 echo 'Creating HTML report...' coverage html diff --git a/tests/state_diagram.xlsx b/tests/state_diagram.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0e23557e211ea85a071ab63ab5b17230efc7450e GIT binary patch literal 6576 zcmaJ_1yq#nwkD)SLWb^!p&RKgkp}558CvPiA%~I>h89F(Bt;seLsDTV0VxTI8CvDS z|DSvGdhYr6x7IiFt@Z9Vc0BLikG2N-9TF65Y;2T%^#om%-vsaGzonNw$eWkv_FbJe zu8z)67<&*LpAzWX1W>fAZwF80ss@K;kN&h4&EcbX791FfZBp?P3!)Z+7!y@^u|(`} z5}&6Muk8NX1elooojkCl;NICTRlAGP7}Ezsb!GzF;ny?0AS8X78)_XD-<+9F&tr=R zYgR9*-^JQ3*#$h@+ijnMD^8c4mamcwW6z?p-c;-=$NH{Ypec(_t{D}laG$X& z^9r^RX^kuD85B}JY6bE7UIaeEX8&Yby8`xvHtW|XW_Lc5d>88lmAStaRrAwZyct^= z^0O^X^XrJy$F4~>Cm9rN4J>Sj*AZ`5ZjLR1hH~@$-wZ==Gs5p?4R%Xzi=);6PO@!A;}!*DJa}FdT!c)wta>zd_v2gibO5 z1{&q;by&R;ffa80yrR-3;b^Q7Pjf!)aJ_rjV~FxtLk2gZ!qU6waqJtu>&T)J-Ssz< z7qyttG+@-B)VT$9q0H*cmnnyH!s|z67o#r4P>&qJ&=d8m$|He%x@1jtI6v@RgKU!F zC6eXpp17`xnffMxnjsJCi}VHoc2b6#{UH1SEk@qnfuGBRUQ^fJ1jE4%9#NCw>>_cC zN+KPSi%!N?A8i83`gX*3X8j^9{t|%s@}kzaOJ>|Er|{`X(BhSw+#WCy&XuP!s*u0* zHZmU0T;Z)= zCZzJIXi988A;D@o1tRZq_uNd}t>_q?V-Ci?jHXNptS%_TRcnDiVz9WZba;kEtE$&W zth*epN?-V6RJ~A0lY59`63O(sdttl$&8AA?@&VymrWO4O<(?QUq;~(oQW5bdcc*CZ zA&3LWKnCG@yBp7t{gGJTCU8@%!@MUVjGg&PfWzPQ_)hqHBaOoW_SE~+Add7+xjix6z`)Avd923<=h@n7Q^*{FMG*zoGAgt6QbjiO z&-}wW!@1CEeiep2OcROWc-{yTQa*Q4tan7mjL%txEv-DCu7v8|10+l!bGmmd$gh}5 znO)HEEZc1{F8s98i+vM>4BGGFO(U6xAEdT2-k+FMy?P@xlp(l26#OLL1X>Mb|?!M;grw#3>gj`JeeqXC7 zIEvkWcU+tAs2=M28JXTN*2(Q^Y3cF)vm8v+If21B;ox}#UCYM~(@!DIhEyzLovdRS zLao6viI0LWeWMAK4K?yBs=Miq#YiE?O>nYoGDRu)R#%U*wL0#278T2&`BG$2I= z^}=K#pI>px7GnH}4-w0k{S&V0vAAdo1yzo@dreVowBu0c1MU{j_wNe z&ZD8tuNT%NZpY_H?8fMaG0(Q*G}!?MIyDO$Ft?yH0W&q?J3CHi7iLEw|3HMinQsEs z^=Ym6aOiH6$Qq~7BC5UT6?^sv^}@1%9f}?QZ~WGF;+pw2Gc97Q7_5m6yHda}LDY`+ zNY#S;u_$8-{;8U?yBpJeleTC_)8j(elH+HZ6iK#Fwq#9zMDPHAZBBz)6i0F*()FE) z(ZV7F!C81k8NoBKbJ^x2p|P2~UF*uJF(GW^N0B}q>2@@|1NMRsOo8f zcGJPIv?M0^ymDk_l1vn4U%B%yoD{@d7RWe?N|`5!9l5UY7u-7$rTiM2_*2-=deyt3 zJi}!oj1K=qYOC9|oz^%ofL=@Nz}qTvaW^ZF=OBPMuM(UJI4q+HpiT`Neb15~u8Wumo{vnR+Bx)2 z&y$qahM45dUtE2IETS_JxW}q&LaE+VC?wO8@q~$!I&82V`n|ehET9z{LbJAcD5cd~ z;gV^UH4!o})A%`&hL&qhE;CV?=`5J+Gf!50klP@YM8C6|OW*Y2;9NaB@xF*6Z=VkM zJEo$fcm-LDav@UjzPpIBpk>NprBde_57F~2p8J|xu=)=F)YD^|_jGd`3O+T%1)$5!Afe^A5u@6eORxXdOi9+u~nWvzbPBZi7sYT<$ zaCOZl^@pc2pYw8F2#$IO%b*bM=y{b&A*`P0)1fG%EOM9J{tJpE zt7X~}h>ver8J+wf&9Oc|v0*3>chyb^8=in!Uyz#f2y>&paWRbdB6Wq#A#4tnMIPI1 z?tZ#6Np5s56T4IrW#pxrUpDb);^G`a$vgewIFPpSGX-cAga35;Y>N9KrOkFpfy_4_ zg3kwZW!D`p%gYp-p_tE98fa;l5-<^MERNB zlM4)CrQLx!2BnTKh3xn<+MrqChTHN9y};rGfsASJh$r`m>;j8c$BRe<3M$^hVbA>Q_dT7TwHC zH8VxZ;*$lAXf`n*XyjHh(^NC#7E-bWr1rfe){QS?=U0dmSLf2Ui6aI|GPqE~3w@f{ zYg-jb?CcG>fqJP18fnktM%0z;jEj2pS#ICdETrtUigAGa6yao$wuEWMoH-+I-4i43 z$|gpjUYY?_*7NwmI{7#b^-+EKOu+v;_s0qoLmDN+slcAM_jrJSRQqURy^QRlUbDYe zAZ9W2lm8@EC$|sMvGI+&$z9XNs+Rbw2WeF;%~kq}H@?8kh|SjDE*;(CHbRW=O75C-j$Y>&d})b53AtO;FWwAqy2ak4GKln=#PcP(eo@ zn>ojI@WK#mkH|6a9)($vvLrGJlNg&@JDc~g>%Py0TXgU72r4xibTGSTR1J+J9Ama{ zN%p7WBtP`Di(&GnHeMMdRxuQ-&2K{#*06A107VJ?L)G%3_~V zB(G-;?7=LzZ;kN z2t&yqvf?tyv>(q+Wn)`dM1Cr_kVf&~ng1kpH?u~?$03t=@fB7w*ZYtb9=8lFL~_7M zNVB%^XenYP2_395nF3iv&~kok}+2SiA`lZ zVUig*X;?w161z4B@2Kq34P-=Q3#7Ac6MVJCE@7bPOj`0?B^amX^1C!xtJ^71z4r*Z zEjcgMUNDiOLCY@Vt{rU^ng9)0CN~3Ip^S^&0USKc0PYO=r*q>JnDOoPZHZAf*1-3v zpnKDr$ySR(N^V2-KtzLhu~?y2%q&;h)oQbKZyye1>FALBZ7Ey1QymlF>nwr+ciW`!;Qbs^Zu_~+*-U3_P`Hge z*%YAQA>|G#p1-Quy;T3+fDXid=NXR--RR_A%KC>cY>Z^M9JObJG$MzV>R9$ZstHQH zkE*m;vnt@s&mnEOmDD|pn5><={;Pb6fXZL`W22yii2PIeLiu<3@;9&Wdk6hD$B?S& z@}BQ*%)t+U@>!o6mTjU~d!iid$2n@h&e}w;NZO~y>ku9)Q(XJKiS&tCM2G6hj!@DL zxL4sW=Ii1+(cOJM*zMszzU{A47>*S$uQ;(l-p~Nu&f?#*ED0>&8^p&M^P)`^oYaNb ztN?ihu#m178&VrGj5Kph!?4H#T@hcLkCqO!2LX8QI1AOPHc2Gwu?$Tf_S`)okxf z>kxE>usa$&GYa^k4WuIWDse1+G2wb&(&FCy3p3LKrA;A9;Cxy%{WsaSBjjzD0oVG} zb{DTS+;)$nt|JxVxDxja(d|}O`7k4R@OC}$GtE^q95=RA?&;DxA`a z(F6${m*03hM8Z$NR_}+r9Nhe=jk`qe1_m{v!XJOI0j0-b!mZ1mLu)@34OI`{r`|H| zs(6y2B_IgYrq;9I1u_gp-CZ)M8dAc0|12Q=bu-}GvgeKC+vP4WHf6k|@`$nyWvC7N zkO9!oBSyn2Ga=}=6}Kxu@uSgA&)$Dib^mv|hv>#JcJ8*?UhW>=yteLM_P3?BtL|s5 z9zMd5GuY$Hj%QT8*)d6q_*!Y%D3j-g3%2^RHO>T>*X>J~dW`O8Kem0_T^tc5H6`d` zFiO1?E;Y3~%0jw1ujzA77(?h&Ros|iROlGu;cSRwH&(9uNs&ggk3C*>sW1%sv4Z4$ zz?nKbefNaU)I753-KfgNF}FFNygT0nOh{@TRA8>BF_oKJfd&-4k~3@)8v>2aP<6Fm z$wkU()ialLTHGoBF^1lLJWv71=UQyA=$#BkJ`rki&230uru>&kBsjE>3V^TD=%`N(4Fc<#EDie3+OIwx_tAbSazzEmCl#% z$N$!bQ#Ni!qHOiPi|niGF|y4fML-L|^{Cu4vwAvjLww{IQuFnh>}Pnl+%wCUd8e8x zb4&9FGM%z3d{fx+Nw_ZL+Zq=KR~a(5{=gp>kk+~JNBln~3gPemu=en{bq6#>>lQfr z4z5VLkuSvX7=V4AaB>MilY)p3UQKla?a$ci4CKQ2LgBu@sFc)EI&#jZwoFX!;e2cU zqLDC1ml~`=0=XXS$1z)~%AQBuFR?vDK@2~ZI3*1Z15@gz;_#fTgULlQSpDw+b)UOt zL5Ea;d?S8^Auk%D9C;n_?m6qt(nMo)b7(-)VB`oT{P@5pTuxOP z{T#F&yPOXGwd;U4h@6dcx4ovl*;bVhb;-s`pXIPbtJ=wk3KAm|;3*3yPz zVs(lhy9J?Nl6|wEf2;oslK#~GJu+@H^Otl3{@*;+*1*8L R9fWi9$lM^JDcSAU{{TPOU1k6P literal 0 HcmV?d00001 diff --git a/tests/test_immunity.py b/tests/test_immunity.py index eb9ca0d91..5a4b8af37 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -7,13 +7,14 @@ import sciris as sc import covasim as cv import numpy as np +import pandas as pd -do_plot = 1 +do_plot = 0 do_save = 0 cv.options.set(interactive=False) # Assume not running interactively base_pars = dict( - pop_size = 10e3, + pop_size = 1e3, verbose = -1, ) @@ -35,6 +36,64 @@ def test_waning(do_plot=False): return msim +def test_states(): + sc.heading('Testing states') + + # Load state diagram + rawdf = pd.read_excel('state_diagram.xlsx', nrows=13) + rawdf = rawdf.set_index('From ↓ to →') + + # Create and run simulation + for use_waning in [True]: + + # Different states are possible with or without waning: resolve discrepancies + df = sc.dcp(rawdf) + if use_waning: + df = df.replace(-0.5, 0) + df = df.replace(-0.1, 1) + else: + df = df.replace(-0.5, -1) + df = df.replace(-0.1, -1) + + pars = dict( + pop_size = 1e3, + pop_infected = 20, + n_days = 70, + use_waning = use_waning, + verbose = 0, + interventions = [ + cv.test_prob(symp_prob=0.4, asymp_prob=0.01), + cv.contact_tracing(trace_probs=0.1), + ] + ) + sim = cv.Sim(pars).run() + ppl = sim.people + + # Check states + states = df.columns.values.tolist() + for s1 in states: + for s2 in states: + if s1 != s2: + relation = df.loc[s1, s2] # e.g. df.loc['susceptible', 'exposed'] + print(f'Checking {s1:13s} → {s2:13s} = {relation:4n} ... ', end='') + inds = cv.true(ppl[s1]) + n_inds = len(inds) + vals2 = ppl[s2][inds] + is_true = cv.true(vals2) + is_false = cv.false(vals2) + n_true = len(is_true) + n_false = len(is_false) + if relation == 1: + errormsg = f'Being {s1}=True implies {s2}=True, but only {n_true}/{n_inds} people are' + assert n_true == n_inds, errormsg + elif relation == -1: + errormsg = f'Being {s1}=True implies {s2}=False, but only {n_false}/{n_inds} people are' + assert n_false == n_inds, errormsg + print(f'ok: {n_true}/{n_inds}') + + return + + # def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): # sc.heading('Test varying properties of immunity') @@ -457,7 +516,8 @@ def get_ind_of_min_value(list, time): cv.options.set(interactive=do_plot) T = sc.tic() - msim1 = test_waning(do_plot=do_plot) + # msim1 = test_waning(do_plot=do_plot) + sim1 = test_states() sc.toc(T) print('Done.') From 394bfafea930b7c5de68eb03fef76967f5af29f5 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 02:56:41 -0700 Subject: [PATCH 421/569] actually easier for single error message --- covasim/interventions.py | 1 + covasim/people.py | 4 ++-- tests/test_immunity.py | 25 +++++++++++++++---------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 3f551f752..8bf3409ef 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1071,6 +1071,7 @@ def identify_contacts(self, sim, trace_inds): continue traceable_inds = sim.people.contacts[lkey].find_contacts(trace_inds) + traceable_inds = np.setdiff1d(traceable_inds, cvu.true(sim.people.dead)) # Do not trace people who are dead if len(traceable_inds): contacts[self.trace_time[lkey]].extend(cvu.binomial_filter(this_trace_prob, traceable_inds)) # Filter the indices according to the probability of being able to trace this layer diff --git a/covasim/people.py b/covasim/people.py index c28909f64..72bc3a2d7 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -352,7 +352,7 @@ def make_susceptible(self, inds): self[key][inds] = False for key in self.meta.strain_states: - if 'by' in key: + if 'by' in key: # TODO: refactor self[key][:, inds] = False else: self[key][inds] = np.nan @@ -424,7 +424,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str self.flows['new_infections'] += len(inds) self.flows['new_reinfections'] += len(cvu.defined(self.date_recovered[inds])) # Record reinfections self.flows_strain['new_infections_by_strain'][strain] += len(inds) - print('HI DEBUG', self.t, len(inds), len(cvu.defined(self.date_recovered[inds]))) + # print('HI DEBUG', self.t, len(inds), len(cvu.defined(self.date_recovered[inds]))) # self.date_recovered[inds] = np.nan # Reset date they recovered - we only store the last recovery # TODO CK # Record transmissions diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 5a4b8af37..e70cac554 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -37,14 +37,13 @@ def test_waning(do_plot=False): def test_states(): - sc.heading('Testing states') - # Load state diagram rawdf = pd.read_excel('state_diagram.xlsx', nrows=13) rawdf = rawdf.set_index('From ↓ to →') # Create and run simulation for use_waning in [True]: + sc.heading(f'Testing state consistency with waning = {use_waning}') # Different states are possible with or without waning: resolve discrepancies df = sc.dcp(rawdf) @@ -56,9 +55,9 @@ def test_states(): df = df.replace(-0.1, -1) pars = dict( - pop_size = 1e3, + pop_size = 10e3, pop_infected = 20, - n_days = 70, + n_days = 120, use_waning = use_waning, verbose = 0, interventions = [ @@ -70,12 +69,13 @@ def test_states(): ppl = sim.people # Check states + errs = [] states = df.columns.values.tolist() for s1 in states: for s2 in states: if s1 != s2: relation = df.loc[s1, s2] # e.g. df.loc['susceptible', 'exposed'] - print(f'Checking {s1:13s} → {s2:13s} = {relation:4n} ... ', end='') + print(f'Checking {s1:13s} → {s2:13s} = {relation:2n} ... ', end='') inds = cv.true(ppl[s1]) n_inds = len(inds) vals2 = ppl[s2][inds] @@ -83,13 +83,18 @@ def test_states(): is_false = cv.false(vals2) n_true = len(is_true) n_false = len(is_false) - if relation == 1: + if relation == 1 and n_true != n_inds: errormsg = f'Being {s1}=True implies {s2}=True, but only {n_true}/{n_inds} people are' - assert n_true == n_inds, errormsg - elif relation == -1: + errs.append(errormsg) + print(f'× {n_true}/{n_inds} error!') + elif relation == -1 and n_false != n_inds: errormsg = f'Being {s1}=True implies {s2}=False, but only {n_false}/{n_inds} people are' - assert n_false == n_inds, errormsg - print(f'ok: {n_true}/{n_inds}') + errs.append(errormsg) + print(f'× {n_true}/{n_inds} error!') + else: + print(f'✓ {n_true}/{n_inds}') + if len(errs): + raise RuntimeError('\n'+sc.newlinejoin(errs)) return From 8c96e3d883dbc9bbe8b7dea364fa0637ee920d07 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 03:13:20 -0700 Subject: [PATCH 422/569] fixed states --- covasim/people.py | 36 ++++++++++++++++++++++-------------- tests/test_immunity.py | 14 ++++++-------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index 72bc3a2d7..a41de6cfb 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -248,6 +248,9 @@ def check_recovery(self): ''' Check for recovery ''' inds = self.check_inds(self.recovered, self.date_recovered, filter_inds=self.is_exp) # TODO TEMP!!!! + if 7723 in inds: + print('yes on ', self.t) + # Now reset all disease states self.exposed[inds] = False self.infectious[inds] = False @@ -258,7 +261,7 @@ def check_recovery(self): # Handle immunity aspects if self.pars['use_waning']: - print(f'DEBUG: {self.t} {len(inds)}') + # print(f'DEBUG: {self.t} {len(inds)}') # Before letting them recover, store information about the strain they had, store symptoms and pre-compute NAbs array self.recovered_strain[inds] = self.exposed_strain[inds] @@ -417,8 +420,8 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str # Update states, strain info, and flows self.susceptible[inds] = False self.recovered[inds] = False + self.diagnosed[inds] = False self.exposed[inds] = True - self.date_exposed[inds] = self.t self.exposed_strain[inds] = strain self.exposed_by_strain[strain, inds] = True self.flows['new_infections'] += len(inds) @@ -433,8 +436,13 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str # Calculate how long before this person can infect other people self.dur_exp2inf[inds] = cvu.sample(**durpars['exp2inf'], size=n_infections) + self.date_exposed[inds] = self.t self.date_infectious[inds] = self.dur_exp2inf[inds] + self.t + # Reset all other dates + for key in ['date_symptomatic', 'date_severe', 'date_critical', 'date_diagnosed', 'date_recovered']: + self[key][inds] = np.nan + # Use prognosis probabilities to determine what happens to them symp_probs = infect_pars['rel_symp_prob']*self.symp_prob[inds]*(1-self.symp_imm[strain, inds]) # Calculate their actual probability of being symptomatic is_symp = cvu.binomial_arr(symp_probs) # Determine if they develop symptoms @@ -641,18 +649,18 @@ def label_lkey(lkey): events = [] dates = { - 'date_critical' : 'became critically ill and needed ICU care', - 'date_dead' : 'died ☹', - 'date_diagnosed' : 'was diagnosed with COVID', - 'date_end_quarantine' : 'ended quarantine', - 'date_infectious' : 'became infectious', - 'date_known_contact' : 'was notified they may have been exposed to COVID', - 'date_pos_test' : 'recieved their positive test result', - 'date_quarantined' : 'entered quarantine', - 'date_recovered' : 'recovered', - 'date_severe' : 'developed severe symptoms and needed hospitalization', - 'date_symptomatic' : 'became symptomatic', - 'date_tested' : 'was tested for COVID', + 'date_critical' : 'became critically ill and needed ICU care', + 'date_dead' : 'died ☹', + 'date_diagnosed' : 'was diagnosed with COVID', + 'date_end_quarantine' : 'ended quarantine', + 'date_infectious' : 'became infectious', + 'date_known_contact' : 'was notified they may have been exposed to COVID', + 'date_pos_test' : 'recieved their positive test result', + 'date_quarantined' : 'entered quarantine', + 'date_recovered' : 'recovered', + 'date_severe' : 'developed severe symptoms and needed hospitalization', + 'date_symptomatic' : 'became symptomatic', + 'date_tested' : 'was tested for COVID', } for attribute, message in dates.items(): diff --git a/tests/test_immunity.py b/tests/test_immunity.py index e70cac554..8d9094eb5 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -42,7 +42,7 @@ def test_states(): rawdf = rawdf.set_index('From ↓ to →') # Create and run simulation - for use_waning in [True]: + for use_waning in [False, True]: sc.heading(f'Testing state consistency with waning = {use_waning}') # Different states are possible with or without waning: resolve discrepancies @@ -55,9 +55,9 @@ def test_states(): df = df.replace(-0.1, -1) pars = dict( - pop_size = 10e3, + pop_size = 1e3, pop_infected = 20, - n_days = 120, + n_days = 70, use_waning = use_waning, verbose = 0, interventions = [ @@ -69,7 +69,7 @@ def test_states(): ppl = sim.people # Check states - errs = [] + errormsg = '' states = df.columns.values.tolist() for s1 in states: for s2 in states: @@ -85,16 +85,14 @@ def test_states(): n_false = len(is_false) if relation == 1 and n_true != n_inds: errormsg = f'Being {s1}=True implies {s2}=True, but only {n_true}/{n_inds} people are' - errs.append(errormsg) print(f'× {n_true}/{n_inds} error!') elif relation == -1 and n_false != n_inds: errormsg = f'Being {s1}=True implies {s2}=False, but only {n_false}/{n_inds} people are' - errs.append(errormsg) print(f'× {n_true}/{n_inds} error!') else: print(f'✓ {n_true}/{n_inds}') - if len(errs): - raise RuntimeError('\n'+sc.newlinejoin(errs)) + if errormsg: + raise RuntimeError(errormsg) return From ee973afb75a2571d646ca09c4976d74fdb8f4553 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 03:18:57 -0700 Subject: [PATCH 423/569] update baseline with new state checking --- covasim/regression/pars_v3.0.0.json | 75 +++++------------------------ tests/baseline.json | 58 +++++++++++----------- tests/benchmark.json | 6 +-- 3 files changed, 44 insertions(+), 95 deletions(-) diff --git a/covasim/regression/pars_v3.0.0.json b/covasim/regression/pars_v3.0.0.json index eeb0d23db..c591b32e3 100644 --- a/covasim/regression/pars_v3.0.0.json +++ b/covasim/regression/pars_v3.0.0.json @@ -33,9 +33,10 @@ "high_cap": 4 }, "beta": 0.016, + "n_imports": 0, "n_strains": 1, "total_strains": 1, - "use_immunity": false, + "use_waning": false, "NAb_init": { "dist": "normal", "par1": 0, @@ -50,20 +51,20 @@ } }, "NAb_kin": null, - "NAb_boost": 2, + "NAb_boost": 1.5, "NAb_eff": { - "slope": 3.433, - "n_50": { - "sus": 0.5, - "symp": 0.1987, - "sev": 0.031 - } + "sus": { + "slope": 2.7, + "n_50": 0.03 + }, + "symp": 0.1, + "sev": 0.52 }, "cross_immunity": 0.5, "rel_imm": { - "asymptomatic": 0.7, - "mild": 0.9, - "severe": 1 + "asymptomatic": 0.85, + "mild": 1, + "severe": 1.5 }, "immunity": null, "vaccine_info": null, @@ -240,58 +241,6 @@ "rel_beta": [ 1.0 ], - "asymp_factor": [ - 1.0 - ], - "dur": [ - { - "exp2inf": { - "dist": "lognormal_int", - "par1": 4.5, - "par2": 1.5 - }, - "inf2sym": { - "dist": "lognormal_int", - "par1": 1.1, - "par2": 0.9 - }, - "sym2sev": { - "dist": "lognormal_int", - "par1": 6.6, - "par2": 4.9 - }, - "sev2crit": { - "dist": "lognormal_int", - "par1": 1.5, - "par2": 2.0 - }, - "asym2rec": { - "dist": "lognormal_int", - "par1": 8.0, - "par2": 2.0 - }, - "mild2rec": { - "dist": "lognormal_int", - "par1": 8.0, - "par2": 2.0 - }, - "sev2rec": { - "dist": "lognormal_int", - "par1": 18.1, - "par2": 6.3 - }, - "crit2rec": { - "dist": "lognormal_int", - "par1": 18.1, - "par2": 6.3 - }, - "crit2die": { - "dist": "lognormal_int", - "par1": 10.7, - "par2": 4.8 - } - } - ], "rel_symp_prob": [ 1.0 ], diff --git a/tests/baseline.json b/tests/baseline.json index 1ea2d06c9..a93e09cd6 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -1,49 +1,49 @@ { "summary": { - "cum_infections": 9829.0, + "cum_infections": 9842.0, "cum_reinfections": 0.0, - "cum_infectious": 9688.0, - "cum_tests": 10783.0, - "cum_diagnoses": 3867.0, - "cum_recoveries": 8551.0, - "cum_symptomatic": 6581.0, - "cum_severe": 468.0, + "cum_infectious": 9693.0, + "cum_tests": 10775.0, + "cum_diagnoses": 3850.0, + "cum_recoveries": 8555.0, + "cum_symptomatic": 6599.0, + "cum_severe": 469.0, "cum_critical": 129.0, "cum_deaths": 30.0, - "cum_quarantined": 4092.0, + "cum_quarantined": 4006.0, "cum_vaccinations": 0.0, "cum_vaccinated": 0.0, - "new_infections": 14.0, + "new_infections": 21.0, "new_reinfections": 0.0, - "new_infectious": 47.0, + "new_infectious": 49.0, "new_tests": 195.0, - "new_diagnoses": 45.0, - "new_recoveries": 157.0, - "new_symptomatic": 34.0, - "new_severe": 6.0, + "new_diagnoses": 41.0, + "new_recoveries": 166.0, + "new_symptomatic": 48.0, + "new_severe": 8.0, "new_critical": 2.0, "new_deaths": 3.0, - "new_quarantined": 153.0, + "new_quarantined": 138.0, "new_vaccinations": 0.0, "new_vaccinated": 0.0, - "n_susceptible": 10171.0, - "n_exposed": 1248.0, - "n_infectious": 1107.0, - "n_symptomatic": 809.0, - "n_severe": 248.0, + "n_susceptible": 10158.0, + "n_exposed": 1257.0, + "n_infectious": 1108.0, + "n_symptomatic": 825.0, + "n_severe": 249.0, "n_critical": 64.0, - "n_diagnosed": 3867.0, - "n_quarantined": 3938.0, + "n_diagnosed": 3850.0, + "n_quarantined": 3865.0, "n_vaccinated": 0.0, "n_alive": 19970.0, - "n_preinfectious": 141.0, - "n_removed": 8581.0, - "prevalence": 0.06249374061091637, - "incidence": 0.0013764624913971094, - "r_eff": 0.12219744828926875, + "n_preinfectious": 149.0, + "n_removed": 8585.0, + "prevalence": 0.06294441662493741, + "incidence": 0.002067336089781453, + "r_eff": 0.18301780973366616, "doubling_time": 30.0, - "test_yield": 0.23076923076923078, - "rel_test_yield": 3.356889722743382, + "test_yield": 0.21025641025641026, + "rel_test_yield": 3.0589651022864017, "share_vaccinated": 0.0, "pop_nabs": 0.0, "pop_protection": 0.0, diff --git a/tests/benchmark.json b/tests/benchmark.json index 5bcf9be38..bc647a166 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,12 +1,12 @@ { "time": { - "initialize": 0.403, - "run": 0.479 + "initialize": 0.413, + "run": 0.498 }, "parameters": { "pop_size": 20000, "pop_type": "hybrid", "n_days": 60 }, - "cpu_performance": 0.963544593007991 + "cpu_performance": 0.9861777951521127 } \ No newline at end of file From 5594b17cd92bda4df1554753caf0c3da46ba957c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 03:23:53 -0700 Subject: [PATCH 424/569] remove strain colors --- covasim/defaults.py | 11 ----------- covasim/plotting.py | 10 ++++++---- tests/devtests/test_variants.py | 3 +-- tests/test_immunity.py | 4 ++-- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 6c1b3cc54..f8d4243be 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -250,17 +250,6 @@ def get_colors(): return c -def get_strain_colors(): - ''' - Specify plot colors -- used in sim.py. - - NB, includes duplicates since stocks and flows are named differently. - ''' - colors = ['#4d771e', '#c78f65', '#c75649', '#e45226', '#e45226', '#b62413', '#732e26', '#b62413'] - return colors - - - # Define the 'overview plots', i.e. the most useful set of plots to explore different aspects of a simulation overview_plots = [ 'cum_infections', diff --git a/covasim/plotting.py b/covasim/plotting.py index 4a92ed9de..71caa3e5d 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -397,12 +397,14 @@ def plot_sim(to_plot=None, sim=None, do_save=None, fig_path=None, fig_args=None, res_t = sim.results['t'] if 'strain' in sim.results and reskey in sim.results['strain']: res = sim.results['strain'][reskey] - for strain in range(sim['total_strains']): - color = cvd.get_strain_colors()[strain] # Choose the color + ns = sim['total_strains'] + colors = sc.gridcolors(ns) + for strain in range(ns): + color = colors[strain] # Choose the color if strain == 0: label = 'wild type' else: - label = sim['strains'][strain-1].strain_label + label = sim['strains'][strain-1].label if res.low is not None and res.high is not None: ax.fill_between(res_t, res.low[strain,:], res.high[strain,:], color=color, **args.fill) # Create the uncertainty bound @@ -455,7 +457,7 @@ def plot_scens(to_plot=None, scens=None, do_save=None, fig_path=None, fig_args=N if strain == 0: label = 'wild type' else: - label = sim['strains'][strain - 1].strain_label + label = sim['strains'][strain - 1].label ax.fill_between(scens.tvec, scendata.low[strain,:], scendata.high[strain,:], color=color, **args.fill) # Create the uncertainty bound ax.plot(scens.tvec, res_y, label=label, c=color, **args.plot) # Plot the actual line diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 77eb45558..c862ef5fc 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -1,6 +1,5 @@ import covasim as cv import sciris as sc -import matplotlib.pyplot as plt import numpy as np @@ -187,7 +186,7 @@ def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): 'New reinfections': ['new_reinfections'], }) if do_plot: - sim.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_reinfection.png', to_plot=to_plot) + sim.plot(do_save=do_save, do_show=do_show, fig_path='results/test_reinfection.png', to_plot=to_plot) return sim diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 8d9094eb5..2da08f144 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -9,7 +9,7 @@ import numpy as np import pandas as pd -do_plot = 0 +do_plot = 1 do_save = 0 cv.options.set(interactive=False) # Assume not running interactively @@ -519,7 +519,7 @@ def get_ind_of_min_value(list, time): cv.options.set(interactive=do_plot) T = sc.tic() - # msim1 = test_waning(do_plot=do_plot) + msim1 = test_waning(do_plot=do_plot) sim1 = test_states() sc.toc(T) From 9abd8e8132a61879096cad22e2bb3adddc53b6c9 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 03:33:13 -0700 Subject: [PATCH 425/569] remove debug comment --- covasim/people.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index a41de6cfb..cd561bd37 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -248,9 +248,6 @@ def check_recovery(self): ''' Check for recovery ''' inds = self.check_inds(self.recovered, self.date_recovered, filter_inds=self.is_exp) # TODO TEMP!!!! - if 7723 in inds: - print('yes on ', self.t) - # Now reset all disease states self.exposed[inds] = False self.infectious[inds] = False From f497ccd7e06d22c3185d70675cddddb4a9ce0328 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 12:41:27 -0700 Subject: [PATCH 426/569] improved state checking --- covasim/interventions.py | 2 +- tests/state_diagram.xlsx | Bin 6576 -> 10142 bytes tests/test_immunity.py | 22 +++++++++++----------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 8bf3409ef..2ab40a856 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1196,7 +1196,7 @@ def apply(self, sim): self.date_vaccinated[v_ind].append(sim.t) # Update vaccine attributes in sim - sim.people.vaccinations = self.vaccinations + sim.people.vaccinations[vacc_inds] += 1 sim.people.vaccination_dates = self.date_vaccinated return diff --git a/tests/state_diagram.xlsx b/tests/state_diagram.xlsx index 0e23557e211ea85a071ab63ab5b17230efc7450e..641158b8bb00fa35ff48bd95b6aaf53394cf5286 100644 GIT binary patch literal 10142 zcmbVyWmKE(vNlDEyK9k#BEj8@2bYrI(Bkf{#obz*qM^9ELy-b4?(QzdwX}TDcc1U= z?%wD9aVBdec~+je?mIJ+nJf2n`b4f2SmF5;M6 zgny6@m$EI=VvLNwBl3i1VqEQ$wpywW(hNalpJN(+DVpfEI;VVR^Q4NDbw)>}Q$vq` z?e#~p#t@@LBhNr}@?0z+y>==DvCIX`@hlpfSYNe6*&De5()_8Vwv2Fs)d(<&V=?|&M@c$#QezSLN?q2eyjuvHh zkg$)Ok63tas9b+bBu!vfmM7Bzd?$k$rA*EAV79MVacj0Z*z zSQbO+!p}-GG@W7mlO6mx}YO-ru9=8AYQN} z6j6S65kp7@vQ^ACgfTipj)9M_mgvOP%W@qiNrSAD%4*KDOR4UI z>0Y0SO!Qk7#?;_+2obYKsZzR4d94mG>HKZ=ja9>)!lg?A&8-Q#Z$vQdG0e@aJb?fg zBr`bT6^g@N@{QWU9l7Ye#Yq3N$GD0{xhNogh^ZfN{}fl~f5nxfle?9%<6~4EYU#$z z@#1+Om51K7DNthdG32K91B=_#CS4dxYvvH&Cu2=k()8Ah=<5p%y_h)@jY}-+?0Vid zy}h*VXl+=6yhQF`wT2X+AAgwl&L+DQQ12-}cl zeY$tXF{o_9kaBhW$hUir>U2K~#CM%CF=1h6Fd6f08xO2QNw8K__PY}TtMKgG9DKH$Be#oy)WJSOGj`=Nn zpy^HBI-$W z4ZQ1yJa!P6!MLHk2~FOWS578ck>AHC9-6G)tYwS=H{(>}5^uaLX{OmxvqZE(+gGiT zI^pKmGwOFQ$)~0QpKpss5Yssmo*b7e3-sEIHs1J3qib5K-KSQ=36x)Fp)&`4tCD!> zDRkI%DM6nb`*y2Mb&&Jgxldl2{V6+Pd1>4L$mBW4JK}yFF_W#OF`i8; zhgVi6JUu2Ja#nZu$&rg9ZN*()M`TC39+4te!zoucch4{M59LK-tuOg!%hZomxY8|x z^<(Z)N4KD6<%ENzlAhUSXZEi=!z^5Wl+HCXl6rO3LFAUi%Q_I<{h~ZOtuKkUh*g-r zlVObthA7l&j z+XC$o#Q|0j#$vWHMv;r-i9iDslKFGG&$ZBloT&p=4t%<9?0?d$bG^=qHAwoOa zV0HUX(^XrinbEtR$o>G%Q3L z3C!;wkJn1-?d}MSb7?g-CyQAJMp5jJOLM$Ql!~m`7t+0Paz7{1_d2J!@SeU9e4nF| zdnOSl!!zti_KFjiCyaG7E=4cDwsCHY`1D+mb@IHB*np(aoi$H zUTL_4(=}dcJI+xy#$HzwjLS2Q!tI&Lbh$vYo#PYjk&)qC-=8tvTO+G8eRV+*Hp*eA z`(Chk%;d(^sgNk!<`q?r z8}A26gjM!)q@+DAqF7@= zD>*3=Y0$3hxj@~UE$!W{LAZtN?38E%88utI=kd@$?n6I=S-}<(UA@5rl=>9`Q^dvZ z-j2r66Ii$R%ciQ$IT>$Z76|i+nNEc2wAs(y}#CJ_UIGpQgk1HR0%bOH)D)d>K zmPlSAA|2vW)pJL3E`N0Nl=C;-`hW-!a#Q zY2O^9DO;&hE9Z{d&5$lP9kP@{hDaR(zx1mP&wZ^xaI2w>5U7!HP|)PnMAHW4bmL6 z=cicd@%e1UnW*J^EVf)Bgxj!$y*ED+5W{jC)Ilw(Qv1E;6h+k{t%HQKw8k&T1TycS zNdMmS2Y)_-d)40OuvLc9aDT&& z7{*O&W2@Xnt~_x!sOF*N7NFvG7X|L=2CCYdPkpqE6^+IxPSaBV9ay0=4W?0I%|uCl`x19tf8rWRiu?3ZdZTtJlLh`q&trR@Ej zYlw3k%*VDkmZi9yyBep=BOl0XVWAaVF~s_7!G;k3K9QqYjAg_xN=?qt8E;*H3Zfyl zP|AIXnI!VUkpQB$AwWe$k*rQ>qR01o0o-RwQ&i!GG@L%v}Dl`qn?9ihAgy~|gx zlm!okYpjojHr*J=mu+|V&Ii;Ezb3*`=lNr!SG_;aU#4N^21R1(Cd7u=0Dp`$?y&e7 zo1gd2e;QZhjMUj$-jjlUw4A6NlG)2!<00@9QTo2M=P@KhZ~_|B51Z<&Md0Q_wZ6 z;Ba~l60)$jARG`RYea-lEMqDS2-LrR6+$M&>;>Zq9QdNNa$5Q=BG6-m7Bw)+bI+Yx zS+^R#86K`Ry6lI+!Lb=O=9p29R^xE==s(QID=~(|Zr{;tgo&VuS05v1N<+_PY0~d2ziyq<6RdXdq1hdX}j&j$JC<+n4jC z#m=L~Y+J9^kz~C7t>3t$gQ`?h8*V!)Ih0zEDFAvU4kHtArB@6ZmNoA)pmP^sfh!#e zA_!BLAT*}S$4*+;O7yFf%kbtxEN17YPJw$M|H;@Wh6VL|g zgO|?ZW$%jPiw3IY0(Nl#kcJelXe(s_?f4m88k8><(Y;NiduYm|O?UKwXRi$YY>-c;2F` zd>ICiEjbP>u{R2so6YI|Mxvq&t~UWYkT7Uw&ib8fbJm7z9 zq9!(lCtIS-Xc<3~)&18-^15x2F3MKw)WFUgTnmY7ZKv=h9r6sU<0u0qupH$bKv|-j zH=#7eI1)Z25Id6q?YW0PQ4FeTT8HiUL`7rq`lyj8vRx@M2qs#7Dd5*z_{oyUP&#x; z2tKe>j!4qnjbk2V;9F6zWNC`C<)B(EElUq!@@WVlTHekmoQ8ueWxWqpI+3@kGtL<~ zXULQR8j4n1(;SY+1-W|Hl3mtMdYaSX$BePO{#!8E8HLd>l4&MU7JVMk=d9zcYTlDq zF?41bA>Bi1rD7Q+-BCRe`p*R(LdVW1f~Fa#`x-H9vc62KMtiwad(_68kwdci!rPO@ zMyB99YDS6|4d@abU$F2y>Ra=^3{FRZWZ>3(+00kbmhNZYZii*ZTfMB*>`mSZR; zuRUMdI?GwfL_0Ad=@K;E2~oTTW>Kvg6m$J>$7?R`#}ycD#*Gh8-&%w5qZ=j^WW%uU zNwTY!^rp}c>Glm{;q?x{(XC$oV0BjW-inJ5%G(@RNha`u`HR6#7S_CTpxK6))g*;M zel*sgPN@Fg(PtZSwz@O6I+DdX=)okmaX!(e?FFD=?<|CJNj&e69vd+t=WOB9i%)<$ zNl?{iNd@K8PaMyM7Ee3yL6*QemRd``Qkfa;i#K)8y<9$35N5KPbKR!@9N97U{c#R2 zYmRQ}NB0bapQ$?Y2f9P!tK!QOdqR~-u1L{h&!6L5A#8ejlY*JEd{4DrW{FTN>i3sT z(7Dp}*ms3SKwmVkuhQGdC=Fd1r6td!>Vyzo6Jyob4g?FnSgWn7we zEk*=xP}%cIY+>(j4%Oc;HQ#v|fvUyd38d5Dxea5i9pj&;413WDNIB7ZCJF5h3GpE7 z5HVA~EBVR{AVAfj^1EITeb=q|fo0Hz9IBWxt2P?8wE*v-!bJgF0{N%CaKX5rWHDtYe%pZIa<$Tp++ z(42^GpnWPzR$;opmTmqdv;0DYqF0~)FT!Fh+&cA0SeEXUjvfh1qH3An6OpE=`VWZe zlNLtSh0-}Fs0Em&+`x0BkeF3RE)+3YEBCl<$e>m!_#$+DzcQ%!7>CrV+8)SnG&KYj zj)@AK0FZUY)G1IIi8Ei1@SQ0B~)x}6~_HJYX*gR=N zt2Es7oHZtprFjLF2!pcmMX&R9bEL}ARqjc7cAdSBVi$gOwmx@v6x|2ZJ`V4!yO@_5 z+c5`>oVoeMKlHd789TiRm~zuBe|&DxN(+AZ{3KsoANt;qX|8IT4ZguqCVuXH z)ccXjq-dq=gplUklZ;l3WPu>rw>64NcM91?=Hfeowp87K#p3`O#SDr9IVQ!!$h2m1 z*H2%&nyF>AE6`*o!QhK2k?1xJ@58@o4k=2kiU8b`g zeWGTL?=P5C6fWP~yn(2U0NhOlG?O+;8ptz=!DjAbPUSFNhY zf^nC#1;+AnXMoJ-@4gkH6861SobM3-pwET-vK>(}YU4hqg&8^LJw=TRwy7SD=2)Gx zRoZQk&&O9WEutTer%yJQL)*U!`&&HYD2c$)UExUVkz^k1{;0VzR=_#`?5HZEp@gvE zz^A2IYOPFVqil>@c*&6CWCmDNsL4k1@OeUm6@G)2I>}bAr_~FJ`5+2`%&auUwaQmJ zqj&JTPPj3PIngEFUw)WPjIm`w4koVA@Bt`FN(0B(r(QY| z@0G1Py!>4(BfKb4)-QU3)OTv$mRp}}S3&WR1`?64YlNbmGVO#_>P+!g2SPpvp2-!L2*bO@$|TJcdR7SZ_ji&-}XsB zR`o_jsMB4P7vY?wrkfhP^pPHnLsc#D7!k*jWU{Cn*5wX)weUk01d))3J&EtNIff`O(Nt z45I+1wnHO}NC3q}*#$-KUxDG&6T&yZK{zxCeGhzv_FvTE#2@%j3UB{{R{>WWfw}O( z&W=SEYelMYEk()(fjEZ06#lCJda(%6Kot?jc#TXN_9BaflA^>M1{%We)~F8*WaEK! z(8Lo#yCndp4`k$lxcnP>9Jtv?vlgtmdD?EhUiE;WRE~WOM%y!vA(2tlol3QY~-x8Pko@b&2|K{Wf-pNzO5mv=4MiX)^q#2#?z^b{-qxxh*vj`YL+nO{ae1C<<@gK5%JinnO! zJ!ywRUpVerJU3QaB1(TIsd}mI1N|NV8mMZ2ky`tBO^7_+C#@=^iD=BW?9v^Mwf6bM zuGd$Q5V)MGzNQCn;n?n2_<0%5qg31AjbpB*=uXAR>d~o`@e_1pLe#q5`g9GbLdHzS z(m^)3M4fOROX9nE7w&p&l3-v2)1IR{8P2^Vh28B_rY4hi>oLGgn%k!)BJ?qFB3vW8 zwJf?MOxa33sqoJ-;k9G0tm5Nx+~2Jwoi}uA1>8pwxJk7tw5xcn2 zrBu%`*Ou+R%-vzF6ME*FwV_QxCsHC8WYiv!;P4Gb41fO^gZDW={ppsH+WHSpMjT;* zm6?R^Y8DAD(pKq6>nB`4`ZOu8l(+^jSM|3wo?i?=Y@+!{W>`Nf39Dmzy1V!hU=Uyc@uTiEE8o`57T-5~{7J3`!->VO z4AZ!Dq=l)`C%$F}`UIHz8fAN5>wNu&G0r4p!)Q8sWa6#6!7GAu|6wn)c}b@aG0fgB z2gp)9$***@s)(Oo$dlvSCqWaK{ zL-n3;4WS)<Dq0^bow6gk>EU2yBSG)AUMVUNN`C0I>G&`+4_51@mJ%u_5CNs zE>^VQBhTk^|6rn&5L;D1vKl{@9!~ui!R=f zbtB?jEz#3ek-_t?r!6_7EV;0_##EFMP7)xJkK@*PzmBsZW2|5cenCI0LB?e&oF$hw zHC|w7QTSdot@WF7qWe!u^P(pgDS0!TC}!h$lrFt*??*4K=SjUTx4b0?Su{Y=y-)b) zE%sO$uD@01!kt!myDNNGQu3?|J&9RyjbCY#WkalPVUy=WtZrf(aKPC5Swvr%THziQ zPc4G})#(ndx?p2gxRs7EyjtQ`5rX zw^RD3yapzj1w8X6hA{mfN5`{yd~M2{N4L2-gm; z_OSx?3Kgkp;g)DJ1UOiNh+ISCipds5G*J{@Va6*rUd!%uxOapa({4Wteo{Xo4ouzl zxzMTzuTgoINBG&cQc=WGTBzEbsk2A(-7WWP! zoK<_==pa`Lf?zE^t!=^CtBX5nvkPzHrC(iime_&I@Fy@Z>G1!=>aibuX=H1t>|kr> z$YN;gVEjnutyOhxJ1OzJm&?uEZ0Sp9(H^>rOu~akVW~`>hIXY^K{zjq?|o`4csQ27 z-K^+*3o>v~R;WSTH!7Bsc!wvM<<6K}CRd|3xK1Db!BndvZ&EpjhL^MW8;T8`EmOE%4B%SG zT)FD`>OjPm*rbn-sD|(V%Es?z3WD$xRg9^_o-qoOQRLy!rUQNag83jHmb^SEz&Q7I zkZ0gV(hV8~f8vHl%+qj2TfpzALgbh6v<+l?Gk#_fT?8BieNZ$IgFJ`czzx25<~_du zHoH2g^`Kpri##>DqP=$B+vmwuew-AR1&cS_i(A#7--RbJAAejs%Nq--4-N_c$FT;i z-yQPC&hF74>9KG2z@vGxxa)*=5T=qEkF|2$ru~i*C=uqRRHu z!S^4AqJ{!x?sQ~v_qJfSH{zYLI_+$@nA-|%LJ{UbVF~7CZxQfu~EF% zFTmK_Zod=GFkD3z_K$#2BPo`z6DiM)@?u3g_cVZhVnMlN4R$3C22A~vmTBU-yt`6u zYB_xLE}vP}nR+`{SFljEy`(yvpIo;pDMP&?_`F-obAtY8r$lzd!o3G8cs&!-1!mdK3u~Ufj+?^9=g{ zYtieg3=9DLc6>vD(xAtXtEFPIQ&XaI4eWj(L{pHuix)P*l36XK2lFg#Loh?}>8_Vy zhvZDL4uSeo8mg?Wo}W>)85{~@P~AKCyBYu&P`?6a$BkDI+6bYDSMpj3eX)y=XQlal z$0KT9TQ{(GbhC~HWX#5qN5FTck(Vv2ZBJk^{X~b}XNB3_Q3`AJyg`=8^|aAnWz+9% zES2Vrt+XJiSqLXW_rj{Z_9v=Twtd@_?x(jz@07W zbU3%gBT)@$yj}pq=uPEnh8c-@XCK|=J)N%i5WkJuK<^keA-=FO==IHwIT1Orzm*oQ zTW=RhYInld=>Z;l+McxTdsAOEHhWR}nAF-|t7`Esb$fn$tB8iz{-cxXq=uG^9vo!)^wCLAVDVu79$)%>e&=y~>EGqIVWvM7e;?3z z9PjyCh#!{vW7y|U<=@-1k24&9i~U12drg<<@|~B zdl7jo`+v)OieEVYK?43n_&w`A7Ms6CpYp$yo>v@ozb%`c3^`iSkeF-=p*~ gVf-ybH2*byD9a%{1T+i`>civfL!NGZ{&@BO08~E>tN;K2 delta 5855 zcmaJ_by$>7*CwPvLY8h=x=WDmmIg_srAv{1q+{tNg{1|NSX!h(IwTdAMkOR9c4?Il ze82B~{k?yDGuQQ8Gjp9e^UOWx+;h(4>(+`8>Srw44?8(%}X-cwIbiKq*-M!LPF#hrF^GSrmF{$Mzm# z`Rm5``^*&)DxSu8GYP_M9PYU&y{qhyAaxv(O|x%r*9>@K5m>x!W*rv!jgt1}jM9N{ zw=wQM$Mq|5c;Tks8(KYbuBHmfH0M*G)1C0J*97g6mK=U~#be*1M=`a6*AYdd2H$F@ zF6ywO=ws1`Q|A{oMKh~2U#A?-i+wvPyBKpVhI!?P04JJPl}Ez)j7d702qB*Ljq-^m zmniVnT}cBqOU+FX9djPeSJ@3>+{APZhatoR2CTfj1Ao^CeHQM05R;(|{`=;@NOqB= zbtS1j`9&8?o1Y%BbVCR7C!0}`u22ccYGq0H$0Zwn)zh$qM`k5D9{j0CcsQ5c~P9CL(+SO<1U$5E{{gUEME2S9X;kc? z8y62VWis!(-FW8gPh>{6L7NhtR=wdbIoYm+xdPl@Khr&@84HBI=I?S+%$38*EQUC< zm;Q{#wI#u{kn*>ob9*;c?4>nK@(eGX8Gl#cGe;~|9BWKDxuIiSa09l+7tt0!`Emx*4&ay;ZDBgZbH2G!w=2Adta9b zw@0EjSu85M{ii3UNuogC=!DW@aVk5yt)7$ zT2k>-aH_Az*i4uIS!5o;*C&S2@jWS&9VUb?wBVyK79Y$3&ogNT*w*thzm$C#nv8$n zMY?Vnc@*?s>}M0b;{opUhtps#=%&J+gh5bHW%dHjBjfXIJ&kEpj_{JW5_=h~<+oC0 z4$M`d5&e-|ShbKE^B%Uj)JVKQxH*NO=Y5>_q{l27>|&3fct2YM4DNy;lc=1Y9c!v9 zHVQUZ41&iUc32nwfG)K71w_=igOFeb#X9mJwTOC6kINQi+*8=F|qHSYie&{>&$G4lVSN>Vg4* zDRSr+=9BqC%F}kqW)B6CF@n%Xm3ST%A3BtHL1Np?J$H&fBBSk9ZTzx3Lq&34JfD=1 zBsK=Rj)phCU)Yd)9G|0bo1(O%pKrzKaDoo>YZf=)9>HhAmKtPtcAlPHSRT0r1R)hI zUqEQDPwON{fZb;CFWja}=nmdjoY@~W3(Ep`sCNQ>2-(<6>g3nVwo0&Lu_rX{%J6&* zrgL&YsTbsrN19O!P1l?eZp`#g*bOo#%w(i3Mo)QNU5jwGD`WXPaUT~4D$Bv(=b z%Kg2#>EaSI@mW|!8S(R2=d#U*qT{o9yEc{6Anu>Dyy6kF>DgBz>m$wR}~$4I9(8$+#GJFbl1t3 zj6^3gTRrYT2ZLEqAd`L~)@%$-OcYMy|CB=N@QvJ5&>ed!4nChcVp3^-m#SW zZwQ9&g%tjrV5-@YL%iS|db?YqAkz^bT5F|VwC%@N`cbb-#N&mI5|hwTme%9qr6!7f z#3&)1bRgTv0)$|z$ko%bK#_|$*?FD#RM>F^Ll}K}#PnyD;z)h?WXM7Ugm&l97n&z6 ztCwsJbGbF;Mcql%#On;5hVubH!4?AUcd;W2-Hr+gIV5 z`6O#Hba1w5HG!Ui_lrVif-37-2<0k&R(!C>5UtdJvxaN`%;C^{11H(OxUxXMe(XuQOnhAcL%5tjWgj5b+(WKcQ zr!Bh)3XZ5^QXQl@HRLBW4hP|{+l%4?2ncpVL2AxJ?2U$|rI!L1scV#uFXz$0inx~Z z_n^+?xly^SoHB_t5tkbN*<`C^#W^I>cLu`nlG`WFlwgs}0W;9K6wgB%+wGD9xgUPS zs|So_*PX5_E7Y3+_H(sH26|Qq_MzgpsbW~2%a&*{938V9@~t%CSqMjdRkA)nnxEM- zwaEOkv?nOXxYX&jsJ&2nJ1i^AWLpu^$5RXuPM?V#_2wIuUj*xRzKSrWrZs@znS%YS z7Wmr{pgJAu)4c}DYk&eN+5VOM!kzkhS6%Uo?_~b$Wle%C>8MXg+JrJaLnLx$vK=Lw zE%tS$Alh-yp65)IYI-JxNSFXC?e)uO4YHNNUWT2*UHm67s|R&9OTXfogQM{go|dD0)PO)Ea+#poeHyzkN+Crpa91=kPuBZi=c{A+$gc>{%ZYns# zlZ^n4{ls_F?;V<9PO8!*36O2?%^~t|hOo9l)b(8{0pas_^fAF%V}lJfOw94wb9_f1 zERl}z9IKu&_!A0n0*e^AnU#&RRWGN(hg^hp&mO;sN|SLXn`e5}@F)N|#%|@69!SMY z()PBGX7!~rTN@%%Gm)t0SD0lc`ZZ+-u~@KDj%RT-=C9A>77p<4><(rUCeK+LB3E;i z5FSj$O5*jlk7C^=QwHUXaIz?+E~_}jvs4Bbt>~HV5Cv4M$Y&)CO8jLhbY@?M@YOzp zW4VQc#{mX4Kk$&+jKjumWprz|OE#l~=><0|wF;FTu(fd8F;AW{gzW~eE%mEJ2zb}P zAI$N+s4=@>EEtd8u{^-s;$B^w6*bzrO!GZiQssqQH1qiVA`|dVZToH5Ty$oEG?D%# zFDaMA@afz_KBg5c{#&(`B9foL>Nka_r42ejghwg$>N~t-zE7J05w`*>L~$X=DYCW* z7^q{^ziPE0ie;1GN?+s#Df2oN<7yD_hs|mT4e@7?BaXJn7Tc{p6*Jxfa@)#yl0?hc z#1SRYO5C~}f}^ra4>wa%J03>6c9Azg}e)$Ms2q}g`_+m;3v zWI75aQ?wX3$)&gIT#yJCXFY!xY| zK?T}0JLm)f>Xvs?1Ns6xk^5a|{Bn$AQ+p}jK6c|`B_R~(yu)RYIm8Zwu^;UZ>W2@O ze4*V@gDT2t&k;d!{TQa|OtN|@Y~B=GS`Ai%VIC&|ZfncsRp%?s<>hBw&+m&qX@=45 zqpNJztP48}amha3O6-|KPSs6a{|Q9GVTzXlxM*mh;{O$h_|fwM)u}qJ9|Q@b4}O7E z&-yiR>=Gn85)|OS&d~?-zfAUtr+sd^4&|q{z<1c2gig*OJJnBiL=$&n`;-W=-xlAA z>go5x?FjqzV}G66WW0D~?I}39mXWNo{oedbN(}+?2Bd@;zin@dlvU}l=EI7);ns+K zeSy(qjhQ_3agbp(V1e1_!K2sa+k0JWH%D~$aY;~t;1ChsxDP|B$do~{?HZ4OFb>N7 zVnb#_j)i`nbp#$!U?Bbi@6%&PhJ!!?PrSuyz0kd%Y{=txIV#ogmF3_+xti|bMc_0V z8X6empL>QF+zkG^kfTB23={y&NbBJTL6T)({Lw3RXD|u3MyvczG+$Nft~M>iKQD0L zm*q>$ZX!v%8U9vu=Bx~di6CUYlXMLwj@%o1Wgm1}89wf6H1};f51L=c?RiVCDsHtR zO7c%pr0N0-xvyd~^ipY-MUs*AY>N^1jUq|)SawM|Y!CxF8g_SP^ofD)P);Y~=ca*Q z^>}E>d`g^(UroB-leWIQ@XFGnKxI>uhG!uyis^^^yHTq4%fM?RI{S+^S{}Q{k=GGQ zal8q8CYbi?>w?(f`~JD2KUTbNj=DqP0&7#wM<$On>rjr)q#qRsTOBnWQJW3L zybXihLCT*@Z?firn^5@gtoc7D0e#Ly3#{t)3X+7L!5>|AKBw)=j!sl2(oM@on>sgH zv@@EkaVEyT?pVe)Wbr)vwf&;Q)d^WrQ-Uc0r!h?7)zG-3Dr%7PmMQmyC6p;u&4Ue2 zi-{!}#(_NcVCTJ;7-2g1$m>nF8uO4pyPJYvY^MHB|6S2D4X}_tB_rUok zXi{3|paT0_8f&@L6SqNSpQLo#gvKD#Gjs#JHwqC7x(#e4UDkKXe~n{y91m82@_CmU zt^1}zP%fg)?zxT76`CIfQ)e5aZyCi*$}BR_3_g}V=|vwL$Pl@tj5H6Yd}v0J{K=rMzsu;sDW)cD%9)OLEmGAG#^OyC0+4ty6?` zk-QJfy)&z4@-`$#kCSV@KbK!c^e8-k{5tPcM{Rz2;XtlSeob&1S1}Ram1 z?iPEZ31M0N8|>r%UoRlJ@dxN#7BWEhmI?X~uE={(uOtYVdHTH(R8pX3C2>E3n(9V| z-!awcsKtrJ!u^2zGBQU{)VyC^nS_G&LRfg!v(bLu=aVTp_t(Lf3~_WX=}wd`EX9gkT_n9g2qb8v7QUz83G^ z-J7ik-2n8*Fme9(;Sb8j3I;liq;zjMVdaX41HJ3-8ma^Z_mguU&4(fVyv>TIA7rI7k&|l9?zq0?=-t<-B-{H@uIX z{Gt_?WB8;-KyxF~VsR)OhqvL8155%Z@c_N1Nx~;9RH7u-Ehw0;TJFlaXJMsr1x5PeOXAePy-lwSLkkE)w*{RSFfb~W(puUtg>Rq%$-LT9M;jEt<(K>Ms;r9 z?X&Yb@5CwSW`0pX+&2NMV*s#my6zw4NEI7Gl-I)d7=KxlR$S-Ne6gTp>U2pK{v4V< z^Wql#IM ze^dEw!w6~(renI5`TNSF`iD#=4s;C+y2-yu{|NqlPtg2B(3k}3#?JXi=x+)m|A&w# z>Fosq9bqTLyIuB=zd={oK^VCApr`DN!0q7Qc6RG(|0&rw%IrPu^n5(Md+DQz6IrN_kRG8 C#EEnO diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 2da08f144..7a6205d54 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -23,7 +23,7 @@ def test_waning(do_plot=False): sc.heading('Testing with and without waning') pars = dict( - n_days = 500, + n_days = 300, beta = 0.008, NAb_decay = dict(form='nab_decay', pars={'init_decay_rate': 0.1, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) ) @@ -32,28 +32,27 @@ def test_waning(do_plot=False): msim = cv.MultiSim([s1,s2]) if do_plot: msim.plot('overview-strain', rotation=30) - sc.maximize() return msim def test_states(): + ''' Test state consistency against state_diagram.xlsx ''' + # Load state diagram - rawdf = pd.read_excel('state_diagram.xlsx', nrows=13) - rawdf = rawdf.set_index('From ↓ to →') + dfs = sc.odict() + for sheet in ['Without waning', 'With waning']: + dfs[sheet] = pd.read_excel('state_diagram.xlsx', sheet_name=sheet) + dfs[sheet] = dfs[sheet].set_index('From ↓ to →') # Create and run simulation for use_waning in [False, True]: sc.heading(f'Testing state consistency with waning = {use_waning}') # Different states are possible with or without waning: resolve discrepancies - df = sc.dcp(rawdf) - if use_waning: - df = df.replace(-0.5, 0) - df = df.replace(-0.1, 1) - else: - df = df.replace(-0.5, -1) - df = df.replace(-0.1, -1) + df = dfs[use_waning] + + # Parameters chosen to be midway through the sim so as few states as possible are empty pars = dict( pop_size = 1e3, pop_infected = 20, @@ -63,6 +62,7 @@ def test_states(): interventions = [ cv.test_prob(symp_prob=0.4, asymp_prob=0.01), cv.contact_tracing(trace_probs=0.1), + cv.vaccine(days=60, prob=0.1) ] ) sim = cv.Sim(pars).run() From 5d7de48f72b9dbbc6f75db55ab4e904cab1f5ecb Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 12:58:44 -0700 Subject: [PATCH 427/569] problem confirmed --- tests/test_immunity.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 7a6205d54..e84c7d647 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -35,6 +35,23 @@ def test_waning(do_plot=False): return msim +def test_rescale(do_plot=False): + sc.heading('Testing with and without waning with rescaling') + pars = dict( + pop_size = 10e3, + pop_scale = 10, + n_days = 300, + beta = 0.008, + NAb_decay = dict(form='nab_decay', pars={'init_decay_rate': 0.1, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) + ) + s1 = cv.Sim(base_pars, **pars, use_waning=False, label='No waning').run() + s2 = cv.Sim(base_pars, **pars, use_waning=True, label='With waning').run() + msim = cv.MultiSim([s1,s2]) + if do_plot: + msim.plot('overview-strain', rotation=30) + return msim + + def test_states(): ''' Test state consistency against state_diagram.xlsx ''' @@ -47,10 +64,7 @@ def test_states(): # Create and run simulation for use_waning in [False, True]: sc.heading(f'Testing state consistency with waning = {use_waning}') - - # Different states are possible with or without waning: resolve discrepancies - df = dfs[use_waning] - + df = dfs[use_waning] # Different states are possible with or without waning # Parameters chosen to be midway through the sim so as few states as possible are empty pars = dict( @@ -519,8 +533,9 @@ def get_ind_of_min_value(list, time): cv.options.set(interactive=do_plot) T = sc.tic() - msim1 = test_waning(do_plot=do_plot) - sim1 = test_states() + # msim1 = test_waning(do_plot=do_plot) + msim2 = test_rescale(do_plot=do_plot) + # sim1 = test_states() sc.toc(T) print('Done.') From 6cfa08fefced7f5f8fc82dd492278e1a95c8e357 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 13:57:29 -0700 Subject: [PATCH 428/569] refactoring state setting --- covasim/defaults.py | 44 +++++++++++++++++++++++------------- covasim/interventions.py | 3 ++- covasim/people.py | 48 +++++++++++++++++++++++----------------- 3 files changed, 59 insertions(+), 36 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index f8d4243be..c431a0659 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -50,14 +50,6 @@ def __init__(self): 'death_prob', # Float 'rel_trans', # Float 'rel_sus', # Float - 'sus_imm', # Float, by strain - 'symp_imm', # Float, by strain - 'sev_imm', # Float, by strain - 'prior_symptoms', # Float - 'vaccinations', # Number of doses given per person - 'vaccine_source', # index of vaccine that individual received - 'init_NAb', # Initial neutralization titre relative to convalescent plasma - 'NAb', # Current neutralization titre relative to convalescent plasma ] # Set the states that a person can be in: these are all booleans per person -- used in people.py @@ -77,19 +69,43 @@ def __init__(self): 'vaccinated', ] + # Strain states -- these are ints self.strain_states = [ 'exposed_strain', - 'exposed_by_strain', 'infectious_strain', - 'infectious_by_strain', 'recovered_strain', ] + # Strain states -- these are ints, by strain + self.by_strain_states = [ + 'exposed_by_strain', + 'infectious_by_strain', + ] + + # Immune states, by strain + self.imm_states = [ + 'sus_imm', # Float, by strain + 'symp_imm', # Float, by strain + 'sev_imm', # Float, by strain + ] + + # Neutralizing antibody states, not by strain + self.nab_states = [ + 'prior_symptoms', # Float + 'init_NAb', # Float, initial neutralization titre relative to convalescent plasma + 'NAb', # Float, current neutralization titre relative to convalescent plasma + ] + + # Additional vaccination states + self.vacc_states = [ + 'vaccinations', # Number of doses given per person + 'vaccine_source', # index of vaccine that individual received + ] + # Set the dates various events took place: these are floats per person -- used in people.py self.dates = [f'date_{state}' for state in self.states] # Convert each state into a date self.dates.append('date_pos_test') # Store the date when a person tested which will come back positive self.dates.append('date_end_quarantine') # Store the date when a person comes out of quarantine - # self.dates.append('date_recovered') # Store the date when a person recovers # self.dates.append('date_vaccinated') # Store the date when a person is vaccinated # Duration of different states: these are floats per person -- used in people.py @@ -101,10 +117,10 @@ def __init__(self): 'dur_disease', ] - self.all_states = self.person + self.states + self.strain_states + self.dates + self.durs + self.all_states = self.person + self.states + self.strain_states + self.by_strain_states + self.imm_states + self.nab_states + self.vacc_states + self.dates + self.durs # Validate - self.state_types = ['person', 'states', 'strain_states', 'dates', 'durs', 'all_states'] + self.state_types = ['person', 'states', 'strain_states', 'by_strain_states', 'imm_states', 'nab_states', 'vacc_states', 'dates', 'durs', 'all_states'] for state_type in self.state_types: states = getattr(self, state_type) n_states = len(states) @@ -117,8 +133,6 @@ def __init__(self): - - #%% Define other defaults # A subset of the above states are used for results diff --git a/covasim/interventions.py b/covasim/interventions.py index 2ab40a856..432fb0827 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1196,8 +1196,9 @@ def apply(self, sim): self.date_vaccinated[v_ind].append(sim.t) # Update vaccine attributes in sim + sim.people.vaccinated[vacc_inds] = True sim.people.vaccinations[vacc_inds] += 1 - sim.people.vaccination_dates = self.date_vaccinated + sim.people.vaccination_dates = self.date_vaccinated # TODO: refactor return diff --git a/covasim/people.py b/covasim/people.py index cd561bd37..d72b0bd6a 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -62,28 +62,29 @@ def __init__(self, pars, strict=True, **kwargs): # Set person properties -- all floats except for UID for key in self.meta.person: - if key in ['uid']: + if key == 'uid': self[key] = np.arange(self.pop_size, dtype=cvd.default_int) - elif key in ['vaccinations']: - self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) - elif key in ['sus_imm', 'symp_imm', 'sev_imm']: # everyone starts out with no immunity - self[key] = np.full((self.total_strains, self.pop_size), 0, dtype=cvd.default_float) else: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) # Set health states -- only susceptible is true by default -- booleans except exposed by strain which should return the strain that ind is exposed to for key in self.meta.states: - if key == 'susceptible': - self[key] = np.full(self.pop_size, True, dtype=bool) - else: - self[key] = np.full(self.pop_size, False, dtype=bool) + val = (key == 'susceptible') # Default value is True for susceptible, false otherwise + self[key] = np.full(self.pop_size, val, dtype=bool) # Set strain states, which store info about which strain a person is exposed to for key in self.meta.strain_states: - if 'by' in key: - self[key] = np.full((self.total_strains, self.pop_size), False, dtype=bool) - else: - self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) + self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) + for key in self.meta.by_strain_states: + self[key] = np.full((self.total_strains, self.pop_size), False, dtype=bool) + + # Set immunity and antibody states + for key in self.meta.imm_states: # Everyone starts out with no immunity + self[key] = np.zeros((self.total_strains, self.pop_size), dtype=cvd.default_float) + for key in self.meta.nab_states: # Everyone starts out with no antibodies + self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) + for key in self.meta.vacc_states: + self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) # Set dates and durations -- both floats for key in self.meta.dates + self.meta.durs: @@ -114,6 +115,7 @@ def __init__(self, pars, strict=True, **kwargs): self[key] = value self._pending_quarantine = defaultdict(list) # Internal cache to record people that need to be quarantined on each timestep {t:(person, quarantine_end_day)} + return @@ -351,17 +353,23 @@ def make_susceptible(self, inds): else: self[key][inds] = False + # Reset strain states for key in self.meta.strain_states: - if 'by' in key: # TODO: refactor - self[key][:, inds] = False - else: - self[key][inds] = np.nan + self[key][inds] = np.nan + for key in self.meta.by_strain_states: + self[key][:, inds] = False - for key in self.meta.dates + self.meta.durs: + # Reset immunity and antibody states + for key in self.meta.imm_states: + self[key][:, inds] = 0 + for key in self.meta.nab_states: self[key][inds] = np.nan + for key in self.meta.vacc_states: + self[key][inds] = 0 - self['init_NAb'][inds] = np.nan - self['NAb'][inds] = np.nan + # Reset dates + for key in self.meta.dates + self.meta.durs: + self[key][inds] = np.nan return From 6c37f22c7493b5f953c499c6858d75417d7c63b4 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 14:09:07 -0700 Subject: [PATCH 429/569] temporary solution so tests pass --- covasim/sim.py | 34 ++++++++++++++++++++-------------- tests/test_immunity.py | 4 ++-- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index dfa852250..122b63c5d 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -488,21 +488,27 @@ def init_immunity(self, create=False): cvimm.init_immunity(self, create=create) return - def init_vaccines(self): + def init_vaccines(self): # TODO: refactor ''' Check if there are any vaccines in simulation, if so initialize vaccine info param''' - if len(self['vaccines']): - nv = len(self['vaccines']) - ns = self['total_strains'] - - self['vaccine_info'] = {} - self['vaccine_info']['rel_imm'] = np.full((nv, ns), np.nan, dtype=cvd.default_float) - - for ind, vacc in enumerate(self['vaccines']): - self['vaccine_info']['rel_imm'][ind,:] = vacc.rel_imm - self['vaccine_info']['doses'] = vacc.doses - self['vaccine_info']['NAb_init'] = vacc.NAb_init - self['vaccine_info']['NAb_boost'] = vacc.NAb_boost - self['vaccine_info']['NAb_eff'] = vacc.NAb_eff + print('TEMP') + # if len(self['vaccines']): + nv = max(1, len(self['vaccines'])) + ns = self['total_strains'] + + self['vaccine_info'] = {} + self['vaccine_info']['rel_imm'] = np.full((nv, ns), np.nan, dtype=cvd.default_float) + self['vaccine_info']['NAb_init'] = dict(dist='normal', par1=0.5, par2= 2) + self['vaccine_info']['doses'] = 2 + self['vaccine_info']['interval'] = 22 + self['vaccine_info']['NAb_boost'] = 2 + self['vaccine_info']['NAb_eff'] = {'sus': {'slope': 2.5, 'n_50': 0.55}} # Parameters to map NAbs to efficacy + + for ind, vacc in enumerate(self['vaccines']): + self['vaccine_info']['rel_imm'][ind,:] = vacc.rel_imm + self['vaccine_info']['doses'] = vacc.doses + self['vaccine_info']['NAb_init'] = vacc.NAb_init + self['vaccine_info']['NAb_boost'] = vacc.NAb_boost + self['vaccine_info']['NAb_eff'] = vacc.NAb_eff return diff --git a/tests/test_immunity.py b/tests/test_immunity.py index e84c7d647..eb24b24bd 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -534,8 +534,8 @@ def get_ind_of_min_value(list, time): T = sc.tic() # msim1 = test_waning(do_plot=do_plot) - msim2 = test_rescale(do_plot=do_plot) - # sim1 = test_states() + # msim2 = test_rescale(do_plot=do_plot) + sim1 = test_states() sc.toc(T) print('Done.') From 232137d9502242689075abdc55fd775b45a463ed Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 14:24:17 -0700 Subject: [PATCH 430/569] fix south africa bug --- covasim/plotting.py | 4 ++-- covasim/sim.py | 5 ++++- tests/test_immunity.py | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 71caa3e5d..4c952424b 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -398,9 +398,9 @@ def plot_sim(to_plot=None, sim=None, do_save=None, fig_path=None, fig_args=None, if 'strain' in sim.results and reskey in sim.results['strain']: res = sim.results['strain'][reskey] ns = sim['total_strains'] - colors = sc.gridcolors(ns) + strain_colors = sc.gridcolors(ns) for strain in range(ns): - color = colors[strain] # Choose the color + color = strain_colors[strain] # Choose the color if strain == 0: label = 'wild type' else: diff --git a/covasim/sim.py b/covasim/sim.py index 122b63c5d..f1c403af2 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -518,7 +518,10 @@ def rescale(self): pop_scale = self['pop_scale'] current_scale = self.rescale_vec[self.t] if current_scale < pop_scale: # We have room to rescale - not_sus_inds = self.people.false('susceptible') # Find everyone not susceptible + if not self['use_waning']: + not_sus_inds = self.people.false('susceptible') # Find everyone not susceptible + else: + not_sus_inds = sc.cat(*[cvu.true(self.people[k]) for k in ['exposed', 'recovered', 'dead']]) # Find everyone not susceptible n_not_sus = len(not_sus_inds) # Number of people who are not susceptible n_people = len(self.people) # Number of people overall current_ratio = n_not_sus/n_people # Current proportion not susceptible diff --git a/tests/test_immunity.py b/tests/test_immunity.py index eb24b24bd..ca91488fd 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -40,6 +40,7 @@ def test_rescale(do_plot=False): pars = dict( pop_size = 10e3, pop_scale = 10, + rescale_factor = 2.0, n_days = 300, beta = 0.008, NAb_decay = dict(form='nab_decay', pars={'init_decay_rate': 0.1, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) @@ -534,8 +535,8 @@ def get_ind_of_min_value(list, time): T = sc.tic() # msim1 = test_waning(do_plot=do_plot) - # msim2 = test_rescale(do_plot=do_plot) - sim1 = test_states() + msim2 = test_rescale(do_plot=do_plot) + # sim1 = test_states() sc.toc(T) print('Done.') From def5a6aa12c6a92052d4cdb142df8c2f4b5bed26 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 16:38:53 -0700 Subject: [PATCH 431/569] working --- covasim/defaults.py | 9 ++++-- covasim/interventions.py | 3 +- covasim/people.py | 48 ++++++++++++++++++++++++----- covasim/sim.py | 37 +++++++++++++---------- tests/baseline.json | 65 +++++++++++++++++++++------------------- tests/benchmark.json | 6 ++-- 6 files changed, 106 insertions(+), 62 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index c431a0659..17b109b4c 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -55,6 +55,7 @@ def __init__(self): # Set the states that a person can be in: these are all booleans per person -- used in people.py self.states = [ 'susceptible', + 'naive', 'exposed', 'infectious', 'symptomatic', @@ -143,6 +144,8 @@ def __init__(self): 'symptomatic': 'Number symptomatic', 'severe': 'Number of severe cases', 'critical': 'Number of critical cases', + 'recovered': 'Number recovered', + 'dead': 'Number dead', 'diagnosed': 'Number of confirmed cases', 'quarantined': 'Number in quarantine', 'vaccinated': 'Number of people vaccinated', @@ -158,13 +161,13 @@ def __init__(self): 'infections': 'infections', 'reinfections': 'reinfections', 'infectious': 'infectious', - 'tests': 'tests', - 'diagnoses': 'diagnoses', - 'recoveries': 'recoveries', 'symptomatic': 'symptomatic cases', 'severe': 'severe cases', 'critical': 'critical cases', + 'recoveries': 'recoveries', 'deaths': 'deaths', + 'tests': 'tests', + 'diagnoses': 'diagnoses', 'quarantined': 'quarantined people', 'vaccinations': 'vaccinations', 'vaccinated': 'vaccinated people' diff --git a/covasim/interventions.py b/covasim/interventions.py index 432fb0827..5645dfb93 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1071,7 +1071,6 @@ def identify_contacts(self, sim, trace_inds): continue traceable_inds = sim.people.contacts[lkey].find_contacts(trace_inds) - traceable_inds = np.setdiff1d(traceable_inds, cvu.true(sim.people.dead)) # Do not trace people who are dead if len(traceable_inds): contacts[self.trace_time[lkey]].extend(cvu.binomial_filter(this_trace_prob, traceable_inds)) # Filter the indices according to the probability of being able to trace this layer @@ -1096,7 +1095,9 @@ def notify_contacts(self, sim, contacts): sim: Simulation object contacts: {trace_time: np.array(inds)} dictionary storing which people to notify ''' + is_dead = cvu.true(sim.people.dead) # Find people who are not alive for trace_time, contact_inds in contacts.items(): + contact_inds = np.setdiff1d(contact_inds, is_dead) # Do not notify contacts who are dead sim.people.known_contact[contact_inds] = True sim.people.date_known_contact[contact_inds] = np.fmin(sim.people.date_known_contact[contact_inds], sim.t + trace_time) sim.people.schedule_quarantine(contact_inds, start_date=sim.t + trace_time, period=self.quar_period - trace_time) # Schedule quarantine for the notified people to start on the date they will be notified diff --git a/covasim/people.py b/covasim/people.py index d72b0bd6a..da6dc46a9 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -69,7 +69,7 @@ def __init__(self, pars, strict=True, **kwargs): # Set health states -- only susceptible is true by default -- booleans except exposed by strain which should return the strain that ind is exposed to for key in self.meta.states: - val = (key == 'susceptible') # Default value is True for susceptible, false otherwise + val = (key in ['susceptible', 'naive']) # Default value is True for susceptible and naive, false otherwise self[key] = np.full(self.pop_size, val, dtype=bool) # Set strain states, which store info about which strain a person is exposed to @@ -182,6 +182,7 @@ def update_states_pre(self, t): return + def update_states_post(self): ''' Perform post-timestep updates ''' self.flows['new_diagnoses'] += self.check_diagnosed() @@ -246,9 +247,19 @@ def check_critical(self): return len(inds) - def check_recovery(self): - ''' Check for recovery ''' - inds = self.check_inds(self.recovered, self.date_recovered, filter_inds=self.is_exp) # TODO TEMP!!!! + def check_recovery(self, inds=None, filter_inds='is_exp'): + ''' + Check for recovery. + + More complex than other functions to allow for recovery to be manually imposed + for a specified set of indices. + ''' + + # Handle more flexible options for setting indices + if filter_inds == 'is_exp': + filter_inds = self.is_exp + if inds is None: + inds = self.check_inds(self.recovered, self.date_recovered, filter_inds=filter_inds) # Now reset all disease states self.exposed[inds] = False @@ -260,7 +271,6 @@ def check_recovery(self): # Handle immunity aspects if self.pars['use_waning']: - # print(f'DEBUG: {self.t} {len(inds)}') # Before letting them recover, store information about the strain they had, store symptoms and pre-compute NAbs array self.recovered_strain[inds] = self.exposed_strain[inds] @@ -343,12 +353,12 @@ def check_quar(self): #%% Methods to make events occur (infection and diagnosis) - def make_susceptible(self, inds): + def make_naive(self, inds): ''' - Make a set of people susceptible. This is used during dynamic resampling. + Make a set of people naive. This is used during dynamic resampling. ''' for key in self.meta.states: - if key == 'susceptible': + if key in ['susceptible', 'naive']: self[key][inds] = True else: self[key][inds] = False @@ -374,6 +384,27 @@ def make_susceptible(self, inds): return + def make_nonnaive(self, inds, set_recovered=False, date_recovered=0): + ''' + Make a set of people non-naive. + + This can be done either by setting only susceptible and naive states, + or else by setting them as if they have been infected and recovered. + ''' + self.make_naive(inds) # First make them naive and reset all other states + + # Make them non-naive + for key in ['susceptible', 'naive']: + self[key][inds] = False + + if set_recovered: + self.date_recovered[inds] = date_recovered # Reset date recovered + self.check_recovered(inds=inds, filter_inds=None) # Set recovered + + return + + + def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, strain=0): ''' Infect people and determine their eventual outcomes. @@ -424,6 +455,7 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str # Update states, strain info, and flows self.susceptible[inds] = False + self.naive[inds] = False self.recovered[inds] = False self.diagnosed[inds] = False self.exposed[inds] = True diff --git a/covasim/sim.py b/covasim/sim.py index f1c403af2..9cde4562f 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -291,6 +291,7 @@ def init_res(*args, **kwargs): # Other variables self.results['n_alive'] = init_res('Number alive', scale=False) + self.results['n_naive'] = init_res('Number never infected', scale=False) self.results['n_preinfectious'] = init_res('Number preinfectious', scale=False, color=dcols.exposed) self.results['n_removed'] = init_res('Number removed', scale=False, color=dcols.recovered) self.results['prevalence'] = init_res('Prevalence', scale=False) @@ -299,7 +300,7 @@ def init_res(*args, **kwargs): self.results['doubling_time'] = init_res('Doubling time', scale=False) self.results['test_yield'] = init_res('Testing yield', scale=False) self.results['rel_test_yield'] = init_res('Relative testing yield', scale=False) - self.results['share_vaccinated'] = init_res('Proportion vaccinated', scale=False) + self.results['frac_vaccinated'] = init_res('Proportion vaccinated', scale=False) self.results['pop_nabs'] = init_res('Population NAb levels', scale=False, color=dcols.pop_nabs) self.results['pop_protection'] = init_res('Population immunity protection', scale=False, color=dcols.pop_protection) self.results['pop_symp_protection'] = init_res('Population symptomatic protection', scale=False, color=dcols.pop_symp_protection) @@ -450,11 +451,13 @@ def init_interventions(self): return + def finalize_interventions(self): for intervention in self['interventions']: if isinstance(intervention, cvi.Intervention): intervention.finalize(self) + def init_analyzers(self): ''' Initialize the analyzers ''' if self._orig_pars and 'analyzers' in self._orig_pars: @@ -465,11 +468,13 @@ def init_analyzers(self): analyzer.initialize(self) return + def finalize_analyzers(self): for analyzer in self['analyzers']: if isinstance(analyzer, cva.Analyzer): analyzer.finalize(self) + def init_strains(self): ''' Initialize the strains ''' if self._orig_pars and 'strains' in self._orig_pars: @@ -488,6 +493,7 @@ def init_immunity(self, create=False): cvimm.init_immunity(self, create=create) return + def init_vaccines(self): # TODO: refactor ''' Check if there are any vaccines in simulation, if so initialize vaccine info param''' print('TEMP') @@ -512,29 +518,27 @@ def init_vaccines(self): # TODO: refactor return + def rescale(self): ''' Dynamically rescale the population -- used during step() ''' if self['rescale']: pop_scale = self['pop_scale'] current_scale = self.rescale_vec[self.t] if current_scale < pop_scale: # We have room to rescale - if not self['use_waning']: - not_sus_inds = self.people.false('susceptible') # Find everyone not susceptible - else: - not_sus_inds = sc.cat(*[cvu.true(self.people[k]) for k in ['exposed', 'recovered', 'dead']]) # Find everyone not susceptible - n_not_sus = len(not_sus_inds) # Number of people who are not susceptible + not_naive_inds = self.people.false('naive') # Find everyone not naive + n_not_naive = len(not_naive_inds) # Number of people who are not naive n_people = len(self.people) # Number of people overall - current_ratio = n_not_sus/n_people # Current proportion not susceptible + current_ratio = n_not_naive/n_people # Current proportion not naive threshold = self['rescale_threshold'] # Threshold to trigger rescaling if current_ratio > threshold: # Check if we've reached point when we want to rescale max_ratio = pop_scale/current_scale # We don't want to exceed the total population size proposed_ratio = max(current_ratio/threshold, self['rescale_factor']) # The proposed ratio to rescale: the rescale factor, unless we've exceeded it scaling_ratio = min(proposed_ratio, max_ratio) # We don't want to scale by more than the maximum ratio self.rescale_vec[self.t:] *= scaling_ratio # Update the rescaling factor from here on - n = int(round(n_not_sus*(1.0-1.0/scaling_ratio))) # For example, rescaling by 2 gives n = 0.5*not_sus_inds - choices = cvu.choose(max_n=n_not_sus, n=n) # Choose who to make susceptible again - new_sus_inds = not_sus_inds[choices] # Convert these back into indices for people - self.people.make_susceptible(new_sus_inds) # Make people susceptible again + n = int(round(n_not_naive*(1.0-1.0/scaling_ratio))) # For example, rescaling by 2 gives n = 0.5*not_naive_inds + choices = cvu.choose(max_n=n_not_naive, n=n) # Choose who to make naive again + new_naive_inds = not_naive_inds[choices] # Convert these back into indices for people + self.people.make_naive(new_naive_inds) # Make people naive again return @@ -827,16 +831,17 @@ def compute_states(self): ''' res = self.results count_recov = 1-self['use_waning'] # If waning is on, don't count recovered people as removed - self.results['n_alive'][:] = self.scaled_pop_size - res['cum_deaths'][:] # Number of people still alive - self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - count_recov*res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents + self.results['n_alive'][:] = self.scaled_pop_size - res['cum_deaths'][:] # Number of people still alive + self.results['n_naive'][:] = self.scaled_pop_size - res['cum_deaths'][:] - res['n_recovered'][:] - res['n_exposed'][:] # Number of people naive + self.results['n_susceptible'][:] = res['n_alive'][:] - res['n_exposed'][:] - count_recov*res['cum_recoveries'][:] # Recalculate the number of susceptible people, not agents self.results['n_preinfectious'][:] = res['n_exposed'][:] - res['n_infectious'][:] # Calculate the number not yet infectious: exposed minus infectious self.results['n_removed'][:] = count_recov*res['cum_recoveries'][:] + res['cum_deaths'][:] # Calculate the number removed: recovered + dead - self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence - self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence + self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence + self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence self.results['strain']['incidence_by_strain'][:] = np.einsum('ji,i->ji',res['strain']['new_infections_by_strain'][:], 1/res['n_susceptible'][:]) # Calculate the incidence self.results['strain']['prevalence_by_strain'][:] = np.einsum('ji,i->ji',res['strain']['new_infections_by_strain'][:], 1/res['n_alive'][:]) # Calculate the prevalence - self.results['share_vaccinated'][:] = res['n_vaccinated'][:]/res['n_alive'][:] # Calculate the share vaccinated + self.results['frac_vaccinated'][:] = res['n_vaccinated'][:]/res['n_alive'][:] # Calculate the share vaccinated return diff --git a/tests/baseline.json b/tests/baseline.json index a93e09cd6..33736bfd6 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -1,50 +1,53 @@ { "summary": { - "cum_infections": 9842.0, + "cum_infections": 9829.0, "cum_reinfections": 0.0, - "cum_infectious": 9693.0, - "cum_tests": 10775.0, - "cum_diagnoses": 3850.0, - "cum_recoveries": 8555.0, - "cum_symptomatic": 6599.0, - "cum_severe": 469.0, + "cum_infectious": 9688.0, + "cum_symptomatic": 6581.0, + "cum_severe": 468.0, "cum_critical": 129.0, + "cum_recoveries": 8551.0, "cum_deaths": 30.0, - "cum_quarantined": 4006.0, + "cum_tests": 10783.0, + "cum_diagnoses": 3867.0, + "cum_quarantined": 4092.0, "cum_vaccinations": 0.0, "cum_vaccinated": 0.0, - "new_infections": 21.0, + "new_infections": 14.0, "new_reinfections": 0.0, - "new_infectious": 49.0, - "new_tests": 195.0, - "new_diagnoses": 41.0, - "new_recoveries": 166.0, - "new_symptomatic": 48.0, - "new_severe": 8.0, + "new_infectious": 47.0, + "new_symptomatic": 34.0, + "new_severe": 6.0, "new_critical": 2.0, + "new_recoveries": 157.0, "new_deaths": 3.0, - "new_quarantined": 138.0, + "new_tests": 195.0, + "new_diagnoses": 45.0, + "new_quarantined": 153.0, "new_vaccinations": 0.0, "new_vaccinated": 0.0, - "n_susceptible": 10158.0, - "n_exposed": 1257.0, - "n_infectious": 1108.0, - "n_symptomatic": 825.0, - "n_severe": 249.0, + "n_susceptible": 10171.0, + "n_exposed": 1248.0, + "n_infectious": 1107.0, + "n_symptomatic": 809.0, + "n_severe": 248.0, "n_critical": 64.0, - "n_diagnosed": 3850.0, - "n_quarantined": 3865.0, + "n_recovered": 8551.0, + "n_dead": 30.0, + "n_diagnosed": 3867.0, + "n_quarantined": 3938.0, "n_vaccinated": 0.0, "n_alive": 19970.0, - "n_preinfectious": 149.0, - "n_removed": 8585.0, - "prevalence": 0.06294441662493741, - "incidence": 0.002067336089781453, - "r_eff": 0.18301780973366616, + "n_naive": 10171.0, + "n_preinfectious": 141.0, + "n_removed": 8581.0, + "prevalence": 0.06249374061091637, + "incidence": 0.0013764624913971094, + "r_eff": 0.12219744828926875, "doubling_time": 30.0, - "test_yield": 0.21025641025641026, - "rel_test_yield": 3.0589651022864017, - "share_vaccinated": 0.0, + "test_yield": 0.23076923076923078, + "rel_test_yield": 3.356889722743382, + "frac_vaccinated": 0.0, "pop_nabs": 0.0, "pop_protection": 0.0, "pop_symp_protection": 0.0 diff --git a/tests/benchmark.json b/tests/benchmark.json index bc647a166..975fa5f7a 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,12 +1,12 @@ { "time": { - "initialize": 0.413, - "run": 0.498 + "initialize": 0.411, + "run": 0.505 }, "parameters": { "pop_size": 20000, "pop_type": "hybrid", "n_days": 60 }, - "cpu_performance": 0.9861777951521127 + "cpu_performance": 0.9815278999602399 } \ No newline at end of file From 495568aab577d007c2e70ad2b6238fb8eb867d99 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 17:19:15 -0700 Subject: [PATCH 432/569] small changes --- tests/state_diagram.xlsx | Bin 10142 -> 11072 bytes tests/test_immunity.py | 69 ++++++++++++++++++++++----------------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/tests/state_diagram.xlsx b/tests/state_diagram.xlsx index 641158b8bb00fa35ff48bd95b6aaf53394cf5286..4b411f01db9a576fdfa7ce8479bd841e8510d69e 100644 GIT binary patch delta 8680 zcmaia1yEegwk{SVIKdfUaDo#gxCIaH5FCQL1qo~{FbvK>a0u?MK|-+L8r&TM1Pcrf zH_5B_-hb+zb8lDe-FtT}{d#rR>RO$p`c(%@?KLtAAp$x&I>L2(4HgL^(sg?s1ELPn z?;hrlOb4V35c;p01bI8t(9oF46kDZeTojZxYT6cPQ-iorrsw(v94AdW_;+|}tc6nJ z`RizLk-yCDE^Ycb_SJj?-Z96WB2Tg~d0ULGEEFQTDkXrdDSjRJpIbSS6Cn#ALQI>v zW2;}RJ^`+XglVtdqS5dOAXoD5#MGRr4wY_DvH=no9~1H1RTXUzNu4K$OBJ6=YG9}Z zm49|@)j@P~KowgdQSW4-WAqztjvBS>@9PVtSpHxu-x5b}IEV(JdD35xv}%WrB+k&h ztDioJcQwtqi6)WJL%eJejLv7%xSXPHIZJK!?NeawBK8P_>r*I*vOrzxU7*hU@q~&P zb~>PV;|^zix{LGmIj*N}A5p;laUB+TfV$cg z3g6Lw!H2E(agP*VQX^TB-06UW?mPA%kKtEXO!jj#E~|?Ny>w*iddg_ZoNG=M_NOU) zxAAi-`c#-OP?as0w<&;+N{=~v4hufZqG+Yx5hy`+H)+Tddik;%((sjUN3jjXlz>)bM@n0+Om~&TGzL{05eyFidYnz!ULV zdbsOzq4QwxVd)fGrQ*n4+LZTW4XM5N`{eNJPQjJ-b5cNlED85QMDxa8_e(t>e zi(c7uA1&rv&$jKVRAjxba;Z_p|~Y|5-q^%>%*xdHzUS1ldYMdTT=XR zi)A}(KQ7sZFTD~BAHcfX8mBh+XtnIb86fv=>WrriB-ZYUedkD=Wo-X4P91216F1^8 zY>Ezgw)%==9vg?TLPb=Ko5ZtOGpd}NjUfk4KAS6I{IaJ_c6Qy_=Dn1r1pLaQ9y&;Z zQ<|zKWA3pybb}?o>I-ywC!UX+g&O_Tx9_u9w6Bo6nsN{tHi*RcYAE8&W{Dx+JdO3u z&)OBJEE}>REfp~<9!fu0Xar)Ei85vE zR!g<$E<`*VJYDluD`1tC2D<0{D6c+tmDi9n}Ur9^KQSRIq6Yul5K(jzF% zD=dl{k%t~`guTQt_nt1J#eq-f#N2n3Hpp^0s|4M>q*++5mMC%F&(etCr=0yi^yhr;*{C|lmHb?{rqegkHRHzfm*eR(TbsJm z7gP1kNbZyej4zHTlOpa3u9C1yP)Er-6rjh?nM|i^%cE7zJMa#L(GgX3fDLLSC?Bdq z6B$<)3GdpbSLKn18`A@&m&fXUKg6>;?sSjNnd z4c&rKX;xu)p#uSU;g$K4=FBsg6vBIczjD>-B;H6acswDQsPoZxrVWdN#yDzV?S@f- z|0$hw&~T(?oeL$(4CRkGk4?L>yntDZA7^g^Z!eYYKEfk{v}P3ppLQ_Mke!g@bg2tq zF-gwtqv&6&08u+!lvr!M0U-{&9}NI?41ZOx&`#gi#m>mM+V5AWQ8v(}wc)eqfFAl+!q*w4K!%Wpx!gt~intDV=@<;?Z9p(Mv;?nt8_JY7NcmkPbND zbk5f3T_gexJ;AKcFxj|Qq0_tEEiYo@x4927cttqXyx*GVF!1l=V~##cH%y)DA)vT~ zMF0LaPMtdsox2jLnNNxsQGtAr>uFn2P?sOPD6KY%gh{4`s^qbjEmr1Jz4=t%#8e!2 zuIvIt&mkGm(E3?ZpS!0q7CkebroNMKd)>)h_hFV=$@+VB<+o+L{iL60-KGlx;Qr`w z@b}ND!#cM(U?Qo@Z^n!1iBA%Hg8TXW%T#oxSv#I5DW=ojs_js;E?3;76jn}mH^zOA z2{KYRpokO&M$t`zkm5uYBoP)VMwg}J+3`OBNNr}!KA*R*bY(pk$pyzT;0tjmgQrqk zg#%iJur*UwB7Lel`>axal6Ox&yj+l;q|d3N&;8VsB@LEkn4N@fnWh!SF;`ZE#AsR= zo85WNik7?5b5*vKU5O4xj$z*3KD>K=zH7aAkoXHhbP?8%|9B$ZI9@+sH(5!63U{Lr z@S%*7EbqS)m!^YS98E1J$p+|0-a0@9pLYG*W6;R!CcRG^BY zA*@n;)hgydeDwf$ND27y!=NYP7%%qv+!!c|f9N|g3DG6~BJHR685R3#qW#ORNORdN zou4W$eQ8bLjar?>dXUQ%x!7zT7|8F5P!;m_tee=I!tJ@Z$p6q@L_W9Yy7t!Su#Hpl*KG{)6<(tsP3Me2tyle-+YS15oLzZM9|f%)dxqbRDL zZ;WjFxs?*%E46M-TI8-DY?eD0)>wV2`>rHG{NP6U1+BxNL8ojP;2nkwDvz_ev$)|j z{4^k836OkeDPvo!pRmU#s*bAyp={wm##DHJk%8QeNY4j-gr57-Ln?j8#bh%mHO3}u z+Ybb$IO{1WMB`qbJbP#jxD($vcBPhV5~nDv>vMU2ZBjYQRR2 zVE3^vXH`ZTB8R7emT!G;4)ckHWd!-`We+x$y)zbzOe3x6VaNO&XJHQ<6*tIHlW-{b4H z-kP%OTIvg>!|>mvrSL22LC?W--*^B+t?dK^gD1aN^Au-?hCDyzqlpS#qu~;6E&wMB zm-=Of()i56DB7!B@~ro|saL5RV^_=j20EyRTha3FvnBnR3}R#Am64mb&uvyl4om)Y(ZrddvzgB0b4b+)oiv)_j`9oUNa2==HBi z4Csxij0kfwWDR5N^-+X4eBjfda_C&#f>JX}%|VyfI+bv&Bea?%yZz>|DRpe!iD%3? z@+w=7fFL_WwqT}$_D0%a@d9RIX5EHEw(3=IdO$^7Qq$oY+I=M0|(D3Kq-UsB`k%_p2-Hd%&F6-CM)pTkY1dpCK(B5Rt_*C#WCdR6;OqOd z&@ce5Py(LBl)N4!hL6lOUC(O2#beu9M-t4i(=xSr^>*2_cJa`!b~L(fulMW8SZ?R^ zXfgbGgBXaJYosHHvf}t+9Mpv0Bvw1&3vPR|=orgsXAwTm;#7)&$jTt)B8e%vJxYIB z{=0^vHYcEW@St>%hhf|E|7##*uwv%c#nAWpHySQ}Yf#lT*3>Y@)uoDw(w5etQdCkm z41!xTQ&mYAN2X*0K>w! zrwlX9HIfnk2iX0dGu4K%PKwf_g@hwFrJ55L%P6{Mpwx|iEV&(lT4o8$`}O{(FYABf z6yxC9(-N_zt#@q~l~bywAInX6nS|+#VPzPcnrK?ckfM)aWgM(e$aN+|DyQ=V@}7ue zeHgW=z8C+1F+ta2IuaDt0t}k<4}nl0cmuS8$&yVO8B%O9taO4?lTC@AMCHo9@68J> z@I=fj#&@=Q6I|oun=xxqXD7<9Ejsv`T5yNDa)SBAw|I;gC0ENZI(Lx%T0eoDjzBGk z#FVZ2TSCg*kLBs|#$no=L8SZS5E{;4(mfa)KPJl6GMG*il)Byz0_2DVYJ1{|V#+b` zw8QqEP7=$Nr;l#E)w>9WSbMtMhw14|d#b(uIOBf{KafeZfPzFM2di0!kEHFUI-4PX!by|c?J_7}wC zaV|CALmot+Ba~YanS0$BioLQVHGo2*35f_dj%#nYORcg$6?J$I5P>V;A3|W{zT!2% z5^YrbT_$Sd5bvAYVTJ%RbUQ@KK?gtAmB>8r4DXG0sEzRl;Hu!p$w3$2;+N1j*$N|{ z#7vf4hwZ$(iZ3X-{!UL$BTE*t2x_Xhg~lAmk}K@p2I#hTXXk0=Og4J;olz>GvurT%S; zDq0QLd9yazfOt!zMHGqd^w=BJ*dSuOy;!+R-804a9t})RyJ7ew!mEva-I_}YSq_}T z{OJWIV~azd?1LJ+=IFxPZ^YqKO<Z9Z6&N<|z7o`Lby~2%Mi~Gcfy}~*-A=H6`EoSO zFf^W`T35%c%~b==8$eF4eke3_r%VBGk>hZpQS@Qn$Y5q~YA&GbvO2FnT{mxa4XL-G z_wr|YyR156oOt-=wnn6+{q12}ZO+lH=6 z@WtrJMq-R(#uea0;ct#D=im~XbzI==Lw)5Wgo{x_>;OCNo~fQ{N&+T8(v zm+#_vcNgbx1guRT&(@sy=i0BG8qenlt!$5bkMNCv~)|)M4;tG`XaY4IND%Zk~{ajgbv5P+Hjz zhQ}+rY*_--nMRy9qTv%HmviZxzcwn`w{AHY`l8-qCsQB0({1y0C$y(y@M8i#LmneH ze4kU43N4&?MU|cnK{u@2M~yR$4QogOYCR4ooNBwxbFnXbFXk!EaFWO()uNo$etcby zQnpKY%vP>CmR5Kk66u%_mTolWpn*bp@ktG3>6z>duEtuO641PopOwwT^Vl#^x#M{Ue5`ECo z(%LVOdzt$kD#^+?PZY^}$e*Y29u^bnjMV#1ll2RR;46^|^x~;9+~@^*#e5oAm=rmS ztxbN;_h;tgP=#+b^4(56EFWI}Y(6Pe^ERwFHYxPjo`41_wx7wrlFBpo$oGa2vtd~qVBd0PBO_7p+ZdYoFeUP*w#9$7c=$?T8E_8>5Zlf<$`+hC z301aHl(#8VoH-RwoaWQSg~|Uh=(hu5P}vG9-dgNTUaUzjg>G8x){$Z@Bd^P%#i6xV z<*l|AM=UDt>l083R@w^rdfM+$DOS4>jVf`2i60WE6)8Zy`2=*iRI9A|S2{OHhh`~e zy?B0yg79iXv=d*|CI1=d8bwa9Q=KB?<{ekX-Z=_6lgGDwI{kX{CYEXQ!y96>@JoT_ z8)clBFQjAHYv@b@8yGPj!F1Vc%uE7yA;{D6tH%N~2@`7)e3K)3Oafl^v1JzmM=ex+ zda)P6)I`8;W|fTwzKu*iQG|}Afx3-M0a3J$hXFS^p$J}AtB_GXZFJB3?(Yz>K z#ar($JNg|b^|X^vY1S7N%=NjGP+3-?ir+In2?b4&%q)})QtA<{Dq zCJ{VD2)Z;9EwXxGu>>zz2oD*8(Su};oSLk_$5}ywJsO{ku~ner4MxJlhoDHK<|3#E z*+_VRQSqKZp7fx;;Dp&38|Y5#_74&MuhKf)-^H{UN*V8mqR}GY13~fB(BjzGa1Oj^!SYSpxObE86 zn6GoYxRcRwHVvnR-ZAojoZR&-cgtY2s)w;}NX&es+VtIM|9haZxbe=Le;U_(dW;--B& zlY9kw$1PpS+hex-D#L@jN8J{Z-J-f#AITjvWC1c(5~kjMOOltNC3pS$v!xNXhVWoh zwAjI!VOs}rk}?innVOZ2}5)%SYuucenw+jyVmNj?Sf{A7-xwKeAaZx>EU(eL-EFaWbs~ z$V~b0{2Y+Zu%(=C)k%wLHVYikztnku!TktSw4YbMHghHuF2F{tOWWvHZd|1slEOoc za(t#+dg=729MJy(&u4z{t+!HFJX=@)P(qrsr;bu`qN;}Tj|v?&b;&d|0l01K&uxwO zf?3o{BVF6IbGs);FRh>5kM2G`!u=@<4LX2d{7#c~OS3DpE}JNi5Bp*@xlxfM?y(fx z;z!mY7CicqGS&|r#B~ZkwqtjF>RKDELe2!{zjqU7y#tW{r1#j@o+ibZ2neWJ|C-*{ zc~LU`shh&Gsh9y}m1Qoxl}kdpV}+GGg3_#nY6+>*mIC{&T`W#wxPbS!Aj3f9uh@bL zOoRdx?yQG|roc}1Ge0V{^t{ybmJZR=_9;Z`#I$=%5@ z`Vu8Q?Wxi`uNG_5xeS~pqw;|xjToJCo@|3Go`rZO=?EZ4boD34f{iGdOG(`M&cTNu zsfV1k>En=J?PNmmSC>w1Yx6=EIz->vK9N;Un$P#Kr-GwavTCD^SYORLl|OPhGmf38 zQjx|R!R0LDFgnBHs9f*xJ~~9S+t`zebcWj_uRhT#s78uqMXz1DA#Fg?PuIsKu;E-1 z?cp^ar_}~RWfoibwaI`r-(sYroNzr4f5i% z*#3E40PX%jIl2*>Ga6EEvLAQF%ar+sXy!YmP-eh-%eZ|8!;gZ?N}Sl1cb+8|VWp+m z+G{_>baa{>akMY#@?O8$Qgk$`VObL`kZYM{$3Ni9hn}jHI~97DM%C)=ip*ciJiNBK zz@`ZJdq9&17@wpdA|N0>`PUhQ*@GDY)h;eV|4Usq|J4?xT>eG*sT2AL47_OPhW4oa z1wKc!ljEQKDFi;PyLr3t75;QeEux>DLiz`wczPLp8)M9pAy9?xuPZ8?gZb~JSellFiTzK8G%zwty2wkVbeg5$tK*G#I=%9f^)Gtc@;^2N!U?L;F( zZr8R<9nu(Wzh&|*Wv^ZK4%JfdeQYrzLesJg)^rNGULYoWz?5SD>dG38%X-|4NxKI9 z;YI@242+o_8hxSmbBA^3j<#(AkPY@035#M~vVEqN*~}azaT_!?xp?)%hfl7TnKE7= zvnRB?KT+jq&+%r0za48&=*`>^yo?an=sRL#lUM5)=h_!)TZA0=r0)3l-Luup=K;T~ zg=pwSRnf(ozafVr{VU{=8VLVrcnAn}*tA4{Zy(Up75rUIL-a5IEGiTRVeISnNR9tj zhJ-0GaDe}e{Ijk=`VV0U2!&{{H3p*J#Yz0Xg#SF3|0^K^)^9?_zeN9B-Tz92OY~Qo zFeCmSdsG-fNcUiv10(Zawtt2c;lCKcNDHf?B8CkzVqyHT_Ad~yRYoEt1bP^pk=ppr z;KvteCHud*-v37EHwOP`2@D7bmd+MxZq6?5oMtXA9Nvx&YOfKI2oe78ZH-8cM%mF7 c%io;Vs0awp{=tiY(836FXJSA#Vg57vU-NP%y8r+H delta 7683 zcmZXZby!qg*YFXL2I-QJ7#LsxN$HNELuL>VP^7yXM!HK{kd|(SPHB|xZibc)5#$A* z_xY~-{@(M)$-UNIXI*=*{o8AGKr_^E)RjdRAFlB} zC=9T90b-w{iqP9Ol^6K^ta+aYKqYNjQ?9IK@Okw36#S_whCcYHp`q|F^X!>qTvB;w z7h~JZ_VR|4&4*IVWok!jAKB7DppM^w9{QWmZr%ZK6LKc)OgphXXU8YVU^fxm%o)&Z29f(Kut*Bl6+~~$ z2@Yalpj3~{2y&LC{KPc$v#_e3-0-1q@Zfn@WKW=inkY^niDUH6N_Zsd%b}1Sxi!n~ zCytNYfs((LZm_B_*86{Q^(5$kmP$K354AE`0fTXSu6^7j?$~EIw22y$Zn23{bR@8{ z?`7OOr!9=DL!F#N7M?b7288yxROyFt=lWc}7qJtpUG6WFztZ7g^$Qvqvr|Iv{ z7a5IAie5t6%{&WmXqYEFo~et7D1s|;$8$>I|owaeoQ zej=yGfm{Px8elF6Vh^xabvhY;XKV>0Y4jo3{SbJ<+{k|0dMI}kniX>Ri<=1|eZc?8 z>#mr73il21bwfTk7{Y4OP|<{=>?WY35UncYYn+gqqTQ@(f`>Be40la1S&=o@>3}bj zZ!-1Qy!kxo?$5N(=6yw@)M%JST3++-DHaf4t}MxIX;18=2F z5zFJRwyYgrTbsV>G4)ikzP(S0ToP|9>GD3JI@0%y6t^Bpz4~>_cwu;`EFNofDKuBE zeXPOv#WL71<{oy>SO=J47p%+mF)Y`&T8xp!9`RB2hdyaV0CFUqUa z=8^(Ip~3!}3V%!_MCE?K8_7NjX`gm`g>(~FB7+oeh7)%^Yeb4Nc)&9FV#e{K-i|#j zL4+)>Zm9UjLcLMd!IxmXrEC+tVppdV;f7pj=AW(O-7+h3>+zj)-R58zP7_~!C4@Ec zXztBbggDb!my1!^Oj||y0!$3btG~FS&obS0JKPMjjXbr}nUNFO3Kz~|EKZ{z*TI}r zR}~~ z_{?00#OG+_oyo*0yc&TyQN89RdKLC^D=yWbptf-yL2-I6@^b3Dh{A}n$b;##oaJ=W zLb3CMoKLNaM}pc&2d`U#+IF0iVvK{n4ut5{1Qx&7XZFiQhV2~RXwQrcm->N>nLfDU z+nK8iny@h*d;NI%a=DtmZVAVi9*cYK?=ufpTx%*8mhQ0w#gVAgmC!Nv^0l#UQIL?F zUmzj@}Ug=QdC8|B#jfHMot{DVBdQJ*{V3I&8+J0}V>_GvO6&li!JV#Pwa$rJ%;0H+8 zMmQIdXWPD}%W)S-mwmd%eU6d5$44G(B4RQV3w;{j8vtYqmAnz!{%Sj#u~TwfNRN(F zyEc*QerTrKGPa3Yv08+Tn@W+VJ~^qHdrR(f3mB&N!L9Ny>?`(;=>VYBZ~3R?Zjf^j zqWc#3M`FXeCbhFtk|QyO?8_O2>)s>ub`e7;i`m(!(WDAmb_R?IxqhrbAg53mm>$idI@7>}*Ckk_=?CD}^t)Uk%$s6Q;a~7di zF*||5r!Z?ygLH-*glJZKe0!~VlXPLnQY)3BL=DRXdkdohF`PF+9rThKwZGw~Selk; z9hAIfaK9W==z^mvOT5{gP{C7=>b>5S9RG!J9s(Ho4FHFbpAqm>k57cz9nEq-ND;Z5fk*2Ht z%}xKp*Ww$;_~Zl?F$~Qj^o`MsNwHh)$?iLMPa%R|(DTGXYWcp-sq3}X^{yT`$L$gf zUn$qiQ+Xre@)=~oUov%gXtTV^8HwWKZd7g^E))P=`Ba1py9|=PV)f}w zJoEWst!OT4ZDmUSX13O`jfHAcdU%tPq~m}k8o9x&Uj!scAuBL_Q}$waBQa0h(RQAL zYBe`^=0vmfRtb3ECAW|d7=v2Wc@DtJ1tB&Z8-aXGhe=gcEt=sPjdRU#?$`fHJf5dVJhqdB}4 z^zUj-F1fQl`Tz~|4+NsA_aWx8n2Sfk=z1Rj8se%{b!wA6u*a}aX-NEQkLV#TAh7;V zT#$fKuh{MGoeydq{zyWm&-W+5t&Tr0SYhDc2S+~BPmB$*1>KD{?r{2af1h{XwjCYQ-Y&p6k!dPGS(}ScYb~B#N(d z$&uhQ+s{M&^Ajjxl%OoJNdb@uJ|1n?wka_zFk$vY1--(ePPY}|*U**$w>6zsQu^>$vnu*S%gNqCg?0!@*o8gHe7RwJTsVk3X9JYmBBPx!k=U`Dw z2TQU+5voRXD9y?Lg1d-5aOQyF3xrF=A#{XP_*~AK zoX$}ayI}?`jozxZQ#qLy$!|TCdDGj6Z%j$8(1e7CP>t8@W)NP-B@;W&O*XKugNzB( z*6J+!ZS@74^XzppXL)D>QyL&_d6ej4HF!4Lo8k?4-HMcoJmy7_#%%?-Lf#)Z6EMxS zeh}-Klys=SkndZxG`r*1;+J{czTbB*e=GWvq5l%lSqIj5nid9PP33(ZZPUEdvIY)!IhYD?2H)c2$|W2g$lr z-u|{|MLm*5vS&njJj(2NKZL3;ST@GYO`If377I@(O znKkiL5A*@keBRmnhL2feaeqg=NHJj>!DvrhN%E-E6CyI9uw@#l-h|1<+H=zc9YC-@ z@KF&az>mHQ0(OKubKchc0uf0?BRtdG0I>skQ zHvsl;`RqZN--DMgHu;X&S`ZXB*L_#bOB?keeY@>PgK5IKXPFuk1ZDDl{W(us>^;bt~(ntycR|X~E5k-rBBcO*cCra695NVjU z44DbAfFOB8H_5L~DZ_^ky@Z=TkCdof@t_H=@9xB5l*WC~V^;C!9)xgm4*{JnaR1qk z3+&#`5-qyWr|_Xq+PWEVE8$rVjI*b~Gac&s7TL<{)RoPQ3kwlh>kFjA)B&`C`%&fd z1-QH71fxM(d4OF)0JI^MFWOpNSTA8#p8@N;kh)PAA)5%D71dmpYaOtvS&mq1sz?VyEek z!?iSvAZ)ghk%Y824%ezR33Q-9F4-^&#X*Qv5{V*6!zhT^IEvx<`yEO|n@ zB&w*aZWK<)laP|zEd|J1HpsClRX2(ve4a4-*YwfJN>deFh)OL$1y@5QyBihoU$1CM zO%p4Ys9>*1z4FNB+xnSnN zhRs-WLvd>1&Edp+(5p`^+2sSzPjg!QIPg|B{xKlzjl&q&sC1HF6!(rA^41AdH}5HH zd~o3$eZGg)O2;|&d`I&{^#AtoFnsKdBN&h4R@Sk}ch|KW|3npc@s4C~|2D!Ic=A zscSC@I~OHug=lAX41Ln3TTz;Kpe(vIqY}P558~#M0V3hCW}<}fFNk#{Ki~+N2p671 zPqKZDtPhQ0NH;8uQ@|&HP`_p+-TDj;i?`+@%N1x2tfCTT=J;;(D+_0)yS1 zqbge9n)v}~@r2lGGVgF58()`Po;Z+cOz}lZmU#V{;0xh0Fqjg_oD)RTmBG6;hGt>OCn<>p&&m@&9U`jhY%24eRSy!B&}^VAVe&mzod$prSK1tFn z_j@AVG+m#Lt~~{_G`1;{&%wejd}hWEI>!i!S##pUl2Wwxh}(t^>6Sq*LO1rSf=Z4F zpIg^BfP{>vhjYbZq5>xYRGl$(s`y{0mk%Ni=IA_vv?knDrKoz%@Gc{RjYgbGV`Z(o z*htSljZT7^r)-(jMw%F3#sqRUuVRzqy=Y|abGdGg)Hs4&<(*Vy*EzgZ?INkkHstS) z0;Xg4^Y~=l#zSnKe&IU$M$eHjP&lP3R zu|BPkGs=ft=H(fQxE5^Fif9AqJ05Vozdjc@Osj%T;yAl%VitB9Mi15FYv+!e^X>=F zi6mfpnkdQ+ty$NM2NSJi3y%W&de!Tlv0Kz#XQG&;oaj=Y?|0^t<6K$LgUM^0f)Kv~^Gyr=^2|)igYoP> zAk_n%H1Ldn>jP5ytRilS3wH61iK8T6euo)|(BEqLSRuaIt$`Du4V2J_4@7+rsF$2{ilYg^%9$SB-**zI=v>xRTc~9m(}N=z%$KgxTK5c4Q}FA##v(Ck zkZb(NupGTJo2C`WKe>te4mAhOh)5raFRzr+wk?ZB`nGaa*hcokyr!ZmX%6Wl&|t@% zyW^#qetTvs1$IvYvT8OfL!ED{e5~*HPDZfjUy;JtOd)hbItXoQX920#a9nUVYp*`7t5MbXa6IeY{t?> zN7@@lG7wRv7K7OflpYsj|E%@VWX66BN|Pg4fa&ef$YM%BNpW^zar|otigsf7CIl8l$dK6oz$Z-qOA;sk zpoeAe=0Ey1AT`DiJ`#wFQ?ccGv1VLLvAR(p;fKEh|5@z~VxLBXG{o5w-efXx7h5Kl z7ANJfGLZeY!F~Xa%?HF_NFdMcmI0hTu*UCewf;&7{!4xJUIM5jnw5=*xdbn12?-DkGdc>4s!p7x{Z2tc;mb#HobiaAA zUUhy`asHm5ipwLP-~;4Ey~##@8p>_u@`R`9BZXmyGRM38r^#zD{<&Zohw zyj%6iDD0D~$foZ9l!(mXB~CS5wb1AQDs~N(a=e>1V;Hs^0_j=h7uVj(cn7oinnTz7 z@#Wj?(J+Oc#LGdipGkWioXC=8)Il6XqDrfQp z7n2OTZnyr+n_QKQ*^K3bY)Gj#*#Zn-=C?%`(MD{tNMHo}o|6X^;k_)4{S6v>lWDun zIAAu-{aX_`?zl8Lk+J=H7BKmlVwHhhc&|cu?f7f!gt#1!PwUC2%~d*b(H9d`R*9y& zI(6UstK)`F3vLLdE`I61(9W^YQ|!LX+j&_hO6!)jsYe48FI5UMZjVTG{D}mUBH2I2 z6JP{rqaoC^Htu-Y2*rd~XA^&GSth#5TYtgWIN`hN*LiWJ#y51iX1M)^@nR5a8!bpV z`?6O}O#7LahpQhce&KPznW#Y-YXgTDc$e?-~c*ScK4XNxlp*)*Ps9-aK? zVf32x+<(N|d_mSZJ%+=_mHNhrT)-YBRC!!gpzaOGUZ&vt9fm_=Z#DSyp*8=ghFCIQ zG}qT5w>r^zTH3HXnwmp*9?g5Q^{4H)oA>SB3tWE|ayQ1ndK21*86MsD;1y9HG>zaB zg#n8ETc?Q_JA)X|H7At zDhwta^J6t8o|_&BlT_V&iQjS-o^-^>69M26Vr0sBC>H-RHZr9C6@Yr)pJ#^+7D>ar=9m$r{e4((@vq`$ZO}AnAJE56^Q0(&5nuc4>aP9Dy;(7I_1_ho`#qvV}936u*cJMO% z2@=v5)PEx9BXKe^!R$I;5c{lDn7iAtl+EEhRHfL(hK$2LvwMB8FS8CJlqk9PgE_3h6fBUg15IQqVEiIU7GV%aPYw!CsBxWUi{OL)4OZe{+IdJcmCZ^=(ATc90# z%l!1m3&OX={_xzz1oQpYY;xhYr#){8sf0q)rdbgva6bM>7_5fUJ}Lj|jh~snNAT~C z*~qXRDwb<<3)eadWYoUVkI0AAOqk!=4W2<8)6mhjVqPo2c-TFK`t%RB6nlpQThuc)@rTqw2kyiL$3X!ybwzT3 zN#4!StHEEg?zyli)Dw3cidPM1Ooc*D8svT%Xl-D-UlV7R(Z!%4aJs6I6qGUd7s?Pb zt~QB8@`B7VWoZiy+&3l ocn+10qFMFaQ7m diff --git a/tests/test_immunity.py b/tests/test_immunity.py index ca91488fd..7e803cd6a 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -10,46 +10,56 @@ import pandas as pd do_plot = 1 -do_save = 0 cv.options.set(interactive=False) # Assume not running interactively +# Shared parameters arcross simulations base_pars = dict( pop_size = 1e3, - verbose = -1, + verbose = -1, ) + #%% Define the tests def test_waning(do_plot=False): sc.heading('Testing with and without waning') - pars = dict( - n_days = 300, - beta = 0.008, - NAb_decay = dict(form='nab_decay', pars={'init_decay_rate': 0.1, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) - ) - s1 = cv.Sim(base_pars, **pars, use_waning=False, label='No waning').run() - s2 = cv.Sim(base_pars, **pars, use_waning=True, label='With waning').run() - msim = cv.MultiSim([s1,s2]) - if do_plot: - msim.plot('overview-strain', rotation=30) - return msim + for rescale in [False, True]: + print(f'Checking with rescale = {rescale}...') + + # Define more parameters specific to this test + pars = dict( + n_days = 90, + beta = 0.008, + NAb_decay = dict(form='nab_decay', pars={'init_decay_rate': 0.1, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) + ) + + # Optionally include rescaling + if rescale: + pars.update( + pop_scale = 10, + rescale_factor = 2.0, # Use a large rescale factor to make differences more obvious + ) + + # Run the simulations and pull out the results + s0 = cv.Sim(base_pars, **pars, use_waning=False, label='No waning').run() + s1 = cv.Sim(base_pars, **pars, use_waning=True, label='With waning').run() + res0 = s0.summary + res1 = s1.summary + + # Check results + for key in ['n_susceptible', 'cum_infections', 'cum_reinfections', 'pop_nabs', 'pop_protection', 'pop_symp_protection']: + v0 = res0[key] + v1 = res1[key] + print(f'Checking {key:20s} ... ', end='') + assert v1 > v0, f'Expected {key} to be higher with waning than without' + print(f'✓ ({v1} > {v0})') + + # Optionally plot + if do_plot: + msim = cv.MultiSim([s0,s1]) + msim.plot('overview-strain', rotation=30) -def test_rescale(do_plot=False): - sc.heading('Testing with and without waning with rescaling') - pars = dict( - pop_size = 10e3, - pop_scale = 10, - rescale_factor = 2.0, - n_days = 300, - beta = 0.008, - NAb_decay = dict(form='nab_decay', pars={'init_decay_rate': 0.1, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) - ) - s1 = cv.Sim(base_pars, **pars, use_waning=False, label='No waning').run() - s2 = cv.Sim(base_pars, **pars, use_waning=True, label='With waning').run() - msim = cv.MultiSim([s1,s2]) - if do_plot: - msim.plot('overview-strain', rotation=30) return msim @@ -534,8 +544,7 @@ def get_ind_of_min_value(list, time): cv.options.set(interactive=do_plot) T = sc.tic() - # msim1 = test_waning(do_plot=do_plot) - msim2 = test_rescale(do_plot=do_plot) + msim1 = test_waning(do_plot=do_plot) # sim1 = test_states() sc.toc(T) From de382308d13201394a3d83a40c01dd1455d49940 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 17:24:19 -0700 Subject: [PATCH 433/569] fix test --- tests/test_immunity.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 7e803cd6a..425e8e506 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -23,8 +23,9 @@ def test_waning(do_plot=False): sc.heading('Testing with and without waning') + msims = dict() - for rescale in [False, True]: + for rescale in [0, 1]: print(f'Checking with rescale = {rescale}...') # Define more parameters specific to this test @@ -46,6 +47,9 @@ def test_waning(do_plot=False): s1 = cv.Sim(base_pars, **pars, use_waning=True, label='With waning').run() res0 = s0.summary res1 = s1.summary + msim = cv.MultiSim([s0,s1]) + msims[rescale] = msim + # Check results for key in ['n_susceptible', 'cum_infections', 'cum_reinfections', 'pop_nabs', 'pop_protection', 'pop_symp_protection']: @@ -57,10 +61,9 @@ def test_waning(do_plot=False): # Optionally plot if do_plot: - msim = cv.MultiSim([s0,s1]) msim.plot('overview-strain', rotation=30) - return msim + return msims def test_states(): @@ -544,8 +547,8 @@ def get_ind_of_min_value(list, time): cv.options.set(interactive=do_plot) T = sc.tic() - msim1 = test_waning(do_plot=do_plot) - # sim1 = test_states() + msims1 = test_waning(do_plot=do_plot) + sim1 = test_states() sc.toc(T) print('Done.') From f715346f92b821035271509c68b0e275ec07a842 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 18:14:34 -0700 Subject: [PATCH 434/569] getting too complicated --- covasim/immunity.py | 106 ++---------------------- covasim/parameters.py | 183 ++++++++++++++++++++++++++++++++++++++++-- covasim/population.py | 4 +- 3 files changed, 186 insertions(+), 107 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 4469907da..e305f95d7 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -6,6 +6,7 @@ import sciris as sc from . import utils as cvu from . import defaults as cvd +from . import parameters as cvpar from . import interventions as cvi @@ -50,52 +51,17 @@ def __init__(self, strain=None, label=None, days=None, n_imports=1, rescale=True def parse_strain_pars(self, strain=None, label=None): ''' Unpack strain information, which may be given in different ways''' - # List of choices currently available: new ones can be added to the list along with their aliases - choices = { - 'wild': ['wild', 'default', 'pre-existing', 'original'], - 'b117': ['b117', 'uk'], - 'b1351': ['b1351', 'sa'], - 'p1': ['p1', 'b11248', 'brazil'], - } + choices, mapping = cvpar.get_strain_choices() + pars = cvpar.get_strain_pars() choicestr = sc.newlinejoin(sc.mergelists(*choices.values())) - reversemap = {name:key for key,synonyms in choices.items() for name in synonyms} # Flip from key:value to value:key - - mapping = dict( # TODO: move to parameters.py - wild = dict(), - - b117 = dict( - rel_beta = 1.5, # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 - rel_severe_prob = 1.8, # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf - ), - - b1351 = dict( - rel_imm = 0.25, - rel_beta = 1.4, - rel_severe_prob = 1.4, - rel_death_prob = 1.4, - ), - - p1 = dict( - rel_imm = 0.5, - rel_beta = 1.4, - rel_severe_prob = 1.4, - rel_death_prob = 2, - ) - ) # Option 1: strains can be chosen from a list of pre-defined strains if isinstance(strain, str): - # Normalize input: lowrcase and remove - normstrain = strain.lower() - for txt in ['.', ' ', 'strain', 'variant', 'voc']: - normstrain = normstrain.replace(txt, '') + # Normalize input: lowercase and remove extra characters + + - if normstrain in reversemap: - strain_pars = mapping[reversemap[normstrain]] - else: - errormsg = f'The selected variant "{strain}" is not implemented; choices are:\n{choicestr}' - raise NotImplementedError(errormsg) # Option 2: strains can be specified as a dict of pars elif isinstance(strain, dict): @@ -177,36 +143,12 @@ def __init__(self, vaccine=None, **kwargs): self.NAb_init = None self.NAb_boost = None self.NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}} # Parameters to map NAbs to efficacy - self.vaccine_strain_info = self.init_strain_vaccine_info() + self.vaccine_strain_info = cvpar.get_vaccine_strain_pars() self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine) for par, val in self.vaccine_pars.items(): setattr(self, par, val) return - def init_strain_vaccine_info(self): - # TODO-- populate this with data! - rel_imm = {} - rel_imm['known_vaccines'] = ['pfizer', 'moderna', 'az', 'j&j'] - rel_imm['known_strains'] = ['wild', 'b117', 'b1351', 'p1'] - for vx in rel_imm['known_vaccines']: - rel_imm[vx] = {} - rel_imm[vx]['wild'] = 1 - - rel_imm['pfizer']['b117'] = 1/2 - rel_imm['pfizer']['b1351'] = 1/6.7 - rel_imm['pfizer']['p1'] = 1/6.5 - - rel_imm['moderna']['b117'] = 1/1.8 - rel_imm['moderna']['b1351'] = 1/4.5 - rel_imm['moderna']['p1'] = 1/8.6 - - rel_imm['az']['b1351'] = .5 - rel_imm['az']['p1'] = .5 - - rel_imm['j&j']['b1351'] = 1/6.7 - rel_imm['j&j']['p1'] = 1/8.6 - - return rel_imm def parse_vaccine_pars(self, vaccine=None): ''' Unpack vaccine information, which may be given in different ways''' @@ -224,40 +166,6 @@ def parse_vaccine_pars(self, vaccine=None): # (TODO: link to actual evidence) # Known parameters on pfizer - if vaccine in choices['pfizer']: - vaccine_pars = dict() - vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2= 2) - vaccine_pars['doses'] = 2 - vaccine_pars['interval'] = 22 - vaccine_pars['NAb_boost'] = 2 - vaccine_pars['label'] = vaccine - - # Known parameters on moderna - elif vaccine in choices['moderna']: - vaccine_pars = dict() - vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) - vaccine_pars['doses'] = 2 - vaccine_pars['interval'] = 29 - vaccine_pars['NAb_boost'] = 2 - vaccine_pars['label'] = vaccine - - # Known parameters on az - elif vaccine in choices['az']: - vaccine_pars = dict() - vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) - vaccine_pars['doses'] = 2 - vaccine_pars['interval'] = 22 - vaccine_pars['NAb_boost'] = 2 - vaccine_pars['label'] = vaccine - - # Known parameters on j&j - elif vaccine in choices['j&j']: - vaccine_pars = dict() - vaccine_pars['NAb_init'] = dict(dist='normal', par1=0.5, par2=2) - vaccine_pars['doses'] = 1 - vaccine_pars['interval'] = None - vaccine_pars['NAb_boost'] = 2 - vaccine_pars['label'] = vaccine else: choicestr = '\n'.join(choices.values()) diff --git a/covasim/parameters.py b/covasim/parameters.py index 21841cea4..05c700d21 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -8,7 +8,7 @@ from . import misc as cvm from . import defaults as cvd -__all__ = ['make_pars', 'reset_layer_pars', 'get_prognoses'] +__all__ = ['make_pars', 'reset_layer_pars', 'get_prognoses', 'get_strain_choices', 'get_vaccine_choices'] def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): @@ -69,19 +69,18 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters pars['NAb_boost'] = 1.5 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source - pars['NAb_eff'] = {'sus': {'slope': 2.7, 'n_50': 0.03}, - 'symp': 0.1, 'sev': 0.52} # Parameters to map NAbs to efficacy + pars['NAb_eff'] = dict(sus=dict(slope=2.7, n_50=0.03), symp=0.1, sev=0.52) # Parameters to map NAbs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains - pars['rel_imm'] = {'asymptomatic': 0.85, 'mild': 1, 'severe': 1.5} # Relative immunity from natural infection varies by symptoms + pars['rel_imm'] = dict(asymptomatic=0.85, mild=1, severe=1.5) # Relative immunity from natural infection varies by symptoms pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py pars['vaccine_info'] = None # Vaccine info in a more easily accessible format # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 - pars['dur'] = {} # Duration parameters: time for disease progression + pars['dur'] = {} pars['dur']['exp2inf'] = dict(dist='lognormal_int', par1=4.5, par2=1.5) # Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, appendix table S2, subtracting inf2sym duration pars['dur']['inf2sym'] = dict(dist='lognormal_int', par1=1.1, par2=0.9) # Duration from infectious to symptomatic; see Linton et al., https://doi.org/10.3390/jcm9020538, from Table 2, 5.6 day incubation period - 4.5 day exp2inf from Lauer et al. pars['dur']['sym2sev'] = dict(dist='lognormal_int', par1=6.6, par2=4.9) # Duration from symptomatic to severe symptoms; see Linton et al., https://doi.org/10.3390/jcm9020538, from Table 2, 6.6 day onset to hospital admission (deceased); see also Wang et al., https://jamanetwork.com/journals/jama/fullarticle/2761044, 7 days (Table 1) @@ -310,8 +309,180 @@ def absolute_prognoses(prognoses): return out +#%% Strain, vaccine, and immunity parameters and functions + +def get_strain_choices(strain=None): + ''' + Define valid pre-defined strain names + ''' + # List of choices currently available: new ones can be added to the list along with their aliases + choices = { + 'wild': ['wild', 'default', 'pre-existing', 'original'], + 'b117': ['b117', 'uk', 'united kingdom'], + 'b1351': ['b1351', 'sa', 'south africa'], + 'p1': ['p1', 'b11248', 'brazil'], + } + mapping = {name:key for key,synonyms in choices.items() for name in synonyms} # Flip from key:value to value:key + + def normalizer(strain): + ''' Normalize a strain string ''' + normstrain = strain.lower() + for txt in ['.', ' ', 'strain', 'variant', 'voc']: + normstrain = normstrain.replace(txt, '') + return normstrain + + if strain is None: + return choices, mapping, normalizer + else: + normstrain = normalizer(strain) + if normstrain in mapping: + strain_pars = pars[mapping[normstrain]] + else: + errormsg = f'The selected variant "{strain}" is not implemented; choices are:\n{choicestr}' + raise NotImplementedError(errormsg) + + +def get_vaccine_choices(): + ''' + Define valid pre-defined vaccine names + ''' + # List of choices currently available: new ones can be added to the list along with their aliases + choices = { + 'pfizer': ['pfizer', 'biontech', 'pfizer-biontech'], + 'moderna': ['moderna'], + 'az': ['az', 'astrazeneca'], + 'jj': ['jj', 'johnson & johnson', 'janssen'], + } + mapping = {name:key for key,synonyms in choices.items() for name in synonyms} # Flip from key:value to value:key + return choices, mapping + + +def get_strain_pars(): + ''' + Define the default parameters for the different strains + ''' + pars = sc.objdict( + + wild = sc.objdict( + rel_imm = 1.0, + rel_beta = 1.0, + rel_severe_prob = 1.0, + rel_crit_prob = 1.0, + rel_death_prob = 1.0, + ), + + b117 = sc.objdict( + rel_imm = 1.0, + rel_beta = 1.5, # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 + rel_severe_prob = 1.8, # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf + rel_crit_prob = 1.0, + rel_death_prob = 1.0, + ), + + b1351 = sc.objdict( + rel_imm = 0.25, + rel_beta = 1.4, + rel_severe_prob = 1.4, + rel_crit_prob = 1.0, + rel_death_prob = 1.4, + ), + + p1 = sc.objdict( + rel_imm = 0.5, + rel_beta = 1.4, + rel_severe_prob = 1.4, + rel_crit_prob = 1.0, + rel_death_prob = 2.0, + ) + ) + + return pars + + +def get_vaccine_strain_pars(): + ''' + Define the effectiveness of each vaccine against each strain + ''' + pars = sc.objdict( + + pfizer = sc.objdict( + wild = 1.0, + b117 = 1/2.0, + b1351 = 1/6.7, + p1 = 1/6.5, + ), + + moderna = sc.objdict( + wild = 1.0, + b117 = 1/1.8, + b1351 = 1/4.5, + p1 = 1/8.6, + ), + + az = sc.objdict( + wild = 1.0, + b117 = 1.0, + b1351 = 1/2, + p1 = 1/2, + ), + + jj = sc.objdict( + wild = 1.0, + b117 = 1.0, + b1351 = 1/6.7, + p1 = 1/8.6, + ), + ) + + return pars + + +def get_vaccine_dose_pars(): + ''' + Define the dosing regimen for each vaccine + ''' + pars = sc.objdict( + + pfizer = sc.objdict( + NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + NAb_init = dict(dist='normal', par1=0.5, par2= 2), + NAb_boost = 2, + doses = 2, + interval = 21, + ), + + moderna = sc.objdict( + NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + NAb_init = dict(dist='normal', par1=0.5, par2= 2), + NAb_boost = 2, + doses = 2, + interval = 28, + ), + + az = sc.objdict( + NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + NAb_init = dict(dist='normal', par1=0.5, par2= 2), + NAb_boost = 2, + doses = 2, + interval = 21, + ), + + jj = sc.objdict( + NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + NAb_init = dict(dist='normal', par1=0.5, par2= 2), + NAb_boost = 2, + doses = 1, + interval = None, + ), + ) + + return pars + + def listify_strain_pars(pars): - ''' Helper function to turn strain parameters into lists ''' + ''' + Helper function to turn strain parameters into lists + ''' for sp in cvd.strain_pars: if sp in pars.keys(): pars['strain_pars'][sp] = sc.promotetolist(pars[sp]) diff --git a/covasim/population.py b/covasim/population.py index 1a152df7d..d106cc7ed 100644 --- a/covasim/population.py +++ b/covasim/population.py @@ -11,7 +11,7 @@ from . import misc as cvm from . import data as cvdata from . import defaults as cvd -from . import parameters as cvpars +from . import parameters as cvpar from . import people as cvppl @@ -83,7 +83,7 @@ def make_people(sim, popdict=None, save_pop=False, popfile=None, die=True, reset # Ensure prognoses are set if sim['prognoses'] is None: - sim['prognoses'] = cvpars.get_prognoses(sim['prog_by_age'], version=sim._default_ver) + sim['prognoses'] = cvpar.get_prognoses(sim['prog_by_age'], version=sim._default_ver) # Actually create the people people = cvppl.People(sim.pars, uid=popdict['uid'], age=popdict['age'], sex=popdict['sex'], contacts=popdict['contacts']) # List for storing the people From 20652fc106a04c54e8d48d69e80e777cc291d6f2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 18:24:26 -0700 Subject: [PATCH 435/569] refactor definitions and split --- covasim/immunity.py | 37 ++++++++++++++++++++----------------- covasim/parameters.py | 20 ++------------------ 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index e305f95d7..264854551 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -51,17 +51,22 @@ def __init__(self, strain=None, label=None, days=None, n_imports=1, rescale=True def parse_strain_pars(self, strain=None, label=None): ''' Unpack strain information, which may be given in different ways''' - choices, mapping = cvpar.get_strain_choices() - pars = cvpar.get_strain_pars() - choicestr = sc.newlinejoin(sc.mergelists(*choices.values())) - # Option 1: strains can be chosen from a list of pre-defined strains if isinstance(strain, str): - # Normalize input: lowercase and remove extra characters - + choices, mapping = cvpar.get_strain_choices() + pars = cvpar.get_strain_pars() + choicestr = sc.newlinejoin(sc.mergelists(*choices.values())) + normstrain = strain.lower() + for txt in ['.', ' ', 'strain', 'variant', 'voc']: + normstrain = normstrain.replace(txt, '') + if normstrain in mapping: + strain_pars = pars[mapping[normstrain]] + else: + errormsg = f'The selected variant "{strain}" is not implemented; choices are:\n{choicestr}' + raise NotImplementedError(errormsg) # Option 2: strains can be specified as a dict of pars elif isinstance(strain, dict): @@ -156,20 +161,18 @@ def parse_vaccine_pars(self, vaccine=None): # Option 1: vaccines can be chosen from a list of pre-defined strains if isinstance(vaccine, str): - # List of choices currently available: new ones can be added to the list along with their aliases - choices = { - 'pfizer': ['pfizer', 'Pfizer', 'Pfizer-BionTech'], - 'moderna': ['moderna', 'Moderna'], - 'az': ['az', 'AstraZeneca', 'astrazeneca'], - 'j&j': ['j&j', 'johnson & johnson', 'Johnson & Johnson'], - } + choices, mapping = cvpar.get_vaccine_choices() + pars = cvpar.get_vaccine_strain_pars() + choicestr = sc.newlinejoin(sc.mergelists(*choices.values())) - # (TODO: link to actual evidence) - # Known parameters on pfizer + normvacc = vaccine.lower() + for txt in ['.', ' ', '&', '-', 'vaccine']: + normvacc = normvacc.replace(txt, '') + if normvacc in mapping: + vaccine_pars = pars[mapping[normvacc]] else: - choicestr = '\n'.join(choices.values()) - errormsg = f'The selected vaccine "{vaccine}" is not implemented; choices are: {choicestr}' + errormsg = f'The selected vaccine "{vaccine}" is not implemented; choices are:\n{choicestr}' raise NotImplementedError(errormsg) # Option 2: strains can be specified as a dict of pars diff --git a/covasim/parameters.py b/covasim/parameters.py index 05c700d21..8df4d8559 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -311,7 +311,7 @@ def absolute_prognoses(prognoses): #%% Strain, vaccine, and immunity parameters and functions -def get_strain_choices(strain=None): +def get_strain_choices(): ''' Define valid pre-defined strain names ''' @@ -323,23 +323,7 @@ def get_strain_choices(strain=None): 'p1': ['p1', 'b11248', 'brazil'], } mapping = {name:key for key,synonyms in choices.items() for name in synonyms} # Flip from key:value to value:key - - def normalizer(strain): - ''' Normalize a strain string ''' - normstrain = strain.lower() - for txt in ['.', ' ', 'strain', 'variant', 'voc']: - normstrain = normstrain.replace(txt, '') - return normstrain - - if strain is None: - return choices, mapping, normalizer - else: - normstrain = normalizer(strain) - if normstrain in mapping: - strain_pars = pars[mapping[normstrain]] - else: - errormsg = f'The selected variant "{strain}" is not implemented; choices are:\n{choicestr}' - raise NotImplementedError(errormsg) + return choices, mapping def get_vaccine_choices(): From 9ba97866368b0443179c100c05c59fcce2c5b37e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 18:37:32 -0700 Subject: [PATCH 436/569] commented out unused function --- covasim/immunity.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 264854551..8f5d19ec2 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -317,22 +317,22 @@ def nab_to_efficacy(nab, ax, function_args): # %% Immunity methods -def update_strain_attributes(people): - for key in people.meta.person: - if 'imm' in key: # everyone starts out with no immunity to either strain. # TODO: refactor - rows,cols = people[key].shape - people[key].resize(rows+1, cols, refcheck=False) - - # Set strain states, which store info about which strain a person is exposed to - for key in people.meta.strain_states: - if 'by' in key: # TODO: refactor - rows,cols = people[key].shape - people[key].resize(rows+1, cols, refcheck=False) - - for key in cvd.new_result_flows_by_strain: - rows, = people[key].shape - people.flows_strain[key].reshape(rows+1, refcheck=False) - return +# def update_strain_attributes(people): +# for key in people.meta.person: +# if 'imm' in key: # everyone starts out with no immunity to either strain. # TODO: refactor +# rows,cols = people[key].shape +# people[key].resize(rows+1, cols, refcheck=False) + +# # Set strain states, which store info about which strain a person is exposed to +# for key in people.meta.strain_states: +# if 'by' in key: # TODO: refactor +# rows,cols = people[key].shape +# people[key].resize(rows+1, cols, refcheck=False) + +# for key in cvd.new_result_flows_by_strain: +# rows, = people[key].shape +# people.flows_strain[key].reshape(rows+1, refcheck=False) +# return def init_immunity(sim, create=False): From f587e626008600fabd5dcb37974495ddee946736 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 19:28:20 -0700 Subject: [PATCH 437/569] fixed strain counting issues --- covasim/defaults.py | 4 +-- covasim/people.py | 12 ++++++--- covasim/sim.py | 10 ++++--- covasim/utils.py | 45 ++++++++++++++++++++++++++----- tests/test_immunity.py | 61 +++++++++++++++++++++++------------------- tests/test_utils.py | 31 +++++++++++++++++++++ 6 files changed, 118 insertions(+), 45 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 17b109b4c..29a2f3877 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -174,8 +174,8 @@ def __init__(self): } result_flows_by_strain = { - 'infections_by_strain': 'infections_by_strain', - 'infectious_by_strain': 'infectious_by_strain', + 'infections_by_strain': 'infections by strain', + 'infectious_by_strain': 'infectious by strain', } result_imm = { diff --git a/covasim/people.py b/covasim/people.py index da6dc46a9..c8ba5f4a1 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -221,8 +221,10 @@ def check_infectious(self): self.infectious[inds] = True self.infectious_strain[inds] = self.exposed_strain[inds] for strain in range(self.pars['n_strains']): - n_this_strain_inds = (self.infectious_strain[inds] == strain).sum() + this_strain_inds = cvu.itrue(self.infectious_strain[inds] == strain, inds) + n_this_strain_inds = len(this_strain_inds) self.flows_strain['new_infectious_by_strain'][strain] += n_this_strain_inds + self.infectious_by_strain[strain, this_strain_inds] = True return len(inds) @@ -268,19 +270,21 @@ def check_recovery(self, inds=None, filter_inds='is_exp'): self.severe[inds] = False self.critical[inds] = False self.recovered[inds] = True + self.recovered_strain[inds] = self.exposed_strain[inds] + self.infectious_strain[inds] = np.nan + self.exposed_strain[inds] = np.nan + self.exposed_by_strain[:, inds] = False + self.infectious_by_strain[:, inds] = False # Handle immunity aspects if self.pars['use_waning']: # Before letting them recover, store information about the strain they had, store symptoms and pre-compute NAbs array - self.recovered_strain[inds] = self.exposed_strain[inds] mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=inds) severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=inds) # Reset additional states self.susceptible[inds] = True - self.infectious_strain[inds] = np.nan - self.exposed_strain[inds] = np.nan self.prior_symptoms[inds] = self.pars['rel_imm']['asymptomatic'] self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] diff --git a/covasim/sim.py b/covasim/sim.py index 9cde4562f..ff06994a6 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -642,15 +642,14 @@ def step(self): # Calculate actual transmission for sources, targets in [[p1, p2], [p2, p1]]: # Loop over the contact network from p1->p2 and p2->p1 source_inds, target_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! - people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, - layer=lkey, strain=strain) # Actually infect people + people.infect(inds=target_inds, hosp_max=hosp_max, icu_max=icu_max, source=source_inds, layer=lkey, strain=strain) # Actually infect people # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): self.results[f'n_{key}'][t] = people.count(key) for key in cvd.result_stocks_by_strain.keys(): for strain in range(ns): - self.results['strain'][f'n_{key}'][strain][t] = people.count_by_strain(key, strain) + self.results['strain'][f'n_{key}'][strain, t] = people.count_by_strain(key, strain) # Update counts for this time step: flows for key,count in people.flows.items(): @@ -778,7 +777,10 @@ def finalize(self, verbose=None, restore_pars=True): # Calculate cumulative results for key in cvd.result_flows.keys(): - self.results[f'cum_{key}'][:] = np.cumsum(self.results[f'new_{key}'][:],axis=0) + self.results[f'cum_{key}'][:] = np.cumsum(self.results[f'new_{key}'][:], axis=0) + for key in cvd.result_flows_by_strain.keys(): + for strain in range(self['total_strains']): + self.results['strain'][f'cum_{key}'][strain, :] = np.cumsum(self.results['strain'][f'new_{key}'][strain, :], axis=0) self.results['cum_infections'].values += self['pop_infected']*self.rescale_vec[0] # Include initially infected people self.results['strain']['cum_infections_by_strain'].values += self['pop_infected']*self.rescale_vec[0] diff --git a/covasim/utils.py b/covasim/utils.py index 5f066bade..222564ad2 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -471,9 +471,9 @@ def choose_w(probs, n, unique=True): # No performance gain from Numba #%% Simple array operations -__all__ += ['true', 'false', 'defined', 'undefined', - 'itrue', 'ifalse', 'idefined', - 'itruei', 'ifalsei', 'idefinedi'] +__all__ += ['true', 'false', 'defined', 'undefined', + 'itrue', 'ifalse', 'idefined', 'iundefined', + 'itruei', 'ifalsei', 'idefinedi', 'iundefinedi'] def true(arr): @@ -486,7 +486,7 @@ def true(arr): **Example**:: - inds = cv.true(np.array([1,0,0,1,1,0,1])) + inds = cv.true(np.array([1,0,0,1,1,0,1])) # Returns array([0, 3, 4, 6]) ''' return arr.nonzero()[0] @@ -502,7 +502,7 @@ def false(arr): inds = cv.false(np.array([1,0,0,1,1,0,1])) ''' - return (~arr).nonzero()[0] + return np.logical_not(arr).nonzero()[0] def defined(arr): @@ -560,7 +560,7 @@ def ifalse(arr, inds): inds = cv.ifalse(np.array([True,False,True,True]), inds=np.array([5,22,47,93])) ''' - return inds[~arr] + return inds[np.logical_not(arr)] def idefined(arr, inds): @@ -578,6 +578,22 @@ def idefined(arr, inds): return inds[~np.isnan(arr)] +def iundefined(arr, inds): + ''' + Returns the indices that are undefined in the array -- name is short for indices[undefined] + + Args: + arr (array): any array, used as a filter + inds (array): any other array (usually, an array of indices) of the same size + + **Example**:: + + inds = cv.iundefined(np.array([3,np.nan,np.nan,4]), inds=np.array([5,22,47,93])) + ''' + return inds[np.isnan(arr)] + + + def itruei(arr, inds): ''' Returns the indices that are true in the array -- name is short for indices[true[indices]] @@ -605,7 +621,7 @@ def ifalsei(arr, inds): inds = cv.ifalsei(np.array([True,False,True,True,False,False,True,False]), inds=np.array([0,1,3,5])) ''' - return inds[~arr[inds]] + return inds[np.logical_not(arr[inds])] def idefinedi(arr, inds): @@ -621,3 +637,18 @@ def idefinedi(arr, inds): inds = cv.idefinedi(np.array([4,np.nan,0,np.nan,np.nan,4,7,4,np.nan]), inds=np.array([0,1,3,5])) ''' return inds[~np.isnan(arr[inds])] + + +def iundefinedi(arr, inds): + ''' + Returns the indices that are undefined in the array -- name is short for indices[defined[indices]] + + Args: + arr (array): any array, used as a filter + inds (array): an array of indices for the original array + + **Example**:: + + inds = cv.iundefinedi(np.array([4,np.nan,0,np.nan,np.nan,4,7,4,np.nan]), inds=np.array([0,1,3,5])) + ''' + return inds[np.isnan(arr[inds])] \ No newline at end of file diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 425e8e506..7e5876ac3 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -125,6 +125,18 @@ def test_states(): return +def test_strains(do_plot=False): + sc.heading('Testing strains...') + + b117 = cv.Strain('b117', days=10, n_imports=20) + p1 = cv.Strain('sa variant', days=20, n_imports=20) + sim = cv.Sim(use_waning=True, strains=[b117, p1], label='With imported infections', **base_pars) + sim.run() + + if do_plot: + sim.plot('overview-strain') + + return sim # def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): # sc.heading('Test varying properties of immunity') @@ -201,15 +213,7 @@ def test_states(): # return sim -# def test_import2strains(do_plot=False, do_show=True, do_save=False): -# sc.heading('Test introducing 2 new strains partway through a sim') -# b117 = cv.Strain('b117', days=1, n_imports=20) -# p1 = cv.Strain('sa variant', days=2, n_imports=20) -# sim = cv.Sim(use_waning=True, strains=[b117, p1], label='With imported infections', **base_pars) -# sim.run() - -# return sim # def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): @@ -515,28 +519,28 @@ def test_states(): #%% Plotting and utilities -def vacc_subtarg(sim): - ''' Subtarget by age''' +# def vacc_subtarg(sim): +# ''' Subtarget by age''' - # retrieves the first ind that is = or < sim.t - ind = get_ind_of_min_value(sim.vxsubtarg.days, sim.t) - age = sim.vxsubtarg.age[ind] - prob = sim.vxsubtarg.prob[ind] - inds = sc.findinds((sim.people.age>=age) * ~sim.people.vaccinated) - vals = prob*np.ones(len(inds)) - return {'inds':inds, 'vals':vals} +# # retrieves the first ind that is = or < sim.t +# ind = get_ind_of_min_value(sim.vxsubtarg.days, sim.t) +# age = sim.vxsubtarg.age[ind] +# prob = sim.vxsubtarg.prob[ind] +# inds = sc.findinds((sim.people.age>=age) * ~sim.people.vaccinated) +# vals = prob*np.ones(len(inds)) +# return {'inds':inds, 'vals':vals} -def get_ind_of_min_value(list, time): - ind = None - for place, t in enumerate(list): - if time >= t: - ind = place +# def get_ind_of_min_value(list, time): +# ind = None +# for place, t in enumerate(list): +# if time >= t: +# ind = place - if ind is None: - errormsg = f'{time} is not within the list of times' - raise ValueError(errormsg) - return ind +# if ind is None: +# errormsg = f'{time} is not within the list of times' +# raise ValueError(errormsg) +# return ind @@ -547,8 +551,9 @@ def get_ind_of_min_value(list, time): cv.options.set(interactive=do_plot) T = sc.tic() - msims1 = test_waning(do_plot=do_plot) - sim1 = test_states() + # msims1 = test_waning(do_plot=do_plot) + # sim1 = test_states() + sim2 = test_strains(do_plot=do_plot) sc.toc(T) print('Done.') diff --git a/tests/test_utils.py b/tests/test_utils.py index e1c68b6af..d5dc271e7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -198,6 +198,36 @@ def test_choose_w(): return x1 +def test_indexing(): + + # Definitions + farr = np.array([1.5,0,0,1,1,0]) # Float array + barr = np.array(farr, dtype=bool) # Boolean array + darr = np.array([0,np.nan,1,np.nan,0,np.nan]) # Defined/undefined array + inds = np.array([0,10,20,30,40,50]) # Indices + inds2 = np.array([1,2,3,4]) # Skip first and last index + + # Test true, false, defined, and undefined + assert cv.true(farr).tolist() == [0,3,4] + assert cv.false(farr).tolist() == [1,2,5] + assert cv.defined(darr).tolist() == [0,2,4] + assert cv.undefined(darr).tolist() == [1,3,5] + + # Test with indexing + assert cv.itrue(barr, inds).tolist() == [0,30,40] + assert cv.ifalse(barr, inds).tolist() == [10,20,50] + assert cv.idefined(darr, inds).tolist() == [0,20,40] + assert cv.iundefined(darr, inds).tolist() == [10,30,50] + + # Test with double indexing + assert cv.itruei(barr, inds2).tolist() == [3,4] + assert cv.ifalsei(barr, inds2).tolist() == [1,2] + assert cv.idefinedi(darr, inds2).tolist() == [2,4] + assert cv.iundefinedi(darr, inds2).tolist() == [1,3] + + return + + def test_doubling_time(): sim = cv.Sim(pop_size=1000) @@ -231,6 +261,7 @@ def test_doubling_time(): samples = test_samples(do_plot=do_plot) people1 = test_choose() people2 = test_choose_w() + inds = test_indexing() dt = test_doubling_time() print('\n'*2) From 9b5a5f7706d3367b5d89da4a3d15dcb31fc3c5ab Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 19:31:34 -0700 Subject: [PATCH 438/569] tidying tests --- tests/test_immunity.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 7e5876ac3..88e114a98 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -69,11 +69,15 @@ def test_waning(do_plot=False): def test_states(): ''' Test state consistency against state_diagram.xlsx ''' + filename = 'state_diagram.xlsx' + sheets = ['Without waning', 'With waning'] + indexcol = 'From ↓ to →' + # Load state diagram dfs = sc.odict() - for sheet in ['Without waning', 'With waning']: - dfs[sheet] = pd.read_excel('state_diagram.xlsx', sheet_name=sheet) - dfs[sheet] = dfs[sheet].set_index('From ↓ to →') + for sheet in sheets: + dfs[sheet] = pd.read_excel(filename, sheet_name=sheet) + dfs[sheet] = dfs[sheet].set_index(indexcol) # Create and run simulation for use_waning in [False, True]: @@ -130,7 +134,7 @@ def test_strains(do_plot=False): b117 = cv.Strain('b117', days=10, n_imports=20) p1 = cv.Strain('sa variant', days=20, n_imports=20) - sim = cv.Sim(use_waning=True, strains=[b117, p1], label='With imported infections', **base_pars) + sim = cv.Sim(use_waning=True, strains=[b117, p1], **base_pars) sim.run() if do_plot: From a2f5dd51f70f9b8e50681f349c11ed7390571c95 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 19:35:22 -0700 Subject: [PATCH 439/569] reordering --- tests/test_immunity.py | 98 +++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 88e114a98..95fe42a4a 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -3,10 +3,8 @@ ''' #%% Imports and settings -# import pytest import sciris as sc import covasim as cv -import numpy as np import pandas as pd do_plot = 1 @@ -21,51 +19,6 @@ #%% Define the tests -def test_waning(do_plot=False): - sc.heading('Testing with and without waning') - msims = dict() - - for rescale in [0, 1]: - print(f'Checking with rescale = {rescale}...') - - # Define more parameters specific to this test - pars = dict( - n_days = 90, - beta = 0.008, - NAb_decay = dict(form='nab_decay', pars={'init_decay_rate': 0.1, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) - ) - - # Optionally include rescaling - if rescale: - pars.update( - pop_scale = 10, - rescale_factor = 2.0, # Use a large rescale factor to make differences more obvious - ) - - # Run the simulations and pull out the results - s0 = cv.Sim(base_pars, **pars, use_waning=False, label='No waning').run() - s1 = cv.Sim(base_pars, **pars, use_waning=True, label='With waning').run() - res0 = s0.summary - res1 = s1.summary - msim = cv.MultiSim([s0,s1]) - msims[rescale] = msim - - - # Check results - for key in ['n_susceptible', 'cum_infections', 'cum_reinfections', 'pop_nabs', 'pop_protection', 'pop_symp_protection']: - v0 = res0[key] - v1 = res1[key] - print(f'Checking {key:20s} ... ', end='') - assert v1 > v0, f'Expected {key} to be higher with waning than without' - print(f'✓ ({v1} > {v0})') - - # Optionally plot - if do_plot: - msim.plot('overview-strain', rotation=30) - - return msims - - def test_states(): ''' Test state consistency against state_diagram.xlsx ''' @@ -129,6 +82,51 @@ def test_states(): return +def test_waning(do_plot=False): + sc.heading('Testing with and without waning') + msims = dict() + + for rescale in [0, 1]: + print(f'Checking with rescale = {rescale}...') + + # Define more parameters specific to this test + pars = dict( + n_days = 90, + beta = 0.008, + NAb_decay = dict(form='nab_decay', pars={'init_decay_rate': 0.1, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) + ) + + # Optionally include rescaling + if rescale: + pars.update( + pop_scale = 10, + rescale_factor = 2.0, # Use a large rescale factor to make differences more obvious + ) + + # Run the simulations and pull out the results + s0 = cv.Sim(base_pars, **pars, use_waning=False, label='No waning').run() + s1 = cv.Sim(base_pars, **pars, use_waning=True, label='With waning').run() + res0 = s0.summary + res1 = s1.summary + msim = cv.MultiSim([s0,s1]) + msims[rescale] = msim + + + # Check results + for key in ['n_susceptible', 'cum_infections', 'cum_reinfections', 'pop_nabs', 'pop_protection', 'pop_symp_protection']: + v0 = res0[key] + v1 = res1[key] + print(f'Checking {key:20s} ... ', end='') + assert v1 > v0, f'Expected {key} to be higher with waning than without' + print(f'✓ ({v1} > {v0})') + + # Optionally plot + if do_plot: + msim.plot('overview-strain', rotation=30) + + return msims + + def test_strains(do_plot=False): sc.heading('Testing strains...') @@ -555,9 +553,9 @@ def test_strains(do_plot=False): cv.options.set(interactive=do_plot) T = sc.tic() - # msims1 = test_waning(do_plot=do_plot) - # sim1 = test_states() - sim2 = test_strains(do_plot=do_plot) + sim1 = test_states() + msims1 = test_waning(do_plot=do_plot) + sim2 = test_strains(do_plot=do_plot) sc.toc(T) print('Done.') From c69d2d4cf5791f6b1ce70e70fe173a3837d25d82 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 19:35:58 -0700 Subject: [PATCH 440/569] update version date --- covasim/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/version.py b/covasim/version.py index 46f6e9c3e..de73762c1 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -5,5 +5,5 @@ __all__ = ['__version__', '__versiondate__', '__license__'] __version__ = '3.0.0' -__versiondate__ = '2021-04-09' +__versiondate__ = '2021-04-12' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From 5c36e6785399a8d2439b55c4ff3092b9122e06ad Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 19:40:59 -0700 Subject: [PATCH 441/569] add custom strain test --- covasim/immunity.py | 4 ++-- tests/test_immunity.py | 28 ++++++---------------------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 8f5d19ec2..569b37f85 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -171,7 +171,7 @@ def parse_vaccine_pars(self, vaccine=None): if normvacc in mapping: vaccine_pars = pars[mapping[normvacc]] - else: + else: # pragma: no cover errormsg = f'The selected vaccine "{vaccine}" is not implemented; choices are:\n{choicestr}' raise NotImplementedError(errormsg) @@ -179,7 +179,7 @@ def parse_vaccine_pars(self, vaccine=None): elif isinstance(vaccine, dict): vaccine_pars = vaccine - else: + else: # pragma: no cover errormsg = f'Could not understand {type(vaccine)}, please specify as a string indexing a predefined vaccine or a dict.' raise ValueError(errormsg) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 95fe42a4a..de98409fd 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -130,9 +130,10 @@ def test_waning(do_plot=False): def test_strains(do_plot=False): sc.heading('Testing strains...') - b117 = cv.Strain('b117', days=10, n_imports=20) - p1 = cv.Strain('sa variant', days=20, n_imports=20) - sim = cv.Sim(use_waning=True, strains=[b117, p1], **base_pars) + b117 = cv.Strain('b117', days=10, n_imports=20) + p1 = cv.Strain('sa variant', days=20, n_imports=20) + cust = cv.Strain(label='Custom', days=40, n_imports=20, strain={'rel_beta': 2, 'rel_symp_prob': 1.6}) + sim = cv.Sim(base_pars, use_waning=True, strains=[b117, p1, cust]) sim.run() if do_plot: @@ -199,23 +200,6 @@ def test_strains(do_plot=False): # return scens -# def test_import1strain(do_plot=False, do_show=True, do_save=False): -# sc.heading('Test introducing a new strain partway through a sim') - -# strain_pars = { -# 'rel_beta': 1.5, -# } -# pars = { -# 'beta': 0.01 -# } -# strain = cv.Strain(strain_pars, days=1, n_imports=20, label='Strain 2: 1.5x more transmissible') -# sim = cv.Sim(use_waning=True, pars=pars, strains=strain, analyzers=cv.snapshot(30, 60), **pars, **base_pars) -# sim.run() - -# return sim - - - # def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): @@ -553,8 +537,8 @@ def test_strains(do_plot=False): cv.options.set(interactive=do_plot) T = sc.tic() - sim1 = test_states() - msims1 = test_waning(do_plot=do_plot) + # sim1 = test_states() + # msims1 = test_waning(do_plot=do_plot) sim2 = test_strains(do_plot=do_plot) sc.toc(T) From 78a37d80b8742231154abceba623fa8b1fda7d69 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 19:57:27 -0700 Subject: [PATCH 442/569] implemented an all option --- covasim/base.py | 4 +++- covasim/defaults.py | 12 ++++++++---- covasim/plotting.py | 4 ++-- tests/test_immunity.py | 40 ++++------------------------------------ 4 files changed, 17 insertions(+), 43 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index b80e13025..71abfd0b1 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -393,9 +393,11 @@ def date(self, ind, *args, dateformat=None, as_date=False): return dates - def result_keys(self): + def result_keys(self, strain=False): ''' Get the actual results objects, not other things stored in sim.results ''' keys = [key for key in self.results.keys() if isinstance(self.results[key], Result)] + if strain: + keys += [key for key in self.results['strain'].keys() if isinstance(self.results['strain'][key], Result)] return keys diff --git a/covasim/defaults.py b/covasim/defaults.py index 29a2f3877..d856614c1 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -306,7 +306,7 @@ def get_colors(): 'pop_symp_protection', ] -def get_sim_plots(which='default'): +def get_sim_plots(which='default', sim=None): ''' Specify which quantities to plot; used in sim.py. @@ -333,11 +333,11 @@ def get_sim_plots(which='default'): ], }) - # Show everything + # Show an overview elif which == 'overview': plots = sc.dcp(overview_plots) - # Show everything plus strains + # Show an overview plus strains elif 'overview' in which and 'strain' in which: plots = sc.dcp(overview_plots) + sc.dcp(overview_strain_plots) @@ -371,8 +371,12 @@ def get_sim_plots(which='default'): ], }) + # Plot absolutely everything + elif which.lower() == 'all': + plots = sim.result_keys(strain=True) + else: # pragma: no cover - errormsg = f'The choice which="{which}" is not supported: choices are "default", "overview", "strain", "overview-strain", or "seir"' + errormsg = f'The choice which="{which}" is not supported: choices are "default", "overview", "strain", "overview-strain", "seir", or "all"' raise ValueError(errormsg) return plots diff --git a/covasim/plotting.py b/covasim/plotting.py index 4c952424b..d5478f29f 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -95,9 +95,9 @@ def handle_to_plot(which, to_plot, n_cols, sim, check_ready=True): # If not specified or specified as a string, load defaults if to_plot is None or isinstance(to_plot, str): if which == 'sim': - to_plot = cvd.get_sim_plots(to_plot) + to_plot = cvd.get_sim_plots(to_plot, sim=sim) elif which =='scens': - to_plot = cvd.get_scen_plots(to_plot) + to_plot = cvd.get_scen_plots(to_plot, sim=sim) else: errormsg = f'"which" must be "sim" or "scens", not "{which}"' raise NotImplementedError(errormsg) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index de98409fd..f2c0ddf84 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -141,6 +141,10 @@ def test_strains(do_plot=False): return sim + +def test_vaccines(do_plot=False): + pass + # def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): # sc.heading('Test varying properties of immunity') @@ -202,42 +206,6 @@ def test_strains(do_plot=False): -# def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): -# sc.heading('Test introducing a new strain with longer duration partway through a sim') - -# pars = sc.mergedicts(base_pars, { -# 'n_days': 120, -# }) - -# strain_pars = { -# 'rel_beta': 1.5, -# 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} -# } - -# strain = cv.Strain(strain=strain_pars, label='Custom strain', days=10, n_imports=30) -# sim = cv.Sim(use_waning=True, pars=pars, strains=strain, label='With imported infections') -# sim.run() - -# return sim - - -# def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): -# sc.heading('Test introducing 2 new strains partway through a sim, with a change_beta intervention') - -# strain2 = {'rel_beta': 1.5, -# 'rel_severe_prob': 1.3} - -# strain3 = {'rel_beta': 2, -# 'rel_symp_prob': 1.6} - -# intervs = cv.change_beta(days=[5, 20, 40], changes=[0.8, 0.7, 0.6]) -# strains = [cv.Strain(strain=strain2, days=10, n_imports=20), -# cv.Strain(strain=strain3, days=30, n_imports=20), -# ] -# sim = cv.Sim(use_waning=True, interventions=intervs, strains=strains, label='With imported infections', **base_pars) -# sim.run() - -# return sim From c1a7d2cf2ce4d0ab7a684f06d04fd30f567ae4e9 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 20:02:04 -0700 Subject: [PATCH 443/569] tidying --- covasim/sim.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index ff06994a6..692d4b391 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -1424,13 +1424,13 @@ def diff_sims(sim1, sim2, skip_key_diffs=False, output=False, die=False): return -def demo(preset=None, overview=False, scens=None, run_args=None, plot_args=None, **kwargs): +def demo(preset=None, to_plot=None, scens=None, run_args=None, plot_args=None, **kwargs): ''' Shortcut for ``cv.Sim().run().plot()``. Args: preset (str): use a preset run configuration; currently the only option is "full" - overview (bool): whether to show the overview plot (all results) + to_plot (str): what to plot scens (dict): dictionary of scenarios to run as a multisim, if preset='full' kwargs (dict): passed to Sim() run_args (dict): passed to sim.run() @@ -1448,8 +1448,8 @@ def demo(preset=None, overview=False, scens=None, run_args=None, plot_args=None, run_args = sc.mergedicts(run_args) plot_args = sc.mergedicts(plot_args) - if overview: - plot_args = sc.mergedicts(plot_args, {'to_plot':'overview'}) + if to_plot: + plot_args = sc.mergedicts(plot_args, {'to_plot':to_plot}) if not preset: sim = Sim(**kwargs) From 19ebd658b662b244765b21e83a004c6bc6318d15 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 20:19:05 -0700 Subject: [PATCH 444/569] vaccines working --- covasim/immunity.py | 32 ++++++++++++++++++++------------ tests/test_immunity.py | 13 ++++++++++++- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 569b37f85..6aa4d609f 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -63,7 +63,8 @@ def parse_strain_pars(self, strain=None, label=None): normstrain = normstrain.replace(txt, '') if normstrain in mapping: - strain_pars = pars[mapping[normstrain]] + normstrain = mapping[normstrain] + strain_pars = pars[normstrain] else: errormsg = f'The selected variant "{strain}" is not implemented; choices are:\n{choicestr}' raise NotImplementedError(errormsg) @@ -128,20 +129,22 @@ class Vaccine(cvi.Intervention): Args: vaccine (dict or str): dictionary of parameters specifying information about the vaccine or label for loading pre-defined vaccine + label (str): if supplying a dictionary, a label for the vaccine must be supplied kwargs (dict): passed to Intervention() **Example**:: moderna = cv.Vaccine('moderna') # Create Moderna vaccine pfizer = cv.Vaccine('pfizer) # Create Pfizer vaccine - j&j = cv.Vaccine('j&j') # Create J&J vaccine + j&j = cv.Vaccine('jj') # Create J&J vaccine az = cv.Vaccine('az) # Create AstraZeneca vaccine interventions += [cv.vaccinate(vaccines=[moderna, pfizer, j&j, az], days=[1, 10, 10, 30])] # Add them all to the sim sim = cv.Sim(interventions=interventions) ''' - def __init__(self, vaccine=None, **kwargs): + def __init__(self, vaccine=None, label=None, **kwargs): super().__init__(**kwargs) + self.label = label self.rel_imm = None # list of length total_strains with relative immunity factor self.doses = None self.interval = None @@ -149,7 +152,7 @@ def __init__(self, vaccine=None, **kwargs): self.NAb_boost = None self.NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}} # Parameters to map NAbs to efficacy self.vaccine_strain_info = cvpar.get_vaccine_strain_pars() - self.vaccine_pars = self.parse_vaccine_pars(vaccine=vaccine) + self.parse_vaccine_pars(vaccine=vaccine) for par, val in self.vaccine_pars.items(): setattr(self, par, val) return @@ -162,7 +165,8 @@ def parse_vaccine_pars(self, vaccine=None): if isinstance(vaccine, str): choices, mapping = cvpar.get_vaccine_choices() - pars = cvpar.get_vaccine_strain_pars() + strain_pars = cvpar.get_vaccine_strain_pars() + dose_pars = cvpar.get_vaccine_dose_pars() choicestr = sc.newlinejoin(sc.mergelists(*choices.values())) normvacc = vaccine.lower() @@ -170,20 +174,27 @@ def parse_vaccine_pars(self, vaccine=None): normvacc = normvacc.replace(txt, '') if normvacc in mapping: - vaccine_pars = pars[mapping[normvacc]] + normvacc = mapping[normvacc] + vaccine_pars = sc.mergedicts(strain_pars[normvacc], dose_pars[normvacc]) else: # pragma: no cover errormsg = f'The selected vaccine "{vaccine}" is not implemented; choices are:\n{choicestr}' raise NotImplementedError(errormsg) + if self.label is None: + self.label = normvacc + # Option 2: strains can be specified as a dict of pars elif isinstance(vaccine, dict): vaccine_pars = vaccine + if self.label is None: + self.label = 'Custom vaccine' else: # pragma: no cover errormsg = f'Could not understand {type(vaccine)}, please specify as a string indexing a predefined vaccine or a dict.' raise ValueError(errormsg) - return vaccine_pars + self.vaccine_pars = vaccine_pars + return def initialize(self, sim): @@ -194,7 +205,7 @@ def initialize(self, sim): for strain in range(ts-1): circulating_strains.append(sim['strains'][strain].label) - if self.NAb_init is None : + if self.NAb_init is None : # TODO: refactor errormsg = 'Did not provide parameters for this vaccine' raise ValueError(errormsg) @@ -202,10 +213,7 @@ def initialize(self, sim): sc.printv('Did not provide rel_imm parameters for this vaccine, trying to find values', 1, sim['verbose']) self.rel_imm = [] for strain in circulating_strains: - if strain in self.vaccine_strain_info['known_strains']: - self.rel_imm.append(self.vaccine_strain_info[self.label][strain]) - else: - self.rel_imm.append(1) + self.rel_imm.append(self.vaccine_strain_info[self.label][strain]) correct_size = len(self.rel_imm) == ts if not correct_size: diff --git a/tests/test_immunity.py b/tests/test_immunity.py index f2c0ddf84..52f247dd3 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -143,6 +143,16 @@ def test_strains(do_plot=False): def test_vaccines(do_plot=False): + sc.heading('Testing vaccines...') + + p1 = cv.Strain('sa variant', days=20, n_imports=20) + pfizer = cv.vaccinate(days=30, vaccine_pars='pfizer') + sim = cv.Sim(base_pars, use_waning=True, strains=p1, interventions=pfizer) + sim.run() + + if do_plot: + sim.plot('overview-strain') + pass # def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): @@ -507,7 +517,8 @@ def test_vaccines(do_plot=False): # sim1 = test_states() # msims1 = test_waning(do_plot=do_plot) - sim2 = test_strains(do_plot=do_plot) + # sim2 = test_strains(do_plot=do_plot) + sim3 = test_vaccines(do_plot=do_plot) sc.toc(T) print('Done.') From 53d299dbb3646b3164404ad8b2fafe74598f3bc3 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 20:31:09 -0700 Subject: [PATCH 445/569] updated plotting --- covasim/base.py | 2 +- covasim/defaults.py | 89 ++++++++++++++++++++------------------------- covasim/plotting.py | 12 ++---- covasim/run.py | 8 ++-- covasim/sim.py | 4 +- 5 files changed, 49 insertions(+), 66 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 71abfd0b1..20c1325f1 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -137,7 +137,7 @@ def __init__(self, name=None, npts=None, scale=True, color=None, n_strains=0): self.name = name # Name of this result self.scale = scale # Whether or not to scale the result by the scale factor if color is None: - color = cvd.get_colors()['default'] + color = cvd.get_default_colors()['default'] self.color = color # Default color if npts is None: npts = 0 diff --git a/covasim/defaults.py b/covasim/defaults.py index d856614c1..fd4875292 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -12,7 +12,7 @@ from .settings import options as cvo # To set options # Specify all externally visible functions this file defines -- other things are available as e.g. cv.defaults.default_int -__all__ = ['default_float', 'default_int', 'get_colors', 'get_sim_plots', 'get_scen_plots'] +__all__ = ['default_float', 'default_int', 'get_default_colors', 'get_default_plots'] #%% Specify what data types to use @@ -232,7 +232,7 @@ def __init__(self): ]) -def get_colors(): +def get_default_colors(): ''' Specify plot colors -- used in sim.py. @@ -306,7 +306,7 @@ def get_colors(): 'pop_symp_protection', ] -def get_sim_plots(which='default', sim=None): +def get_default_plots(which='default', kind='sim', sim=None): ''' Specify which quantities to plot; used in sim.py. @@ -314,29 +314,48 @@ def get_sim_plots(which='default', sim=None): which (str): either 'default' or 'overview' ''' - # Default plots + # Default plots -- different for sims and scenarios if which in [None, 'default']: - plots = sc.odict({ - 'Total counts': [ + + if kind == 'sim': + plots = sc.odict({ + 'Total counts': [ + 'cum_infections', + 'n_infectious', + 'cum_diagnoses', + ], + 'Daily counts': [ + 'new_infections', + 'new_diagnoses', + ], + 'Health outcomes': [ + 'cum_severe', + 'cum_critical', + 'cum_deaths', + ], + }) + + elif kind == 'scens': + plots = sc.odict({ + 'Cumulative infections': [ 'cum_infections', - 'n_infectious', - 'cum_diagnoses', ], - 'Daily counts': [ + 'New infections per day': [ 'new_infections', - 'new_diagnoses', ], - 'Health outcomes': [ - 'cum_severe', - 'cum_critical', + 'Cumulative deaths': [ 'cum_deaths', ], - }) + }) # Show an overview elif which == 'overview': plots = sc.dcp(overview_plots) + # Plot absolutely everything + elif which.lower() == 'all': + plots = sim.result_keys(strain=True) + # Show an overview plus strains elif 'overview' in which and 'strain' in which: plots = sc.dcp(overview_plots) + sc.dcp(overview_strain_plots) @@ -362,43 +381,15 @@ def get_sim_plots(which='default', sim=None): # Plot SEIR compartments elif which.lower() == 'seir': - plots = sc.odict({ - 'SEIR states': [ - 'n_susceptible', - 'n_preinfectious', - 'n_infectious', - 'n_removed', - ], - }) - - # Plot absolutely everything - elif which.lower() == 'all': - plots = sim.result_keys(strain=True) + plots = [ + 'n_susceptible', + 'n_preinfectious', + 'n_infectious', + 'n_removed', + ], else: # pragma: no cover - errormsg = f'The choice which="{which}" is not supported: choices are "default", "overview", "strain", "overview-strain", "seir", or "all"' + errormsg = f'The choice which="{which}" is not supported: choices are "default", "overview", "all", "strain", "overview-strain", or "seir"' raise ValueError(errormsg) - return plots - -def get_scen_plots(which='default'): - ''' Default scenario plots -- used in run.py ''' - if which in [None, 'default']: - plots = sc.odict({ - 'Cumulative infections': [ - 'cum_infections', - ], - 'New infections per day': [ - 'new_infections', - ], - 'Cumulative deaths': [ - 'cum_deaths', - ], - }) - elif which == 'overview': - plots = sc.dcp(overview_plots) - else: # pragma: no cover - errormsg = f'The choice which="{which}" is not supported' - raise ValueError(errormsg) return plots - diff --git a/covasim/plotting.py b/covasim/plotting.py index d5478f29f..4f774d6b3 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -84,7 +84,7 @@ def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None return args -def handle_to_plot(which, to_plot, n_cols, sim, check_ready=True): +def handle_to_plot(kind, to_plot, n_cols, sim, check_ready=True): ''' Handle which quantities to plot ''' # Check that results are ready @@ -94,13 +94,7 @@ def handle_to_plot(which, to_plot, n_cols, sim, check_ready=True): # If not specified or specified as a string, load defaults if to_plot is None or isinstance(to_plot, str): - if which == 'sim': - to_plot = cvd.get_sim_plots(to_plot, sim=sim) - elif which =='scens': - to_plot = cvd.get_scen_plots(to_plot, sim=sim) - else: - errormsg = f'"which" must be "sim" or "scens", not "{which}"' - raise NotImplementedError(errormsg) + to_plot = cvd.get_default_plots(to_plot, kind=kind, sim=sim) # If a list of keys has been supplied if isinstance(to_plot, list): @@ -764,7 +758,7 @@ def plotly_sim(sim, do_show=False): go = import_plotly() # Load Plotly plots = [] - to_plot = cvd.get_sim_plots() + to_plot = cvd.get_default_plots() for p,title,keylabels in to_plot.enumitems(): fig = go.Figure() for key in keylabels: diff --git a/covasim/run.py b/covasim/run.py index 74ad11df7..1304de8dc 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -420,7 +420,7 @@ def plot(self, to_plot=None, inds=None, plot_sims=False, color_by_sim=None, max_ other options. Args: - to_plot (list) : list or dict of which results to plot; see cv.get_sim_plots() for structure + to_plot (list) : list or dict of which results to plot; see cv.get_default_plots() for structure inds (list) : if not combined or reduced, the indices of the simulations to plot (if None, plot all) plot_sims (bool) : whether to plot individual sims, even if combine() or reduce() has been used color_by_sim (bool) : if True, set colors based on the simulation type; otherwise, color by result type; True implies a scenario-style plotting, False implies sim-style plotting @@ -473,10 +473,8 @@ def plot(self, to_plot=None, inds=None, plot_sims=False, color_by_sim=None, max_ # Handle what to plot if to_plot is None: - if color_by_sim: - to_plot = cvd.get_scen_plots() - else: - to_plot = cvd.get_sim_plots() + kind = 'scens' if color_by_sim else 'sim' + to_plot = cvd.get_default_plots(kind=kind) # Handle colors if colors is None: diff --git a/covasim/sim.py b/covasim/sim.py index 692d4b391..38e9f1e2c 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -276,7 +276,7 @@ def init_res(*args, **kwargs): output = cvb.Result(*args, **kwargs, npts=self.npts) return output - dcols = cvd.get_colors() # Get default colors + dcols = cvd.get_default_colors() # Get default colors # Flows and cumulative flows for key,label in cvd.result_flows.items(): @@ -1228,7 +1228,7 @@ def plot(self, *args, **kwargs): Plot the results of a single simulation. Args: - to_plot (dict): Dict of results to plot; see get_sim_plots() for structure + to_plot (dict): Dict of results to plot; see get_default_plots() for structure do_save (bool): Whether or not to save the figure fig_path (str): Path to save the figure fig_args (dict): Dictionary of kwargs to be passed to pl.figure() From c5f074e960e899eeb379aadcd5fe6436f296852b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 21:03:28 -0700 Subject: [PATCH 446/569] results look a little odd --- covasim/immunity.py | 66 +++---- tests/test_immunity.py | 423 +++++++---------------------------------- 2 files changed, 96 insertions(+), 393 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 6aa4d609f..e76854dd9 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -380,28 +380,6 @@ def init_immunity(sim, create=False): immunity['sus'][j][i] = cross_immunity[circulating_strains[j]][circulating_strains[i]] sim['immunity'] = immunity - else: - # if we know all the circulating strains, then update, otherwise use defaults - known_strains = ['wild', 'b117', 'b1351', 'p1'] - cross_immunity = create_cross_immunity(circulating_strains, rel_imms) - if sc.checktype(sim['immunity']['sus'], 'arraylike'): - correct_size = sim['immunity']['sus'].shape == (ts, ts) - if not correct_size: - errormsg = f'Wrong dimensions for immunity["sus"]: you provided a matrix sized {sim["immunity"]["sus"].shape}, but it should be sized {(ts, ts)}' - raise ValueError(errormsg) - for i in range(ts): - for j in range(ts): - if i != j: - if circulating_strains[i] in known_strains and circulating_strains[j] in known_strains: - sim['immunity']['sus'][j][i] = cross_immunity[circulating_strains[j]][ - circulating_strains[i]] - - elif sc.checktype(sim['immunity']['sus'], dict): - raise NotImplementedError - else: - errormsg = f'Type of immunity["sus"] not understood: you provided {type(sim["immunity"]["sus"])}, but it should be an array or dict.' - raise ValueError(errormsg) - # Next, precompute the NAb kinetics and store these for access during the sim sim['NAb_kin'] = pre_compute_waning(length=sim['n_days'], form=sim['NAb_decay']['form'], pars=sim['NAb_decay']['pars']) @@ -483,8 +461,7 @@ def check_immunity(people, strain, sus=True, inds=None): -# %% Methods for computing waning -# __all__ += ['pre_compute_waning'] +#%% Methods for computing waning def pre_compute_waning(length, form='nab_decay', pars=None): ''' @@ -541,16 +518,30 @@ def pre_compute_waning(length, form='nab_decay', pars=None): def nab_decay(length, init_decay_rate, init_decay_time, decay_decay_rate): ''' Returns an array of length 'length' containing the evaluated function NAb decay - function at each point - Uses exponential decay, with the rate of exponential decay also set to exponentilly decay (!) after 250 days + function at each point. + + Uses exponential decay, with the rate of exponential decay also set to exponentially + decay (!) after 250 days. + + Args: + length (int): number of points + init_decay_rate (float): initial rate of exponential decay + init_decay_time (float): time on the first exponential decay + decay_decay_rate (float): the rate at which the decay decays ''' + def f1(t, init_decay_rate): + ''' Simple exponential decay ''' + return np.exp(-t*init_decay_rate) + + def f2(t, init_decay_rate, init_decay_time, decay_decay_rate): + ''' Complex exponential decay ''' + return np.exp(-t*(init_decay_rate*np.exp(-(t-init_decay_time)*decay_decay_rate))) + + t = np.arange(length, dtype=cvd.default_int) + y1 = f1(cvu.true(t<=init_decay_time), init_decay_rate) + y2 = f2(cvu.true(t>init_decay_time), init_decay_rate, init_decay_time, decay_decay_rate) + y = np.concatenate([y1,y2]) - f1 = lambda t, init_decay_rate: np.exp(-t*init_decay_rate) - f2 = lambda t, init_decay_rate, init_decay_time, decay_decay_rate: np.exp(-t*(init_decay_rate*np.exp(-(t-init_decay_time)*decay_decay_rate))) - t = np.arange(length, dtype=cvd.default_int) - y1 = f1(cvu.true(tinit_decay_time), init_decay_rate, init_decay_time, decay_decay_rate) - y = np.concatenate([y1,y2]) return y @@ -563,7 +554,7 @@ def exp_decay(length, init_val, half_life, delay=None): t = np.arange(length-delay, dtype=cvd.default_int) growth = linear_growth(delay, init_val/delay) decay = init_val * np.exp(-decay_rate * t) - result = np.concatenate(growth, decay, axis=None) + result = np.concatenate([growth, decay], axis=None) else: t = np.arange(length, dtype=cvd.default_int) result = init_val * np.exp(-decay_rate * t) @@ -576,13 +567,11 @@ def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp, delay=No if delay is not None: t = np.arange(length - delay, dtype=cvd.default_int) growth = linear_growth(delay, init_val / delay) - decay = (init_val + (lower_asymp - init_val) / ( - 1 + (t / half_val) ** decay_rate)) + decay = (init_val + (lower_asymp - init_val) / (1 + (t / half_val)**decay_rate)) result = np.concatenate((growth, decay), axis=None) else: t = np.arange(length, dtype=cvd.default_int) - result = (init_val + (lower_asymp - init_val) / ( - 1 + (t / half_val) ** decay_rate)) + result = (init_val + (lower_asymp - init_val) / (1 + (t / half_val)**decay_rate)) return result # TODO: make this robust to /0 errors @@ -590,8 +579,7 @@ def linear_decay(length, init_val, slope): ''' Calculate linear decay ''' t = np.arange(length, dtype=cvd.default_int) result = init_val - slope*t - if result < 0: - result = 0 + result = np.maximum(result, 0) return result diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 52f247dd3..3edd9eaef 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -6,6 +6,7 @@ import sciris as sc import covasim as cv import pandas as pd +import pylab as pl do_plot = 1 cv.options.set(interactive=False) # Assume not running interactively @@ -89,7 +90,7 @@ def test_waning(do_plot=False): for rescale in [0, 1]: print(f'Checking with rescale = {rescale}...') - # Define more parameters specific to this test + # Define parameters specific to this test pars = dict( n_days = 90, beta = 0.008, @@ -153,358 +154,71 @@ def test_vaccines(do_plot=False): if do_plot: sim.plot('overview-strain') - pass - -# def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): -# sc.heading('Test varying properties of immunity') - -# # Define baseline parameters -# n_runs = 3 -# base_sim = cv.Sim(use_waning=True, n_days=400, pars=base_pars) - -# # Define the scenarios -# b1351 = cv.Strain('b1351', days=100, n_imports=20) - -# scenarios = { -# 'baseline': { -# 'name': 'Default Immunity (decay at log(2)/90)', -# 'pars': { -# 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, -# 'decay_decay_rate': 0.001}), -# }, -# }, -# 'faster_immunity': { -# 'name': 'Faster Immunity (decay at log(2)/30)', -# 'pars': { -# 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, -# 'decay_decay_rate': 0.001}), -# }, -# }, -# 'baseline_b1351': { -# 'name': 'Default Immunity (decay at log(2)/90), B1351 on day 100', -# 'pars': { -# 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, -# 'decay_decay_rate': 0.001}), -# 'strains': [b1351], -# }, -# }, -# 'faster_immunity_b1351': { -# 'name': 'Faster Immunity (decay at log(2)/30), B1351 on day 100', -# 'pars': { -# 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, -# 'decay_decay_rate': 0.001}), -# 'strains': [b1351], -# }, -# }, -# } - -# metapars = {'n_runs': n_runs} -# scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) -# scens.run() - -# to_plot = sc.objdict({ -# 'New infections': ['new_infections'], -# 'New re-infections': ['new_reinfections'], -# 'Population Nabs': ['pop_nabs'], -# 'Population Immunity': ['pop_protection'], -# }) -# if do_plot: -# scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_basic_immunity.png', to_plot=to_plot) - -# return scens - - - - - - - -# #%% Vaccination tests - -# def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): -# sc.heading('Test vaccination with a single strain') -# sc.heading('Setting up...') - -# pars = sc.mergedicts(base_pars, { -# 'beta': 0.015, -# 'n_days': 120, -# }) - -# pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer') -# sim = cv.Sim( -# use_waning=True, -# pars=pars, -# interventions=pfizer -# ) -# sim.run() - -# to_plot = sc.objdict({ -# 'New infections': ['new_infections'], -# 'Cumulative infections': ['cum_infections'], -# 'New reinfections': ['new_reinfections'], -# }) -# if do_plot: -# sim.plot(do_save=do_save, do_show=do_show, fig_path=f'results/test_reinfection.png', to_plot=to_plot) - -# return sim - - -# def test_synthpops(): -# sim = cv.Sim(use_waning=True, **sc.mergedicts(base_pars, dict(pop_size=5000, pop_type='synthpops'))) -# sim.popdict = cv.make_synthpop(sim, with_facilities=True, layer_mapping={'LTCF': 'f'}) -# sim.reset_layer_pars() - -# # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 -# sim.vxsubtarg = sc.objdict() -# sim.vxsubtarg.age = [75, 65, 50, 18] -# sim.vxsubtarg.prob = [.05, .05, .05, .05] -# sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] -# pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) -# sim['interventions'] += [pfizer] - -# sim.run() -# return sim - - - -# #%% Multisim and scenario tests - -# def test_vaccine_1strain_scen(do_plot=False, do_show=True, do_save=False): -# sc.heading('Run a basic sim with 1 strain, pfizer vaccine') - -# # Define baseline parameters -# n_runs = 3 -# base_sim = cv.Sim(use_waning=True, pars=base_pars) - -# # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 -# base_sim.vxsubtarg = sc.objdict() -# base_sim.vxsubtarg.age = [75, 65, 50, 18] -# base_sim.vxsubtarg.prob = [.05, .05, .05, .05] -# base_sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] -# pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) - -# # Define the scenarios - -# scenarios = { -# 'baseline': { -# 'name': 'No Vaccine', -# 'pars': {} -# }, -# 'pfizer': { -# 'name': 'Pfizer starting on day 20', -# 'pars': { -# 'interventions': [pfizer], -# } -# }, -# } - -# metapars = {'n_runs': n_runs} -# scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) -# scens.run() - -# to_plot = sc.objdict({ -# 'New infections': ['new_infections'], -# 'Cumulative infections': ['cum_infections'], -# 'New reinfections': ['new_reinfections'], -# # 'Cumulative reinfections': ['cum_reinfections'], -# }) -# if do_plot: -# scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_basic_vaccination.png', to_plot=to_plot) - -# return scens - - -# def test_vaccine_2strains_scen(do_plot=False, do_show=True, do_save=False): -# sc.heading('Run a basic sim with b117 strain on day 10, pfizer vaccine day 20') - -# # Define baseline parameters -# n_runs = 3 -# base_sim = cv.Sim(use_waning=True, pars=base_pars) - -# # Vaccinate 75+, then 65+, then 50+, then 18+ on days 20, 40, 60, 80 -# base_sim.vxsubtarg = sc.objdict() -# base_sim.vxsubtarg.age = [75, 65, 50, 18] -# base_sim.vxsubtarg.prob = [.01, .01, .01, .01] -# base_sim.vxsubtarg.days = subtarg_days = [60, 150, 200, 220] -# jnj = cv.vaccinate(days=subtarg_days, vaccine_pars='j&j', subtarget=vacc_subtarg) -# b1351 = cv.Strain('b1351', days=10, n_imports=20) -# p1 = cv.Strain('p1', days=100, n_imports=100) - -# # Define the scenarios - -# scenarios = { -# 'baseline': { -# 'name': 'B1351 on day 10, No Vaccine', -# 'pars': { -# 'strains': [b1351] -# } -# }, -# 'b1351': { -# 'name': 'B1351 on day 10, J&J starting on day 60', -# 'pars': { -# 'interventions': [jnj], -# 'strains': [b1351], -# } -# }, -# 'p1': { -# 'name': 'B1351 on day 10, J&J starting on day 60, p1 on day 100', -# 'pars': { -# 'interventions': [jnj], -# 'strains': [b1351, p1], -# } -# }, -# } - -# metapars = {'n_runs': n_runs} -# scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) -# scens.run() - -# to_plot = sc.objdict({ -# 'New infections': ['new_infections'], -# 'Cumulative infections': ['cum_infections'], -# 'New reinfections': ['new_reinfections'], -# # 'Cumulative reinfections': ['cum_reinfections'], -# }) -# if do_plot: -# scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_vaccine_b1351.png', to_plot=to_plot) - -# return scens - - -# def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): -# sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') - -# strain_pars = {'dur':{'inf2sym': {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9}}} -# strains = cv.Strain(strain=strain_pars, label='10 days til symptoms', days=10, n_imports=30) -# tp = cv.test_prob(symp_prob=0.2) # Add an efficient testing program - -# pars = sc.mergedicts(base_pars, { -# 'beta': 0.015, # Make beta higher than usual so people get infected quickly -# 'n_days': 120, -# 'interventions': tp -# }) -# n_runs = 1 -# base_sim = cv.Sim(use_waning=True, pars=pars) - -# # Define the scenarios -# scenarios = { -# 'baseline': { -# 'name':'1 day to symptoms', -# 'pars': {} -# }, -# 'slowsymp': { -# 'name':'10 days to symptoms', -# 'pars': {'strains': [strains]} -# } -# } - -# metapars = {'n_runs': n_runs} -# scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) -# scens.run() - -# to_plot = sc.objdict({ -# 'New infections': ['new_infections'], -# 'Cumulative infections': ['cum_infections'], -# 'New diagnoses': ['new_diagnoses'], -# 'Cumulative diagnoses': ['cum_diagnoses'], -# }) -# if do_plot: -# scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_strainduration.png', to_plot=to_plot) - -# return scens - - -# def test_waning_vs_not(do_plot=False, do_show=True, do_save=False): -# sc.heading('Testing waning...') - -# # Define baseline parameters -# pars = sc.mergedicts(base_pars, { -# 'pop_size': 10e3, -# 'pop_scale': 50, -# 'n_days': 150, -# 'use_waning': False, -# }) - -# n_runs = 3 -# base_sim = cv.Sim(pars=pars) - -# # Define the scenarios -# scenarios = { -# 'no_waning': { -# 'name': 'No waning', -# 'pars': { -# } -# }, -# 'waning': { -# 'name': 'Waning', -# 'pars': { -# 'use_waning': True, -# } -# }, -# } - -# metapars = {'n_runs': n_runs} -# scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) -# scens.run() - -# to_plot = sc.objdict({ -# 'New infections': ['new_infections'], -# 'New reinfections': ['new_reinfections'], -# 'Cumulative infections': ['cum_infections'], -# 'Cumulative reinfections': ['cum_reinfections'], -# }) -# if do_plot: -# scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_waning_vs_not.png', to_plot=to_plot) - -# return scens - - -# def test_msim(do_plot=False): -# sc.heading('Testing multisim...') - -# # basic test for vaccine -# b117 = cv.Strain('b117', days=0) -# sim = cv.Sim(use_waning=True, strains=[b117], **base_pars) -# msim = cv.MultiSim(sim, n_runs=2) -# msim.run() -# msim.reduce() - -# to_plot = sc.objdict({ -# 'Total infections': ['cum_infections'], -# 'New infections per day': ['new_infections'], -# 'New Re-infections per day': ['new_reinfections'], -# }) - -# if do_plot: -# msim.plot(to_plot=to_plot, do_save=0, do_show=1, legend_args={'loc': 'upper left'}, axis_args={'hspace': 0.4}, interval=35) - -# return msim - - -#%% Plotting and utilities - -# def vacc_subtarg(sim): -# ''' Subtarget by age''' - -# # retrieves the first ind that is = or < sim.t -# ind = get_ind_of_min_value(sim.vxsubtarg.days, sim.t) -# age = sim.vxsubtarg.age[ind] -# prob = sim.vxsubtarg.prob[ind] -# inds = sc.findinds((sim.people.age>=age) * ~sim.people.vaccinated) -# vals = prob*np.ones(len(inds)) -# return {'inds':inds, 'vals':vals} - - -# def get_ind_of_min_value(list, time): -# ind = None -# for place, t in enumerate(list): -# if time >= t: -# ind = place - -# if ind is None: -# errormsg = f'{time} is not within the list of times' -# raise ValueError(errormsg) -# return ind + return sim + + +def test_decays(do_plot=False): + sc.heading('Testing decay parameters...') + + n = 300 + x = pl.arange(n) + + pars = sc.objdict( + nab_decay = dict( + func = cv.immunity.nab_decay, + length = n, + init_decay_rate = 0.01, + init_decay_time= 100, + decay_decay_rate = 0.001, + ), + + exp_decay = dict( + func = cv.immunity.exp_decay, + length = n, + init_val = 0.8, + half_life= 100, + delay = 20, + ), + + logistic_decay = dict( + func = cv.immunity.logistic_decay, + length = n, + init_val = 0.8, + decay_rate = 0.1, + half_val = 0.3, + lower_asymp = 0.01, + delay = 20, + ), + + linear_decay = dict( + func = cv.immunity.linear_decay, + length = n, + init_val = 0.8, + slope = 0.01, + ), + + linear_growth = dict( + func = cv.immunity.linear_growth, + length = n, + slope = 0.01, + ), + ) + + # Calculate all the delays + res = sc.objdict() + for key,par in pars.items(): + func = par.pop('func') + res[key] = func(**par) + + if do_plot: + pl.figure(figsize=(12,8)) + for key,y in res.items(): + pl.semilogy(x, y, label=key, lw=3, alpha=0.7) + pl.legend() + pl.show() + + return res + @@ -518,7 +232,8 @@ def test_vaccines(do_plot=False): # sim1 = test_states() # msims1 = test_waning(do_plot=do_plot) # sim2 = test_strains(do_plot=do_plot) - sim3 = test_vaccines(do_plot=do_plot) + # sim3 = test_vaccines(do_plot=do_plot) + res = test_decays(do_plot=do_plot) sc.toc(T) print('Done.') From 517a6e1ed486ebdab74bbfe1db366f60bf4b9e17 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 21:09:15 -0700 Subject: [PATCH 447/569] not fixed yet --- covasim/immunity.py | 3 ++- tests/test_immunity.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index e76854dd9..bfbb37c1a 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -567,8 +567,9 @@ def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp, delay=No if delay is not None: t = np.arange(length - delay, dtype=cvd.default_int) growth = linear_growth(delay, init_val / delay) - decay = (init_val + (lower_asymp - init_val) / (1 + (t / half_val)**decay_rate)) + decay = lower_asymp + init_val / (1 + (t / half_val)**decay_rate) result = np.concatenate((growth, decay), axis=None) + # import traceback; traceback.print_exc(); import pdb; pdb.set_trace() else: t = np.arange(length, dtype=cvd.default_int) result = (init_val + (lower_asymp - init_val) / (1 + (t / half_val)**decay_rate)) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 3edd9eaef..292158129 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -167,9 +167,9 @@ def test_decays(do_plot=False): nab_decay = dict( func = cv.immunity.nab_decay, length = n, - init_decay_rate = 0.01, + init_decay_rate = 0.05, init_decay_time= 100, - decay_decay_rate = 0.001, + decay_decay_rate = 0.01, ), exp_decay = dict( From d3531c427a296a5f803ac9a2537d7168c4acda08 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 21:32:51 -0700 Subject: [PATCH 448/569] test coverage good now --- covasim/immunity.py | 25 ++----------------------- tests/test_immunity.py | 12 +----------- 2 files changed, 3 insertions(+), 34 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index bfbb37c1a..b3bd6d733 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -469,8 +469,7 @@ def pre_compute_waning(length, form='nab_decay', pars=None): - 'nab_decay' : specific decay function taken from https://doi.org/10.1101/2021.03.09.21252641 - 'exp_decay' : exponential decay. Parameters should be init_val and half_life (half_life can be None/nan) - - 'logistic_decay' : logistic decay (TODO fill in details) - - 'linear' : linear decay (TODO fill in details) + - 'linear' : linear decay - others TBC! Args: @@ -485,7 +484,6 @@ def pre_compute_waning(length, form='nab_decay', pars=None): choices = [ 'nab_decay', # Default if no form is provided 'exp_decay', - 'logistic_decay', 'linear_growth', 'linear_decay' ] @@ -498,9 +496,6 @@ def pre_compute_waning(length, form='nab_decay', pars=None): if pars['half_life'] is None: pars['half_life'] = np.nan output = exp_decay(length, **pars) - elif form == 'logistic_decay': - output = logistic_decay(length, **pars) - elif form == 'linear_growth': output = linear_growth(length, **pars) @@ -508,8 +503,7 @@ def pre_compute_waning(length, form='nab_decay', pars=None): output = linear_decay(length, **pars) else: - choicestr = '\n'.join(choices) - errormsg = f'The selected functional form "{form}" is not implemented; choices are: {choicestr}' + errormsg = f'The selected functional form "{form}" is not implemented; choices are: {sc.strjoin(choices)}' raise NotImplementedError(errormsg) return output @@ -561,21 +555,6 @@ def exp_decay(length, init_val, half_life, delay=None): return result -def logistic_decay(length, init_val, decay_rate, half_val, lower_asymp, delay=None): - ''' Calculate logistic decay ''' - - if delay is not None: - t = np.arange(length - delay, dtype=cvd.default_int) - growth = linear_growth(delay, init_val / delay) - decay = lower_asymp + init_val / (1 + (t / half_val)**decay_rate) - result = np.concatenate((growth, decay), axis=None) - # import traceback; traceback.print_exc(); import pdb; pdb.set_trace() - else: - t = np.arange(length, dtype=cvd.default_int) - result = (init_val + (lower_asymp - init_val) / (1 + (t / half_val)**decay_rate)) - return result # TODO: make this robust to /0 errors - - def linear_decay(length, init_val, slope): ''' Calculate linear decay ''' t = np.arange(length, dtype=cvd.default_int) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 292158129..549388120 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -169,7 +169,7 @@ def test_decays(do_plot=False): length = n, init_decay_rate = 0.05, init_decay_time= 100, - decay_decay_rate = 0.01, + decay_decay_rate = 0.002, ), exp_decay = dict( @@ -180,16 +180,6 @@ def test_decays(do_plot=False): delay = 20, ), - logistic_decay = dict( - func = cv.immunity.logistic_decay, - length = n, - init_val = 0.8, - decay_rate = 0.1, - half_val = 0.3, - lower_asymp = 0.01, - delay = 20, - ), - linear_decay = dict( func = cv.immunity.linear_decay, length = n, From 982c28d26ef86cd0050f8beb5b467133c0cade9c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 21:36:50 -0700 Subject: [PATCH 449/569] increase test coverage --- covasim/defaults.py | 15 +++++++-------- covasim/misc.py | 8 ++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index fd4875292..51d9a7c29 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -107,7 +107,6 @@ def __init__(self): self.dates = [f'date_{state}' for state in self.states] # Convert each state into a date self.dates.append('date_pos_test') # Store the date when a person tested which will come back positive self.dates.append('date_end_quarantine') # Store the date when a person comes out of quarantine - # self.dates.append('date_vaccinated') # Store the date when a person is vaccinated # Duration of different states: these are floats per person -- used in people.py self.durs = [ @@ -126,7 +125,7 @@ def __init__(self): states = getattr(self, state_type) n_states = len(states) n_unique_states = len(set(states)) - if n_states != n_unique_states: + if n_states != n_unique_states: # pragma: no cover errormsg = f'In {state_type}, only {n_unique_states} of {n_states} state names are unique' raise ValueError(errormsg) @@ -335,7 +334,7 @@ def get_default_plots(which='default', kind='sim', sim=None): ], }) - elif kind == 'scens': + elif kind == 'scens': # pragma: no cover plots = sc.odict({ 'Cumulative infections': [ 'cum_infections', @@ -349,19 +348,19 @@ def get_default_plots(which='default', kind='sim', sim=None): }) # Show an overview - elif which == 'overview': + elif which == 'overview': # pragma: no cover plots = sc.dcp(overview_plots) # Plot absolutely everything - elif which.lower() == 'all': + elif which.lower() == 'all': # pragma: no cover plots = sim.result_keys(strain=True) # Show an overview plus strains - elif 'overview' in which and 'strain' in which: + elif 'overview' in which and 'strain' in which: # pragma: no cover plots = sc.dcp(overview_plots) + sc.dcp(overview_strain_plots) # Show default but with strains - elif 'strain' in which: + elif 'strain' in which: # pragma: no cover plots = sc.odict({ 'Total counts': [ 'cum_infections_by_strain', @@ -380,7 +379,7 @@ def get_default_plots(which='default', kind='sim', sim=None): }) # Plot SEIR compartments - elif which.lower() == 'seir': + elif which.lower() == 'seir': # pragma: no cover plots = [ 'n_susceptible', 'n_preinfectious', diff --git a/covasim/misc.py b/covasim/misc.py index 6f2c48889..97c7787f8 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -424,7 +424,7 @@ def git_info(filename=None, check=False, comments=None, old_info=None, die=False old_info = sc.loadjson(filename, **kwargs) string = '' old_cv_info = old_info['covasim'] if 'covasim' in old_info else old_info - if cv_info != old_cv_info: + if cv_info != old_cv_info: # pragma: no cover string = f'Git information differs: {cv_info} vs. {old_cv_info}' if die: raise ValueError(string) @@ -617,7 +617,7 @@ def get_doubling_time(sim, series=None, interval=None, start_day=None, end_day=N # Validate inputs: interval if interval is not None: - if len(interval) != 2: + if len(interval) != 2: # pragma: no cover sc.printv(f"Interval should be a list/array/tuple of length 2, not {len(interval)}. Resetting to length of series.", 1, verbose) interval = [0,len(series)] start_day, end_day = interval[0], interval[1] @@ -629,12 +629,12 @@ def get_doubling_time(sim, series=None, interval=None, start_day=None, end_day=N # Deal with moving window if moving_window is not None: - if not sc.isnumber(moving_window): + if not sc.isnumber(moving_window): # pragma: no cover sc.printv("Moving window should be an integer; ignoring and calculating single result", 1, verbose) doubling_time = get_doubling_time(sim, series=series, start_day=start_day, end_day=end_day, moving_window=None, exp_approx=exp_approx) else: - if not isinstance(moving_window,int): + if not isinstance(moving_window,int): # pragma: no cover sc.printv(f"Moving window should be an integer; recasting {moving_window} the nearest integer... ", 1, verbose) moving_window = int(moving_window) if moving_window < 2: From 737149a4123bb8d8e1a0914539724a409a79a0ea Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 22:03:18 -0700 Subject: [PATCH 450/569] minor tidying --- tests/test_immunity.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 549388120..f86fce97a 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -196,6 +196,7 @@ def test_decays(do_plot=False): # Calculate all the delays res = sc.objdict() + res.x = x for key,par in pars.items(): func = par.pop('func') res[key] = func(**par) @@ -219,10 +220,10 @@ def test_decays(do_plot=False): cv.options.set(interactive=do_plot) T = sc.tic() - # sim1 = test_states() - # msims1 = test_waning(do_plot=do_plot) - # sim2 = test_strains(do_plot=do_plot) - # sim3 = test_vaccines(do_plot=do_plot) + sim1 = test_states() + msims1 = test_waning(do_plot=do_plot) + sim2 = test_strains(do_plot=do_plot) + sim3 = test_vaccines(do_plot=do_plot) res = test_decays(do_plot=do_plot) sc.toc(T) From d7ff99543ada446a8849a858bafc4f1a5a3526c7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 22:22:08 -0700 Subject: [PATCH 451/569] rename vaccine to simple_vaccine --- covasim/base.py | 8 +++++--- covasim/interventions.py | 10 +++++----- covasim/plotting.py | 38 +++++++++++++++---------------------- docs/tutorials/t05.ipynb | 12 ++++++------ tests/test_immunity.py | 2 +- tests/test_interventions.py | 4 ++-- 6 files changed, 34 insertions(+), 40 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 20c1325f1..e39f5df40 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -393,10 +393,12 @@ def date(self, ind, *args, dateformat=None, as_date=False): return dates - def result_keys(self, strain=False): + def result_keys(self, main=True, strain=False): ''' Get the actual results objects, not other things stored in sim.results ''' - keys = [key for key in self.results.keys() if isinstance(self.results[key], Result)] - if strain: + keys = [] + if main: + keys += [key for key in self.results.keys() if isinstance(self.results[key], Result)] + if strain and 'strain' in self.results: keys += [key for key in self.results['strain'].keys() if isinstance(self.results['strain'][key], Result)] return keys diff --git a/covasim/interventions.py b/covasim/interventions.py index 5645dfb93..3e15b8259 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1107,12 +1107,12 @@ def notify_contacts(self, sim, contacts): #%% Treatment and prevention interventions -__all__+= ['vaccine', 'vaccinate'] +__all__+= ['simple_vaccine', 'vaccinate'] -class vaccine(Intervention): +class simple_vaccine(Intervention): ''' - Apply a vaccine to a subset of the population. In addition to changing the + Apply a simple vaccine to a subset of the population. In addition to changing the relative susceptibility and the probability of developing symptoms if still infected, this intervention stores several types of data: @@ -1134,8 +1134,8 @@ class vaccine(Intervention): **Examples**:: - interv = cv.vaccine(days=50, prob=0.3, rel_sus=0.5, rel_symp=0.1) - interv = cv.vaccine(days=[10,20,30,40], prob=0.8, rel_sus=0.5, cumulative=[1, 0.3, 0.1, 0]) # A vaccine with efficacy up to the 3rd dose + interv = cv.simple_vaccine(days=50, prob=0.3, rel_sus=0.5, rel_symp=0.1) + interv = cv.simple_vaccine(days=[10,20,30,40], prob=0.8, rel_sus=0.5, cumulative=[1, 0.3, 0.1, 0]) # A vaccine with efficacy up to the 3rd dose ''' def __init__(self, days, prob=1.0, rel_sus=0.0, rel_symp=0.0, subtarget=None, cumulative=False, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object diff --git a/covasim/plotting.py b/covasim/plotting.py index 4f774d6b3..bc6e59c6a 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -100,11 +100,9 @@ def handle_to_plot(kind, to_plot, n_cols, sim, check_ready=True): if isinstance(to_plot, list): to_plot_list = to_plot # Store separately to_plot = sc.odict() # Create the dict + reskeys = sim.result_keys() for reskey in to_plot_list: - if 'strain' in sim.results and reskey in sim.results['strain']: - name = sim.results['strain'][reskey].name - else: - name = sim.results[reskey].name + name = sim.results[reskey].name if reskey in reskeys else sim.results['strain'][reskey].name to_plot[name] = [reskey] # Use the result name as the key and the reskey as the value to_plot = sc.odict(sc.dcp(to_plot)) # In case it's supplied as a dict @@ -385,25 +383,21 @@ def plot_sim(to_plot=None, sim=None, do_save=None, fig_path=None, fig_args=None, fig, figs = create_figs(args, sep_figs, fig, ax) # Do the plotting + strain_keys = sim.result_keys(main=False, strain=True) for pnum,title,keylabels in to_plot.enumitems(): ax = create_subplots(figs, fig, ax, n_rows, n_cols, pnum, args.fig, sep_figs, log_scale, title) for resnum,reskey in enumerate(keylabels): res_t = sim.results['t'] - if 'strain' in sim.results and reskey in sim.results['strain']: + if reskey in strain_keys: res = sim.results['strain'][reskey] ns = sim['total_strains'] strain_colors = sc.gridcolors(ns) for strain in range(ns): color = strain_colors[strain] # Choose the color - if strain == 0: - label = 'wild type' - else: - label = sim['strains'][strain-1].label + label = 'wild type' if strain == 0 else sim['strains'][strain-1].label if res.low is not None and res.high is not None: - ax.fill_between(res_t, res.low[strain,:], res.high[strain,:], color=color, - **args.fill) # Create the uncertainty bound + ax.fill_between(res_t, res.low[strain,:], res.high[strain,:], color=color, **args.fill) # Create the uncertainty bound ax.plot(res_t, res.values[strain,:], label=label, **args.plot, c=color) # Actually plot the sim! - else: res = sim.results[reskey] color = set_line_options(colors, reskey, resnum, res.color) # Choose the color @@ -444,16 +438,15 @@ def plot_scens(to_plot=None, scens=None, do_save=None, fig_path=None, fig_args=N resdata = scens.results[reskey] for snum,scenkey,scendata in resdata.enumitems(): sim = scens.sims[scenkey][0] # Pull out the first sim in the list for this scenario - if 'by_strain' in reskey: # TODO: refactor - for strain in range(sim['total_strains']): + strain_keys = sim.result_keys(main=False, strain=True) + if reskey in strain_keys: + ns = sim['total_strains'] + strain_colors = sc.gridcolors(ns) + for strain in range(ns): res_y = scendata.best[strain,:] - color = default_colors[strain] # Choose the color - if strain == 0: - label = 'wild type' - else: - label = sim['strains'][strain - 1].label - ax.fill_between(scens.tvec, scendata.low[strain,:], scendata.high[strain,:], color=color, - **args.fill) # Create the uncertainty bound + color = strain_colors[strain] # Choose the color + label = 'wild type' if strain == 0 else sim['strains'][strain - 1].label + ax.fill_between(scens.tvec, scendata.low[strain,:], scendata.high[strain,:], color=color, **args.fill) # Create the uncertainty bound ax.plot(scens.tvec, res_y, label=label, c=color, **args.plot) # Plot the actual line if args.show['data']: plot_data(sim, ax, reskey, args.scatter, color=color) # Plot the data @@ -461,8 +454,7 @@ def plot_scens(to_plot=None, scens=None, do_save=None, fig_path=None, fig_args=N res_y = scendata.best color = set_line_options(colors, scenkey, snum, default_colors[snum]) # Choose the color label = set_line_options(labels, scenkey, snum, scendata.name) # Choose the label - ax.fill_between(scens.tvec, scendata.low, scendata.high, color=color, - **args.fill) # Create the uncertainty bound + ax.fill_between(scens.tvec, scendata.low, scendata.high, color=color, **args.fill) # Create the uncertainty bound ax.plot(scens.tvec, res_y, label=label, c=color, **args.plot) # Plot the actual line if args.show['data']: plot_data(sim, ax, reskey, args.scatter, color=color) # Plot the data diff --git a/docs/tutorials/t05.ipynb b/docs/tutorials/t05.ipynb index 97a128d3e..f10b3c923 100644 --- a/docs/tutorials/t05.ipynb +++ b/docs/tutorials/t05.ipynb @@ -199,7 +199,7 @@ "Vaccines can do one of two things: they can stop you from becoming infected in the first place (acquisition blocking), or they can stop you from developing symptoms or severe disease once infected (symptom blocking). The Covasim vaccine intervention lets you control both of these options. In its simplest form, a vaccine is like a change beta intervention. For example, this vaccine:\n", "\n", "```python\n", - "vaccine = cv.vaccine(days=30, prob=1.0, rel_sus=0.3, rel_symp=1.0)\n", + "vaccine = cv.simple_vaccine(days=30, prob=1.0, rel_sus=0.3, rel_symp=1.0)\n", "```\n", "\n", "is equivalent to this beta change:\n", @@ -211,7 +211,7 @@ "But that's not very realistic. A vaccine given on days 30 and 44 (two weeks later), with efficacy of 50% per dose which accumulates, given to 60% of the population, and which blocks 50% of acquisition and (among those who get infected even so) 90% of symptoms, would look like this:\n", "\n", "```python\n", - "vaccine = cv.vaccine(days=[30, 44], cumulative=[0.5, 0.5], prob=0.6, rel_sus=0.5, rel_symp=0.1)\n", + "vaccine = cv.simple_vaccine(days=[30, 44], cumulative=[0.5, 0.5], prob=0.6, rel_sus=0.5, rel_symp=0.1)\n", "```" ] }, @@ -247,7 +247,7 @@ " return output\n", "\n", "# Define the vaccine\n", - "vaccine = cv.vaccine(days=20, rel_sus=0.8, rel_symp=0.06, subtarget=vaccinate_by_age)\n", + "vaccine = cv.simple_vaccine(days=20, rel_sus=0.8, rel_symp=0.06, subtarget=vaccinate_by_age)\n", "\n", "# Create, run, and plot the simulations\n", "sim1 = cv.Sim(label='Baseline')\n", @@ -263,7 +263,7 @@ "source": [ "If you have a simple conditional, you can also define subtargeting using a lambda function, e.g. this is a vaccine with 90% probability of being given to people over age 75, and 10% probability of being applied to everyone else (i.e. people under age 75%):\n", "```python\n", - "vaccine = cv.vaccine(days=20, prob=0.1, subtarget=dict(inds=lambda sim: cv.true(sim.people.age>50), vals=0.9))\n", + "vaccine = cv.simple_vaccine(days=20, prob=0.1, subtarget=dict(inds=lambda sim: cv.true(sim.people.age>50), vals=0.9))\n", "```" ] }, @@ -550,8 +550,8 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", - "language": "python", + "display_name": "Python 3 (Spyder)", + "language": "python3", "name": "python3" }, "language_info": { diff --git a/tests/test_immunity.py b/tests/test_immunity.py index f86fce97a..997f7997a 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -48,7 +48,7 @@ def test_states(): interventions = [ cv.test_prob(symp_prob=0.4, asymp_prob=0.01), cv.contact_tracing(trace_probs=0.1), - cv.vaccine(days=60, prob=0.1) + cv.simple_vaccine(days=60, prob=0.1) ] ) sim = cv.Sim(pars).run() diff --git a/tests/test_interventions.py b/tests/test_interventions.py index c7b120bd9..46b997e94 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -91,8 +91,8 @@ def check_inf(interv, sim, thresh=10, close_day=18): i8d = cv.contact_tracing(start_day=40, trace_probs=dict(h=0.9, s=0.7, w=0.7, c=0.3), trace_time=dict(h=0, s=1, w=1, c=3)) # Start tracing for TTQ # 9. Vaccine - i9a = cv.vaccine(days=20, prob=1.0, rel_sus=1.0, rel_symp=0.0) - i9b = cv.vaccine(days=50, prob=1.0, rel_sus=0.0, rel_symp=0.0) + i9a = cv.simple_vaccine(days=20, prob=1.0, rel_sus=1.0, rel_symp=0.0) + i9b = cv.simple_vaccine(days=50, prob=1.0, rel_sus=0.0, rel_symp=0.0) #%% Create the simulations sims = sc.objdict() From f4629a4a813ea338d5a6af7491ab89934d60f586 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 23:02:04 -0700 Subject: [PATCH 452/569] refactored by_strain --- covasim/base.py | 22 ++++++++---- covasim/defaults.py | 2 +- covasim/plotting.py | 4 +-- covasim/run.py | 88 +++++++++++++++++++++++---------------------- covasim/sim.py | 24 ++++--------- 5 files changed, 72 insertions(+), 68 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index e39f5df40..2b8aca0c3 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -393,13 +393,23 @@ def date(self, ind, *args, dateformat=None, as_date=False): return dates - def result_keys(self, main=True, strain=False): - ''' Get the actual results objects, not other things stored in sim.results ''' + def result_keys(self, which='main'): + ''' + Get the actual results objects, not other things stored in sim.results. + + If which is 'main', return only the main results keys. If 'strain', return + only strain keys. If 'all', return all keys. + + ''' keys = [] - if main: - keys += [key for key in self.results.keys() if isinstance(self.results[key], Result)] - if strain and 'strain' in self.results: - keys += [key for key in self.results['strain'].keys() if isinstance(self.results['strain'][key], Result)] + choices = ['main', 'strain', 'all'] + if which in ['main', 'all']: + keys += [key for key,res in self.results.items() if isinstance(res, Result)] + if which in ['strain', 'all'] and 'strain' in self.results: + keys += [key for key,res in self.results['strain'].items() if isinstance(res, Result)] + if which not in choices: # pragma: no cover + errormsg = f'Choice "which" not available; choices are: {sc.strjoin(choices)}' + raise ValueError(errormsg) return keys diff --git a/covasim/defaults.py b/covasim/defaults.py index 51d9a7c29..9196f325b 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -353,7 +353,7 @@ def get_default_plots(which='default', kind='sim', sim=None): # Plot absolutely everything elif which.lower() == 'all': # pragma: no cover - plots = sim.result_keys(strain=True) + plots = sim.result_keys('all') # Show an overview plus strains elif 'overview' in which and 'strain' in which: # pragma: no cover diff --git a/covasim/plotting.py b/covasim/plotting.py index bc6e59c6a..e3e1cb58a 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -383,7 +383,7 @@ def plot_sim(to_plot=None, sim=None, do_save=None, fig_path=None, fig_args=None, fig, figs = create_figs(args, sep_figs, fig, ax) # Do the plotting - strain_keys = sim.result_keys(main=False, strain=True) + strain_keys = sim.result_keys('strain') for pnum,title,keylabels in to_plot.enumitems(): ax = create_subplots(figs, fig, ax, n_rows, n_cols, pnum, args.fig, sep_figs, log_scale, title) for resnum,reskey in enumerate(keylabels): @@ -438,7 +438,7 @@ def plot_scens(to_plot=None, scens=None, do_save=None, fig_path=None, fig_args=N resdata = scens.results[reskey] for snum,scenkey,scendata in resdata.enumitems(): sim = scens.sims[scenkey][0] # Pull out the first sim in the list for this scenario - strain_keys = sim.result_keys(main=False, strain=True) + strain_keys = sim.result_keys('strain') if reskey in strain_keys: ns = sim['total_strains'] strain_colors = sc.gridcolors(ns) diff --git a/covasim/run.py b/covasim/run.py index 1304de8dc..d813c1518 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -240,34 +240,36 @@ def reduce(self, quantiles=None, use_mean=False, bounds=None, output=False): # Perform the statistics raw = {} - reskeys = reduced_sim.result_keys() - for reskey in reskeys: - if 'by_strain' in reskey: - raw[reskey] = np.zeros((reduced_sim['total_strains'], reduced_sim.npts, len(self.sims))) - else: - raw[reskey] = np.zeros((reduced_sim.npts, len(self.sims))) + mainkeys = reduced_sim.result_keys('main') + strainkeys = reduced_sim.result_keys('strain') + for reskey in mainkeys: + raw[reskey] = np.zeros((reduced_sim.npts, len(self.sims))) for s,sim in enumerate(self.sims): vals = sim.results[reskey].values - if 'by_strain' in reskey: - raw[reskey][:, :, s] = vals - else: - raw[reskey][:, s] = vals + raw[reskey][:, s] = vals + for reskey in strainkeys: + raw[reskey] = np.zeros((reduced_sim['total_strains'], reduced_sim.npts, len(self.sims))) + for s,sim in enumerate(self.sims): + vals = sim.results['strain'][reskey].values + raw[reskey][:, :, s] = vals - for reskey in reskeys: - if 'by_strain' in reskey: - axis=2 + for reskey in mainkeys + strainkeys: + if reskey in mainkeys: + axis = 1 + results = reduced_sim.results else: - axis=1 + axis = 2 + results = reduced_sim.results['strain'] if use_mean: r_mean = np.mean(raw[reskey], axis=axis) r_std = np.std(raw[reskey], axis=axis) - reduced_sim.results[reskey].values[:] = r_mean - reduced_sim.results[reskey].low = r_mean - bounds * r_std - reduced_sim.results[reskey].high = r_mean + bounds * r_std + results[reskey].values[:] = r_mean + results[reskey].low = r_mean - bounds * r_std + results[reskey].high = r_mean + bounds * r_std else: - reduced_sim.results[reskey].values[:] = np.quantile(raw[reskey], q=0.5, axis=axis) - reduced_sim.results[reskey].low = np.quantile(raw[reskey], q=quantiles['low'], axis=axis) - reduced_sim.results[reskey].high = np.quantile(raw[reskey], q=quantiles['high'], axis=axis) + results[reskey].values[:] = np.quantile(raw[reskey], q=0.5, axis=axis) + results[reskey].low = np.quantile(raw[reskey], q=quantiles['low'], axis=axis) + results[reskey].high = np.quantile(raw[reskey], q=quantiles['high'], axis=axis) # Compute and store final results reduced_sim.compute_summary() @@ -901,10 +903,10 @@ def __init__(self, sim=None, metapars=None, scenarios=None, basepars=None, scenf return - def result_keys(self): + def result_keys(self, which='all'): ''' Attempt to retrieve the results keys from the base sim ''' try: - keys = self.base_sim.result_keys() + keys = self.base_sim.result_keys(which=which) except Exception as E: errormsg = f'Could not retrieve result keys since base sim not accessible: {str(E)}' raise ValueError(errormsg) @@ -935,7 +937,8 @@ def print_heading(string): print(string) return - reskeys = self.result_keys() # Shorten since used extensively + mainkeys = self.result_keys('main') + strainkeys = self.result_keys('strain') # Loop over scenarios for scenkey,scen in self.scenarios.items(): @@ -973,28 +976,26 @@ def print_heading(string): # Process the simulations print_heading(f'Processing {scenkey}') scenraw = {} - for reskey in reskeys: - if 'by_strain' in reskey: - scenraw[reskey] = np.zeros((ns, self.npts, len(scen_sims))) - else: - scenraw[reskey] = np.zeros((self.npts, len(scen_sims))) + for reskey in mainkeys: + scenraw[reskey] = np.zeros((self.npts, len(scen_sims))) for s,sim in enumerate(scen_sims): - if 'by_strain' in reskey: - scenraw[reskey][:,:,s] = sim.results[reskey].values - else: - scenraw[reskey][:,s] = sim.results[reskey].values + scenraw[reskey][:,s] = sim.results[reskey].values + for reskey in strainkeys: + scenraw[reskey] = np.zeros((ns, self.npts, len(scen_sims))) + for s,sim in enumerate(scen_sims): + scenraw[reskey][:,:,s] = sim.results['strain'][reskey].values scenres = sc.objdict() scenres.best = {} scenres.low = {} scenres.high = {} - for reskey in reskeys: - axis = 2 if 'by_strain' in reskey else 1 + for reskey in mainkeys + strainkeys: + axis = 1 if reskey in mainkeys else 2 scenres.best[reskey] = np.quantile(scenraw[reskey], q=0.5, axis=axis) # Changed from median to mean for smoother plots scenres.low[reskey] = np.quantile(scenraw[reskey], q=self['quantiles']['low'], axis=axis) scenres.high[reskey] = np.quantile(scenraw[reskey], q=self['quantiles']['high'], axis=axis) - for reskey in reskeys: + for reskey in mainkeys + strainkeys: self.results[reskey][scenkey]['name'] = scenname for blh in ['best', 'low', 'high']: self.results[reskey][scenkey][blh] = scenres[blh][reskey] @@ -1037,16 +1038,19 @@ def compare(self, t=None, output=False): # Compute dataframe x = defaultdict(dict) + strainkeys = self.result_keys('strain') for scenkey in self.scenarios.keys(): for reskey in self.result_keys(): - if 'by_strain' in reskey: - val = self.results[reskey][scenkey].best[0, day] # Only prints results for infections by first strain - reskey = reskey+'0' # Add strain number to the summary output + if reskey in strainkeys: + for strain in range(self.base_sim['total_strains']): + val = self.results[reskey][scenkey].best[strain, day] # Only prints results for infections by first strain + strainkey = reskey + str(strain) # Add strain number to the summary output + x[scenkey][strainkey] = int(val) else: val = self.results[reskey][scenkey].best[day] - if reskey not in ['r_eff', 'doubling_time']: - val = int(val) - x[scenkey][reskey] = val + if reskey not in ['r_eff', 'doubling_time']: + val = int(val) + x[scenkey][reskey] = val df = pd.DataFrame.from_dict(x).astype(object) if not output: @@ -1116,7 +1120,7 @@ def to_excel(self, filename=None): spreadsheet = sc.Spreadsheet() spreadsheet.freshbytes() with pd.ExcelWriter(spreadsheet.bytes, engine='xlsxwriter') as writer: - for key in self.result_keys(): + for key in self.result_keys('main'): # Multidimensional strain keys can't be exported result_df = pd.DataFrame.from_dict(sc.flattendict(self.results[key], sep='_')) result_df.to_excel(writer, sheet_name=key) spreadsheet.load() diff --git a/covasim/sim.py b/covasim/sim.py index 38e9f1e2c..a463b305c 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -766,14 +766,11 @@ def finalize(self, verbose=None, restore_pars=True): # Scale the results for reskey in self.result_keys(): - if 'by_strain' in reskey: - # resize results to include only active strains - self.results[reskey].values = self.results[reskey].values[:self['n_strains'], :] if self.results[reskey].scale: # Scale the result dynamically - if 'by_strain' in reskey: - self.results[reskey].values = np.einsum('ij,j->ij',self.results[reskey].values,self.rescale_vec) - else: - self.results[reskey].values *= self.rescale_vec + self.results[reskey].values *= self.rescale_vec + for reskey in self.result_keys('strain'): + if self.results['strain'][reskey].scale: # Scale the result dynamically + self.results['strain'][reskey].values = np.einsum('ij,j->ij', self.results['strain'][reskey].values, self.rescale_vec) # Calculate cumulative results for key in cvd.result_flows.keys(): @@ -781,8 +778,8 @@ def finalize(self, verbose=None, restore_pars=True): for key in cvd.result_flows_by_strain.keys(): for strain in range(self['total_strains']): self.results['strain'][f'cum_{key}'][strain, :] = np.cumsum(self.results['strain'][f'new_{key}'][strain, :], axis=0) - self.results['cum_infections'].values += self['pop_infected']*self.rescale_vec[0] # Include initially infected people - self.results['strain']['cum_infections_by_strain'].values += self['pop_infected']*self.rescale_vec[0] + for res in [self.results['cum_infections'], self.results['strain']['cum_infections_by_strain']]: # Include initially infected people + res.values += self['pop_infected']*self.rescale_vec[0] # Finalize interventions and analyzers self.finalize_interventions() @@ -1048,14 +1045,7 @@ def compute_summary(self, full=None, t=None, update=True, output=False, require_ summary = sc.objdict() for key in self.result_keys(): - if 'by_strain' in key: - summary[key] = self.results[key][:,t] - # TODO: the following line rotates the results - do we need this? - # TODO: the following line rotates the results - do we need this? - #if len(self.results[key]) < t: - # self.results[key].values = np.rot90(self.results[key].values) - else: - summary[key] = self.results[key][t] + summary[key] = self.results[key][t] # Update the stored state if update: From a3783bd49086e6c94787fd4fa415494549fb9807 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 23:06:49 -0700 Subject: [PATCH 453/569] refactored flows --- covasim/people.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index c8ba5f4a1..c7ce547c7 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -46,11 +46,11 @@ def __init__(self, pars, strict=True, **kwargs): # Handle pars and population size if sc.isnumber(pars): # Interpret as a population size pars = {'pop_size':pars} # Ensure it's a dictionary - self.pars = pars # Equivalent to self.set_pars(pars) - self.pop_size = int(pars['pop_size']) - self.location = pars.get('location') # Try to get location, but set to None otherwise + self.pars = pars # Equivalent to self.set_pars(pars) + self.pop_size = int(pars['pop_size']) + self.location = pars.get('location') # Try to get location, but set to None otherwise self.total_strains = pars.get('total_strains', 1) # Assume 1 strain if not supplied - self.version = cvv.__version__ # Store version info + self.version = cvv.__version__ # Store version info # Other initialization self.t = 0 # Keep current simulation time @@ -95,10 +95,7 @@ def __init__(self, pars, strict=True, **kwargs): self._lock = strict # If strict is true, stop further keys from being set (does not affect attributes) # Store flows to be computed during simulation - self.flows = {key:0 for key in cvd.new_result_flows} - self.flows_strain = {} - for key in cvd.new_result_flows_by_strain: - self.flows_strain[key] = np.full(self.total_strains, 0, dtype=cvd.default_float) + self.init_flows() # Although we have called init(), we still need to call initialize() self.initialized = False @@ -119,6 +116,15 @@ def __init__(self, pars, strict=True, **kwargs): return + def init_flows(self): + ''' Initialize flows to be zero ''' + self.flows = {key:0 for key in cvd.new_result_flows} + self.flows_strain = {} + for key in cvd.new_result_flows_by_strain: + self.flows_strain[key] = np.zeros(self.total_strains, dtype=cvd.default_float) + return + + def initialize(self): ''' Perform initializations ''' self.set_prognoses() @@ -169,10 +175,7 @@ def update_states_pre(self, t): self.is_exp = self.true('exposed') # For storing the interim values since used in every subsequent calculation # Perform updates - self.flows = {key:0 for key in cvd.new_result_flows} - self.flows_strain = {} - for key in cvd.new_result_flows_by_strain: - self.flows_strain[key] = np.full(self.pars['total_strains'], 0, dtype=cvd.default_float) + self.init_flows() self.flows['new_infectious'] += self.check_infectious() # For people who are exposed and not infectious, check if they begin being infectious self.flows['new_symptomatic'] += self.check_symptomatic() self.flows['new_severe'] += self.check_severe() From af94706e13c88fff315c1bf4f391d6e6b6ff2fc9 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 23:11:56 -0700 Subject: [PATCH 454/569] refactor initial infections --- covasim/sim.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index a463b305c..f6d7d1153 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -413,10 +413,9 @@ def init_people(self, save_pop=False, load_pop=False, popfile=None, reset=False, self.people.initialize() # Fully initialize the people # Create the seed infections - pop_infected_per_strain = cvd.default_int(self['pop_infected']/self['n_strains']) # TODO: refactor - for strain in range(self['n_strains']): - inds = cvu.choose(self['pop_size'], pop_infected_per_strain) - self.people.infect(inds=inds, layer='seed_infection', strain=strain) + inds = cvu.choose(self['pop_size'], self['pop_infected']) + self.people.infect(inds=inds, layer='seed_infection') + return From bc51319d13f28d9b5b8180dc8b185fab831f383d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 23:46:41 -0700 Subject: [PATCH 455/569] added new parameters --- covasim/base.py | 18 +++++++++++++++++- covasim/parameters.py | 2 ++ covasim/sim.py | 17 ++++++++++++++++- tests/test_immunity.py | 2 +- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 2b8aca0c3..86214ba0e 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -245,13 +245,29 @@ def _brief(self): def update_pars(self, pars=None, create=False, **kwargs): ''' Ensure that metaparameters get used properly before being updated ''' + + # Merge everything together pars = sc.mergedicts(pars, kwargs) if pars: + + # Define aliases + mapping = dict( + n_agents = 'pop_size', + init_infected = 'pop_infected', + ) + for key1,key2 in mapping.items(): + if key1 in pars: + pars[key2] = pars.pop(key1) + + # Handle other special parameters if pars.get('pop_type'): cvpar.reset_layer_pars(pars, force=False) if pars.get('prog_by_age'): pars['prognoses'] = cvpar.get_prognoses(by_age=pars['prog_by_age'], version=self._default_ver) # Reset prognoses - super().update_pars(pars=pars, create=create) # Call update_pars() for ParsObj + + # Call update_pars() for ParsObj + super().update_pars(pars=pars, create=create) + return diff --git a/covasim/parameters.py b/covasim/parameters.py index 8df4d8559..112cd2d61 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -44,9 +44,11 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Rescaling parameters pars['pop_scale'] = 1 # Factor by which to scale the population -- e.g. pop_scale=10 with pop_size=100e3 means a population of 1 million + pars['scaled_pop'] = None # The total scaled population, i.e. the number of agents times the scale factor pars['rescale'] = True # Enable dynamic rescaling of the population -- starts with pop_scale=1 and scales up dynamically as the epidemic grows pars['rescale_threshold'] = 0.05 # Fraction susceptible population that will trigger rescaling if rescaling pars['rescale_factor'] = 1.2 # Factor by which the population is rescaled on each step + pars['frac_susceptible'] = 1.0 # What proportion of the population is susceptible to infection # Network parameters, generally initialized after the population has been constructed pars['contacts'] = None # The number of contacts per layer; set by reset_layer_pars() below diff --git a/covasim/sim.py b/covasim/sim.py index f6d7d1153..3f1477392 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -198,8 +198,18 @@ def validate_pars(self, validate_layers=True): validate_layers (bool): whether to validate layer parameters as well via validate_layer_pars() -- usually yes, except during initialization ''' + # Handle population size + pop_size = self.pars.get('pop_size') + scaled_pop = self.pars.get('scaled_pop') + pop_scale = self.pars.get('pop_scale') + if scaled_pop is not None: # If scaled_pop is supplied, try to use it + if pop_scale is not None: # Normal case, recalculate number of agents + self['pop_size'] = scaled_pop/pop_scale + else: # Special case, recalculate population scale + self['pop_scale'] = scaled_pop/pop_size + # Handle types - for key in ['pop_size', 'pop_infected', 'pop_size']: + for key in ['pop_size', 'pop_infected']: try: self[key] = int(self[key]) except Exception as E: @@ -412,6 +422,11 @@ def init_people(self, save_pop=False, load_pop=False, popfile=None, reset=False, self.people = cvpop.make_people(self, save_pop=save_pop, popfile=popfile, reset=reset, verbose=verbose, **kwargs) self.people.initialize() # Fully initialize the people + # Handle anyone who isn't susceptible + if self['frac_susceptible'] < 1: + inds = cvu.choose(self['pop_size'], np.round((1-self['frac_susceptible'])*self['pop_size'])) + self.people.make_nonnaive(inds=inds) + # Create the seed infections inds = cvu.choose(self['pop_size'], self['pop_infected']) self.people.infect(inds=inds, layer='seed_infection') diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 997f7997a..deff33c60 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -74,7 +74,7 @@ def test_states(): print(f'× {n_true}/{n_inds} error!') elif relation == -1 and n_false != n_inds: errormsg = f'Being {s1}=True implies {s2}=False, but only {n_false}/{n_inds} people are' - print(f'× {n_true}/{n_inds} error!') + print(f'× {n_false}/{n_inds} error!') else: print(f'✓ {n_true}/{n_inds}') if errormsg: From 263933dfa5ff5c82dc26530ea51508c85bc492f6 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 11 Apr 2021 23:50:19 -0700 Subject: [PATCH 456/569] update state diagram as per robyn rename --- tests/state_diagram.xlsx | Bin 11072 -> 11083 bytes tests/test_immunity.py | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/state_diagram.xlsx b/tests/state_diagram.xlsx index 4b411f01db9a576fdfa7ce8479bd841e8510d69e..dfe09e0b5feaaac377e9d0998768ef46eef264d0 100644 GIT binary patch delta 7834 zcmaiZbyQVdw?5t79J&u3k^<7*AxL*gOCD;2baOZ$EeHrG9g+u7T2fG?yF-vh@}lp1 zzx%!4Z~X4vf9yT>95bG|)|_Lkxt@;(Lk76oYRD)g2pAX`2yDU$wYX%6$PZr_E2125 zoLYFl2(^97Lxdu!%S`MN<&-k6TMwc2GspvQW(Rd|$stFatgJ5A-oXR(R#eKQk^<=7Dfz6`fz zh@8llY%Q9V^&Prg>AM)KcWVOZk+?Ac;%8`UQx=#jM?;Tc_g( z$z-h}d`eNmInvb4rsypWW%ltENGOgmgtNXfB48cr8orgM9s49QLD9QqCV%$_pvjy$ zju73h)0wysKJ7qJ2bIFh43`Kfi@=)sSmgYUk8DrrIfAd zR-JcMptl38?+)~NWK^jmNuk9dBl6l^E~y1wZS8HunL9*z#D=!@RB6jJX^SS{YKDMA z=~HKGZYo_#Rw@h>xif4t4&AP5pqM`;WE@jwIQF~H{VH@1f=MLCK6NHhO@=diIb+>! zPQxwq!=lAFZa{i%ZFXXqJrQH3I=S1jnF2RllkXysX2}Bgaelz9j6=I|wNq&PhBf?7 zm^K7R5T8l7&>{zE!44XnP-DTMZq9vH?pq+N+{6uDw{je$Jn!CVisFy*ckv@4FH$md_o zj}-i$6-q|T{@l@lm)je|S1wU_OJhk?ivv<(;3dAG4YtFtZK5aZMAl-dymm)~q1YI1 za+H(kL*t1|*mvN(GyyI-5@P7#PU~mu0cfCk(L%R82)uv z#P>wn75vJrBcUsjDn8in#_csIR0KaA&R3|8d>pz;aYu0`fTdr?3 z+x&*#_hD(dVo8Q#dRU*(uoQIyoK!q5K2cSfYq;vYXtI!u>aSl8z+lg?>;cz!m`q-T zK~Z`6TvZ9JNkL;1TUpAfhDYKwQUC*EpaY$tcQ$J&vH2u@klYKkPyF@iQ*`PMUu$^! z7YKIKT`{^XXG7=*62H)YZOIybb&W?)DtpmqK9`$@n%47jKq$Ce(_oUblRaHEhw)l_ zld5f@5|mX^HQC*m(vlQrrXsLM6)y>lVpxPBrAVsCAk0yZF32i#6GkGzt-z3$jY|hb zpE)YAFIeYSDJX+Svg;*7Fn4Uh=TlXI&(@SJO+OeOp{J4Olb>I7SL<$%jeVJB-$Kl8 zWP>u@Z4uANPO+A&w7F4C54o=U@S)W*p4<~8f|J^Th8iJ*^|Q4ItLuJ-yFZM7kfhjfP>j=y zs{mJGt1A?-M^^8N;L0_Zp5|9fe6mGL=J*}Hg*yAS%LXDKZnL7WZc|?W)=A*rDwuNH zcvxkbC?ECI04S=xo4b@MA}5pV4^(YILwk8?u;fL%D40_V)R&?Yi9AzAp02kU5A(PL zNeZp;74~T0J-*bapWGF~@4-I()}^7}i#GQ|B3d)kOX5ptsUqVpG2TjlAa8<~bF~{w zpqsoo{iMX}cmlSa*Y%P`MbqN`nmyKyYq-%gbzi=5MUPl9jD_p5)F8*cPF^ABqn3S< z{QB^yiCHmN0#Ho=;v4d|yg;s**tc~|A?j8Z%CEPPH#(y~hunn+herkZc}YCCM^9F& zLT4das8aCuD_^1%X>`J3X1F^X&$zTUiNDzIeTk6#B-SfXWsU22{sXzTYGA$a-1z>1xpXuV$=kB=`=r0Fko&{*p zm;%Ofk^m!SD>{H#Nd=hVjlv#g8m_0R=@Vd?me_gvJBWCrGAJ57bpR_;yX(KMX}BF7~acU;`3 z*{{p+(U}p=8pEOmJM9@lk1*muY=8nJn5eH1`BBj*|K@&*!YkkLMK3c@TB}A7?Eq&4 z=6yT;FS?Va;`oATqnGV{HB20*wLBxkZ;du590BiZy2CnHi%Ui`bqKu8ZL`W`vua%A z$)XMK$uwvAyBWSGZ>X-bhC*V8LW)i>RqzCo!*!FxwZ4q-orrRsIflISPA~|OZ5(W^ zo7`@Y`m_Tf0n?xv<96Sml>QR6Ov6QKS1f%$R#BP#)%T#m1x&zn<&Y*q7t*6Rv-68x z^8}C?08>R;=2l9u-jrW84tv$Twb3zOu@B%^b4S5sThr+=Zvb2A9q#xXE=X#r3TY#$ z>~Ky^Ge2fgQZ{QQZbJ7I(Cz-j-|)JxUFU=uX*i~u-KrQRIdtX|4JxEFzjacD1S%rg zK5~uK1c`cbcXaD^{c|KF^X_64-twsRo(d51>k0@;paFH^mTJX^96XgO^^{)jdpn>p zD0&4FP_9ze98%_;W)acoZ^kgO^SJTFc+>0w?33&mvExoA6+3R(6%a!#p_dBYI#Dk` zUSidDl-mL8?Ap#X95zh5e1z|Ns?>gT(fAV1rCE(324dd5c=mC{zJ9=c=rUi42nvLT zE3)kQQU)_7t)cF?*F0K`c(fTbn<21`5pRj=jA0G9tQ|&qL^sy+kr>tO;$5OA$@ z>_ZkmUsib9hF|?b1qN4J)E{5HKUP^sL_W;IM3RFmU4kKadLGG9!aj zWrNOYgH+3d-O$*$fk8P;Y!-Vkzfx?Z>cdYgsdMFy5lB&s4g%q2*v?G!rp( z)@n>7(*;+CAyh?lRd_Yo#D#0%fQ-)031E+YV^QXmSeXVR_I6kksQ%c2fZk%p{QSEiZ zvJ5OMu!bv;9#wzjQl(Hxyi8C+)!HLc)M0Y7Hwmk)Ry6%))8;1G_*8OGj}CQ{u4)`C z@CnME^h{Ks8!DE>6_ywdId8Pz*d{IG1S$e$d$b+o)gg*K(sXT0YXc}sC8E|V12{^XqIDmk zEJ+m}wuwws)HACwW-lr33%agob;xhOUxe0eH+A&u^di#a=+N0IO~iQAehI&mi2FdZ4$a$Mz%=d^Ag>KjX|NEM5Ws`5P$aa;Q~m0bjy_5 zou7;?7y9m21=oGo+}{QD6--J3-md)=yi1c3yc7~q#>xd!v~Y_Z&ZDE=)aE?$UQ@21 zIa>VWSQbe$)68n2p^n#v88TByu1YHMJb{=Y`%ac}?b7^E^bJ*WvW`hPvZEM(YJrp3 zlOXbDU|%xCyt=G~u;m#D43PMb;aP96of>^aauo0TmRdAa?Y;FnCxSMYhYyA zBw1ZXbN?{SqGPara1k8EKtMgk-f z!={6I&BhpT0xPlO)@?^!6)wz2NFp{1T2mpp2PW4Y66Ni7s~vT34zBff3ZHena7@mT z4x|F0@=gtdXz`Bqd!VFaUw;#-EqhPReqpy`A1VN_#O-(S&6snXsc)Pd#)ShM@&8^kFI#hL)S3~NQVkGqkUPzL8Bv^Y!y=LNl$4`r7 zjTu^i`R*j!XZ+M)=FR8f;DA#kcPM1Yn8G0km1}TqZj&7gkDIGKy$!bxj?ZPDw*L!z z@7PRgm`%{qRne1H?&nK-I>*kY0kcHZ=IqlzsY1t-W)Eaxjd@3Xh#R@P`$Uke#s&8* z*-2fX(7mXH!uS;R#*d_9LDKF4Hc{V`07Ot56@Gwcu&55pp5)S_^Ri z7Hp+%Yc8|w-4)`dZ4c4>g_`r$ADueOCHXP*yE3$VI=z~T&fS+i=4NmQf}3}E<{P-@ zm$R?W2&;Vwt7kX8C6;+o`u5BQ_Fkn-g?~mxgVe|Se+3wBCM7SA4|>IP{K! z=hrYN&6YHXE-EcvZw0SDd2D)rH?)O*?Rg-fdLKjq1TB}`XX6?|_I8BL{Q1S#aNq&d zh->iqXdZFTcY)S9Tfxap!p*JhAF@K4IW)5yoy=a%t)IFQ*MGKS8;EbMY?yUvgekgD z3VT8?D-=Y^%8hffAx__^GZt9^p5P0y9x=Yz`5qH_h#^(g%q)+1ZV5AGv_qQvic!q>fKTsJkQ5j3X^*FDrvgx8X!gx3l)f;Mct7A~jt z6KnXI6+ad6KTmrMq5~x=H!yyUljY>7bX*Vhp5RmNE*7_jCQcWx`>|N6$x8yb0t}d z$YlsKkk`+ij8?QEvYXIWmSe$96nu!VjmhuS%eeMegh-Je%|A=D{Y{Gw^Gm!p#^YjC z>yMS?6pM@?#cxu9C`Hg?CyLXE%m!+_SWacdZ{Cbo zNc=|*>3(yb8w=JO8lZGPwM0#NT&N)PM2dw`#M2{2_@3#_>3HPOQqZhQbfvk*gxoa6H?II6#3@xKflYJ%H3VN zN5mXIwa}bwJdy?6L3DyLDJZHaccsih)Hwd@4PFUp_-pBtgwbc-%sB?RFx7U7wvOIU zE0TCs5LkE-Zy&aU=qhEBcNA0p3fMt(mon-5s}C&OtU|1w&^y8!RA@~FyN^C+_=#Fd|97*lZc zE-3jdsxhIHLLq6E0g17WlR^<`o&k|xs8C88Zt$=pjugEmq@@Zgi|@LssbbfYeyCDeWKC` z9Dy)2Mw*#kfB-cHOCGrpnLjjBnu}hT0DSFrM>CV2#8})d(c)9vJ6!udD(bD zri{GAeeof(0Y5-rrJ@|CZk5wW+OdVEvyyr>-*wuq!TLnzZD=gYAv*xs$$I_mdpX%u=!0?Zg3dBj4b@z8B)9C}s)>=@i0 zM1ifp{rw9w-UH^*&Zfn!HuJ-innhSG4F!OgB8IJbPvH@g_+Π_KqfeJvv)i8u3j1neJ2jW3e{uDT!VB zR(Cf~*jzJtn@bekE!~E&sFNKPJUzeIsADFteL;fM{V+ou>);||PEv9o+@IR(!p3gR zTQv1^Xcn7B7`XzCC|^UmR%XCVDk+uA^ELsO!Gqj_u4}`>SxVeRciM#2%?^H+^vus`^+oHMETQ}A-XPt8-ZjYhepTg6&122;)0*(D+oj*f=AqPhpd1?k z0agD$j?ML046J{#LXQ~P02#M=J{CUz~f1 z43WO>h8KH-8YD>v$B&0Sls&(hw)YT{WwS85Ep*I^(VxG=4|=|^@S~}|8c!Y?NKLG_ zXF%jP8SErak}x>YD&zC`+lR|AM%8py>+6%0&6wx;C+}W4vF_W^0~4a(x7rr$X!vW8 z^97vucz9@R@wBq40J_{mvnl26>hyTmEHFRF?5;f+C*+ zXQ3Gxq?||~A~OI~38)YgeyFW_3hh5^pp6R+bY!q^i(T6k(j#3a$>@z<|t&pWsnrH@e?E?~FtR}b1 zLqBbIIK5m0pT8F+3u~(6+V>d3Cm*w6%@-buoF!{uTxKeDc-+xc0S@4{mauCGdcG{n zi5);kv9xxRp*^Se{TAu>K59+qKFLKyKtMI&#S0Pq z)4@nR6+qDAYXF(j!;R_qbffEpW`J|E)F-<5DLbj>Ts}=4cxA(=rSnK9iQHa6n6lg- zJvkHcIq7;h8JDwno|B*Jy-b=KS`^S-*mT~!VT6wZiZA^iMnRqr=7~ zuGfFr2`SmVvS-#T~>v-v&9YFlO=H*Vr7J3hB9{)g>{evp|GjjeOJAXd}2nhAnETn&WslK1J_^+mN z0}Eq3P_DYa={8seSm+P2whk7>{Vn|0RX&WJU5x&>{Li73@|(nFx~G!|6WHV{|Lkld&}bl0fG1*7QHYMPFA$%;NQak0%Cle Aod5s; delta 7923 zcmaiZ1yCK^wk;Z5gX_i#4grF@2M?~nePe+jfyP2$<7@~7cXtmGf(6&$?hqUT8;3{E z`R~4S@BgoUS9P!I)ob>cYf9CeYYggq*TGVMjfg}5hl+{{cik3IjYR~H_%uQp;iZ9P z&GQxEoP$Z(pe&T|&7qTF->z5RgcB!(1FEG;lRq8?cxP#*qt!>v#q?S^d>4r@vNEzq z<`xdNdJo0f$mMU5*-w|hOpMYG)QkGCQh=PW*x-Q-;rYtXATN|s?#zlPQi}}tc_Jn# z>p01Xc^VWs^!RpHg!sjfM>D6S8KOBL;YJT@z)iG*x^mEq@CAvOXq$Q~*4Y>p3}v>C zJ%C$73S0c@^Y)Urj0`~8AI1D%`%6Lc!d~~cM$(0JW%;sSqe#(dYm%c+2QtCS-@Y=X zrd)61U)N{dbW0~=w;CEd;9Lr0hMboVHB(K}sU7fn&7&s%vKZP8A9;0~y8s4cLXkz^ zw!hppvA6^^Apda0#irY)QKQ?tE2t2ZKvs0f!{%iDyTtQ3m_J<7%T*6#1Ucr}(d z;GvR0TN-&IlmueH?u5wTo4&+P;3{P+IRzhsk%)X^mg87!=CG8`v6Xh%6J~JJ--DV} zXSK0ZL>b6Yk)$tPw(0VQS5W~`fp-xjsOCyF9M{A#MkDUAeRevBAk5PL6H5#xe6kxPHf#VP|87pF<=9z|tjzhV!gWNmDg# ziASm7l;-6Yg$;;<4>!V|Vi>#VODM6R(^)b1ZRK^+9L`EXcTX7>ma9cd><=?EB6!JX zhl}^ut#<Z7nD87MDASHx{db=yY%CVuKB^VN`MvF|mPOv*&aVtC#V+6-YBJ*Hh&C67VTvqM7D zkWUQr(nVOAR6Q;B1wG`^YA0?f3fVFxDB*n$_Jtb$XJYqwHW@}zZ1QLMY+?v5@EuOf zxi@bGn_VqCsPmzk1wi;64c8`vtTJ9Q>)7YA@CW@_?|U|?jxl9l*UhQa4b(TWV|hz) z^yw{)U8xIS^v#IxRQgRWjwlnu@A0n^F^iv#khLp9j$bgnovJB|QZsMIJrG8PSJMI3 zsSzN2&lDR;xmuZu!8CmHWoUHaj7{y%+V;;>r$2q5)58c*gaAqfSja9+mKH1U8(#;b z(l9}?W=3qN7L3X>ibM15Z~%{I`YW0WU*q7$|&!GJ}vkJJqRI&dI3lG$qQ3^nBXPzcK zA;a#}5Wr-ToCWrf^siMTx49@WS9|<}9eF<+02*k1YMvn-KCg?M5plFXEK?(`qsnN* zX8H!zvy~AX#`3fbHu-8vs*7$77)L-Qzl5mf zvX(AXr>Zf_snzc}Bq+1*e04JWz)?ZbOKk!cvK>$jh% zAb$pPBlo6^lq~DJ6PKZbSR7NcJ4O+D-wT+{J~)RxBL7KOniVu60wg#%U(El=R_Ifb zLb)(e07Lg&0ixcO%QxyGhhlY3C!$+kEQhy=6{@4EQ{Nn4fO@sKawI+DAI{yod-(A; zq;(mtXgVfv(jy-5NwPbrol(?;$GP0T0F~tRp%oe{*3tXoEBnAhvj4AN2HoMuxG~q~ zCO~1_L+^=curARz8DFKZ&#+rI6HG?mWK0lsQD^d;2+H|n((Yk@9TWMVV9puFyI zH6bsL+VR~qq z`_{TZv0$T3T>=cdg0d#;&}-QG(b&cuV8G9KTqWPwU6a8NGJH`l(q(tONWsE<85 zx|Z4c8t2bJ$b z9yJv7b#R+UeO+8t-rp_KGpWT)=JFQ;sw0sen^bzw_F4i*~{#Jcfx55HQ~vG{YjQyK+@T6rkg_Mt&%&{VZY4zb$}O7{7CCGC8)eQwLA7;O z?N4P1q6at1Zz$~s^*W_XyhAWSl`&R#7B}qrLjz(Kf64cjvUWB4@wXY*(h$4g>G>d!kaHjUunM0sF}XBK&C!XPwtay~&N^~((b!if&mUU+@5I-SU8yA- zIPv&pAxg8#mKpklA^ zjlFJ{pSn3b~L z{&wo27L?rkOi4c`gP3R_P6e@P>)dvEM4{fD>;~z`=xnhmEY}%6c=c>EFYMjD@rRjr zk!L4$sL{(f@btt-v4dmrPJ1KcxNreAHM41b zDrH0*hajJ#NPXv8$@q=!W{sZvE0e^pYUj`>8Z&%Th*Ea3gwVaD__EQ5fMpww&$>87 zCeL4#&aR}_7?uW0A#|uLwyk8cs|>ftVfew4ZHpscAi&cVCm7A9B{NO6+=nhdEl?wA zJeL#V5z!Z3sa*|R89=UX;;#&BL*)c`bnx{3SZEkrA^0y7l5@Ke7(O#scRsKA5r<`O z6M;X?PRrEl+0*I3+WC@xrMP+EHEDFrf;J zC%EOoqGKYjok8$8gIys)L0TGCCX$ex)2;lM2mbP zO}YBS#WIraImMUtK1}&-ff{BB%loxH^jCFHc8Yd%?QRa=)YiMUkIXJn*N@>QxJ<-w zMzb~w`jYUrfFW5Q&DtbLpMdL3mPB3$ISlwf$gwu`tg)^KZ=W$<*J3JyBD8tHtZ$Iw z*#ob?RuE~@TSkUtJ2Y#Zpf5>piI5|6ZK>0VRluk}5Jqd-j$UMU>+gXG64(VUTm@*{YwWw1v~ z;n^{{_T>j!=f*wLJnOUH^#1xh$BMvKrO}rfri;=UCZ}!T3NOh>uW<3ji_F?|Y|~+3 z?$CgFKXD(ejbr$>8K-JG+KGD5JN@0W%PV#sqOn+)>Yu?6B9LLq&G7WyE;OYcIpS(S zF~O8rgd5wn=aWl~iXRpAr*0q|N5C%_-`IWGb8b1xxaOy9i}4wboJ0Jn`KF@_gINxN4V|-epW1H3VUvv@ z)2!sNkMDXoF;@2BF}xLwu;aYD{k>1p+hH8y$9j`#u|`hV1n3mi!Lz_En&0+I>Vasy z^nqn7-DdaEC`97Wh%oy7n%CjO2)YWOWmp2eE7{I#_r)db=Bb_Pzz8+{>Ma52&n*NY z3a(y7m`D(9W%L@C9rp_{YE$yy${o@Au6Fo8)ejxoEK)xz99)v{e^fu3aQ}TlSqSNX zR0X0>E0>k(zs&3#?JdxZKlJxyxHYY4yIL38n>JL1unnN1f`ms*#j$n~?zrUVYWk$v z{B9--TSoD6I*#V3#rge@d!n9mVuEiMd#YC4gJqk}g9;ZOGFS4{X6{Ymg*qlqyS}#_ zwtrQ8jA;ReOuDWF^;^L67FbCAPCzrS&%L`iHx#gW`*^kr=AUi5 z1~;6~5?I?E_Z;mt0Hb~xF}+{n$sbP_XY$UQlFGVt!#!N~h5Ns?Q4CZ!^0t1r;*Dw) zzT*3^C2eZ$ISCzaF3T)wz>%(`Eub=M|Zb(=W8$Lt#J>O7s} zb?q|y^)<1*O|=${FRjrfb#(A(>R{u9By^N?V4l+2Zs1d#ip!=Yb-FRYc_aF1ocMA! zb>sJXdE4eK2Sab96;=}Uu{+%sUsrrvDjGiq;63`CA;l2iLtUAHT9_v@o6*n z`UUCUwl(o-0r&bYW_}meC3Tb^HoQM)-3H^%ydyC0orA`pwez-jjhj_P_2%4T;y|2h*R{qJ z(_8c@zQNWP*>i|ZKlGm1iD}P_#dU(O3Ma( zl}%qn3Hz&g$A%39=R`(VJl`_e-pUetXiBr90d>+eJ%{KH@Z!7RSnA@eafrUIL&f+k zhj4F&y)_g@5mMRlNMsgUQ#)MZ%y+%gsCt0(qIBF~-xGSj&PHUSUe<|y6X0i#O`WRI z`*bf4y8g0o*S?)ihyuN8GU&+VE=v?5%e0RQtD_xeT^-s`Y!DBIt8pPSk77%{a(bC$>mw%P>6C%mVI7b-4d%&No z`2iXo;f&Dp{tfFlG(l;Ra@3;9QkrNyyq0xNb-Um z8sup=@43o%&ymQO-u=j<)2}mcWSKHQxFJIMbScnuqkXeS^ttUTq+bvY9d zIaZuvkcA)Rc?5k%ixC;Jc+BZmY zrpY9N3m=9mgFuU@5l|$-3lhRb3`6TiFh~57q{zovPK-4YmxQ*Nuj&P5ry>S;IHC3? z2D;-reS-x5U)gr|_tC9JQYL#LD70{RfeI3qAaq>9uxBzT=5XABdJ>MH-#o?J`5tXR zbnAB?YE&E7ti*Qi<@9at^u>dq&KbhKALxJ8IA+=K zIL2V+cBc=c>@x5~0trNmix!3{1D}I192*a`z^Koty2OA$}(r{Yn9V7nj z1HrxzcgsG&V)L@Nyvf@(oc~?tQ{Vl2*L|Ny3h%%RG2Fks3JmogmtXc8S6=p3Eu;Le zzPcuvz73vuh7rM~=QEcXE9@VaK8cY5^QG}4^&2^=<#xF6*l%e&XLobHjc^>z%Ug*s z=Zt)Me-!@Bo77tmQ3*4Q?zP5_buw*G5r*!D;Ayz7lw zw2a><#kaw6{?6AH^(#wu+6NyKmwqjXr(r=xtZ-6(oJmSkyyupw;O#cskxqNZ-K}94 zmf5VfQ5V4-J!nB;qAX0kWksBurX_#<`m3cemZtDPW5sT{G1I~|mmtM`W_0SpRDZDV zm6%>_$HP#=^a|F&9X#?Z(V0OLFjIG&vJpCk_9EO~6M1z-l|*Qh=rKV^=Ta;_-GkbZrde7F-p{R7bTHY$4Si5vo zcDT(A__oh#y0mvJ9{n-m-O4Sm1*$XT2kOGAFi(>!Wotn_*R3YuzDlk z5%pWG*EgKcKzZAF)oU|nQsI0o_}Y~9E|rEAxW4oqv_n{Ej84$V57cra^O{-my?Xnl#|?zS&7+tLd)sayu&iO4+Q zNHbdJ{AH#=#>@FQCYf*`TXf}+W8PMj)TKD~e0%?6pwvV5>eO-Y?>15)nDiysZFNrQ zLWl52YZht6g!x=A`xj8;az;&*F{|_pxa^V3nQ`<)jfy1B7$$ESi`EeuOXYfp``IzP z&DMcLq~ntV;tH}>eicFtD{9T+4M{zMeyTnWzAfjXXg9C<7_BxCBD>JcuT2W9`V=7? zWq;Cphu_QY(7`gHsyKhii!q&c|%YALQspMo_ z&9W+*FW)@Hj1R_hkrp~@`OGd4L8cUzAbWZp3lkb z*w6z$$f8Ih5QYs6Sv9RhDP5dGOh4-_&J!nVvKNgAzFpmX>zKl5_amKeF?02@XRwBx z?{l*;A&Qn|&>L{z^*j;j1BMj)cURUZ9MJKEN9 zKrYBjBs7wB(eAlcdJ}V`#BJc{#KP4tZ$9}RX398$^zM+dz68~yU8kFI{x-~AA;Z~0 zSSbOH@lW`MM$eW}&edPhk=7OGV}e#wH-4HVEvi=*Fis&_k|$n z&&)rYUy}cBcmEzSp`3(-PpALLZ0fJ!5eVS^#q?_aGq3+$2#4@Vhzxr80`o82yBFjL z_aG>UhJy7!|MAbbBKS98M(A4>I+H)j{nHM3|F#B>9uCgZ*+SjT*~OjH!r9I050Utv z5TAKaiuazxK0KN7$yoom;-PCS)PL>2L2hK(^69(HGdQ^C|5yPB*USiAV Date: Sun, 11 Apr 2021 23:54:05 -0700 Subject: [PATCH 457/569] refactored number of strains --- covasim/run.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index d813c1518..a7508ab88 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -970,11 +970,9 @@ def print_heading(string): else: scen_sims = multi_run(scen_sim, **run_args, **kwargs) # This is where the sims actually get run - # Get number of strains - ns = scen_sims[0].results['strain']['cum_infections_by_strain'].values.shape[0] - # Process the simulations print_heading(f'Processing {scenkey}') + ns = scen_sims[0]['total_strains'] # Get number of strains scenraw = {} for reskey in mainkeys: scenraw[reskey] = np.zeros((self.npts, len(scen_sims))) From 7bc5a0448c6bcf2de175f2f99e968ddc924cd36b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 00:33:44 -0700 Subject: [PATCH 458/569] working for now, but needs refactoring --- covasim/immunity.py | 29 ++++++++++++++++++----------- covasim/interventions.py | 28 +++++++++++++--------------- covasim/parameters.py | 16 +++++++++++++++- covasim/sim.py | 35 +++++------------------------------ 4 files changed, 51 insertions(+), 57 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index b3bd6d733..4ae312a7f 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -226,7 +226,7 @@ def initialize(self, sim): # %% NAb methods -def init_nab(people, inds, prior_inf=True): +def init_nab(people, inds, prior_inf=True, vacc_info=None): ''' Draws an initial NAb level for individuals. Can come from a natural infection or vaccination and depends on if there is prior immunity: @@ -236,6 +236,10 @@ def init_nab(people, inds, prior_inf=True): depending upon vaccine source. If individual has existing NAb, multiply booster impact ''' + if vacc_info is None: + print('Note: using default vaccine dosing information') + vacc_info = cvpar.get_vaccine_dose_pars()['default'] + NAb_arrays = people.NAb[inds] prior_NAb_inds = cvu.idefined(NAb_arrays, inds) # Find people with prior NAbs no_prior_NAb_inds = np.setdiff1d(inds, prior_NAb_inds) # Find people without prior NAbs @@ -260,10 +264,10 @@ def init_nab(people, inds, prior_inf=True): # NAbs from a vaccine else: - NAb_boost = people.pars['vaccine_info']['NAb_boost'] # Boosting factor for vaccination + NAb_boost = vacc_info['NAb_boost'] # Boosting factor for vaccination # 1) No prior NAb: draw NAb from a distribution and compute if len(no_prior_NAb_inds): - init_NAb = cvu.sample(**people.pars['vaccine_info']['NAb_init'], size=len(no_prior_NAb_inds)) + init_NAb = cvu.sample(**vacc_info['NAb_init'], size=len(no_prior_NAb_inds)) people.init_NAb[no_prior_NAb_inds] = 2**init_NAb # 2) Prior NAb (from natural or vaccine dose 1): multiply existing NAb by boost factor @@ -386,7 +390,7 @@ def init_immunity(sim, create=False): return -def check_immunity(people, strain, sus=True, inds=None): +def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): ''' Calculate people's immunity on this timestep from prior infections + vaccination @@ -398,6 +402,12 @@ def check_immunity(people, strain, sus=True, inds=None): Gets called from sim before computing trans_sus, sus=True, inds=None Gets called from people.infect() to calculate prog/trans, sus=False, inds= inds of people being infected ''' + if vacc_info is None: + print('Note: using default vaccine dosing information') + vacc_info = cvpar.get_vaccine_dose_pars()['default'] + vacc_strain = cvpar.get_vaccine_strain_pars()['default'] + + was_inf = cvu.true(people.t >= people.date_recovered) # Had a previous exposure, now recovered is_vacc = cvu.true(people.vaccinated) # Vaccinated date_rec = people.date_recovered # Date recovered @@ -407,12 +417,10 @@ def check_immunity(people, strain, sus=True, inds=None): # If vaccines are present, extract relevant information about them vacc_present = len(is_vacc) if vacc_present: - vacc_info = people.pars['vaccine_info'] vx_nab_eff_pars = vacc_info['NAb_eff'] + # PART 1: Immunity to infection for susceptible individuals if sus: - ### PART 1: - # Immunity to infection for susceptible individuals is_sus = cvu.true(people.susceptible) # Currently susceptible was_inf_same = cvu.true((people.recovered_strain == strain) & (people.t >= date_rec)) # Had a previous exposure to the same strain, now recovered was_inf_diff = np.setdiff1d(was_inf, was_inf_same) # Had a previous exposure to a different strain, now recovered @@ -423,7 +431,7 @@ def check_immunity(people, strain, sus=True, inds=None): if len(is_sus_vacc): vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) - vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] + vaccine_scale = vacc_strain[strain] # TODO: handle this better current_NAbs = people.NAb[is_sus_vacc] people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus', vx_nab_eff_pars) @@ -439,15 +447,14 @@ def check_immunity(people, strain, sus=True, inds=None): current_NAbs = people.NAb[unique_inds] people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus', nab_eff_pars) + # PART 2: Immunity to disease for currently-infected people else: - ### PART 2: - # Immunity to disease for currently-infected people is_inf_vacc = np.intersect1d(inds, is_vacc) was_inf = np.intersect1d(inds, was_inf) if len(is_inf_vacc): # Immunity for infected people who've been vaccinated vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) - vaccine_scale = vacc_info['rel_imm'][vaccine_source, strain] + vaccine_scale = vacc_strain[strain] # TODO: handle this better current_NAbs = people.NAb[is_inf_vacc] people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['symp'][strain], 'symp', nab_eff_pars) people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['sev'][strain], 'sev', nab_eff_pars) diff --git a/covasim/interventions.py b/covasim/interventions.py index 3e15b8259..4a76f1432 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1223,7 +1223,7 @@ class vaccinate(Intervention): **Examples**:: - interv = cv.vaccine(days=50, prob=0.3, ) + interv = cv.vaccinate(days=50, prob=0.3, ) ''' def __init__(self, days, prob=1.0, vaccine_pars=None, subtarget=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object @@ -1235,20 +1235,21 @@ def __init__(self, days, prob=1.0, vaccine_pars=None, subtarget=None, **kwargs): self.vaccine_ind = None return + def initialize(self, sim): ''' Fix the dates and store the vaccinations ''' + super().initialize() self.first_dose_eligible = process_days(sim, self.days) # days that group becomes eligible - self.second_dose_days = [None] * (sim['n_days']+1) # inds who get second dose (if relevant) - self.vaccinated = [None]*(sim['n_days']+1) # keep track of inds of people vaccinated on each day + self.second_dose_days = [None]*(sim['n_days']+1) # inds who get second dose (if relevant) + self.vaccinated = [None]*(sim['n_days']+1) # keep track of inds of people vaccinated on each day self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person - self.vaccination_dates = np.full(sim.n, np.nan) # Store the dates when people are vaccinated + self.vaccination_dates = np.full(sim.n, np.nan) # Store the dates when people are vaccinated self.vaccine_ind = len(sim['vaccines']) vaccine = cvi.Vaccine(self.vaccine_pars) vaccine.initialize(sim) sim['vaccines'].append(vaccine) self.doses = vaccine.doses self.interval = vaccine.interval - self.initialized = True return @@ -1284,16 +1285,13 @@ def apply(self, sim): # Update vaccine attributes in sim sim.people.vaccinated[vacc_inds] = True sim.people.vaccine_source[vacc_inds] = self.vaccine_ind - self.update_vaccine_info(sim, vacc_inds) - - return + self.vaccinations[vacc_inds] += 1 + self.vaccination_dates[vacc_inds] = sim.t - def update_vaccine_info(self, sim, vacc_inds): - self.vaccinations[vacc_inds] += 1 - self.vaccination_dates[vacc_inds] = sim.t + # Update vaccine attributes in sim + sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] + sim.people.date_vaccinated[vacc_inds] = self.vaccination_dates[vacc_inds] + cvi.init_nab(sim.people, vacc_inds, prior_inf=False) - # Update vaccine attributes in sim - sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] - sim.people.date_vaccinated[vacc_inds] = self.vaccination_dates[vacc_inds] - cvi.init_nab(sim.people, vacc_inds, prior_inf=False) return + diff --git a/covasim/parameters.py b/covasim/parameters.py index 112cd2d61..02cb94fca 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -75,7 +75,6 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = dict(asymptomatic=0.85, mild=1, severe=1.5) # Relative immunity from natural infection varies by symptoms pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py - pars['vaccine_info'] = None # Vaccine info in a more easily accessible format # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 @@ -391,6 +390,13 @@ def get_vaccine_strain_pars(): ''' pars = sc.objdict( + default = sc.objdict( + wild = 1.0, + b117 = 1.0, + b1351 = 1.0, + p1 = 1.0, + ), + pfizer = sc.objdict( wild = 1.0, b117 = 1/2.0, @@ -429,6 +435,14 @@ def get_vaccine_dose_pars(): ''' pars = sc.objdict( + default = sc.objdict( + NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + NAb_init = dict(dist='normal', par1=0.5, par2= 2), + NAb_boost = 2, + doses = 1, + interval = None, + ), + pfizer = sc.objdict( NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, NAb_init = dict(dist='normal', par1=0.5, par2= 2), diff --git a/covasim/sim.py b/covasim/sim.py index 3f1477392..c1231b136 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -108,13 +108,12 @@ def initialize(self, reset=False, **kwargs): self.t = 0 # The current time index self.validate_pars() # Ensure parameters have valid values self.set_seed() # Reset the random seed before the population is created - if self['use_waning']: - self.init_strains() # ...and the strains.... # TODO: move out of if? - self.init_immunity() # ... and information about immunity/cross-immunity. + self.init_strains() # Initialize the strains + self.init_immunity() # initialize information about immunity (if use_waning=True) self.init_results() # After initializing the strain, create the results structure self.init_people(save_pop=self.save_pop, load_pop=self.load_pop, popfile=self.popfile, reset=reset, **kwargs) # Create all the people (slow) self.init_interventions() # Initialize the interventions... - self.init_vaccines() # Initialize vaccine information + # self.init_vaccines() # Initialize vaccine information self.init_analyzers() # ...and the analyzers... self.validate_layer_pars() # Once the population is initialized, validate the layer parameters again self.set_seed() # Reset the random seed again so the random number stream is consistent @@ -504,32 +503,8 @@ def init_strains(self): def init_immunity(self, create=False): ''' Initialize immunity matrices and precompute NAb waning for each strain ''' - cvimm.init_immunity(self, create=create) - return - - - def init_vaccines(self): # TODO: refactor - ''' Check if there are any vaccines in simulation, if so initialize vaccine info param''' - print('TEMP') - # if len(self['vaccines']): - nv = max(1, len(self['vaccines'])) - ns = self['total_strains'] - - self['vaccine_info'] = {} - self['vaccine_info']['rel_imm'] = np.full((nv, ns), np.nan, dtype=cvd.default_float) - self['vaccine_info']['NAb_init'] = dict(dist='normal', par1=0.5, par2= 2) - self['vaccine_info']['doses'] = 2 - self['vaccine_info']['interval'] = 22 - self['vaccine_info']['NAb_boost'] = 2 - self['vaccine_info']['NAb_eff'] = {'sus': {'slope': 2.5, 'n_50': 0.55}} # Parameters to map NAbs to efficacy - - for ind, vacc in enumerate(self['vaccines']): - self['vaccine_info']['rel_imm'][ind,:] = vacc.rel_imm - self['vaccine_info']['doses'] = vacc.doses - self['vaccine_info']['NAb_init'] = vacc.NAb_init - self['vaccine_info']['NAb_boost'] = vacc.NAb_boost - self['vaccine_info']['NAb_eff'] = vacc.NAb_eff - + if self['use_waning']: + cvimm.init_immunity(self, create=create) return From 87dc368f42ce1a3abaaf6cd561b1f95cc91217f2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 00:35:29 -0700 Subject: [PATCH 459/569] tidying --- covasim/immunity.py | 4 ++-- tests/test_immunity.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 4ae312a7f..bcb89fc9e 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -237,7 +237,7 @@ def init_nab(people, inds, prior_inf=True, vacc_info=None): ''' if vacc_info is None: - print('Note: using default vaccine dosing information') + # print('Note: using default vaccine dosing information') vacc_info = cvpar.get_vaccine_dose_pars()['default'] NAb_arrays = people.NAb[inds] @@ -403,7 +403,7 @@ def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): Gets called from people.infect() to calculate prog/trans, sus=False, inds= inds of people being infected ''' if vacc_info is None: - print('Note: using default vaccine dosing information') + # print('Note: using default vaccine dosing information') vacc_info = cvpar.get_vaccine_dose_pars()['default'] vacc_strain = cvpar.get_vaccine_strain_pars()['default'] diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 79e67548e..ca451bd5b 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -213,7 +213,6 @@ def test_decays(do_plot=False): - #%% Run as a script if __name__ == '__main__': From 408407c70706eb22724169dcf3e5227381f25422 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 00:47:48 -0700 Subject: [PATCH 460/569] remove total_strains --- covasim/immunity.py | 19 ++++++++----------- covasim/misc.py | 2 +- covasim/parameters.py | 5 ++--- covasim/people.py | 8 ++++---- covasim/plotting.py | 4 ++-- covasim/run.py | 6 +++--- covasim/sim.py | 6 ++---- tests/test_immunity.py | 8 ++++---- 8 files changed, 26 insertions(+), 32 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index bcb89fc9e..9534b530b 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -88,6 +88,10 @@ def parse_strain_pars(self, strain=None, label=None): def initialize(self, sim): super().initialize() + # Store the index of this strain, and increment the number of strains in the simulation + self.index = sim['n_strains'] + sim['n_strains'] += 1 + if not hasattr(self, 'rel_imm'): # TODO: refactor self.rel_imm = 1 @@ -104,19 +108,12 @@ def initialize(self, sim): def apply(self, sim): - if sim.t == self.days: # Time to introduce strain # TODO: use find_day - - # Check number of strains - prev_strains = sim['n_strains'] # TODO: refactor to be explicit - sim['n_strains'] += 1 - # Update strain-specific people attributes - #cvu.update_strain_attributes(sim.people) # don't think we need to do this if we just create people arrays with number of total strains in sim susceptible_inds = cvu.true(sim.people.susceptible) n_imports = sc.randround(self.n_imports/sim.rescale_vec[sim.t]) # Round stochastically to the nearest number of imports importation_inds = np.random.choice(susceptible_inds, n_imports) - sim.people.infect(inds=importation_inds, layer='importation', strain=prev_strains) + sim.people.infect(inds=importation_inds, layer='importation', strain=self.index) return @@ -145,7 +142,7 @@ class Vaccine(cvi.Intervention): def __init__(self, vaccine=None, label=None, **kwargs): super().__init__(**kwargs) self.label = label - self.rel_imm = None # list of length total_strains with relative immunity factor + self.rel_imm = None # list of length n_strains with relative immunity factor self.doses = None self.interval = None self.NAb_init = None @@ -200,7 +197,7 @@ def parse_vaccine_pars(self, vaccine=None): def initialize(self, sim): super().initialize() - ts = sim['total_strains'] + ts = sim['n_strains'] circulating_strains = ['wild'] # assume wild is circulating for strain in range(ts-1): circulating_strains.append(sim['strains'][strain].label) @@ -354,7 +351,7 @@ def init_immunity(sim, create=False): if not sim['use_waning']: return - ts = sim['total_strains'] + ts = sim['n_strains'] immunity = {} # Pull out all of the circulating strains for cross-immunity diff --git a/covasim/misc.py b/covasim/misc.py index 97c7787f8..4d9ee1a87 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -250,7 +250,7 @@ def migrate_strains(pars, verbose=True): ''' pars['use_waning'] = False pars['n_strains'] = 1 - pars['total_strains'] = 1 + pars['n_strains'] = 1 pars['strains'] = [] return diff --git a/covasim/parameters.py b/covasim/parameters.py index 02cb94fca..17b1fde55 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -61,9 +61,8 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated # Parameters that control settings and defaults for multi-strain runs - pars['n_imports'] = 0 # Average daily number of imported cases (actual number is drawn from Poisson distribution) - pars['n_strains'] = 1 # The number of strains currently circulating in the population - pars['total_strains'] = 1 # Set during sim initialization, once strains have been specified and processed + pars['n_imports'] = 0 # Average daily number of imported cases (actual number is drawn from Poisson distribution) + pars['n_strains'] = 1 # The number of strains circulating in the population # Parameters used to calculate immunity pars['use_waning'] = False # Whether to use dynamically calculated immunity diff --git a/covasim/people.py b/covasim/people.py index c7ce547c7..eee48d7d3 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -49,7 +49,7 @@ def __init__(self, pars, strict=True, **kwargs): self.pars = pars # Equivalent to self.set_pars(pars) self.pop_size = int(pars['pop_size']) self.location = pars.get('location') # Try to get location, but set to None otherwise - self.total_strains = pars.get('total_strains', 1) # Assume 1 strain if not supplied + self.n_strains = pars.get('n_strains', 1) # Assume 1 strain if not supplied self.version = cvv.__version__ # Store version info # Other initialization @@ -76,11 +76,11 @@ def __init__(self, pars, strict=True, **kwargs): for key in self.meta.strain_states: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) for key in self.meta.by_strain_states: - self[key] = np.full((self.total_strains, self.pop_size), False, dtype=bool) + self[key] = np.full((self.n_strains, self.pop_size), False, dtype=bool) # Set immunity and antibody states for key in self.meta.imm_states: # Everyone starts out with no immunity - self[key] = np.zeros((self.total_strains, self.pop_size), dtype=cvd.default_float) + self[key] = np.zeros((self.n_strains, self.pop_size), dtype=cvd.default_float) for key in self.meta.nab_states: # Everyone starts out with no antibodies self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) for key in self.meta.vacc_states: @@ -121,7 +121,7 @@ def init_flows(self): self.flows = {key:0 for key in cvd.new_result_flows} self.flows_strain = {} for key in cvd.new_result_flows_by_strain: - self.flows_strain[key] = np.zeros(self.total_strains, dtype=cvd.default_float) + self.flows_strain[key] = np.zeros(self.n_strains, dtype=cvd.default_float) return diff --git a/covasim/plotting.py b/covasim/plotting.py index e3e1cb58a..d2e37a74a 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -390,7 +390,7 @@ def plot_sim(to_plot=None, sim=None, do_save=None, fig_path=None, fig_args=None, res_t = sim.results['t'] if reskey in strain_keys: res = sim.results['strain'][reskey] - ns = sim['total_strains'] + ns = sim['n_strains'] strain_colors = sc.gridcolors(ns) for strain in range(ns): color = strain_colors[strain] # Choose the color @@ -440,7 +440,7 @@ def plot_scens(to_plot=None, scens=None, do_save=None, fig_path=None, fig_args=N sim = scens.sims[scenkey][0] # Pull out the first sim in the list for this scenario strain_keys = sim.result_keys('strain') if reskey in strain_keys: - ns = sim['total_strains'] + ns = sim['n_strains'] strain_colors = sc.gridcolors(ns) for strain in range(ns): res_y = scendata.best[strain,:] diff --git a/covasim/run.py b/covasim/run.py index a7508ab88..186045558 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -248,7 +248,7 @@ def reduce(self, quantiles=None, use_mean=False, bounds=None, output=False): vals = sim.results[reskey].values raw[reskey][:, s] = vals for reskey in strainkeys: - raw[reskey] = np.zeros((reduced_sim['total_strains'], reduced_sim.npts, len(self.sims))) + raw[reskey] = np.zeros((reduced_sim['n_strains'], reduced_sim.npts, len(self.sims))) for s,sim in enumerate(self.sims): vals = sim.results['strain'][reskey].values raw[reskey][:, :, s] = vals @@ -972,7 +972,7 @@ def print_heading(string): # Process the simulations print_heading(f'Processing {scenkey}') - ns = scen_sims[0]['total_strains'] # Get number of strains + ns = scen_sims[0]['n_strains'] # Get number of strains scenraw = {} for reskey in mainkeys: scenraw[reskey] = np.zeros((self.npts, len(scen_sims))) @@ -1040,7 +1040,7 @@ def compare(self, t=None, output=False): for scenkey in self.scenarios.keys(): for reskey in self.result_keys(): if reskey in strainkeys: - for strain in range(self.base_sim['total_strains']): + for strain in range(self.base_sim['n_strains']): val = self.results[reskey][scenkey].best[strain, day] # Only prints results for infections by first strain strainkey = reskey + str(strain) # Add strain number to the summary output x[scenkey][strainkey] = int(val) diff --git a/covasim/sim.py b/covasim/sim.py index c1231b136..cb9014c4c 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -315,7 +315,7 @@ def init_res(*args, **kwargs): self.results['pop_symp_protection'] = init_res('Population symptomatic protection', scale=False, color=dcols.pop_symp_protection) # Handle strains - ns = self['total_strains'] + ns = self['n_strains'] self.results['strain'] = {} self.results['strain']['prevalence_by_strain'] = init_res('Prevalence by strain', scale=False, n_strains=ns) self.results['strain']['incidence_by_strain'] = init_res('Incidence by strain', scale=False, n_strains=ns) @@ -496,8 +496,6 @@ def init_strains(self): for strain in self['strains']: strain.initialize(self) - # Calculate the total number of strains that will be active at some point in the sim - self['total_strains'] = self['n_strains'] + len(self['strains']) return @@ -765,7 +763,7 @@ def finalize(self, verbose=None, restore_pars=True): for key in cvd.result_flows.keys(): self.results[f'cum_{key}'][:] = np.cumsum(self.results[f'new_{key}'][:], axis=0) for key in cvd.result_flows_by_strain.keys(): - for strain in range(self['total_strains']): + for strain in range(self['n_strains']): self.results['strain'][f'cum_{key}'][strain, :] = np.cumsum(self.results['strain'][f'new_{key}'][strain, :], axis=0) for res in [self.results['cum_infections'], self.results['strain']['cum_infections_by_strain']]: # Include initially infected people res.values += self['pop_infected']*self.rescale_vec[0] diff --git a/tests/test_immunity.py b/tests/test_immunity.py index ca451bd5b..615bc20e9 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -220,11 +220,11 @@ def test_decays(do_plot=False): cv.options.set(interactive=do_plot) T = sc.tic() - sim1 = test_states() - msims1 = test_waning(do_plot=do_plot) + # sim1 = test_states() + # msims1 = test_waning(do_plot=do_plot) sim2 = test_strains(do_plot=do_plot) - sim3 = test_vaccines(do_plot=do_plot) - res = test_decays(do_plot=do_plot) + # sim3 = test_vaccines(do_plot=do_plot) + # res = test_decays(do_plot=do_plot) sc.toc(T) print('Done.') From 77f836db94a74e47244cc63127410a5ada96a3ee Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 00:54:39 -0700 Subject: [PATCH 461/569] update readme --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 5b42ecde5..2cd7503f5 100644 --- a/README.rst +++ b/README.rst @@ -40,9 +40,9 @@ Covasim has been used for analyses in over a dozen countries, both to inform pol 4. **Lessons learned from Vietnam's COVID-19 response: the role of adaptive behaviour change and testing in epidemic control**. Pham QD, Stuart RM, Nguyen TV, Luong QC, Tran DQ, Phan LT, Dang TQ, Tran DN, Mistry D, Klein DJ, Abeysuriya RG, Oron AP, Kerr CC (in press; accepted 2021-02-25). *Lancet Global Health*; doi: https://doi.org/10.1101/2020.12.18.20248454. -5. **The potential contribution of face coverings to the control of SARS-CoV-2 transmission in schools and broader society in the UK: a modelling study**. Panovska-Griffiths J, Kerr CC, Waites W, Stuart RM, Mistry D, Foster D, Klein DJ, Viner R, Bonnell C (in press; accepted 2021-04-08). *Nature Scientific Reports* doi: https://doi.org/10.1101/2020.09.28.20202937. +5. **The role of masks, testing and contact tracing in preventing COVID-19 resurgences: a case study from New South Wales, Australia**. Stuart RM, Abeysuriya RG, Kerr CC, Mistry D, Klein DJ, Gray R, Hellard M, Scott N (in press; accepted 2021-03-19). *BMJ Open* doi: https://doi.org/10.1101/2020.09.02.20186742. -6. **The role of masks, testing and contact tracing in preventing COVID-19 resurgences: a case study from New South Wales, Australia**. Stuart RM, Abeysuriya RG, Kerr CC, Mistry D, Klein DJ, Gray R, Hellard M, Scott N (under review; posted 2020-09-03). *medRxiv* 2020.09.02.20186742; doi: https://doi.org/10.1101/2020.09.02.20186742. +6. **The potential contribution of face coverings to the control of SARS-CoV-2 transmission in schools and broader society in the UK: a modelling study**. Panovska-Griffiths J, Kerr CC, Waites W, Stuart RM, Mistry D, Foster D, Klein DJ, Viner R, Bonnell C (in press; accepted 2021-04-08). *Nature Scientific Reports* doi: https://doi.org/10.1101/2020.09.28.20202937. 7. **Schools are not islands: Balancing COVID-19 risk and educational benefits using structural and temporal countermeasures**. Cohen JA, Mistry D, Kerr CC, Klein DJ (under review; posted 2020-09-10). *medRxiv* 2020.09.08.20190942; doi: https://doi.org/10.1101/2020.09.08.20190942. From 6697e20c229abd767b576acb5e8d87be9ab1aea7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 01:01:54 -0700 Subject: [PATCH 462/569] working on strain parameters --- covasim/parameters.py | 14 +++----------- tests/test_immunity.py | 8 ++++---- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 17b1fde55..956587f5f 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -141,7 +141,9 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Handle strain pars if 'strain_pars' not in pars: pars['strain_pars'] = {} # Populated just below - pars = listify_strain_pars(pars) # Turn strain parameters into lists + for sp in cvd.strain_pars: + if sp in pars.keys(): + pars['strain_pars'][sp] = [pars[sp]] return pars @@ -478,13 +480,3 @@ def get_vaccine_dose_pars(): return pars -def listify_strain_pars(pars): - ''' - Helper function to turn strain parameters into lists - ''' - for sp in cvd.strain_pars: - if sp in pars.keys(): - pars['strain_pars'][sp] = sc.promotetolist(pars[sp]) - return pars - - diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 615bc20e9..ca451bd5b 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -220,11 +220,11 @@ def test_decays(do_plot=False): cv.options.set(interactive=do_plot) T = sc.tic() - # sim1 = test_states() - # msims1 = test_waning(do_plot=do_plot) + sim1 = test_states() + msims1 = test_waning(do_plot=do_plot) sim2 = test_strains(do_plot=do_plot) - # sim3 = test_vaccines(do_plot=do_plot) - # res = test_decays(do_plot=do_plot) + sim3 = test_vaccines(do_plot=do_plot) + res = test_decays(do_plot=do_plot) sc.toc(T) print('Done.') From 290a2f9c39628d8fb160678bf11fc9d73731d3e6 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 01:14:25 -0700 Subject: [PATCH 463/569] refactored strain --- covasim/defaults.py | 1 + covasim/immunity.py | 34 +++++++++++----------------------- covasim/parameters.py | 4 ++++ 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 9196f325b..09c948ed6 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -190,6 +190,7 @@ def __init__(self): # Parameters that can vary by strain strain_pars = [ + 'rel_imm', 'rel_beta', 'rel_symp_prob', 'rel_severe_prob', diff --git a/covasim/immunity.py b/covasim/immunity.py index 9534b530b..c6e2ec9ca 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -36,15 +36,9 @@ class Strain(cvi.Intervention): def __init__(self, strain=None, label=None, days=None, n_imports=1, rescale=True, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object - - # Handle inputs - self.days = days + self.days = days # Handle inputs self.n_imports = cvd.default_int(n_imports) - - # Strains can be defined in different ways: process these here - self.strain_pars = self.parse_strain_pars(strain=strain, label=label) - for par, val in self.strain_pars.items(): - setattr(self, par, val) + self.parse_strain_pars(strain=strain, label=label) # Strains can be defined in different ways: process these here return @@ -81,8 +75,9 @@ def parse_strain_pars(self, strain=None, label=None): # Set label self.label = label if label else normstrain + self.p = sc.objdict(strain_pars) # Convert to an objdict and save - return strain_pars + return def initialize(self, sim): @@ -92,29 +87,22 @@ def initialize(self, sim): self.index = sim['n_strains'] sim['n_strains'] += 1 - if not hasattr(self, 'rel_imm'): # TODO: refactor - self.rel_imm = 1 - # Update strain info - for strain_key in cvd.strain_pars: - if hasattr(self, strain_key): # TODO: refactor - newval = getattr(self, strain_key) - sim['strain_pars'][strain_key].append(newval) - else: # use default - sc.printv(f'{strain_key} not provided for this strain, using default value', 1, sim['verbose']) - sim['strain_pars'][strain_key].append(sim['strain_pars'][strain_key][0]) + defaults = cvpar.get_strain_pars()['wild'] + for key in cvd.strain_pars: + if key not in self.p: + self.p[key] = defaults[key] + sim['strain_pars'][key].append(self.p[key]) return def apply(self, sim): - if sim.t == self.days: # Time to introduce strain # TODO: use find_day - # Update strain-specific people attributes + for ind in cvi.find_day(self.days, sim.t, interv=self, sim=sim): # Time to introduce strain susceptible_inds = cvu.true(sim.people.susceptible) n_imports = sc.randround(self.n_imports/sim.rescale_vec[sim.t]) # Round stochastically to the nearest number of imports importation_inds = np.random.choice(susceptible_inds, n_imports) sim.people.infect(inds=importation_inds, layer='importation', strain=self.index) - return @@ -359,7 +347,7 @@ def init_immunity(sim, create=False): rel_imms = dict() for strain in sim['strains']: circulating_strains.append(strain.label) - rel_imms[strain.label] = strain.rel_imm + rel_imms[strain.label] = strain.p.rel_imm # If immunity values have been provided, process them if sim['immunity'] is None or create: diff --git a/covasim/parameters.py b/covasim/parameters.py index 956587f5f..0da22291a 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -352,6 +352,7 @@ def get_strain_pars(): wild = sc.objdict( rel_imm = 1.0, rel_beta = 1.0, + rel_symp_prob = 1.0, rel_severe_prob = 1.0, rel_crit_prob = 1.0, rel_death_prob = 1.0, @@ -360,6 +361,7 @@ def get_strain_pars(): b117 = sc.objdict( rel_imm = 1.0, rel_beta = 1.5, # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 + rel_symp_prob = 1.0, rel_severe_prob = 1.8, # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf rel_crit_prob = 1.0, rel_death_prob = 1.0, @@ -368,6 +370,7 @@ def get_strain_pars(): b1351 = sc.objdict( rel_imm = 0.25, rel_beta = 1.4, + rel_symp_prob = 1.0, rel_severe_prob = 1.4, rel_crit_prob = 1.0, rel_death_prob = 1.4, @@ -376,6 +379,7 @@ def get_strain_pars(): p1 = sc.objdict( rel_imm = 0.5, rel_beta = 1.4, + rel_symp_prob = 1.0, rel_severe_prob = 1.4, rel_crit_prob = 1.0, rel_death_prob = 2.0, From d7d75334224e019ce80b439788a79f488ccc5ecb Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 01:33:51 -0700 Subject: [PATCH 464/569] refactored vaccines and strains --- covasim/immunity.py | 115 ++++++--------------------------------- covasim/interventions.py | 10 ++-- covasim/parameters.py | 40 +++++++++++++- 3 files changed, 60 insertions(+), 105 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index c6e2ec9ca..cc74d7139 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -130,16 +130,16 @@ class Vaccine(cvi.Intervention): def __init__(self, vaccine=None, label=None, **kwargs): super().__init__(**kwargs) self.label = label - self.rel_imm = None # list of length n_strains with relative immunity factor - self.doses = None - self.interval = None - self.NAb_init = None - self.NAb_boost = None - self.NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}} # Parameters to map NAbs to efficacy + # self.rel_imm = None # list of length n_strains with relative immunity factor + # self.doses = None + # self.interval = None + # self.NAb_init = None + # self.NAb_boost = None + # self.NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}} # Parameters to map NAbs to efficacy self.vaccine_strain_info = cvpar.get_vaccine_strain_pars() self.parse_vaccine_pars(vaccine=vaccine) - for par, val in self.vaccine_pars.items(): - setattr(self, par, val) + # for par, val in self.vaccine_pars.items(): + # setattr(self, par, val) return @@ -178,38 +178,11 @@ def parse_vaccine_pars(self, vaccine=None): errormsg = f'Could not understand {type(vaccine)}, please specify as a string indexing a predefined vaccine or a dict.' raise ValueError(errormsg) - self.vaccine_pars = vaccine_pars + self.p = sc.objdict(vaccine_pars) return - def initialize(self, sim): - super().initialize() - - ts = sim['n_strains'] - circulating_strains = ['wild'] # assume wild is circulating - for strain in range(ts-1): - circulating_strains.append(sim['strains'][strain].label) - - if self.NAb_init is None : # TODO: refactor - errormsg = 'Did not provide parameters for this vaccine' - raise ValueError(errormsg) - - if self.rel_imm is None: # TODO: refactor - sc.printv('Did not provide rel_imm parameters for this vaccine, trying to find values', 1, sim['verbose']) - self.rel_imm = [] - for strain in circulating_strains: - self.rel_imm.append(self.vaccine_strain_info[self.label][strain]) - - correct_size = len(self.rel_imm) == ts - if not correct_size: - errormsg = 'Did not provide relative immunity for each strain' - raise ValueError(errormsg) - - return - - - -# %% NAb methods +#%% NAb methods def init_nab(people, inds, prior_inf=True, vacc_info=None): ''' @@ -313,25 +286,6 @@ def nab_to_efficacy(nab, ax, function_args): # %% Immunity methods - -# def update_strain_attributes(people): -# for key in people.meta.person: -# if 'imm' in key: # everyone starts out with no immunity to either strain. # TODO: refactor -# rows,cols = people[key].shape -# people[key].resize(rows+1, cols, refcheck=False) - -# # Set strain states, which store info about which strain a person is exposed to -# for key in people.meta.strain_states: -# if 'by' in key: # TODO: refactor -# rows,cols = people[key].shape -# people[key].resize(rows+1, cols, refcheck=False) - -# for key in cvd.new_result_flows_by_strain: -# rows, = people[key].shape -# people.flows_strain[key].reshape(rows+1, refcheck=False) -# return - - def init_immunity(sim, create=False): ''' Initialize immunity matrices with all strains that will eventually be in the sim''' @@ -354,14 +308,13 @@ def init_immunity(sim, create=False): # Initialize immunity for ax in cvd.immunity_axes: if ax == 'sus': # Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] - immunity[ax] = np.full((ts, ts), sim['cross_immunity'], - dtype=cvd.default_float) # Default for off-diagnonals + immunity[ax] = np.full((ts, ts), sim['cross_immunity'], dtype=cvd.default_float) # Default for off-diagnonals np.fill_diagonal(immunity[ax], 1) # Default for own-immunity else: # Progression and transmission are matrices of scalars of size sim['n_strains'] - immunity[ax] = np.full(ts, 1, dtype=cvd.default_float) + immunity[ax] = np.ones(ts, dtype=cvd.default_float) - known_strains = ['wild', 'b117', 'b1351', 'p1'] # TODO: only appear once - cross_immunity = create_cross_immunity(circulating_strains, rel_imms) + cross_immunity = cvpar.get_cross_immunity() + known_strains = cross_immunity.keys() for i in range(ts): for j in range(ts): if i != j: @@ -370,7 +323,7 @@ def init_immunity(sim, create=False): sim['immunity'] = immunity # Next, precompute the NAb kinetics and store these for access during the sim - sim['NAb_kin'] = pre_compute_waning(length=sim['n_days'], form=sim['NAb_decay']['form'], pars=sim['NAb_decay']['pars']) + sim['NAb_kin'] = precompute_waning(length=sim['n_days'], form=sim['NAb_decay']['form'], pars=sim['NAb_decay']['pars']) return @@ -455,7 +408,7 @@ def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): #%% Methods for computing waning -def pre_compute_waning(length, form='nab_decay', pars=None): +def precompute_waning(length, form='nab_decay', pars=None): ''' Process functional form and parameters into values: @@ -558,38 +511,4 @@ def linear_decay(length, init_val, slope): def linear_growth(length, slope): ''' Calculate linear growth ''' t = np.arange(length, dtype=cvd.default_int) - return (slope * t) - - -def create_cross_immunity(circulating_strains, rel_imms): # TODO: refactor - known_strains = ['wild', 'b117', 'b1351', 'p1'] - known_cross_immunity = dict() - known_cross_immunity['wild'] = {} # cross-immunity to wild - known_cross_immunity['wild']['b117'] = 0.5 - known_cross_immunity['wild']['b1351'] = 0.5 - known_cross_immunity['wild']['p1'] = 0.5 - known_cross_immunity['b117'] = {} # cross-immunity to b117 - known_cross_immunity['b117']['wild'] = rel_imms['b117'] if 'b117' in circulating_strains else .5 - known_cross_immunity['b117']['b1351'] = 0.8 - known_cross_immunity['b117']['p1'] = 0.8 - known_cross_immunity['b1351'] = {} # cross-immunity to b1351 - known_cross_immunity['b1351']['wild'] = rel_imms['b1351'] if 'b1351' in circulating_strains else 0.066 - known_cross_immunity['b1351']['b117'] = 0.1 - known_cross_immunity['b1351']['p1'] = 0.1 - known_cross_immunity['p1'] = {} # cross-immunity to p1 - known_cross_immunity['p1']['wild'] = rel_imms['p1'] if 'p1' in circulating_strains else 0.17 - known_cross_immunity['p1']['b117'] = 0.2 - known_cross_immunity['p1']['b1351'] = 0.2 - - cross_immunity = {} - cs = len(circulating_strains) - for i in range(cs): - cross_immunity[circulating_strains[i]] = {} - for j in range(cs): - if circulating_strains[j] in known_strains: - if i != j: - if circulating_strains[i] in known_strains: - cross_immunity[circulating_strains[i]][circulating_strains[j]] = \ - known_cross_immunity[circulating_strains[i]][circulating_strains[j]] - - return cross_immunity + return (slope * t) \ No newline at end of file diff --git a/covasim/interventions.py b/covasim/interventions.py index 4a76f1432..792faf7c3 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1245,11 +1245,11 @@ def initialize(self, sim): self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person self.vaccination_dates = np.full(sim.n, np.nan) # Store the dates when people are vaccinated self.vaccine_ind = len(sim['vaccines']) - vaccine = cvi.Vaccine(self.vaccine_pars) - vaccine.initialize(sim) - sim['vaccines'].append(vaccine) - self.doses = vaccine.doses - self.interval = vaccine.interval + self.vaccine = cvi.Vaccine(self.vaccine_pars) + self.vaccine.initialize(sim) + sim['vaccines'].append(self.vaccine) + self.doses = self.vaccine.p.doses + self.interval = self.vaccine.p.interval return diff --git a/covasim/parameters.py b/covasim/parameters.py index 0da22291a..0b60ee31f 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -334,6 +334,7 @@ def get_vaccine_choices(): ''' # List of choices currently available: new ones can be added to the list along with their aliases choices = { + 'default': ['default', None], 'pfizer': ['pfizer', 'biontech', 'pfizer-biontech'], 'moderna': ['moderna'], 'az': ['az', 'astrazeneca'], @@ -389,6 +390,43 @@ def get_strain_pars(): return pars +def get_cross_immunity(): + ''' + Get the cross immunity between each strain and each other strain + ''' + pars = sc.objdict( + + wild = sc.objdict( + wild = 1.0, + b117 = 0.5, + b1351 = 0.5, + p1 = 0.5, + ), + + b117 = sc.objdict( + wild = 0.5, + b117 = 1.0, + b1351 = 0.8, + p1 = 0.8, + ), + + b1351 = sc.objdict( + wild = 0.066, + b117 = 0.1, + b1351 = 1.0, + p1 = 0.1, + ), + + p1 = sc.objdict( + wild = 0.17, + b117 = 0.2, + b1351 = 0.2, + p1 = 1.0, + ), + ) + return pars + + def get_vaccine_strain_pars(): ''' Define the effectiveness of each vaccine against each strain @@ -430,7 +468,6 @@ def get_vaccine_strain_pars(): p1 = 1/8.6, ), ) - return pars @@ -480,7 +517,6 @@ def get_vaccine_dose_pars(): interval = None, ), ) - return pars From 7f1d1f3c42b1865c5763fd0348366df70e8a7e30 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 01:43:01 -0700 Subject: [PATCH 465/569] refactored dict form --- covasim/immunity.py | 26 +++++++++++++------------- covasim/parameters.py | 8 ++++---- tests/test_immunity.py | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index cc74d7139..068fd01de 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -20,18 +20,19 @@ class Strain(cvi.Intervention): Add a new strain to the sim Args: - day (int): day on which new variant is introduced. # TODO: update with correct name and find_day - n_imports (int): the number of imports of the strain to be added - strain (dict): dictionary of parameters specifying information about the strain - kwargs (dict): passed to Intervention() + strain (str/dict): name of strain, or dictionary of parameters specifying information about the strain + label (str): if strain is supplied as a dict, the name of the strain + days (int/list): day(s) on which new variant is introduced. + n_imports (int): the number of imports of the strain to be added + rescale (bool): whether the number of imports should be rescaled with the population + kwargs (dict): passed to Intervention() **Example**:: b117 = cv.Strain('b117', days=10) # Make strain B117 active from day 10 p1 = cv.Strain('p1', days=15) # Make strain P1 active from day 15 - # Make a custom strain active from day 20 my_var = cv.Strain(strain={'rel_beta': 2.5}, label='My strain', days=20) - sim = cv.Sim(strains=[b117, p1, my_var]) # Add them all to the sim + sim = cv.Sim(strains=[b117, p1, my_var]).run() # Add them all to the sim ''' def __init__(self, strain=None, label=None, days=None, n_imports=1, rescale=True, **kwargs): @@ -323,7 +324,7 @@ def init_immunity(sim, create=False): sim['immunity'] = immunity # Next, precompute the NAb kinetics and store these for access during the sim - sim['NAb_kin'] = precompute_waning(length=sim['n_days'], form=sim['NAb_decay']['form'], pars=sim['NAb_decay']['pars']) + sim['NAb_kin'] = precompute_waning(length=sim['n_days'], pars=sim['NAb_decay']) return @@ -408,24 +409,23 @@ def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): #%% Methods for computing waning -def precompute_waning(length, form='nab_decay', pars=None): +def precompute_waning(length, pars=None): ''' Process functional form and parameters into values: - - 'nab_decay' : specific decay function taken from https://doi.org/10.1101/2021.03.09.21252641 - - 'exp_decay' : exponential decay. Parameters should be init_val and half_life (half_life can be None/nan) - - 'linear' : linear decay - - others TBC! + - 'nab_decay' : specific decay function taken from https://doi.org/10.1101/2021.03.09.21252641 + - 'exp_decay' : exponential decay. Parameters should be init_val and half_life (half_life can be None/nan) + - 'linear_decay': linear decay Args: length (float): length of array to return, i.e., for how long waning is calculated - form (str): the functional form to use pars (dict): passed to individual immunity functions Returns: array of length 'length' of values ''' + form = pars.pop('form') choices = [ 'nab_decay', # Default if no form is provided 'exp_decay', diff --git a/covasim/parameters.py b/covasim/parameters.py index 0b60ee31f..858bbe7a3 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -66,14 +66,14 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Parameters used to calculate immunity pars['use_waning'] = False # Whether to use dynamically calculated immunity - pars['NAb_init'] = dict(dist='normal', par1= 0, par2= 2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 - pars['NAb_decay'] = dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 + pars['NAb_init'] = dict(dist='normal', par1=0, par2=2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 + pars['NAb_decay'] = dict(form='nab_decay', init_decay_rate=np.log(2)/90, init_decay_time=250, decay_decay_rate=0.001) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters - pars['NAb_boost'] = 1.5 # Multiplicative factor applied to a person's NAb levels if they get reinfected. TODO, add source + pars['NAb_boost'] = 1.5 # Multiplicative factor applied to a person's NAb levels if they get reinfected. # TODO: add source pars['NAb_eff'] = dict(sus=dict(slope=2.7, n_50=0.03), symp=0.1, sev=0.52) # Parameters to map NAbs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = dict(asymptomatic=0.85, mild=1, severe=1.5) # Relative immunity from natural infection varies by symptoms - pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in Immunity.py + pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in immunity.py # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains pars['rel_beta'] = 1.0 diff --git a/tests/test_immunity.py b/tests/test_immunity.py index ca451bd5b..352c96b7d 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -94,7 +94,7 @@ def test_waning(do_plot=False): pars = dict( n_days = 90, beta = 0.008, - NAb_decay = dict(form='nab_decay', pars={'init_decay_rate': 0.1, 'init_decay_time': 250, 'decay_decay_rate': 0.001}) + NAb_decay = dict(form='nab_decay', init_decay_rate=0.1, init_decay_time=250, decay_decay_rate=0.001) ) # Optionally include rescaling From e974d1fde789a506efeeeafff9f7c16cd5f147d0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 01:45:39 -0700 Subject: [PATCH 466/569] renamed parameters --- covasim/immunity.py | 20 ++++++++++---------- covasim/parameters.py | 2 +- tests/test_immunity.py | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 068fd01de..f0d796c49 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -454,7 +454,7 @@ def precompute_waning(length, pars=None): return output -def nab_decay(length, init_decay_rate, init_decay_time, decay_decay_rate): +def nab_decay(length, decay_rate1, decay_time1, decay_rate2): ''' Returns an array of length 'length' containing the evaluated function NAb decay function at each point. @@ -464,21 +464,21 @@ def nab_decay(length, init_decay_rate, init_decay_time, decay_decay_rate): Args: length (int): number of points - init_decay_rate (float): initial rate of exponential decay - init_decay_time (float): time on the first exponential decay - decay_decay_rate (float): the rate at which the decay decays + decay_rate1 (float): initial rate of exponential decay + decay_time1 (float): time on the first exponential decay + decay_rate2 (float): the rate at which the decay decays ''' - def f1(t, init_decay_rate): + def f1(t, decay_rate1): ''' Simple exponential decay ''' - return np.exp(-t*init_decay_rate) + return np.exp(-t*decay_rate1) - def f2(t, init_decay_rate, init_decay_time, decay_decay_rate): + def f2(t, decay_rate1, decay_time1, decay_rate2): ''' Complex exponential decay ''' - return np.exp(-t*(init_decay_rate*np.exp(-(t-init_decay_time)*decay_decay_rate))) + return np.exp(-t*(decay_rate1*np.exp(-(t-decay_time1)*decay_rate2))) t = np.arange(length, dtype=cvd.default_int) - y1 = f1(cvu.true(t<=init_decay_time), init_decay_rate) - y2 = f2(cvu.true(t>init_decay_time), init_decay_rate, init_decay_time, decay_decay_rate) + y1 = f1(cvu.true(t<=decay_time1), decay_rate1) + y2 = f2(cvu.true(t>decay_time1), decay_rate1, decay_time1, decay_rate2) y = np.concatenate([y1,y2]) return y diff --git a/covasim/parameters.py b/covasim/parameters.py index 858bbe7a3..3080b7328 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -67,7 +67,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Parameters used to calculate immunity pars['use_waning'] = False # Whether to use dynamically calculated immunity pars['NAb_init'] = dict(dist='normal', par1=0, par2=2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 - pars['NAb_decay'] = dict(form='nab_decay', init_decay_rate=np.log(2)/90, init_decay_time=250, decay_decay_rate=0.001) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 + pars['NAb_decay'] = dict(form='nab_decay', decay_rate1=np.log(2)/90, decay_time1=250, decay_rate2=0.001) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters pars['NAb_boost'] = 1.5 # Multiplicative factor applied to a person's NAb levels if they get reinfected. # TODO: add source pars['NAb_eff'] = dict(sus=dict(slope=2.7, n_50=0.03), symp=0.1, sev=0.52) # Parameters to map NAbs to efficacy diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 352c96b7d..a70698976 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -94,7 +94,7 @@ def test_waning(do_plot=False): pars = dict( n_days = 90, beta = 0.008, - NAb_decay = dict(form='nab_decay', init_decay_rate=0.1, init_decay_time=250, decay_decay_rate=0.001) + NAb_decay = dict(form='nab_decay', decay_rate1=0.1, decay_time1=250, decay_rate2=0.001) ) # Optionally include rescaling @@ -167,9 +167,9 @@ def test_decays(do_plot=False): nab_decay = dict( func = cv.immunity.nab_decay, length = n, - init_decay_rate = 0.05, - init_decay_time= 100, - decay_decay_rate = 0.002, + decay_rate1 = 0.05, + decay_time1= 100, + decay_rate2 = 0.002, ), exp_decay = dict( From 1248f72b62f051af504579fc79eafd64cfa2eff4 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 01:48:28 -0700 Subject: [PATCH 467/569] global rename --- covasim/defaults.py | 6 +- covasim/immunity.py | 124 ++++++++++++++++++++--------------------- covasim/parameters.py | 40 ++++++------- covasim/people.py | 2 +- covasim/sim.py | 12 ++-- tests/test_immunity.py | 2 +- 6 files changed, 93 insertions(+), 93 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 09c948ed6..b84fd74a4 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -93,8 +93,8 @@ def __init__(self): # Neutralizing antibody states, not by strain self.nab_states = [ 'prior_symptoms', # Float - 'init_NAb', # Float, initial neutralization titre relative to convalescent plasma - 'NAb', # Float, current neutralization titre relative to convalescent plasma + 'init_nab', # Float, initial neutralization titre relative to convalescent plasma + 'nab', # Float, current neutralization titre relative to convalescent plasma ] # Additional vaccination states @@ -178,7 +178,7 @@ def __init__(self): } result_imm = { - 'pop_nabs': 'Population average NAbs', + 'pop_nabs': 'Population average nabs', 'pop_protection': 'Population average protective immunity' } diff --git a/covasim/immunity.py b/covasim/immunity.py index f0d796c49..7d5a42364 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -134,9 +134,9 @@ def __init__(self, vaccine=None, label=None, **kwargs): # self.rel_imm = None # list of length n_strains with relative immunity factor # self.doses = None # self.interval = None - # self.NAb_init = None - # self.NAb_boost = None - # self.NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}} # Parameters to map NAbs to efficacy + # self.nab_init = None + # self.nab_boost = None + # self.nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}} # Parameters to map nabs to efficacy self.vaccine_strain_info = cvpar.get_vaccine_strain_pars() self.parse_vaccine_pars(vaccine=vaccine) # for par, val in self.vaccine_pars.items(): @@ -183,64 +183,64 @@ def parse_vaccine_pars(self, vaccine=None): return -#%% NAb methods +#%% nab methods def init_nab(people, inds, prior_inf=True, vacc_info=None): ''' - Draws an initial NAb level for individuals. + Draws an initial nab level for individuals. Can come from a natural infection or vaccination and depends on if there is prior immunity: - 1) a natural infection. If individual has no existing NAb, draw from distribution - depending upon symptoms. If individual has existing NAb, multiply booster impact - 2) Vaccination. If individual has no existing NAb, draw from distribution - depending upon vaccine source. If individual has existing NAb, multiply booster impact + 1) a natural infection. If individual has no existing nab, draw from distribution + depending upon symptoms. If individual has existing nab, multiply booster impact + 2) Vaccination. If individual has no existing nab, draw from distribution + depending upon vaccine source. If individual has existing nab, multiply booster impact ''' if vacc_info is None: # print('Note: using default vaccine dosing information') vacc_info = cvpar.get_vaccine_dose_pars()['default'] - NAb_arrays = people.NAb[inds] - prior_NAb_inds = cvu.idefined(NAb_arrays, inds) # Find people with prior NAbs - no_prior_NAb_inds = np.setdiff1d(inds, prior_NAb_inds) # Find people without prior NAbs + nab_arrays = people.nab[inds] + prior_nab_inds = cvu.idefined(nab_arrays, inds) # Find people with prior nabs + no_prior_nab_inds = np.setdiff1d(inds, prior_nab_inds) # Find people without prior nabs - # prior_NAb = people.NAb[prior_NAb_inds] # Array of NAb levels on this timestep for people with some NAbs - peak_NAb = people.init_NAb[prior_NAb_inds] + # prior_nab = people.nab[prior_nab_inds] # Array of nab levels on this timestep for people with some nabs + peak_nab = people.init_nab[prior_nab_inds] - # NAbs from infection + # nabs from infection if prior_inf: - NAb_boost = people.pars['NAb_boost'] # Boosting factor for natural infection - # 1) No prior NAb: draw NAb from a distribution and compute - if len(no_prior_NAb_inds): - init_NAb = cvu.sample(**people.pars['NAb_init'], size=len(no_prior_NAb_inds)) - prior_symp = people.prior_symptoms[no_prior_NAb_inds] - no_prior_NAb = (2**init_NAb) * prior_symp - people.init_NAb[no_prior_NAb_inds] = no_prior_NAb - - # 2) Prior NAb: multiply existing NAb by boost factor - if len(prior_NAb_inds): - init_NAb = peak_NAb * NAb_boost - people.init_NAb[prior_NAb_inds] = init_NAb - - # NAbs from a vaccine + nab_boost = people.pars['nab_boost'] # Boosting factor for natural infection + # 1) No prior nab: draw nab from a distribution and compute + if len(no_prior_nab_inds): + init_nab = cvu.sample(**people.pars['nab_init'], size=len(no_prior_nab_inds)) + prior_symp = people.prior_symptoms[no_prior_nab_inds] + no_prior_nab = (2**init_nab) * prior_symp + people.init_nab[no_prior_nab_inds] = no_prior_nab + + # 2) Prior nab: multiply existing nab by boost factor + if len(prior_nab_inds): + init_nab = peak_nab * nab_boost + people.init_nab[prior_nab_inds] = init_nab + + # nabs from a vaccine else: - NAb_boost = vacc_info['NAb_boost'] # Boosting factor for vaccination - # 1) No prior NAb: draw NAb from a distribution and compute - if len(no_prior_NAb_inds): - init_NAb = cvu.sample(**vacc_info['NAb_init'], size=len(no_prior_NAb_inds)) - people.init_NAb[no_prior_NAb_inds] = 2**init_NAb + nab_boost = vacc_info['nab_boost'] # Boosting factor for vaccination + # 1) No prior nab: draw nab from a distribution and compute + if len(no_prior_nab_inds): + init_nab = cvu.sample(**vacc_info['nab_init'], size=len(no_prior_nab_inds)) + people.init_nab[no_prior_nab_inds] = 2**init_nab - # 2) Prior NAb (from natural or vaccine dose 1): multiply existing NAb by boost factor - if len(prior_NAb_inds): - init_NAb = peak_NAb * NAb_boost - people.init_NAb[prior_NAb_inds] = init_NAb + # 2) Prior nab (from natural or vaccine dose 1): multiply existing nab by boost factor + if len(prior_nab_inds): + init_nab = peak_nab * nab_boost + people.init_nab[prior_nab_inds] = init_nab return def check_nab(t, people, inds=None): - ''' Determines current NAbs based on date since recovered/vaccinated.''' + ''' Determines current nabs based on date since recovered/vaccinated.''' - # Indices of people who've had some NAb event + # Indices of people who've had some nab event rec_inds = cvu.defined(people.date_recovered[inds]) vac_inds = cvu.defined(people.date_vaccinated[inds]) both_inds = np.intersect1d(rec_inds, vac_inds) @@ -251,19 +251,19 @@ def check_nab(t, people, inds=None): t_since_boost[vac_inds] = t-people.date_vaccinated[inds[vac_inds]] t_since_boost[both_inds] = t-np.maximum(people.date_recovered[inds[both_inds]],people.date_vaccinated[inds[both_inds]]) - # Set current NAbs - people.NAb[inds] = people.pars['NAb_kin'][t_since_boost] * people.init_NAb[inds] + # Set current nabs + people.nab[inds] = people.pars['nab_kin'][t_since_boost] * people.init_nab[inds] return def nab_to_efficacy(nab, ax, function_args): ''' - Convert NAb levels to immunity protection factors, using the functional form + Convert nab levels to immunity protection factors, using the functional form given in this paper: https://doi.org/10.1101/2021.03.09.21252641 Args: - nab (arr): an array of NAb levels + nab (arr): an array of nab levels ax (str): can be 'sus', 'symp' or 'sev', corresponding to the efficacy of protection against infection, symptoms, and severe disease respectively Returns: @@ -323,8 +323,8 @@ def init_immunity(sim, create=False): immunity['sus'][j][i] = cross_immunity[circulating_strains[j]][circulating_strains[i]] sim['immunity'] = immunity - # Next, precompute the NAb kinetics and store these for access during the sim - sim['NAb_kin'] = precompute_waning(length=sim['n_days'], pars=sim['NAb_decay']) + # Next, precompute the nab kinetics and store these for access during the sim + sim['nab_kin'] = precompute_waning(length=sim['n_days'], pars=sim['nab_decay']) return @@ -350,13 +350,13 @@ def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): was_inf = cvu.true(people.t >= people.date_recovered) # Had a previous exposure, now recovered is_vacc = cvu.true(people.vaccinated) # Vaccinated date_rec = people.date_recovered # Date recovered - immunity = people.pars['immunity'] # cross-immunity/own-immunity scalars to be applied to NAb level before computing efficacy - nab_eff_pars = people.pars['NAb_eff'] + immunity = people.pars['immunity'] # cross-immunity/own-immunity scalars to be applied to nab level before computing efficacy + nab_eff_pars = people.pars['nab_eff'] # If vaccines are present, extract relevant information about them vacc_present = len(is_vacc) if vacc_present: - vx_nab_eff_pars = vacc_info['NAb_eff'] + vx_nab_eff_pars = vacc_info['nab_eff'] # PART 1: Immunity to infection for susceptible individuals if sus: @@ -371,20 +371,20 @@ def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): if len(is_sus_vacc): vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) vaccine_scale = vacc_strain[strain] # TODO: handle this better - current_NAbs = people.NAb[is_sus_vacc] - people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale, 'sus', vx_nab_eff_pars) + current_nabs = people.nab[is_sus_vacc] + people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_nabs * vaccine_scale, 'sus', vx_nab_eff_pars) if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain - current_NAbs = people.NAb[is_sus_was_inf_same] - people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, strain], 'sus', nab_eff_pars) + current_nabs = people.nab[is_sus_was_inf_same] + people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_nabs * immunity['sus'][strain, strain], 'sus', nab_eff_pars) if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain prior_strains = people.recovered_strain[is_sus_was_inf_diff] prior_strains_unique = cvd.default_int(np.unique(prior_strains)) for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] - current_NAbs = people.NAb[unique_inds] - people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_NAbs * immunity['sus'][strain, unique_strain], 'sus', nab_eff_pars) + current_nabs = people.nab[unique_inds] + people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_nabs * immunity['sus'][strain, unique_strain], 'sus', nab_eff_pars) # PART 2: Immunity to disease for currently-infected people else: @@ -394,14 +394,14 @@ def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): if len(is_inf_vacc): # Immunity for infected people who've been vaccinated vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) vaccine_scale = vacc_strain[strain] # TODO: handle this better - current_NAbs = people.NAb[is_inf_vacc] - people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['symp'][strain], 'symp', nab_eff_pars) - people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_NAbs * vaccine_scale * immunity['sev'][strain], 'sev', nab_eff_pars) + current_nabs = people.nab[is_inf_vacc] + people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_nabs * vaccine_scale * immunity['symp'][strain], 'symp', nab_eff_pars) + people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_nabs * vaccine_scale * immunity['sev'][strain], 'sev', nab_eff_pars) if len(was_inf): # Immunity for reinfected people - current_NAbs = people.NAb[was_inf] - people.symp_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['symp'][strain], 'symp', nab_eff_pars) - people.sev_imm[strain, was_inf] = nab_to_efficacy(current_NAbs * immunity['sev'][strain], 'sev', nab_eff_pars) + current_nabs = people.nab[was_inf] + people.symp_imm[strain, was_inf] = nab_to_efficacy(current_nabs * immunity['symp'][strain], 'symp', nab_eff_pars) + people.sev_imm[strain, was_inf] = nab_to_efficacy(current_nabs * immunity['sev'][strain], 'sev', nab_eff_pars) return @@ -456,7 +456,7 @@ def precompute_waning(length, pars=None): def nab_decay(length, decay_rate1, decay_time1, decay_rate2): ''' - Returns an array of length 'length' containing the evaluated function NAb decay + Returns an array of length 'length' containing the evaluated function nab decay function at each point. Uses exponential decay, with the rate of exponential decay also set to exponentially diff --git a/covasim/parameters.py b/covasim/parameters.py index 3080b7328..bbfb3a28e 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -66,11 +66,11 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Parameters used to calculate immunity pars['use_waning'] = False # Whether to use dynamically calculated immunity - pars['NAb_init'] = dict(dist='normal', par1=0, par2=2) # Parameters for the distribution of the initial level of log2(NAb) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 - pars['NAb_decay'] = dict(form='nab_decay', decay_rate1=np.log(2)/90, decay_time1=250, decay_rate2=0.001) # Parameters describing the kinetics of decay of NAbs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 - pars['NAb_kin'] = None # Constructed during sim initialization using the NAb_decay parameters - pars['NAb_boost'] = 1.5 # Multiplicative factor applied to a person's NAb levels if they get reinfected. # TODO: add source - pars['NAb_eff'] = dict(sus=dict(slope=2.7, n_50=0.03), symp=0.1, sev=0.52) # Parameters to map NAbs to efficacy + pars['nab_init'] = dict(dist='normal', par1=0, par2=2) # Parameters for the distribution of the initial level of log2(nab) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 + pars['nab_decay'] = dict(form='nab_decay', decay_rate1=np.log(2)/90, decay_time1=250, decay_rate2=0.001) # Parameters describing the kinetics of decay of nabs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 + pars['nab_kin'] = None # Constructed during sim initialization using the nab_decay parameters + pars['nab_boost'] = 1.5 # Multiplicative factor applied to a person's nab levels if they get reinfected. # TODO: add source + pars['nab_eff'] = dict(sus=dict(slope=2.7, n_50=0.03), symp=0.1, sev=0.52) # Parameters to map nabs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = dict(asymptomatic=0.85, mild=1, severe=1.5) # Relative immunity from natural infection varies by symptoms pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in immunity.py @@ -478,41 +478,41 @@ def get_vaccine_dose_pars(): pars = sc.objdict( default = sc.objdict( - NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, - NAb_init = dict(dist='normal', par1=0.5, par2= 2), - NAb_boost = 2, + nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + nab_init = dict(dist='normal', par1=0.5, par2= 2), + nab_boost = 2, doses = 1, interval = None, ), pfizer = sc.objdict( - NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, - NAb_init = dict(dist='normal', par1=0.5, par2= 2), - NAb_boost = 2, + nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + nab_init = dict(dist='normal', par1=0.5, par2= 2), + nab_boost = 2, doses = 2, interval = 21, ), moderna = sc.objdict( - NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, - NAb_init = dict(dist='normal', par1=0.5, par2= 2), - NAb_boost = 2, + nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + nab_init = dict(dist='normal', par1=0.5, par2= 2), + nab_boost = 2, doses = 2, interval = 28, ), az = sc.objdict( - NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, - NAb_init = dict(dist='normal', par1=0.5, par2= 2), - NAb_boost = 2, + nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + nab_init = dict(dist='normal', par1=0.5, par2= 2), + nab_boost = 2, doses = 2, interval = 21, ), jj = sc.objdict( - NAb_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, - NAb_init = dict(dist='normal', par1=0.5, par2= 2), - NAb_boost = 2, + nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + nab_init = dict(dist='normal', par1=0.5, par2= 2), + nab_boost = 2, doses = 1, interval = None, ), diff --git a/covasim/people.py b/covasim/people.py index eee48d7d3..544f7e4cc 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -282,7 +282,7 @@ def check_recovery(self, inds=None, filter_inds='is_exp'): # Handle immunity aspects if self.pars['use_waning']: - # Before letting them recover, store information about the strain they had, store symptoms and pre-compute NAbs array + # Before letting them recover, store information about the strain they had, store symptoms and pre-compute nabs array mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=inds) severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=inds) diff --git a/covasim/sim.py b/covasim/sim.py index cb9014c4c..089bec208 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -310,7 +310,7 @@ def init_res(*args, **kwargs): self.results['test_yield'] = init_res('Testing yield', scale=False) self.results['rel_test_yield'] = init_res('Relative testing yield', scale=False) self.results['frac_vaccinated'] = init_res('Proportion vaccinated', scale=False) - self.results['pop_nabs'] = init_res('Population NAb levels', scale=False, color=dcols.pop_nabs) + self.results['pop_nabs'] = init_res('Population nab levels', scale=False, color=dcols.pop_nabs) self.results['pop_protection'] = init_res('Population immunity protection', scale=False, color=dcols.pop_protection) self.results['pop_symp_protection'] = init_res('Population symptomatic protection', scale=False, color=dcols.pop_symp_protection) @@ -500,7 +500,7 @@ def init_strains(self): def init_immunity(self, create=False): - ''' Initialize immunity matrices and precompute NAb waning for each strain ''' + ''' Initialize immunity matrices and precompute nab waning for each strain ''' if self['use_waning']: cvimm.init_immunity(self, create=create) return @@ -594,9 +594,9 @@ def step(self): prel_trans = people.rel_trans prel_sus = people.rel_sus - # Check NAbs. Take set difference so we don't compute NAbs for anyone currently infected + # Check nabs. Take set difference so we don't compute nabs for anyone currently infected if self['use_waning']: - has_nabs = np.setdiff1d(cvu.defined(people.init_NAb), cvu.false(people.susceptible)) + has_nabs = np.setdiff1d(cvu.defined(people.init_nab), cvu.false(people.susceptible)) if len(has_nabs): cvimm.check_nab(t, people, inds=has_nabs) # Iterate through n_strains to calculate infections @@ -645,8 +645,8 @@ def step(self): for strain in range(ns): self.results['strain'][key][strain][t] += count[strain] - # Update NAb and immunity for this time step - self.results['pop_nabs'][t] = np.sum(people.NAb[cvu.defined(people.NAb)])/len(people) + # Update nab and immunity for this time step + self.results['pop_nabs'][t] = np.sum(people.nab[cvu.defined(people.nab)])/len(people) self.results['pop_protection'][t] = np.nanmean(people.sus_imm) self.results['pop_symp_protection'][t] = np.nanmean(people.symp_imm) diff --git a/tests/test_immunity.py b/tests/test_immunity.py index a70698976..8e7525a8a 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -94,7 +94,7 @@ def test_waning(do_plot=False): pars = dict( n_days = 90, beta = 0.008, - NAb_decay = dict(form='nab_decay', decay_rate1=0.1, decay_time1=250, decay_rate2=0.001) + nab_decay = dict(form='nab_decay', decay_rate1=0.1, decay_time1=250, decay_rate2=0.001) ) # Optionally include rescaling From 1406a3607a70a296163cf3f854e92b050ce43fa8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 02:02:32 -0700 Subject: [PATCH 468/569] updating changelog --- CHANGELOG.rst | 43 +++++++++++++++++++++++-------------------- covasim/parameters.py | 2 +- covasim/people.py | 10 +++++----- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 288e0aec3..911f88eb9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,7 +16,7 @@ Coming soon These are the major improvements we are currently working on. If there is a specific bugfix or feature you would like to see, please `create an issue `__. - Expanded tutorials (health care workers, vaccination, calibration, exercises, etc.) -- Multi-region and geospatial support +- Multi-region and geographical support - Economics and costing analysis @@ -24,46 +24,49 @@ These are the major improvements we are currently working on. If there is a spec Latest versions (3.0.x) ~~~~~~~~~~~~~~~~~~~~~~~ -Version 3.0.0 (2021-04-XX) +Version 3.0.0 (2021-04-12) -------------------------- -This version contains a number of major updates. +This version introduces fully featured vaccines, variants, and immunity. **Note:** These new features are still under development; please use with caution and email us at covasim@idmod.org if you have any questions or issues. Highlights ^^^^^^^^^^ - **Model structure**: The model now follows an "SEIS"-type structure, instead of the previous "SEIR" structure. This means that after recovering from an infection, agents return to the "susceptible" compartment. Each agent in the simulation has properties ``sus_imm``, ``trans_imm`` and ``prog_imm``, which respectively determine their immunity to acquiring an infection, transmitting an infection, or developing a more severe case of COVID-19. All these immunity levels are initially zero. They can be boosted by either natural infection or vaccination, and thereafter they can wane over time or remain permanently elevated. - **Multi-strain modeling**: Model functionality has been extended to allow for modeling of multiple different co-circulating strains with different properties. This means you can now do e.g. ``b117 = cv.Strain('b117', days=1, n_imports=20)`` followed by ``sim = cv.Sim(strains=b117)`` to import strain B117. Further examples are contained in ``devtests/test_variants.py``. -- **New methods for vaccine modeling**: A new ``vaccinate`` intervention has been added, which allows more flexible modeling of vaccinations. Vaccines, like natural infections, are assumed to boost agents' immunity. +- **New methods for vaccine modeling**: A new ``cv.vaccinate()`` intervention has been added, which allows more flexible modeling of vaccinations. Vaccines, like natural infections, are assumed to boost agents' immunity. +- **Consistency**: By default, results from Covasim 3.0 should exactly match Covasim 2.0. To use the new features, you will need to manually specify ``cv.Sim(use_waning=True)``. State changes ^^^^^^^^^^^^^ -- The ``recovered`` state has been removed. +- Several new states have been added, including ``people.naive``, which stores whether or not a person has ever been exposed to COVID before. Parameter changes ^^^^^^^^^^^^^^^^^ -- The parameter ``n_imports`` has been removed, as importations are now handled by the ``strains`` functionality. -- A subset of existing parameters have been made strain-specific, meaning that they are allowed to differ by strain. These include: ``rel_beta``, which specifies the relative transmissibility of a new strain compared to the wild strain; ``asymp_factor``, all of the ``dur`` parameters, ``rel_symp_prob``, ``rel_severe_prob``, ``rel_crit_prob``, and the newly-added immunity parameters ``imm_pars`` (see next point). The list of parameters that can vary by strain is specified in ``covasim/defaults.py``. -- Two new parameters have been added to hold information about the strains in the simulation: - - The parameter ``n_strains`` is an integer, updated on each time-step, that specifies how many strains are in circulation at that time-step. - - The parameter ``total_strains`` is an integer that specifies how many strains will be in ciruclation at some point during the course of the simulation. +- A subset of existing parameters have been made strain-specific, meaning that they are allowed to differ by strain. These include: ``rel_beta``, which specifies the relative transmissibility of a new strain compared to the wild strain; ``rel_symp_prob``, ``rel_severe_prob``, ``rel_crit_prob``, and the newly-added immunity parameters ``rel_imm`` (see next point). The list of parameters that can vary by strain is specified in ``defaults.py``. +- The parameter ``n_strains`` is an integer that specifies how many strains will be in circulation at some point during the course of the simulation. - Seven new parameters have been added to characterize agents' immunity levels: - - The parameter ``NAb_init`` specifies a distribution for the level of neutralizing antibodies that agents have following an infection. These values are on log2 scale, and by default they follow a normal distribution. - - The parameter ``NAb_decay`` is a dictionary specifying the kinetics of decay for neutralizing antibodies over time. - - The parameter ``NAb_kin`` is constructed during sim initialization, and contains pre-computed evaluations of the NAb decay functions described above over time. - - The parameter ``NAb_boost`` is a multiplicative factor applied to a person's NAb levels if they get reinfected. + - The parameter ``nab_init`` specifies a distribution for the level of neutralizing antibodies that agents have following an infection. These values are on log2 scale, and by default they follow a normal distribution. + - The parameter ``nab_decay`` is a dictionary specifying the kinetics of decay for neutralizing antibodies over time. + - The parameter ``nab_kin`` is constructed during sim initialization, and contains pre-computed evaluations of the nab decay functions described above over time. + - The parameter ``nab_boost`` is a multiplicative factor applied to a person's nab levels if they get reinfected. - The parameter ``cross_immunity``. By default, infection with one strain of SARS-CoV-2 is assumed to grant 50% immunity to infection with a different strain. This default assumption of 50% cross-immunity can be modified via this parameter (which will then apply to all strains in the simulation), or it can be modified on a per-strain basis using the ``immunity`` parameter described below. - The parameter ``immunity`` is a matrix of size ``total_strains`` by ``total_strains``. Row ``i`` specifies the immunity levels that people who have been infected with strain ``i`` have to other strains. The entries of this matrix are then multiplied by the time-dependent immunity levels contained in the ``immune_degree`` parameter to determine a person's immunity at each time-step. By default, this will be ``[[1]]`` for a single-strain simulation and ``[[1, 0.5],[0.5, 1]]`` a 2-strain simulation. - - The parameter ``rel_imm`` is a dictionary with keys ``asymptomatic``, ``mild`` and ``severe``. These contain scalars specifying the relative immunity levels for someone who had an asymptomatic, mild, or severe infection. By default, values of 0.98, 0.99, and 1.0 are used. + - The parameter ``rel_imm`` is a dictionary with keys ``asymp``, ``mild`` and ``severe``. These contain scalars specifying the relative immunity levels for someone who had an asymptomatic, mild, or severe infection. By default, values of 0.98, 0.99, and 1.0 are used. - The parameter ``strains`` contains information about any circulating strains that have been specified as additional to the default strain, and the parameter ``vaccines`` contains information about any vaccines in use. These are initialized as ``None`` and then populated by the user. +Changes to results +^^^^^^^^^^^^^^^^^^ +- New results have been added to store information by strain, in ``sim.results['strain']``: ``cum_infections_by_strain``, ``cum_infectious_by_strain``, ``new_reinfections``, ``new_infections_by_strain``, ``new_infectious_by_strain``, ``prevalence_by_strain``, ``incidence_by_strain``. + New functions, methods and classes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - The newly-added file ``immunity.py`` contains functions, methods, and classes related to calculating immunity. This includes the ``Strain`` class, the ``Vaccine`` class. -- A new ``vaccinate`` intervention has been added. Compared to the previous ``vaccine`` intervention, this new intervention allows vaccination to boost agents' immunity against infection, transmission, and progression. +- A new ``cv.vaccinate()`` intervention has been added. Compared to the previous ``vaccine`` intervention (now renamed ``cv.simple_vaccine()``), this new intervention allows vaccination to boost agents' immunity against infection, transmission, and progression. +- A new function ``cv.demo()`` has been added as a shortcut to ``cv.Sim().run().plot()``. -Changes to results -^^^^^^^^^^^^^^^^^^ -- ``results[n_recovered]``, ``results[cum_recovered]`` and ``results[new_recovered]`` have all been removed, since the ``recovered`` state has been removed. However, ``results[recoveries]`` still exists, and stores information about how many people cleared their infection at each time-step. -- New results have been added to store information by strain: ``cum_infections_by_strain``, ``cum_infectious_by_strain``, ``new_reinfections``, ``new_infections_by_strain``, ``new_infectious_by_strain``, ``prevalence_by_strain``, ``incidence_by_strain``. +Regression information +^^^^^^^^^^^^^^^^^^^^^^ +- TBC +- *GitHub info*: PR `927 `__ diff --git a/covasim/parameters.py b/covasim/parameters.py index bbfb3a28e..153714bcc 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -72,7 +72,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['nab_boost'] = 1.5 # Multiplicative factor applied to a person's nab levels if they get reinfected. # TODO: add source pars['nab_eff'] = dict(sus=dict(slope=2.7, n_50=0.03), symp=0.1, sev=0.52) # Parameters to map nabs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains - pars['rel_imm'] = dict(asymptomatic=0.85, mild=1, severe=1.5) # Relative immunity from natural infection varies by symptoms + pars['rel_imm'] = dict(asymp=0.85, mild=1, severe=1.5) # Relative immunity from natural infection varies by symptoms pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in immunity.py # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains diff --git a/covasim/people.py b/covasim/people.py index 544f7e4cc..f908bebc8 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -283,13 +283,13 @@ def check_recovery(self, inds=None, filter_inds='is_exp'): if self.pars['use_waning']: # Before letting them recover, store information about the strain they had, store symptoms and pre-compute nabs array - mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=inds) - severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=inds) + mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=inds) + severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=inds) # Reset additional states - self.susceptible[inds] = True - self.prior_symptoms[inds] = self.pars['rel_imm']['asymptomatic'] - self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] + self.susceptible[inds] = True + self.prior_symptoms[inds] = self.pars['rel_imm']['asymp'] + self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] if len(inds): cvi.init_nab(self, inds, prior_inf=True) From 455fd53635ae2b8a94f762270ee71d0031e379fa Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 02:18:47 -0700 Subject: [PATCH 469/569] minor tidying --- covasim/immunity.py | 1 + covasim/run.py | 2 +- tests/devtests/test_variants.py | 24 ++++++++++-------------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 7d5a42364..194d721fb 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -425,6 +425,7 @@ def precompute_waning(length, pars=None): array of length 'length' of values ''' + pars = sc.dcp(pars) form = pars.pop('form') choices = [ 'nab_decay', # Default if no form is provided diff --git a/covasim/run.py b/covasim/run.py index 186045558..bc70dfd9b 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -955,7 +955,7 @@ def print_heading(string): scen_sim = sc.dcp(self.base_sim) scen_sim.label = scenkey - scen_sim.update_pars(scenpars, **kwargs) # Update the parameters, if provided + scen_sim.update_pars(scenpars) # Update the parameters, if provided if 'strains' in scenpars: # Process strains scen_sim.init_strains() scen_sim.init_immunity(create=True) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index c862ef5fc..632baa9cb 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -35,45 +35,41 @@ def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): 'baseline': { 'name': 'Default Immunity (decay at log(2)/90)', 'pars': { - 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), + 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/90, decay_time1=250, decay_rate2=0.001), }, }, # 'slower_immunity': { # 'name': 'Slower Immunity (decay at log(2)/150)', # 'pars': { - # 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 150, 'init_decay_time': 250, - # 'decay_decay_rate': 0.001}), + # 'nab_decay': dict(form='nab_decay', pars={'decay_rate1': np.log(2) / 150, 'decay_time1': 250, + # 'decay_rate2': 0.001}), # }, # }, 'faster_immunity': { 'name': 'Faster Immunity (decay at log(2)/30)', 'pars': { - 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), + 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/30, decay_time1=250, decay_rate2=0.001), }, }, 'baseline_b1351': { 'name': 'Default Immunity (decay at log(2)/90), B1351 on day 100', 'pars': { - 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), + 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/90, decay_time1=250, decay_rate2=0.001), 'strains': [b1351], }, }, # 'slower_immunity_b1351': { # 'name': 'Slower Immunity (decay at log(2)/150), B1351 on day 100', # 'pars': { - # 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 150, 'init_decay_time': 250, - # 'decay_decay_rate': 0.001}), + # 'nab_decay': dict(form='nab_decay', pars={'decay_rate1': np.log(2) / 150, 'decay_time1': 250, + # 'decay_rate2': 0.001}), # 'strains': [b1351], # }, # }, 'faster_immunity_b1351': { 'name': 'Faster Immunity (decay at log(2)/30), B1351 on day 100', 'pars': { - 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), + 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/30, decay_time1=250, decay_rate2=0.001), 'strains': [b1351], }, }, @@ -523,8 +519,8 @@ def get_ind_of_min_value(list, time): sim5 = test_vaccine_1strain() # Run multisim and scenario tests - scens0 = test_vaccine_1strain_scen() #TODO, NOT WORKING CURRENTLY - scens1 = test_vaccine_2strains_scen() #TODO, NOT WORKING CURRENTLY + scens0 = test_vaccine_1strain_scen() + scens1 = test_vaccine_2strains_scen() scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) msim0 = test_msim() From aef9e583b79bf3c80e4a6bcf90864b9c74f5c565 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 12 Apr 2021 19:18:33 +0200 Subject: [PATCH 470/569] add tutorial --- docs/tutorials/t11.ipynb | 160 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 docs/tutorials/t11.ipynb diff --git a/docs/tutorials/t11.ipynb b/docs/tutorials/t11.ipynb new file mode 100644 index 000000000..c54df8ccf --- /dev/null +++ b/docs/tutorials/t11.ipynb @@ -0,0 +1,160 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# T11 - Immunity methods\n", + "\n", + "This tutorial covers several of the features new to Covasim 3.0, including waning immunity, multi-strain modelling, and advanced vaccination methods.\n", + "\n", + "## Using waning immunity\n", + "\n", + "By default, infection is assumed to confer lifelong perfect immunity, meaning that people who have been infected cannot be infected again.\n", + "However, this can be changed by setting `use_waning=True` when initializing a simulation.\n", + "When `use_waning` is set to True, agents in the simulation are assigned an initial level of neutralizing antibodies after recovering from an infection, drawn from a distribution defined in the parameter dictionary.\n", + "This level decays over time, leading to declines in the efficacy of protection against infection, symptoms, and severe symptoms.\n", + "The following example creates simulations without waning immunity (the default), and compares it to simulations with different speeds of immunity waning." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import covasim as cv\n", + "cv.options.set(dpi=100, show=False, close=True, verbose=0) # Standard options for Jupyter notebook\n", + "\n", + "# Create sims with and without waning immunity\n", + "sim_nowaning = cv.Sim(n_days=120, label='No waning immunity')\n", + "sim_waning = cv.Sim(use_waning=True, n_days=120, label='Waning immunity')\n", + "\n", + "# Now create an alternative sim with faster decay for neutralizing antibodies\n", + "sim_fasterwaning = cv.Sim(\n", + " use_waning=True,\n", + " n_days=120,\n", + " label='Faster waning immunity',\n", + " pars={'NAb_decay': dict(form='nab_decay',\n", + " pars={'init_decay_rate': np.log(2) / 30,\n", + " 'init_decay_time': 250,\n", + " 'decay_decay_rate': 0.001})}\n", + " )\n", + "\n", + "\n", + "# Create a multisim, run, and plot results\n", + "msim = cv.MultiSim([sim_nowaning, sim_waning, sim_fasterwaning])\n", + "msim.run()\n", + "msim.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Multi-strain modelling\n", + "\n", + "The next examples show how to introduce new strains into a simulation.\n", + "These can either be known variants of concern, or custom new strains.\n", + "New strains may have differing levels of transmissibility, symptomaticity, severity, and mortality.\n", + "When introducing new strains, `use_waning` must be set to `True`.\n", + "The model includes known information about the levels of cross-immunity between different strains.\n", + "Cross-immunity can also be manually adjusted, as illustrated in the example below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import sciris as sc\n", + "import covasim as cv\n", + "\n", + "# Define three new strains: B117, B1351, and a custom-defined strain\n", + "b117 = cv.Strain('b117', days=5, n_imports=10)\n", + "b1351 = cv.Strain('b1351', days=10, n_imports=10)\n", + "custom = cv.Strain('Custom', days=15, n_imports=10)\n", + "\n", + "# Create the simulation\n", + "sim = cv.Sim(use_waning=True, strains=[b117, b1351, custom])\n", + "\n", + "# Run and plot\n", + "sim.run()\n", + "sim.plot()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Advanced vaccination methods\n", + "\n", + "The intervention `cv.vaccinate()` allows you to introduce a selection of known vaccines into the model, each of which is pre-populated with known parameters on their efficacy against different variants, their durations of protection, and the levels of protection that they afford against infection and disease progression.\n", + "When using `cv.vaccinate()`, `use_waning` must be set to `True`.\n", + "The following example illustrates how to use the `cv.vaccinate()` intervention." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import covasim as cv\n", + "\n", + "# Create some base parameters\n", + "pars = {\n", + " 'beta': 0.015,\n", + " 'n_days': 120,\n", + "}\n", + "\n", + "# Define a Pfizer vaccine\n", + "pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer')\n", + "sim = cv.Sim(\n", + " use_waning=True,\n", + " pars=pars,\n", + " interventions=pfizer\n", + ")\n", + "sim.run()\n", + "sim.plot()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file From 3d29d7fb5e265d2207e806a7a584ffd7d50af147 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 12 Apr 2021 20:45:16 +0200 Subject: [PATCH 471/569] fix custom --- docs/tutorials/t11.ipynb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/t11.ipynb b/docs/tutorials/t11.ipynb index c54df8ccf..eeaa1a602 100644 --- a/docs/tutorials/t11.ipynb +++ b/docs/tutorials/t11.ipynb @@ -76,7 +76,10 @@ "# Define three new strains: B117, B1351, and a custom-defined strain\n", "b117 = cv.Strain('b117', days=5, n_imports=10)\n", "b1351 = cv.Strain('b1351', days=10, n_imports=10)\n", - "custom = cv.Strain('Custom', days=15, n_imports=10)\n", + "custom = cv.Strain('1.5x more transmissible',\n", + " days=15,\n", + " n_imports=10,\n", + " strain_pars = {'rel_beta': 1.5})\n", "\n", "# Create the simulation\n", "sim = cv.Sim(use_waning=True, strains=[b117, b1351, custom])\n", From fa9b69aa44fcfb00d16cbfa195ce55e06d0a692a Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 12 Apr 2021 21:50:46 +0200 Subject: [PATCH 472/569] fix some tests; --- covasim/immunity.py | 1 + covasim/run.py | 7 +++--- tests/devtests/test_variants.py | 41 +++++++++------------------------ 3 files changed, 16 insertions(+), 33 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 7d5a42364..194d721fb 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -425,6 +425,7 @@ def precompute_waning(length, pars=None): array of length 'length' of values ''' + pars = sc.dcp(pars) form = pars.pop('form') choices = [ 'nab_decay', # Default if no form is provided diff --git a/covasim/run.py b/covasim/run.py index 186045558..8889a5c1d 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -882,9 +882,10 @@ def __init__(self, sim=None, metapars=None, scenarios=None, basepars=None, scenf self.basepars = sc.mergedicts({}, basepars) self.base_sim.update_pars(self.basepars) self.base_sim.validate_pars() - self.base_sim.init_strains() - self.base_sim.init_immunity() - self.base_sim.init_results() + if not self.base_sim.initialized: + self.base_sim.init_strains() + self.base_sim.init_immunity() + self.base_sim.init_results() # Copy quantities from the base sim to the main object self.npts = self.base_sim.npts diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index c862ef5fc..3adedd237 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -35,45 +35,26 @@ def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): 'baseline': { 'name': 'Default Immunity (decay at log(2)/90)', 'pars': { - 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), - }, + 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/90, decay_time1=250,decay_rate2= 0.001) + } }, - # 'slower_immunity': { - # 'name': 'Slower Immunity (decay at log(2)/150)', - # 'pars': { - # 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 150, 'init_decay_time': 250, - # 'decay_decay_rate': 0.001}), - # }, - # }, 'faster_immunity': { 'name': 'Faster Immunity (decay at log(2)/30)', 'pars': { - 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), + 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2) / 30, decay_time1= 250, decay_rate2=0.001), }, }, 'baseline_b1351': { 'name': 'Default Immunity (decay at log(2)/90), B1351 on day 100', 'pars': { - 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2)/90, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), + 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/90, decay_time1=250,decay_rate2=0.001), 'strains': [b1351], }, }, - # 'slower_immunity_b1351': { - # 'name': 'Slower Immunity (decay at log(2)/150), B1351 on day 100', - # 'pars': { - # 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 150, 'init_decay_time': 250, - # 'decay_decay_rate': 0.001}), - # 'strains': [b1351], - # }, - # }, 'faster_immunity_b1351': { 'name': 'Faster Immunity (decay at log(2)/30), B1351 on day 100', 'pars': { - 'NAb_decay': dict(form='nab_decay', pars={'init_decay_rate': np.log(2) / 30, 'init_decay_time': 250, - 'decay_decay_rate': 0.001}), + 'nab_decay': dict(form='nab_decay', decay_rate1 = np.log(2) / 30, decay_time1= 250, decay_rate2= 0.001), 'strains': [b1351], }, }, @@ -81,7 +62,7 @@ def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): metapars = {'n_runs': n_runs} scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run() + scens.run(debug=True) to_plot = sc.objdict({ 'New infections': ['new_infections'], @@ -299,7 +280,7 @@ def test_vaccine_2strains_scen(do_plot=False, do_show=True, do_save=False): metapars = {'n_runs': n_runs} scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run() + scens.run(debug=True) to_plot = sc.objdict({ 'New infections': ['new_infections'], @@ -342,7 +323,7 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): metapars = {'n_runs': n_runs} scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run() + scens.run(debug=True) to_plot = sc.objdict({ 'New infections': ['new_infections'], @@ -523,13 +504,13 @@ def get_ind_of_min_value(list, time): sim5 = test_vaccine_1strain() # Run multisim and scenario tests - scens0 = test_vaccine_1strain_scen() #TODO, NOT WORKING CURRENTLY + scens0 = test_vaccine_1strain_scen() scens1 = test_vaccine_2strains_scen() #TODO, NOT WORKING CURRENTLY - scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show)#TODO, NOT WORKING CURRENTLY msim0 = test_msim() # Run immunity tests - sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show)#TODO, NOT WORKING CURRENTLY # Run test to compare sims with and without waning scens3 = test_waning_vs_not(do_plot=do_plot, do_save=do_save, do_show=do_show) From 77616952ec743669f35f7c406575a74be63472ef Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 16:27:33 -0700 Subject: [PATCH 473/569] reworking tutorial naming --- docs/tutorials.rst | 22 ++++++++++--------- .../{t10.ipynb => tut_advanced.ipynb} | 0 .../{t06.ipynb => tut_analyzers.ipynb} | 0 .../{t07.ipynb => tut_calibration.ipynb} | 0 .../{t08.ipynb => tut_deployment.ipynb} | 0 .../{t05.ipynb => tut_interventions.ipynb} | 0 docs/tutorials/{t01.ipynb => tut_intro.ipynb} | 0 .../tutorials/{t04.ipynb => tut_people.ipynb} | 0 .../{t02.ipynb => tut_plotting.ipynb} | 0 .../{t03.ipynb => tut_running.ipynb} | 0 docs/tutorials/{t09.ipynb => tut_tips.ipynb} | 0 .../{t11.ipynb => tut_variants.ipynb} | 0 12 files changed, 12 insertions(+), 10 deletions(-) rename docs/tutorials/{t10.ipynb => tut_advanced.ipynb} (100%) rename docs/tutorials/{t06.ipynb => tut_analyzers.ipynb} (100%) rename docs/tutorials/{t07.ipynb => tut_calibration.ipynb} (100%) rename docs/tutorials/{t08.ipynb => tut_deployment.ipynb} (100%) rename docs/tutorials/{t05.ipynb => tut_interventions.ipynb} (100%) rename docs/tutorials/{t01.ipynb => tut_intro.ipynb} (100%) rename docs/tutorials/{t04.ipynb => tut_people.ipynb} (100%) rename docs/tutorials/{t02.ipynb => tut_plotting.ipynb} (100%) rename docs/tutorials/{t03.ipynb => tut_running.ipynb} (100%) rename docs/tutorials/{t09.ipynb => tut_tips.ipynb} (100%) rename docs/tutorials/{t11.ipynb => tut_variants.ipynb} (100%) diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 84b951b2f..23483dfea 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -3,13 +3,15 @@ .. toctree:: :maxdepth: 1 - tutorials/t01 - tutorials/t02 - tutorials/t03 - tutorials/t04 - tutorials/t05 - tutorials/t06 - tutorials/t07 - tutorials/t08 - tutorials/t09 - tutorials/t10 + tutorials/tut_intro.ipynb + tutorials/tut_plotting.ipynb + tutorials/tut_running.ipynb + tutorials/tut_people.ipynb + tutorials/tut_interventions.ipynb + tutorials/tut_analyzers.ipynb + tutorials/tut_calibration.ipynb + tutorials/tut_variants.ipynb + tutorials/tut_deployment.ipynb + tutorials/tut_tips.ipynb + tutorials/tut_advanced.ipynb + diff --git a/docs/tutorials/t10.ipynb b/docs/tutorials/tut_advanced.ipynb similarity index 100% rename from docs/tutorials/t10.ipynb rename to docs/tutorials/tut_advanced.ipynb diff --git a/docs/tutorials/t06.ipynb b/docs/tutorials/tut_analyzers.ipynb similarity index 100% rename from docs/tutorials/t06.ipynb rename to docs/tutorials/tut_analyzers.ipynb diff --git a/docs/tutorials/t07.ipynb b/docs/tutorials/tut_calibration.ipynb similarity index 100% rename from docs/tutorials/t07.ipynb rename to docs/tutorials/tut_calibration.ipynb diff --git a/docs/tutorials/t08.ipynb b/docs/tutorials/tut_deployment.ipynb similarity index 100% rename from docs/tutorials/t08.ipynb rename to docs/tutorials/tut_deployment.ipynb diff --git a/docs/tutorials/t05.ipynb b/docs/tutorials/tut_interventions.ipynb similarity index 100% rename from docs/tutorials/t05.ipynb rename to docs/tutorials/tut_interventions.ipynb diff --git a/docs/tutorials/t01.ipynb b/docs/tutorials/tut_intro.ipynb similarity index 100% rename from docs/tutorials/t01.ipynb rename to docs/tutorials/tut_intro.ipynb diff --git a/docs/tutorials/t04.ipynb b/docs/tutorials/tut_people.ipynb similarity index 100% rename from docs/tutorials/t04.ipynb rename to docs/tutorials/tut_people.ipynb diff --git a/docs/tutorials/t02.ipynb b/docs/tutorials/tut_plotting.ipynb similarity index 100% rename from docs/tutorials/t02.ipynb rename to docs/tutorials/tut_plotting.ipynb diff --git a/docs/tutorials/t03.ipynb b/docs/tutorials/tut_running.ipynb similarity index 100% rename from docs/tutorials/t03.ipynb rename to docs/tutorials/tut_running.ipynb diff --git a/docs/tutorials/t09.ipynb b/docs/tutorials/tut_tips.ipynb similarity index 100% rename from docs/tutorials/t09.ipynb rename to docs/tutorials/tut_tips.ipynb diff --git a/docs/tutorials/t11.ipynb b/docs/tutorials/tut_variants.ipynb similarity index 100% rename from docs/tutorials/t11.ipynb rename to docs/tutorials/tut_variants.ipynb From 2c9588f505ffd1a354cd9143717521a0b497808b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 16:42:15 -0700 Subject: [PATCH 474/569] tutorials not working yet --- docs/tutorials/tut_advanced.ipynb | 2 +- docs/tutorials/tut_calibration.ipynb | 4 ++-- docs/tutorials/tut_deployment.ipynb | 2 +- docs/tutorials/tut_tips.ipynb | 2 +- docs/tutorials/tut_variants.ipynb | 31 ++++++++++++++-------------- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/docs/tutorials/tut_advanced.ipynb b/docs/tutorials/tut_advanced.ipynb index eb2cf9492..5f5a4ca97 100644 --- a/docs/tutorials/tut_advanced.ipynb +++ b/docs/tutorials/tut_advanced.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# T10 - Advanced features\n", + "# T11 - Advanced features\n", "\n", "This tutorial covers advanced features of Covasim, including custom population options and changing the internal computational methods.\n", "\n", diff --git a/docs/tutorials/tut_calibration.ipynb b/docs/tutorials/tut_calibration.ipynb index 9552a4f45..e46a33fb6 100644 --- a/docs/tutorials/tut_calibration.ipynb +++ b/docs/tutorials/tut_calibration.ipynb @@ -263,8 +263,8 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (Spyder)", - "language": "python3", + "display_name": "Python 3", + "language": "python", "name": "python3" }, "language_info": { diff --git a/docs/tutorials/tut_deployment.ipynb b/docs/tutorials/tut_deployment.ipynb index a06be4c05..d571ca1ef 100644 --- a/docs/tutorials/tut_deployment.ipynb +++ b/docs/tutorials/tut_deployment.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# T8 - Deployment\n", + "# T9 - Deployment\n", "\n", "This tutorial provides several useful recipes for deploying Covasim.\n", "\n", diff --git a/docs/tutorials/tut_tips.ipynb b/docs/tutorials/tut_tips.ipynb index b1df9199c..1c1eaf8df 100644 --- a/docs/tutorials/tut_tips.ipynb +++ b/docs/tutorials/tut_tips.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# T9 - Tips and tricks\n", + "# T10 - Tips and tricks\n", "\n", "This tutorial contains suggestions that aren't essential to follow, but which may make your life easier.\n", "\n", diff --git a/docs/tutorials/tut_variants.ipynb b/docs/tutorials/tut_variants.ipynb index eeaa1a602..08dd9b6da 100644 --- a/docs/tutorials/tut_variants.ipynb +++ b/docs/tutorials/tut_variants.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# T11 - Immunity methods\n", + "# T8 - Immunity methods\n", "\n", "This tutorial covers several of the features new to Covasim 3.0, including waning immunity, multi-strain modelling, and advanced vaccination methods.\n", "\n", @@ -33,14 +33,11 @@ "\n", "# Now create an alternative sim with faster decay for neutralizing antibodies\n", "sim_fasterwaning = cv.Sim(\n", - " use_waning=True,\n", - " n_days=120,\n", " label='Faster waning immunity',\n", - " pars={'NAb_decay': dict(form='nab_decay',\n", - " pars={'init_decay_rate': np.log(2) / 30,\n", - " 'init_decay_time': 250,\n", - " 'decay_decay_rate': 0.001})}\n", - " )\n", + " n_days=120,\n", + " use_waning=True,\n", + " nab_decay=dict(form='nab_decay', decay_rate1=np.log(2)/30, decay_time1=250, decay_rate2=0.001)\n", + ")\n", "\n", "\n", "# Create a multisim, run, and plot results\n", @@ -76,10 +73,7 @@ "# Define three new strains: B117, B1351, and a custom-defined strain\n", "b117 = cv.Strain('b117', days=5, n_imports=10)\n", "b1351 = cv.Strain('b1351', days=10, n_imports=10)\n", - "custom = cv.Strain('1.5x more transmissible',\n", - " days=15,\n", - " n_imports=10,\n", - " strain_pars = {'rel_beta': 1.5})\n", + "custom = cv.Strain(label='1.5x more transmissible', strain = {'rel_beta': 1.5}, days=15, n_imports=10)\n", "\n", "# Create the simulation\n", "sim = cv.Sim(use_waning=True, strains=[b117, b1351, custom])\n", @@ -124,12 +118,19 @@ "sim.run()\n", "sim.plot()\n" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", - "language": "python", + "display_name": "Python 3 (Spyder)", + "language": "python3", "name": "python3" }, "language_info": { @@ -160,4 +161,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} From 4877032e487e5d68594065954c637a1d878f0452 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 17:04:05 -0700 Subject: [PATCH 475/569] working now, new debug option --- docs/Makefile | 8 ++++++- docs/build_docs | 33 ++++++++++++++++++++------ docs/tutorials/tut_interventions.ipynb | 4 ++-- docs/tutorials/tut_variants.ipynb | 11 +++++++-- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index c38d8ebf5..683b5a7ef 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -11,7 +11,7 @@ BUILDDIR = _build PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -v -jauto -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others +DEBUGSPHINXOPTS = -vv -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help @@ -65,6 +65,12 @@ html: @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." +.PHONY: debug +debug: + $(SPHINXBUILD) -b html $(DEBUGSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Debug build finished. The HTML pages are in $(BUILDDIR)/html." + .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml diff --git a/docs/build_docs b/docs/build_docs index a56dd87bf..1586dc13a 100755 --- a/docs/build_docs +++ b/docs/build_docs @@ -1,23 +1,42 @@ #!/bin/bash # -# To rebuild notebooks, type -# ./build_docs auto +# To run in debug mode (serial), type +# ./build_docs debug # -# Otherwise, Jupyter notebooks will not be rebuilt. +# To not rebuild notebooks, type +# ./build_docs never +# +# Otherwise, Jupyter notebooks will be rebuilt in parallel. -start=$SECONDS -export NBSPHINX_EXECUTE=$1 echo 'Building docs...' +start=$SECONDS make clean # Delete -make html # Actually make -duration=$(( SECONDS - start )) + + +# Handle notebook build options +if [[ "$*" == *"never"* ]]; then + export NBSPHINX_EXECUTE=never +else + export NBSPHINX_EXECUTE=auto +fi + + +# Handle notebook build options +if [[ "$*" == *"debug"* ]]; then + make debug # Actually make +else + make html # Actually make +fi + echo 'Cleaning up tutorial files...' cd tutorials ./clean_outputs cd .. + +duration=$(( SECONDS - start )) echo "Docs built after $duration seconds." echo "Index:" echo "`pwd`/_build/html/index.html" \ No newline at end of file diff --git a/docs/tutorials/tut_interventions.ipynb b/docs/tutorials/tut_interventions.ipynb index f10b3c923..bb4d25baf 100644 --- a/docs/tutorials/tut_interventions.ipynb +++ b/docs/tutorials/tut_interventions.ipynb @@ -550,8 +550,8 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (Spyder)", - "language": "python3", + "display_name": "Python 3", + "language": "python", "name": "python3" }, "language_info": { diff --git a/docs/tutorials/tut_variants.ipynb b/docs/tutorials/tut_variants.ipynb index 08dd9b6da..5016b151f 100644 --- a/docs/tutorials/tut_variants.ipynb +++ b/docs/tutorials/tut_variants.ipynb @@ -119,6 +119,13 @@ "sim.plot()\n" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, @@ -129,8 +136,8 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (Spyder)", - "language": "python3", + "display_name": "Python 3", + "language": "python", "name": "python3" }, "language_info": { From bf34cb6b84848ad68205e78bb5b98ff3165e1e10 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 17:04:58 -0700 Subject: [PATCH 476/569] rename --- docs/tutorials.rst | 2 +- docs/tutorials/{tut_variants.ipynb => tut_immunity.ipynb} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/tutorials/{tut_variants.ipynb => tut_immunity.ipynb} (100%) diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 23483dfea..b59614973 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -10,7 +10,7 @@ tutorials/tut_interventions.ipynb tutorials/tut_analyzers.ipynb tutorials/tut_calibration.ipynb - tutorials/tut_variants.ipynb + tutorials/tut_immunity.ipynb tutorials/tut_deployment.ipynb tutorials/tut_tips.ipynb tutorials/tut_advanced.ipynb diff --git a/docs/tutorials/tut_variants.ipynb b/docs/tutorials/tut_immunity.ipynb similarity index 100% rename from docs/tutorials/tut_variants.ipynb rename to docs/tutorials/tut_immunity.ipynb From 1af8904f39b4fc8116517cf99e92afc1422caf43 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 17:20:14 -0700 Subject: [PATCH 477/569] local build issues --- docs/index.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 99475bb00..683ea57de 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,7 +4,12 @@ Welcome to Covasim Covasim is a stochastic agent-based simulator, written in Python, for exploring and analyzing the COVID-19 epidemic. -**There's a lot here, where should I start?** Take a quick look at the overview, which provides a general introduction. Then when you're ready to sink your teeth in, the tutorials will help you get started using Covasim. +**There's a lot here, where should I start?** + +- Take a quick look at the overview, which provides a general introduction. +- When you're ready to sink your teeth in, the tutorials will help you get started using Covasim. +- If you're looking for a specific feature or keyword, you should be able to find it with the search feature (top left). +- Still have questions? Send us an email at covasim@idmod.org. We're happy to help! Full contents ============= From cc9321d2a4d987bc7eac6f2cc04451c410269b29 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 18:36:42 -0700 Subject: [PATCH 478/569] more changelog updates --- CHANGELOG.rst | 31 +++++++++++++++++++++++++------ covasim/analysis.py | 2 +- covasim/sim.py | 8 ++++---- covasim/utils.py | 2 +- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 911f88eb9..cd38d9cd1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,21 +26,22 @@ Latest versions (3.0.x) Version 3.0.0 (2021-04-12) -------------------------- -This version introduces fully featured vaccines, variants, and immunity. **Note:** These new features are still under development; please use with caution and email us at covasim@idmod.org if you have any questions or issues. +This version introduces fully featured vaccines, variants, and immunity. **Note:** These new features are still under development; please use with caution and email us at covasim@idmod.org if you have any questions or issues. We expect there to be several more releases over the next few weeks as we refine these new features. Highlights ^^^^^^^^^^ - **Model structure**: The model now follows an "SEIS"-type structure, instead of the previous "SEIR" structure. This means that after recovering from an infection, agents return to the "susceptible" compartment. Each agent in the simulation has properties ``sus_imm``, ``trans_imm`` and ``prog_imm``, which respectively determine their immunity to acquiring an infection, transmitting an infection, or developing a more severe case of COVID-19. All these immunity levels are initially zero. They can be boosted by either natural infection or vaccination, and thereafter they can wane over time or remain permanently elevated. - **Multi-strain modeling**: Model functionality has been extended to allow for modeling of multiple different co-circulating strains with different properties. This means you can now do e.g. ``b117 = cv.Strain('b117', days=1, n_imports=20)`` followed by ``sim = cv.Sim(strains=b117)`` to import strain B117. Further examples are contained in ``devtests/test_variants.py``. - **New methods for vaccine modeling**: A new ``cv.vaccinate()`` intervention has been added, which allows more flexible modeling of vaccinations. Vaccines, like natural infections, are assumed to boost agents' immunity. -- **Consistency**: By default, results from Covasim 3.0 should exactly match Covasim 2.0. To use the new features, you will need to manually specify ``cv.Sim(use_waning=True)``. +- **Consistency**: By default, results from Covasim 3.0.0 should exactly match Covasim 2.1.2. To use the new features, you will need to manually specify ``cv.Sim(use_waning=True)``. State changes ^^^^^^^^^^^^^ -- Several new states have been added, including ``people.naive``, which stores whether or not a person has ever been exposed to COVID before. +- Several new states have been added, such as ``people.naive``, which stores whether or not a person has ever been exposed to COVID before. Parameter changes ^^^^^^^^^^^^^^^^^ +- A new control parameter, ``use_waning``, has been added that controls whether to use new waning immunity dynamics ("SEIS" structure) or the old dynamics where post-infection immunity was perfect and did not wane ("SEIR" structure). By default, ``use_waning=False``. - A subset of existing parameters have been made strain-specific, meaning that they are allowed to differ by strain. These include: ``rel_beta``, which specifies the relative transmissibility of a new strain compared to the wild strain; ``rel_symp_prob``, ``rel_severe_prob``, ``rel_crit_prob``, and the newly-added immunity parameters ``rel_imm`` (see next point). The list of parameters that can vary by strain is specified in ``defaults.py``. - The parameter ``n_strains`` is an integer that specifies how many strains will be in circulation at some point during the course of the simulation. - Seven new parameters have been added to characterize agents' immunity levels: @@ -49,23 +50,41 @@ Parameter changes - The parameter ``nab_kin`` is constructed during sim initialization, and contains pre-computed evaluations of the nab decay functions described above over time. - The parameter ``nab_boost`` is a multiplicative factor applied to a person's nab levels if they get reinfected. - The parameter ``cross_immunity``. By default, infection with one strain of SARS-CoV-2 is assumed to grant 50% immunity to infection with a different strain. This default assumption of 50% cross-immunity can be modified via this parameter (which will then apply to all strains in the simulation), or it can be modified on a per-strain basis using the ``immunity`` parameter described below. - - The parameter ``immunity`` is a matrix of size ``total_strains`` by ``total_strains``. Row ``i`` specifies the immunity levels that people who have been infected with strain ``i`` have to other strains. The entries of this matrix are then multiplied by the time-dependent immunity levels contained in the ``immune_degree`` parameter to determine a person's immunity at each time-step. By default, this will be ``[[1]]`` for a single-strain simulation and ``[[1, 0.5],[0.5, 1]]`` a 2-strain simulation. + - The parameter ``immunity`` is a matrix of size ``total_strains`` by ``total_strains``. Row ``i`` specifies the immunity levels that people who have been infected with strain ``i`` have to other strains. - The parameter ``rel_imm`` is a dictionary with keys ``asymp``, ``mild`` and ``severe``. These contain scalars specifying the relative immunity levels for someone who had an asymptomatic, mild, or severe infection. By default, values of 0.98, 0.99, and 1.0 are used. - The parameter ``strains`` contains information about any circulating strains that have been specified as additional to the default strain, and the parameter ``vaccines`` contains information about any vaccines in use. These are initialized as ``None`` and then populated by the user. +- The parameter ``frac_susceptible`` will initialize the simulation with less than 100% of the population to be susceptible to COVID (to represent, for example, a baseline level of population immunity). Note that this is intended for quick explorations only, since people are selected at random, whereas in reality higher-risk people will typically be infected first and preferentially be immune. This is primarily designed for use with ``use_waning=False``. +- The parameter ``scaled_pop``, if supplied, can be used in place of ``pop_scale`` or ``pop_size``. For example, if you specify ``cv.Sim(pop_size=100e3, scaled_pop=550e3)``, it will automatically calculate ``pop_scale=5.5``. +- Aliases have been added for several parameters: ``pop_size`` can also be supplied as ``n_agents``, and ``pop_infected`` can also be supplied as ``init_infected``. This only applies when creating a sim; otherwise, the default names will be used for these parameters. Changes to results ^^^^^^^^^^^^^^^^^^ -- New results have been added to store information by strain, in ``sim.results['strain']``: ``cum_infections_by_strain``, ``cum_infectious_by_strain``, ``new_reinfections``, ``new_infections_by_strain``, ``new_infectious_by_strain``, ``prevalence_by_strain``, ``incidence_by_strain``. +- New results have been added to store information by strain, as well as population immunity levels. In addition to new entries in ``sim.results``, such as ``pop_nabs`` (population level neutralizing antibodies) and ``new_reinfections``, there is a new set of results ``sim.results.strain``: ``cum_infections_by_strain``, ``cum_infectious_by_strain``, ``new_infections_by_strain``, ``new_infectious_by_strain``, ``prevalence_by_strain``, ``incidence_by_strain``. New functions, methods and classes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - The newly-added file ``immunity.py`` contains functions, methods, and classes related to calculating immunity. This includes the ``Strain`` class, the ``Vaccine`` class. - A new ``cv.vaccinate()`` intervention has been added. Compared to the previous ``vaccine`` intervention (now renamed ``cv.simple_vaccine()``), this new intervention allows vaccination to boost agents' immunity against infection, transmission, and progression. - A new function ``cv.demo()`` has been added as a shortcut to ``cv.Sim().run().plot()``. +- There is a new ``sim.people.make_nonnaive()`` method, as the opposite of ``sim.people.make_naive()``. +- New functions ``cv.iundefined()`` and ``cv.iundefinedi()`` have been added for completeness. + +Renamed functions and methods +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +- ``cv.vaccine()`` is now called ``cv.simple_vaccine()``. +- ``cv.get_sim_plots()`` is now called ``cv.get_default_plots()``; ``cv.get_scen_plots()`` is now ``cv.get_default_plots(kind='scen')``. +- ``sim.people.make_susceptible()`` is now called ``sim.people.make_naive()``. + +Bugfixes +^^^^^^^^ +- ``n_imports`` now scales correctly with population scale (previously they were unscaled). +- ``cv.ifalse()`` and related functions now work correctly with non-boolean arrays (previously they used the ``~`` operator instead of ``np.logical_not()``, which gave incorrect results for int or float arrays). Regression information ^^^^^^^^^^^^^^^^^^^^^^ -- TBC +- As noted above, with ``cv.Sim(use_waning=False)`` (the default), results should be the same as Covasim 2.1.2, except for new results keys mentioned above (which will mostly be zeros, since they are only populated with immunity turned on). +- Scripts using ``cv.vaccine()`` should be updated to use ``cv.simple_vaccine()``. +- Scripts calling ``sim.people.make_susceptible()`` should now call ``sim.people.make_naive()``. - *GitHub info*: PR `927 `__ diff --git a/covasim/analysis.py b/covasim/analysis.py index 29343ae96..de80c52bf 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -596,7 +596,7 @@ def initialize(self, sim): else: self.days = sim.day(self.days) - self.keys = ['exposed', 'infectious', 'symptomatic', 'severe', 'critical', 'known_contact', 'quarantined', 'diagnosed', 'dead'] # Extra: 'recovered' + self.keys = ['exposed', 'infectious', 'symptomatic', 'severe', 'critical', 'known_contact', 'quarantined', 'diagnosed', 'recovered', 'dead'] self.basekeys = ['stocks', 'trans', 'source', 'test', 'quar'] # Categories of things to plot self.extrakeys = ['layer_counts', 'extra'] self.initialized = True diff --git a/covasim/sim.py b/covasim/sim.py index 089bec208..f18d7fe80 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -202,10 +202,10 @@ def validate_pars(self, validate_layers=True): scaled_pop = self.pars.get('scaled_pop') pop_scale = self.pars.get('pop_scale') if scaled_pop is not None: # If scaled_pop is supplied, try to use it - if pop_scale is not None: # Normal case, recalculate number of agents - self['pop_size'] = scaled_pop/pop_scale - else: # Special case, recalculate population scale + if pop_scale in [None, 1.0]: # Normal case, recalculate population scale self['pop_scale'] = scaled_pop/pop_size + else: # Special case, recalculate number of agents + self['pop_size'] = int(scaled_pop/pop_scale) # Handle types for key in ['pop_size', 'pop_infected']: @@ -1070,7 +1070,7 @@ def summarize(self, full=False, t=None, output=False): labelstr = f' "{self.label}"' if self.label else '' string = f'Simulation{labelstr} summary:\n' for key in self.result_keys(): - if full or key.startswith('cum_') and 'by_strain' not in key: + if full or key.startswith('cum_'): string += f' {summary[key]:5.0f} {self.results[key].name.lower()}\n' # Print or return string diff --git a/covasim/utils.py b/covasim/utils.py index 222564ad2..1b8822f80 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -79,7 +79,7 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=True, parallel=safe_parallel) +@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat, nbfloat[:]), cache=cache, parallel=safe_parallel) def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, iso_factor, quar_factor, immunity_factors): # pragma: no cover ''' Calculate relative transmissibility and susceptibility ''' f_asymp = symp + ~symp * asymp_factor # Asymptomatic factor, changes e.g. [0,1] with a factor of 0.8 to [0.8,1.0] From 0e2c1f7e99a9e9db4ca58633906b913cdaf2b227 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 20:51:11 -0700 Subject: [PATCH 479/569] working on inspection --- CHANGELOG.rst | 2 +- covasim/analysis.py | 10 +++++----- covasim/interventions.py | 4 ++++ covasim/parameters.py | 2 +- covasim/run.py | 5 +++-- covasim/sim.py | 5 ++++- tests/devtests/test_variants.py | 23 +++++++++++------------ 7 files changed, 29 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cd38d9cd1..12f35f41d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,7 +15,7 @@ Coming soon These are the major improvements we are currently working on. If there is a specific bugfix or feature you would like to see, please `create an issue `__. -- Expanded tutorials (health care workers, vaccination, calibration, exercises, etc.) +- Expanded tutorials (health care workers, calibration, exercises, etc.) - Multi-region and geographical support - Economics and costing analysis diff --git a/covasim/analysis.py b/covasim/analysis.py index de80c52bf..bd8ba5c1d 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -56,7 +56,7 @@ def finalize(self, sim=None): final operations after the simulation is complete (e.g. rescaling) ''' if self.finalized: - raise Exception('Analyzer already finalized') # Raise an error because finalizing multiple times has a high probability of producing incorrect results e.g. applying rescale factors twice + raise RuntimeError('Analyzer already finalized') # Raise an error because finalizing multiple times has a high probability of producing incorrect results e.g. applying rescale factors twice self.finalized = True return @@ -251,6 +251,7 @@ def from_sim(self, sim): def initialize(self, sim): + super().initialize() # Handle days self.start_day = sc.date(sim['start_day'], as_date=False) # Get the start day, as a string @@ -284,8 +285,6 @@ def initialize(self, sim): self.data = self.datafile # Use it directly self.datafile = None - self.initialized = True - return @@ -434,6 +433,7 @@ def __init__(self, states=None, edges=None, **kwargs): def initialize(self, sim): + super().initialize() if self.states is None: self.states = ['exposed', 'severe', 'dead', 'tested', 'diagnosed'] @@ -444,7 +444,7 @@ def initialize(self, sim): self.bins = self.edges[:-1] # Don't include the last edge in the bins self.start_day = sim['start_day'] - self.initialized = True + return @@ -591,6 +591,7 @@ def __init__(self, days=None, verbose=True, reporter=None, save_inds=False, **kw def initialize(self, sim): + super().initialize() if self.days is None: self.days = sc.dcp(sim.tvec) else: @@ -599,7 +600,6 @@ def initialize(self, sim): self.keys = ['exposed', 'infectious', 'symptomatic', 'severe', 'critical', 'known_contact', 'quarantined', 'diagnosed', 'recovered', 'dead'] self.basekeys = ['stocks', 'trans', 'source', 'test', 'quar'] # Categories of things to plot self.extrakeys = ['layer_counts', 'extra'] - self.initialized = True return diff --git a/covasim/interventions.py b/covasim/interventions.py index 792faf7c3..7ba899acc 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -272,6 +272,10 @@ def _store_args(self): f1 = inspect.getouterframes(f0) # The list of outer frames parent = f1[2].frame # The parent frame, e.g. change_beta.__init__() _,_,_,values = inspect.getargvalues(parent) # Get the values of the arguments + print('DEBUG') + print(f0) + print(f1) + print(values) if values: self.input_args = {} for key,value in values.items(): diff --git a/covasim/parameters.py b/covasim/parameters.py index 153714bcc..48514e697 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -338,7 +338,7 @@ def get_vaccine_choices(): 'pfizer': ['pfizer', 'biontech', 'pfizer-biontech'], 'moderna': ['moderna'], 'az': ['az', 'astrazeneca'], - 'jj': ['jj', 'johnson & johnson', 'janssen'], + 'jj': ['jj', 'jnj', 'johnson & johnson', 'janssen'], } mapping = {name:key for key,synonyms in choices.items() for name in synonyms} # Flip from key:value to value:key return choices, mapping diff --git a/covasim/run.py b/covasim/run.py index 51e6143d9..77f368566 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -957,11 +957,12 @@ def print_heading(string): scen_sim.label = scenkey scen_sim.update_pars(scenpars) # Update the parameters, if provided + scen_sim.validate_pars() if 'strains' in scenpars: # Process strains scen_sim.init_strains() scen_sim.init_immunity(create=True) - if 'imm_pars' in scenpars: # Process immunity - scen_sim.init_immunity(create=True) + elif 'imm_pars' in scenpars: # Process immunity + scen_sim.init_immunity(create=True) # TODO: refactor run_args = dict(n_runs=self['n_runs'], noise=self['noise'], noisepar=self['noisepar'], keep_people=keep_people, verbose=verbose) if debug: diff --git a/covasim/sim.py b/covasim/sim.py index f18d7fe80..4a76ef7e8 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -255,6 +255,8 @@ def validate_pars(self, validate_layers=True): self['interventions'][i] = cvi.InterventionDict(**interv) self['analyzers'] = sc.promotetolist(self['analyzers'], keepnone=False) self['strains'] = sc.promotetolist(self['strains'], keepnone=False) + for key in ['interventions', 'analyzers', 'strains']: + self[key] = sc.dcp(self[key]) # All of these have initialize functions that run into issues if they're reused # Optionally handle layer parameters if validate_layers: @@ -494,7 +496,8 @@ def init_strains(self): self['strains'] = self._orig_pars.pop('strains') # Restore for strain in self['strains']: - strain.initialize(self) + if not strain.initialized: + strain.initialize(self) return diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 5d22c4b98..5b00f0d43 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -146,7 +146,6 @@ def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): sc.heading('Test vaccination with a single strain') - sc.heading('Setting up...') pars = sc.mergedicts(base_pars, { 'beta': 0.015, @@ -490,21 +489,21 @@ def get_ind_of_min_value(list, time): if __name__ == '__main__': sc.tic() - # Run simplest possible test - test_simple(do_plot=do_plot) + # # Run simplest possible test + # test_simple(do_plot=do_plot) - # Run more complex single-sim tests - sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + # # Run more complex single-sim tests + # sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - # Run Vaccine tests - sim4 = test_synthpops() - sim5 = test_vaccine_1strain() + # # Run Vaccine tests + # sim4 = test_synthpops() + # sim5 = test_vaccine_1strain() # Run multisim and scenario tests - scens0 = test_vaccine_1strain_scen() + # scens0 = test_vaccine_1strain_scen() scens1 = test_vaccine_2strains_scen() #TODO, NOT WORKING CURRENTLY scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show)#TODO, NOT WORKING CURRENTLY msim0 = test_msim() From fea9e3a154794d2f04aaae00451158685652c947 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 21:20:50 -0700 Subject: [PATCH 480/569] add copying --- CHANGELOG.rst | 1 + covasim/immunity.py | 15 +++++++------- covasim/interventions.py | 7 +------ covasim/run.py | 6 +++--- tests/devtests/test_variants.py | 35 +++++++++++++++++---------------- tests/test_analysis.py | 3 +-- 6 files changed, 31 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 12f35f41d..bf6ed95f2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -79,6 +79,7 @@ Bugfixes ^^^^^^^^ - ``n_imports`` now scales correctly with population scale (previously they were unscaled). - ``cv.ifalse()`` and related functions now work correctly with non-boolean arrays (previously they used the ``~`` operator instead of ``np.logical_not()``, which gave incorrect results for int or float arrays). +- Interventions and analyzers are now deep-copied when supplied to a sim; this means that the same ones can be created and then used in multiple sims. Scenarios also now deep-copy their inputs. Regression information ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/covasim/immunity.py b/covasim/immunity.py index 194d721fb..b6e2f81ac 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -15,7 +15,7 @@ __all__ = ['Strain', 'Vaccine'] -class Strain(cvi.Intervention): +class Strain(sc.prettyobj): ''' Add a new strain to the sim @@ -35,11 +35,11 @@ class Strain(cvi.Intervention): sim = cv.Sim(strains=[b117, p1, my_var]).run() # Add them all to the sim ''' - def __init__(self, strain=None, label=None, days=None, n_imports=1, rescale=True, **kwargs): - super().__init__(**kwargs) # Initialize the Intervention object + def __init__(self, strain=None, label=None, days=None, n_imports=1, rescale=True): self.days = days # Handle inputs self.n_imports = cvd.default_int(n_imports) self.parse_strain_pars(strain=strain, label=label) # Strains can be defined in different ways: process these here + self.initialized = False return @@ -82,7 +82,6 @@ def parse_strain_pars(self, strain=None, label=None): def initialize(self, sim): - super().initialize() # Store the index of this strain, and increment the number of strains in the simulation self.index = sim['n_strains'] @@ -95,6 +94,8 @@ def initialize(self, sim): self.p[key] = defaults[key] sim['strain_pars'][key].append(self.p[key]) + self.initialized = True + return @@ -107,7 +108,7 @@ def apply(self, sim): return -class Vaccine(cvi.Intervention): +class Vaccine(sc.prettyobj): ''' Add a new vaccine to the sim (called by interventions.py vaccinate() @@ -116,7 +117,6 @@ class Vaccine(cvi.Intervention): Args: vaccine (dict or str): dictionary of parameters specifying information about the vaccine or label for loading pre-defined vaccine label (str): if supplying a dictionary, a label for the vaccine must be supplied - kwargs (dict): passed to Intervention() **Example**:: @@ -128,8 +128,7 @@ class Vaccine(cvi.Intervention): sim = cv.Sim(interventions=interventions) ''' - def __init__(self, vaccine=None, label=None, **kwargs): - super().__init__(**kwargs) + def __init__(self, vaccine=None, label=None): self.label = label # self.rel_imm = None # list of length n_strains with relative immunity factor # self.doses = None diff --git a/covasim/interventions.py b/covasim/interventions.py index 7ba899acc..42ac8820d 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -272,10 +272,6 @@ def _store_args(self): f1 = inspect.getouterframes(f0) # The list of outer frames parent = f1[2].frame # The parent frame, e.g. change_beta.__init__() _,_,_,values = inspect.getargvalues(parent) # Get the values of the arguments - print('DEBUG') - print(f0) - print(f1) - print(values) if values: self.input_args = {} for key,value in values.items(): @@ -1231,7 +1227,6 @@ class vaccinate(Intervention): ''' def __init__(self, days, prob=1.0, vaccine_pars=None, subtarget=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object - self._store_args() # Store the input arguments so the intervention can be recreated self.days = sc.dcp(days) self.prob = prob self.subtarget = subtarget @@ -1250,7 +1245,7 @@ def initialize(self, sim): self.vaccination_dates = np.full(sim.n, np.nan) # Store the dates when people are vaccinated self.vaccine_ind = len(sim['vaccines']) self.vaccine = cvi.Vaccine(self.vaccine_pars) - self.vaccine.initialize(sim) + # self.vaccine.initialize(sim) sim['vaccines'].append(self.vaccine) self.doses = self.vaccine.p.doses self.interval = self.vaccine.p.interval diff --git a/covasim/run.py b/covasim/run.py index 77f368566..2164efc5c 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -872,14 +872,14 @@ def __init__(self, sim=None, metapars=None, scenarios=None, basepars=None, scenf self.scenarios = scenarios # Handle metapars - self.metapars = sc.mergedicts({}, metapars) + self.metapars = sc.dcp(sc.mergedicts(metapars)) self.update_pars(self.metapars) # Create the simulation and handle basepars if sim is None: sim = cvs.Sim() - self.base_sim = sim - self.basepars = sc.mergedicts({}, basepars) + self.base_sim = sc.dcp(sim) + self.basepars = sc.dcp(sc.mergedicts(basepars)) self.base_sim.update_pars(self.basepars) self.base_sim.validate_pars() if not self.base_sim.initialized: diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 5b00f0d43..8e75b5e43 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -3,9 +3,10 @@ import numpy as np -do_plot = 0 -do_show = 0 -do_save = 0 +do_plot = 0 +do_show = 0 +do_save = 0 +debug = 0 base_pars = dict( pop_size = 10e3, @@ -62,7 +63,7 @@ def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): metapars = {'n_runs': n_runs} scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run(debug=True) + scens.run(debug=debug) to_plot = sc.objdict({ 'New infections': ['new_infections'], @@ -279,7 +280,7 @@ def test_vaccine_2strains_scen(do_plot=False, do_show=True, do_save=False): metapars = {'n_runs': n_runs} scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run(debug=True) + scens.run(debug=debug) to_plot = sc.objdict({ 'New infections': ['new_infections'], @@ -322,7 +323,7 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): metapars = {'n_runs': n_runs} scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run(debug=True) + scens.run(debug=debug) to_plot = sc.objdict({ 'New infections': ['new_infections'], @@ -489,21 +490,21 @@ def get_ind_of_min_value(list, time): if __name__ == '__main__': sc.tic() - # # Run simplest possible test - # test_simple(do_plot=do_plot) + # Run simplest possible test + test_simple(do_plot=do_plot) - # # Run more complex single-sim tests - # sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - # sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + # Run more complex single-sim tests + sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) - # # Run Vaccine tests - # sim4 = test_synthpops() - # sim5 = test_vaccine_1strain() + # Run Vaccine tests + sim4 = test_synthpops() + sim5 = test_vaccine_1strain() # Run multisim and scenario tests - # scens0 = test_vaccine_1strain_scen() + scens0 = test_vaccine_1strain_scen() scens1 = test_vaccine_2strains_scen() #TODO, NOT WORKING CURRENTLY scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show)#TODO, NOT WORKING CURRENTLY msim0 = test_msim() diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 7c0362bb7..f00904fb9 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -51,7 +51,7 @@ def test_age_hist(): agehist.compute_windows() agehist.get() # Not used, but check get agehist.get(day_list[1]) - assert len(age_analyzer.window_hists) == len(day_list), "Number of histograms should equal number of days" + assert len(agehist.window_hists) == len(day_list), "Number of histograms should equal number of days" # Check plot() if do_plot: @@ -63,7 +63,6 @@ def test_age_hist(): sim = cv.Sim(pars, analyzers=daily_age) sim.run() - return agehist From 07061cc7e3b5c0370292143b904492089a9047d8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 21:26:18 -0700 Subject: [PATCH 481/569] reorganized --- tests/devtests/test_variants.py | 233 ++++++++++++-------------------- 1 file changed, 90 insertions(+), 143 deletions(-) diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 8e75b5e43..1fa591d0e 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -22,61 +22,6 @@ def test_simple(do_plot=False): return -def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): - sc.heading('Test varying properties of immunity') - - # Define baseline parameters - n_runs = 3 - base_sim = cv.Sim(use_waning=True, n_days=400, pars=base_pars) - - # Define the scenarios - b1351 = cv.Strain('b1351', days=100, n_imports=20) - - scenarios = { - 'baseline': { - 'name': 'Default Immunity (decay at log(2)/90)', - 'pars': { - 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/90, decay_time1=250, decay_rate2=0.001), - }, - }, - 'faster_immunity': { - 'name': 'Faster Immunity (decay at log(2)/30)', - 'pars': { - 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/30, decay_time1=250, decay_rate2=0.001), - }, - }, - 'baseline_b1351': { - 'name': 'Default Immunity (decay at log(2)/90), B1351 on day 100', - 'pars': { - 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/90, decay_time1=250, decay_rate2=0.001), - 'strains': [b1351], - }, - }, - 'faster_immunity_b1351': { - 'name': 'Faster Immunity (decay at log(2)/30), B1351 on day 100', - 'pars': { - 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/30, decay_time1=250, decay_rate2=0.001), - 'strains': [b1351], - }, - }, - } - - metapars = {'n_runs': n_runs} - scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run(debug=debug) - - to_plot = sc.objdict({ - 'New infections': ['new_infections'], - 'New re-infections': ['new_reinfections'], - 'Population Nabs': ['pop_nabs'], - 'Population Immunity': ['pop_protection'], - }) - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_basic_immunity.png', to_plot=to_plot) - - return scens - - def test_import1strain(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing a new strain partway through a sim') @@ -337,6 +282,83 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): return scens +def test_msim(do_plot=False): + sc.heading('Testing multisim...') + + # basic test for vaccine + b117 = cv.Strain('b117', days=0) + sim = cv.Sim(use_waning=True, strains=[b117], **base_pars) + msim = cv.MultiSim(sim, n_runs=2) + msim.run() + msim.reduce() + + to_plot = sc.objdict({ + 'Total infections': ['cum_infections'], + 'New infections per day': ['new_infections'], + 'New Re-infections per day': ['new_reinfections'], + }) + + if do_plot: + msim.plot(to_plot=to_plot, do_save=0, do_show=1, legend_args={'loc': 'upper left'}, axis_args={'hspace': 0.4}, interval=35) + + return msim + + +def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): + sc.heading('Test varying properties of immunity') + + # Define baseline parameters + n_runs = 3 + base_sim = cv.Sim(use_waning=True, n_days=400, pars=base_pars) + + # Define the scenarios + b1351 = cv.Strain('b1351', days=100, n_imports=20) + + scenarios = { + 'baseline': { + 'name': 'Default Immunity (decay at log(2)/90)', + 'pars': { + 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/90, decay_time1=250, decay_rate2=0.001), + }, + }, + 'faster_immunity': { + 'name': 'Faster Immunity (decay at log(2)/30)', + 'pars': { + 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/30, decay_time1=250, decay_rate2=0.001), + }, + }, + 'baseline_b1351': { + 'name': 'Default Immunity (decay at log(2)/90), B1351 on day 100', + 'pars': { + 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/90, decay_time1=250, decay_rate2=0.001), + 'strains': [b1351], + }, + }, + 'faster_immunity_b1351': { + 'name': 'Faster Immunity (decay at log(2)/30), B1351 on day 100', + 'pars': { + 'nab_decay': dict(form='nab_decay', decay_rate1=np.log(2)/30, decay_time1=250, decay_rate2=0.001), + 'strains': [b1351], + }, + }, + } + + metapars = {'n_runs': n_runs} + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run(debug=debug) + + to_plot = sc.objdict({ + 'New infections': ['new_infections'], + 'New re-infections': ['new_reinfections'], + 'Population Nabs': ['pop_nabs'], + 'Population Immunity': ['pop_protection'], + }) + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_basic_immunity.png', to_plot=to_plot) + + return scens + + def test_waning_vs_not(do_plot=False, do_show=True, do_save=False): sc.heading('Testing waning...') @@ -382,85 +404,7 @@ def test_waning_vs_not(do_plot=False, do_show=True, do_save=False): return scens -def test_msim(do_plot=False): - sc.heading('Testing multisim...') - - # basic test for vaccine - b117 = cv.Strain('b117', days=0) - sim = cv.Sim(use_waning=True, strains=[b117], **base_pars) - msim = cv.MultiSim(sim, n_runs=2) - msim.run() - msim.reduce() - - to_plot = sc.objdict({ - 'Total infections': ['cum_infections'], - 'New infections per day': ['new_infections'], - 'New Re-infections per day': ['new_reinfections'], - }) - - if do_plot: - msim.plot(to_plot=to_plot, do_save=0, do_show=1, legend_args={'loc': 'upper left'}, axis_args={'hspace': 0.4}, interval=35) - - return msim - - -#%% Plotting and utilities - -# def plot_results(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): - -# results = sim.results -# results_to_plot = results[key] - -# # extract data for plotting -# x = sim.results['t'] -# y = results_to_plot.values -# y = np.transpose(y) - -# fig, ax = plt.subplots() -# ax.plot(x, y) - -# ax.set(xlabel='Day of simulation', ylabel=results_to_plot.name, title=title) - -# if labels is None: -# labels = [0]*len(y[0]) -# for strain in range(len(y[0])): -# labels[strain] = f'Strain {strain +1}' -# ax.legend(labels) - -# if do_show: -# plt.show() -# if do_save: -# cv.savefig(f'results/{filename}.png') - -# return - - -# def plot_shares(sim, key, title, filename=None, do_show=True, do_save=False, labels=None): - -# results = sim.results -# n_strains = sim.results['new_infections_by_strain'].values.shape[0] # TODO: this should be stored in the sim somewhere more intuitive! -# prop_new = {f'Strain {s}': sc.safedivide(results[key+'_by_strain'].values[s,:], results[key].values, 0) for s in range(n_strains)} -# num_new = {f'Strain {s}': results[key+'_by_strain'].values[s,:] for s in range(n_strains)} - -# # extract data for plotting -# x = sim.results['t'] -# fig, ax = plt.subplots(2,1,sharex=True) -# ax[0].stackplot(x, prop_new.values(), -# labels=prop_new.keys()) -# ax[0].legend(loc='upper left') -# ax[0].set_title(title) -# ax[1].stackplot(sim.results['t'], num_new.values(), -# labels=num_new.keys()) -# ax[1].legend(loc='upper left') -# ax[1].set_title(title) - -# if do_show: -# plt.show() -# if do_save: -# cv.savefig(f'results/{filename}.png') - -# return - +#%% Utilities def vacc_subtarg(sim): ''' Subtarget by age''' @@ -490,14 +434,17 @@ def get_ind_of_min_value(list, time): if __name__ == '__main__': sc.tic() + # Gather keywords + kw = dict(do_plot=do_plot, do_save=do_save, do_show=do_show) + # Run simplest possible test test_simple(do_plot=do_plot) # Run more complex single-sim tests - sim0 = test_import1strain(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim1 = test_import2strains(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim2 = test_importstrain_longerdur(do_plot=do_plot, do_save=do_save, do_show=do_show) - sim3 = test_import2strains_changebeta(do_plot=do_plot, do_save=do_save, do_show=do_show) + sim0 = test_import1strain(**kw) + sim1 = test_import2strains(**kw) + sim2 = test_importstrain_longerdur(**kw) + sim3 = test_import2strains_changebeta(**kw) # Run Vaccine tests sim4 = test_synthpops() @@ -505,15 +452,15 @@ def get_ind_of_min_value(list, time): # Run multisim and scenario tests scens0 = test_vaccine_1strain_scen() - scens1 = test_vaccine_2strains_scen() #TODO, NOT WORKING CURRENTLY - scens2 = test_strainduration_scen(do_plot=do_plot, do_save=do_save, do_show=do_show)#TODO, NOT WORKING CURRENTLY - msim0 = test_msim() + scens1 = test_vaccine_2strains_scen() + scens2 = test_strainduration_scen(**kw) + msim0 = test_msim() # Run immunity tests - sim_immunity0 = test_varyingimmunity(do_plot=do_plot, do_save=do_save, do_show=do_show)#TODO, NOT WORKING CURRENTLY + sim_immunity0 = test_varyingimmunity(**kw) # Run test to compare sims with and without waning - scens3 = test_waning_vs_not(do_plot=do_plot, do_save=do_save, do_show=do_show) + scens3 = test_waning_vs_not(**kw) sc.toc() From a9df724fe0672dd0087e0e4289627c8c66954e5e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 21:32:53 -0700 Subject: [PATCH 482/569] convert to regular dicts --- covasim/parameters.py | 44 +++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 48514e697..56be8932b 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -348,9 +348,9 @@ def get_strain_pars(): ''' Define the default parameters for the different strains ''' - pars = sc.objdict( + pars = dict( - wild = sc.objdict( + wild = dict( rel_imm = 1.0, rel_beta = 1.0, rel_symp_prob = 1.0, @@ -359,7 +359,7 @@ def get_strain_pars(): rel_death_prob = 1.0, ), - b117 = sc.objdict( + b117 = dict( rel_imm = 1.0, rel_beta = 1.5, # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 rel_symp_prob = 1.0, @@ -368,7 +368,7 @@ def get_strain_pars(): rel_death_prob = 1.0, ), - b1351 = sc.objdict( + b1351 = dict( rel_imm = 0.25, rel_beta = 1.4, rel_symp_prob = 1.0, @@ -377,7 +377,7 @@ def get_strain_pars(): rel_death_prob = 1.4, ), - p1 = sc.objdict( + p1 = dict( rel_imm = 0.5, rel_beta = 1.4, rel_symp_prob = 1.0, @@ -394,30 +394,30 @@ def get_cross_immunity(): ''' Get the cross immunity between each strain and each other strain ''' - pars = sc.objdict( + pars = dict( - wild = sc.objdict( + wild = dict( wild = 1.0, b117 = 0.5, b1351 = 0.5, p1 = 0.5, ), - b117 = sc.objdict( + b117 = dict( wild = 0.5, b117 = 1.0, b1351 = 0.8, p1 = 0.8, ), - b1351 = sc.objdict( + b1351 = dict( wild = 0.066, b117 = 0.1, b1351 = 1.0, p1 = 0.1, ), - p1 = sc.objdict( + p1 = dict( wild = 0.17, b117 = 0.2, b1351 = 0.2, @@ -431,37 +431,37 @@ def get_vaccine_strain_pars(): ''' Define the effectiveness of each vaccine against each strain ''' - pars = sc.objdict( + pars = dict( - default = sc.objdict( + default = dict( wild = 1.0, b117 = 1.0, b1351 = 1.0, p1 = 1.0, ), - pfizer = sc.objdict( + pfizer = dict( wild = 1.0, b117 = 1/2.0, b1351 = 1/6.7, p1 = 1/6.5, ), - moderna = sc.objdict( + moderna = dict( wild = 1.0, b117 = 1/1.8, b1351 = 1/4.5, p1 = 1/8.6, ), - az = sc.objdict( + az = dict( wild = 1.0, b117 = 1.0, b1351 = 1/2, p1 = 1/2, ), - jj = sc.objdict( + jj = dict( wild = 1.0, b117 = 1.0, b1351 = 1/6.7, @@ -475,9 +475,9 @@ def get_vaccine_dose_pars(): ''' Define the dosing regimen for each vaccine ''' - pars = sc.objdict( + pars = dict( - default = sc.objdict( + default = dict( nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, @@ -485,7 +485,7 @@ def get_vaccine_dose_pars(): interval = None, ), - pfizer = sc.objdict( + pfizer = dict( nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, @@ -493,7 +493,7 @@ def get_vaccine_dose_pars(): interval = 21, ), - moderna = sc.objdict( + moderna = dict( nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, @@ -501,7 +501,7 @@ def get_vaccine_dose_pars(): interval = 28, ), - az = sc.objdict( + az = dict( nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, @@ -509,7 +509,7 @@ def get_vaccine_dose_pars(): interval = 21, ), - jj = sc.objdict( + jj = dict( nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, From d5a0c5c96abe835760a5f9853a46b5b80bcbbe27 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 22:52:17 -0700 Subject: [PATCH 483/569] renaming strain --- covasim/immunity.py | 44 ++++++++++++++++++++++++--------- covasim/parameters.py | 38 +++++++++++++++++++--------- covasim/sim.py | 14 +++++++---- tests/devtests/test_variants.py | 22 ++++++++--------- tests/test_immunity.py | 8 +++--- 5 files changed, 84 insertions(+), 42 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index b6e2f81ac..6616f9293 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -12,10 +12,10 @@ # %% Define strain class -__all__ = ['Strain', 'Vaccine'] +__all__ = ['strain', 'Vaccine'] -class Strain(sc.prettyobj): +class strain(sc.prettyobj): ''' Add a new strain to the sim @@ -29,9 +29,9 @@ class Strain(sc.prettyobj): **Example**:: - b117 = cv.Strain('b117', days=10) # Make strain B117 active from day 10 - p1 = cv.Strain('p1', days=15) # Make strain P1 active from day 15 - my_var = cv.Strain(strain={'rel_beta': 2.5}, label='My strain', days=20) + b117 = cv.strain('b117', days=10) # Make strain B117 active from day 10 + p1 = cv.strain('p1', days=15) # Make strain P1 active from day 15 + my_var = cv.strain(strain={'rel_beta': 2.5}, label='My strain', days=20) sim = cv.Sim(strains=[b117, p1, my_var]).run() # Add them all to the sim ''' @@ -51,7 +51,6 @@ def parse_strain_pars(self, strain=None, label=None): choices, mapping = cvpar.get_strain_choices() pars = cvpar.get_strain_pars() - choicestr = sc.newlinejoin(sc.mergelists(*choices.values())) normstrain = strain.lower() for txt in ['.', ' ', 'strain', 'variant', 'voc']: @@ -61,17 +60,27 @@ def parse_strain_pars(self, strain=None, label=None): normstrain = mapping[normstrain] strain_pars = pars[normstrain] else: - errormsg = f'The selected variant "{strain}" is not implemented; choices are:\n{choicestr}' + errormsg = f'The selected variant "{strain}" is not implemented; choices are:\n{choices}' raise NotImplementedError(errormsg) # Option 2: strains can be specified as a dict of pars elif isinstance(strain, dict): strain_pars = strain + label = strain_pars.pop('label', label) # Allow including the label in the parameters if label is None: - label = 'Custom strain' + label = 'custom' + + # Check that valid keys have been supplied: + invalid = [] + for key in strain_pars.keys(): + if key not in cvd.strain_pars: + invalid.append(key) + if len(invalid): + errormsg = f'Could not parse strain keys "{sc.strjoin(invalid)}"; valid keys are: "{sc.strjoin(cvd.strain_pars)}"' + raise sc.KeyNotFoundError(errormsg) else: - errormsg = f'Could not understand {type(strain)}, please specify as a dict or a predefined strain:\n{choicestr}' + errormsg = f'Could not understand {type(strain)}, please specify as a dict or a predefined strain:\n{choices}' raise ValueError(errormsg) # Set label @@ -83,16 +92,29 @@ def parse_strain_pars(self, strain=None, label=None): def initialize(self, sim): + # Check we haven't already initialized + if self.initialized: + errormsg = 'Strains cannot be re-initialized since the change the state of the sim' + raise RuntimeError(errormsg) + # Store the index of this strain, and increment the number of strains in the simulation self.index = sim['n_strains'] sim['n_strains'] += 1 + # Store the mapping of this strain + existing = list(sim['strain_map'].values()) + if self.label in existing: + errormsg = f'Cannot add new strain with label "{self.label}"; already exists in strains: {sc.strjoin(existing)}' + raise ValueError(errormsg) + else: + sim['strain_map'][self.index] = self.label + # Update strain info - defaults = cvpar.get_strain_pars()['wild'] + defaults = cvpar.get_strain_pars(default=True) for key in cvd.strain_pars: if key not in self.p: self.p[key] = defaults[key] - sim['strain_pars'][key].append(self.p[key]) + sim['strain_pars'][self.label][key] = self.p[key] self.initialized = True diff --git a/covasim/parameters.py b/covasim/parameters.py index 56be8932b..595500df9 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -104,7 +104,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Efficacy of protection measures pars['iso_factor'] = None # Multiply beta by this factor for diagnosed cases to represent isolation; set by reset_layer_pars() below pars['quar_factor'] = None # Quarantine multiplier on transmissibility and susceptibility; set by reset_layer_pars() below - pars['quar_period'] = 14 # Number of days to quarantine for; assumption based on standard policies + pars['quar_period'] = 14 # Number of days to quarantine for; assumption based on standard policies # Events and interventions pars['interventions'] = [] # The interventions present in this simulation; populated by the user @@ -140,10 +140,11 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Handle strain pars if 'strain_pars' not in pars: - pars['strain_pars'] = {} # Populated just below + pars['strain_map'] = {0:'wild'} # Reverse mapping from number to strain key + pars['strain_pars'] = dict(wild={}) # Populated just below for sp in cvd.strain_pars: if sp in pars.keys(): - pars['strain_pars'][sp] = [pars[sp]] + pars['strain_pars']['wild'][sp] = pars[sp] return pars @@ -344,7 +345,7 @@ def get_vaccine_choices(): return choices, mapping -def get_strain_pars(): +def get_strain_pars(default=False): ''' Define the default parameters for the different strains ''' @@ -387,10 +388,13 @@ def get_strain_pars(): ) ) - return pars + if default: + return pars['wild'] + else: + return pars -def get_cross_immunity(): +def get_cross_immunity(default=False): ''' Get the cross immunity between each strain and each other strain ''' @@ -424,10 +428,14 @@ def get_cross_immunity(): p1 = 1.0, ), ) - return pars + + if default: + return pars['wild'] + else: + return pars -def get_vaccine_strain_pars(): +def get_vaccine_strain_pars(default=False): ''' Define the effectiveness of each vaccine against each strain ''' @@ -468,10 +476,14 @@ def get_vaccine_strain_pars(): p1 = 1/8.6, ), ) - return pars + + if default: + return pars['default'] + else: + return pars -def get_vaccine_dose_pars(): +def get_vaccine_dose_pars(default=False): ''' Define the dosing regimen for each vaccine ''' @@ -517,6 +529,10 @@ def get_vaccine_dose_pars(): interval = None, ), ) - return pars + + if default: + return pars['default'] + else: + return pars diff --git a/covasim/sim.py b/covasim/sim.py index 4a76ef7e8..2758895fc 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -495,9 +495,13 @@ def init_strains(self): if self._orig_pars and 'strains' in self._orig_pars: self['strains'] = self._orig_pars.pop('strains') # Restore - for strain in self['strains']: - if not strain.initialized: - strain.initialize(self) + for i,strain in enumerate(self['strains']): + if isinstance(strain, cvimm.strain): + if not strain.initialized: + strain.initialize(self) + else: # pragma: no cover + errormsg = f'Strain {i} ({strain}) is not a cv.strain object; please create using cv.strain()' + raise TypeError(errormsg) return @@ -561,7 +565,7 @@ def step(self): # Add strains for strain in self['strains']: - if isinstance(strain, cvimm.Strain): + if isinstance(strain, cvimm.strain): strain.apply(self) # Apply interventions @@ -575,7 +579,7 @@ def step(self): intervention(self) # If it's a function, call it directly else: # pragma: no cover errormsg = f'Intervention {i} ({intervention}) is neither callable nor an Intervention object' - raise ValueError(errormsg) + raise TypeError(errormsg) people.update_states_post() # Check for state changes after interventions diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 1fa591d0e..b902a7063 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -31,7 +31,7 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): pars = { 'beta': 0.01 } - strain = cv.Strain(strain_pars, days=1, n_imports=20, label='Strain 2: 1.5x more transmissible') + strain = cv.strain(strain_pars, days=1, n_imports=20, label='Strain 2: 1.5x more transmissible') sim = cv.Sim(use_waning=True, pars=pars, strains=strain, analyzers=cv.snapshot(30, 60), **pars, **base_pars) sim.run() @@ -41,8 +41,8 @@ def test_import1strain(do_plot=False, do_show=True, do_save=False): def test_import2strains(do_plot=False, do_show=True, do_save=False): sc.heading('Test introducing 2 new strains partway through a sim') - b117 = cv.Strain('b117', days=1, n_imports=20) - p1 = cv.Strain('sa variant', days=2, n_imports=20) + b117 = cv.strain('b117', days=1, n_imports=20) + p1 = cv.strain('sa variant', days=2, n_imports=20) sim = cv.Sim(use_waning=True, strains=[b117, p1], label='With imported infections', **base_pars) sim.run() @@ -61,7 +61,7 @@ def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} } - strain = cv.Strain(strain=strain_pars, label='Custom strain', days=10, n_imports=30) + strain = cv.strain(strain=strain_pars, label='Custom strain', days=10, n_imports=30) sim = cv.Sim(use_waning=True, pars=pars, strains=strain, label='With imported infections') sim.run() @@ -78,8 +78,8 @@ def test_import2strains_changebeta(do_plot=False, do_show=True, do_save=False): 'rel_symp_prob': 1.6} intervs = cv.change_beta(days=[5, 20, 40], changes=[0.8, 0.7, 0.6]) - strains = [cv.Strain(strain=strain2, days=10, n_imports=20), - cv.Strain(strain=strain3, days=30, n_imports=20), + strains = [cv.strain(strain=strain2, days=10, n_imports=20), + cv.strain(strain=strain3, days=30, n_imports=20), ] sim = cv.Sim(use_waning=True, interventions=intervs, strains=strains, label='With imported infections', **base_pars) sim.run() @@ -195,8 +195,8 @@ def test_vaccine_2strains_scen(do_plot=False, do_show=True, do_save=False): base_sim.vxsubtarg.prob = [.01, .01, .01, .01] base_sim.vxsubtarg.days = subtarg_days = [60, 150, 200, 220] jnj = cv.vaccinate(days=subtarg_days, vaccine_pars='j&j', subtarget=vacc_subtarg) - b1351 = cv.Strain('b1351', days=10, n_imports=20) - p1 = cv.Strain('p1', days=100, n_imports=100) + b1351 = cv.strain('b1351', days=10, n_imports=20) + p1 = cv.strain('p1', days=100, n_imports=100) # Define the scenarios @@ -243,7 +243,7 @@ def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') strain_pars = {'dur':{'inf2sym': {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9}}} - strains = cv.Strain(strain=strain_pars, label='10 days til symptoms', days=10, n_imports=30) + strains = cv.strain(strain=strain_pars, label='10 days til symptoms', days=10, n_imports=30) tp = cv.test_prob(symp_prob=0.2) # Add an efficient testing program pars = sc.mergedicts(base_pars, { @@ -286,7 +286,7 @@ def test_msim(do_plot=False): sc.heading('Testing multisim...') # basic test for vaccine - b117 = cv.Strain('b117', days=0) + b117 = cv.strain('b117', days=0) sim = cv.Sim(use_waning=True, strains=[b117], **base_pars) msim = cv.MultiSim(sim, n_runs=2) msim.run() @@ -312,7 +312,7 @@ def test_varyingimmunity(do_plot=False, do_show=True, do_save=False): base_sim = cv.Sim(use_waning=True, n_days=400, pars=base_pars) # Define the scenarios - b1351 = cv.Strain('b1351', days=100, n_imports=20) + b1351 = cv.strain('b1351', days=100, n_imports=20) scenarios = { 'baseline': { diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 8e7525a8a..4f10f02f6 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -131,9 +131,9 @@ def test_waning(do_plot=False): def test_strains(do_plot=False): sc.heading('Testing strains...') - b117 = cv.Strain('b117', days=10, n_imports=20) - p1 = cv.Strain('sa variant', days=20, n_imports=20) - cust = cv.Strain(label='Custom', days=40, n_imports=20, strain={'rel_beta': 2, 'rel_symp_prob': 1.6}) + b117 = cv.strain('b117', days=10, n_imports=20) + p1 = cv.strain('sa variant', days=20, n_imports=20) + cust = cv.strain(label='Custom', days=40, n_imports=20, strain={'rel_beta': 2, 'rel_symp_prob': 1.6}) sim = cv.Sim(base_pars, use_waning=True, strains=[b117, p1, cust]) sim.run() @@ -146,7 +146,7 @@ def test_strains(do_plot=False): def test_vaccines(do_plot=False): sc.heading('Testing vaccines...') - p1 = cv.Strain('sa variant', days=20, n_imports=20) + p1 = cv.strain('sa variant', days=20, n_imports=20) pfizer = cv.vaccinate(days=30, vaccine_pars='pfizer') sim = cv.Sim(base_pars, use_waning=True, strains=p1, interventions=pfizer) sim.run() From 058bff61e745487f7505bcfebe8e88c5e383f421 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 23:12:29 -0700 Subject: [PATCH 484/569] strains are working --- covasim/immunity.py | 8 +++++--- covasim/people.py | 3 ++- covasim/sim.py | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 6616f9293..d20a87dba 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -33,6 +33,7 @@ class strain(sc.prettyobj): p1 = cv.strain('p1', days=15) # Make strain P1 active from day 15 my_var = cv.strain(strain={'rel_beta': 2.5}, label='My strain', days=20) sim = cv.Sim(strains=[b117, p1, my_var]).run() # Add them all to the sim + sim2 = cv.Sim(strains=cv.strain('b117', days=0, n_imports=20), pop_infected=0).run() # Replace default strain with b117 ''' def __init__(self, strain=None, label=None, days=None, n_imports=1, rescale=True): @@ -60,7 +61,7 @@ def parse_strain_pars(self, strain=None, label=None): normstrain = mapping[normstrain] strain_pars = pars[normstrain] else: - errormsg = f'The selected variant "{strain}" is not implemented; choices are:\n{choices}' + errormsg = f'The selected variant "{strain}" is not implemented; choices are:\n{sc.pp(choices, doprint=False)}' raise NotImplementedError(errormsg) # Option 2: strains can be specified as a dict of pars @@ -80,7 +81,7 @@ def parse_strain_pars(self, strain=None, label=None): raise sc.KeyNotFoundError(errormsg) else: - errormsg = f'Could not understand {type(strain)}, please specify as a dict or a predefined strain:\n{choices}' + errormsg = f'Could not understand {type(strain)}, please specify as a dict or a predefined strain:\n{sc.pp(choices, doprint=False)}' raise ValueError(errormsg) # Set label @@ -104,13 +105,14 @@ def initialize(self, sim): # Store the mapping of this strain existing = list(sim['strain_map'].values()) if self.label in existing: - errormsg = f'Cannot add new strain with label "{self.label}"; already exists in strains: {sc.strjoin(existing)}' + errormsg = f'Cannot add new strain with label "{self.label}", it already exists in the list of strains: {sc.strjoin(existing)}' raise ValueError(errormsg) else: sim['strain_map'][self.index] = self.label # Update strain info defaults = cvpar.get_strain_pars(default=True) + sim['strain_pars'][self.label] = {} for key in cvd.strain_pars: if key not in self.p: self.p[key] = defaults[key] diff --git a/covasim/people.py b/covasim/people.py index f908bebc8..a16b8a542 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -454,8 +454,9 @@ def infect(self, inds, hosp_max=None, icu_max=None, source=None, layer=None, str strain_keys = ['rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob'] infect_pars = {k:self.pars[k] for k in strain_keys} if strain: + strain_label = self.pars['strain_map'][strain] for k in strain_keys: - infect_pars[k] *= self.pars['strain_pars'][k][strain] + infect_pars[k] *= self.pars['strain_pars'][strain_label][k] n_infections = len(inds) durpars = self.pars['dur'] diff --git a/covasim/sim.py b/covasim/sim.py index 2758895fc..150d7bff6 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -617,7 +617,8 @@ def step(self): rel_beta = self['rel_beta'] asymp_factor = self['asymp_factor'] if strain: - rel_beta *= self['strain_pars']['rel_beta'][strain] + strain_label = self.pars['strain_map'][strain] + rel_beta *= self['strain_pars'][strain_label]['rel_beta'] beta = cvd.default_float(self['beta'] * rel_beta) for lkey, layer in contacts.items(): From 2deaaf7d94045014b1744bf10d90b3406ded1634 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 23:21:21 -0700 Subject: [PATCH 485/569] rename --- covasim/immunity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index d20a87dba..6cfc5d30b 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -51,7 +51,7 @@ def parse_strain_pars(self, strain=None, label=None): if isinstance(strain, str): choices, mapping = cvpar.get_strain_choices() - pars = cvpar.get_strain_pars() + known_strain_pars = cvpar.get_strain_pars() normstrain = strain.lower() for txt in ['.', ' ', 'strain', 'variant', 'voc']: @@ -59,7 +59,7 @@ def parse_strain_pars(self, strain=None, label=None): if normstrain in mapping: normstrain = mapping[normstrain] - strain_pars = pars[normstrain] + strain_pars = known_strain_pars[normstrain] else: errormsg = f'The selected variant "{strain}" is not implemented; choices are:\n{sc.pp(choices, doprint=False)}' raise NotImplementedError(errormsg) From 3ad712ff6d2d7d8117f44457f8f9cef15c3ca3ba Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 12 Apr 2021 23:45:27 -0700 Subject: [PATCH 486/569] update date --- CHANGELOG.rst | 2 +- covasim/parameters.py | 10 +++++----- covasim/version.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bf6ed95f2..1762558ad 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,7 +24,7 @@ These are the major improvements we are currently working on. If there is a spec Latest versions (3.0.x) ~~~~~~~~~~~~~~~~~~~~~~~ -Version 3.0.0 (2021-04-12) +Version 3.0.0 (2021-04-13) -------------------------- This version introduces fully featured vaccines, variants, and immunity. **Note:** These new features are still under development; please use with caution and email us at covasim@idmod.org if you have any questions or issues. We expect there to be several more releases over the next few weeks as we refine these new features. diff --git a/covasim/parameters.py b/covasim/parameters.py index 595500df9..9154d99ec 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -490,7 +490,7 @@ def get_vaccine_dose_pars(default=False): pars = dict( default = dict( - nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + nab_eff = dict(sus=dict(slope=2.5, n_50=0.55)), nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, doses = 1, @@ -498,7 +498,7 @@ def get_vaccine_dose_pars(default=False): ), pfizer = dict( - nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + nab_eff = dict(sus=dict(slope=2.5, n_50=0.55)), nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, doses = 2, @@ -506,7 +506,7 @@ def get_vaccine_dose_pars(default=False): ), moderna = dict( - nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + nab_eff = dict(sus=dict(slope=2.5, n_50=0.55)), nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, doses = 2, @@ -514,7 +514,7 @@ def get_vaccine_dose_pars(default=False): ), az = dict( - nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + nab_eff = dict(sus=dict(slope=2.5, n_50=0.55)), nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, doses = 2, @@ -522,7 +522,7 @@ def get_vaccine_dose_pars(default=False): ), jj = dict( - nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}}, + nab_eff = dict(sus=dict(slope=2.5, n_50=0.55)), nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, doses = 1, diff --git a/covasim/version.py b/covasim/version.py index de73762c1..055f08788 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -5,5 +5,5 @@ __all__ = ['__version__', '__versiondate__', '__license__'] __version__ = '3.0.0' -__versiondate__ = '2021-04-12' +__versiondate__ = '2021-04-13' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From ed2d15155b5eb30b011f40cf253dc0cde04ee1a3 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 13 Apr 2021 00:03:04 -0700 Subject: [PATCH 487/569] move vaccine into vaccinate --- covasim/immunity.py | 118 ++++++++------------------------------- covasim/interventions.py | 59 +++++++++++++++++--- 2 files changed, 73 insertions(+), 104 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 6cfc5d30b..bfa7930ee 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -10,9 +10,9 @@ from . import interventions as cvi -# %% Define strain class +# %% Define strain class -- all other functions are for internal use only -__all__ = ['strain', 'Vaccine'] +__all__ = ['strain'] class strain(sc.prettyobj): @@ -132,90 +132,18 @@ def apply(self, sim): return -class Vaccine(sc.prettyobj): - ''' - Add a new vaccine to the sim (called by interventions.py vaccinate() - - stores number of doses for vaccine and a dictionary to pass to init_immunity for each dose - - Args: - vaccine (dict or str): dictionary of parameters specifying information about the vaccine or label for loading pre-defined vaccine - label (str): if supplying a dictionary, a label for the vaccine must be supplied - - **Example**:: - - moderna = cv.Vaccine('moderna') # Create Moderna vaccine - pfizer = cv.Vaccine('pfizer) # Create Pfizer vaccine - j&j = cv.Vaccine('jj') # Create J&J vaccine - az = cv.Vaccine('az) # Create AstraZeneca vaccine - interventions += [cv.vaccinate(vaccines=[moderna, pfizer, j&j, az], days=[1, 10, 10, 30])] # Add them all to the sim - sim = cv.Sim(interventions=interventions) - ''' - - def __init__(self, vaccine=None, label=None): - self.label = label - # self.rel_imm = None # list of length n_strains with relative immunity factor - # self.doses = None - # self.interval = None - # self.nab_init = None - # self.nab_boost = None - # self.nab_eff = {'sus': {'slope': 2.5, 'n_50': 0.55}} # Parameters to map nabs to efficacy - self.vaccine_strain_info = cvpar.get_vaccine_strain_pars() - self.parse_vaccine_pars(vaccine=vaccine) - # for par, val in self.vaccine_pars.items(): - # setattr(self, par, val) - return - - - def parse_vaccine_pars(self, vaccine=None): - ''' Unpack vaccine information, which may be given in different ways''' - - # Option 1: vaccines can be chosen from a list of pre-defined strains - if isinstance(vaccine, str): - choices, mapping = cvpar.get_vaccine_choices() - strain_pars = cvpar.get_vaccine_strain_pars() - dose_pars = cvpar.get_vaccine_dose_pars() - choicestr = sc.newlinejoin(sc.mergelists(*choices.values())) - normvacc = vaccine.lower() - for txt in ['.', ' ', '&', '-', 'vaccine']: - normvacc = normvacc.replace(txt, '') - - if normvacc in mapping: - normvacc = mapping[normvacc] - vaccine_pars = sc.mergedicts(strain_pars[normvacc], dose_pars[normvacc]) - else: # pragma: no cover - errormsg = f'The selected vaccine "{vaccine}" is not implemented; choices are:\n{choicestr}' - raise NotImplementedError(errormsg) - - if self.label is None: - self.label = normvacc - - # Option 2: strains can be specified as a dict of pars - elif isinstance(vaccine, dict): - vaccine_pars = vaccine - if self.label is None: - self.label = 'Custom vaccine' - - else: # pragma: no cover - errormsg = f'Could not understand {type(vaccine)}, please specify as a string indexing a predefined vaccine or a dict.' - raise ValueError(errormsg) - - self.p = sc.objdict(vaccine_pars) - return - - -#%% nab methods +#%% Neutralizing antibody methods def init_nab(people, inds, prior_inf=True, vacc_info=None): ''' - Draws an initial nab level for individuals. + Draws an initial neutralizing antibody (NAb) level for individuals. Can come from a natural infection or vaccination and depends on if there is prior immunity: - 1) a natural infection. If individual has no existing nab, draw from distribution - depending upon symptoms. If individual has existing nab, multiply booster impact - 2) Vaccination. If individual has no existing nab, draw from distribution - depending upon vaccine source. If individual has existing nab, multiply booster impact + 1) a natural infection. If individual has no existing NAb, draw from distribution + depending upon symptoms. If individual has existing NAb, multiply booster impact + 2) Vaccination. If individual has no existing NAb, draw from distribution + depending upon vaccine source. If individual has existing NAb, multiply booster impact ''' if vacc_info is None: @@ -223,31 +151,29 @@ def init_nab(people, inds, prior_inf=True, vacc_info=None): vacc_info = cvpar.get_vaccine_dose_pars()['default'] nab_arrays = people.nab[inds] - prior_nab_inds = cvu.idefined(nab_arrays, inds) # Find people with prior nabs - no_prior_nab_inds = np.setdiff1d(inds, prior_nab_inds) # Find people without prior nabs - - # prior_nab = people.nab[prior_nab_inds] # Array of nab levels on this timestep for people with some nabs + prior_nab_inds = cvu.idefined(nab_arrays, inds) # Find people with prior NAb + no_prior_nab_inds = np.setdiff1d(inds, prior_nab_inds) # Find people without prior NAb peak_nab = people.init_nab[prior_nab_inds] - # nabs from infection + # NAb from infection if prior_inf: nab_boost = people.pars['nab_boost'] # Boosting factor for natural infection - # 1) No prior nab: draw nab from a distribution and compute + # 1) No prior NAb: draw NAb from a distribution and compute if len(no_prior_nab_inds): init_nab = cvu.sample(**people.pars['nab_init'], size=len(no_prior_nab_inds)) prior_symp = people.prior_symptoms[no_prior_nab_inds] no_prior_nab = (2**init_nab) * prior_symp people.init_nab[no_prior_nab_inds] = no_prior_nab - # 2) Prior nab: multiply existing nab by boost factor + # 2) Prior NAb: multiply existing NAb by boost factor if len(prior_nab_inds): init_nab = peak_nab * nab_boost people.init_nab[prior_nab_inds] = init_nab - # nabs from a vaccine + # NAb from a vaccine else: nab_boost = vacc_info['nab_boost'] # Boosting factor for vaccination - # 1) No prior nab: draw nab from a distribution and compute + # 1) No prior NAb: draw NAb from a distribution and compute if len(no_prior_nab_inds): init_nab = cvu.sample(**vacc_info['nab_init'], size=len(no_prior_nab_inds)) people.init_nab[no_prior_nab_inds] = 2**init_nab @@ -261,7 +187,7 @@ def init_nab(people, inds, prior_inf=True, vacc_info=None): def check_nab(t, people, inds=None): - ''' Determines current nabs based on date since recovered/vaccinated.''' + ''' Determines current NAb based on date since recovered/vaccinated.''' # Indices of people who've had some nab event rec_inds = cvu.defined(people.date_recovered[inds]) @@ -274,7 +200,7 @@ def check_nab(t, people, inds=None): t_since_boost[vac_inds] = t-people.date_vaccinated[inds[vac_inds]] t_since_boost[both_inds] = t-np.maximum(people.date_recovered[inds[both_inds]],people.date_vaccinated[inds[both_inds]]) - # Set current nabs + # Set current NAb people.nab[inds] = people.pars['nab_kin'][t_since_boost] * people.init_nab[inds] return @@ -282,15 +208,15 @@ def check_nab(t, people, inds=None): def nab_to_efficacy(nab, ax, function_args): ''' - Convert nab levels to immunity protection factors, using the functional form + Convert NAb levels to immunity protection factors, using the functional form given in this paper: https://doi.org/10.1101/2021.03.09.21252641 Args: - nab (arr): an array of nab levels + nab (arr): an array of NAb levels ax (str): can be 'sus', 'symp' or 'sev', corresponding to the efficacy of protection against infection, symptoms, and severe disease respectively Returns: - an array the same size as nab, containing the immunity protection factors for the specified axis + an array the same size as NAb, containing the immunity protection factors for the specified axis ''' if ax not in ['sus', 'symp', 'sev']: @@ -346,7 +272,7 @@ def init_immunity(sim, create=False): immunity['sus'][j][i] = cross_immunity[circulating_strains[j]][circulating_strains[i]] sim['immunity'] = immunity - # Next, precompute the nab kinetics and store these for access during the sim + # Next, precompute the NAb kinetics and store these for access during the sim sim['nab_kin'] = precompute_waning(length=sim['n_days'], pars=sim['nab_decay']) return @@ -373,7 +299,7 @@ def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): was_inf = cvu.true(people.t >= people.date_recovered) # Had a previous exposure, now recovered is_vacc = cvu.true(people.vaccinated) # Vaccinated date_rec = people.date_recovered # Date recovered - immunity = people.pars['immunity'] # cross-immunity/own-immunity scalars to be applied to nab level before computing efficacy + immunity = people.pars['immunity'] # cross-immunity/own-immunity scalars to be applied to NAb level before computing efficacy nab_eff_pars = people.pars['nab_eff'] # If vaccines are present, extract relevant information about them diff --git a/covasim/interventions.py b/covasim/interventions.py index 42ac8820d..604b6a138 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1132,6 +1132,8 @@ class simple_vaccine(Intervention): cumulative (bool): whether cumulative doses have cumulative effects (default false); can also be an array for efficacy per dose, with the last entry used for multiple doses; thus True = [1] and False = [1,0] kwargs (dict): passed to Intervention() + Note: this intervention is still under development and should be used with caution. + **Examples**:: interv = cv.simple_vaccine(days=50, prob=0.3, rel_sus=0.5, rel_symp=0.1) @@ -1157,11 +1159,12 @@ def initialize(self, sim): super().initialize() self.days = process_days(sim, self.days) self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person - self.date_vaccinated = [[] for p in range(sim.n)] # Store the dates when people are vaccinated + self.vaccination_dates = [[] for p in range(sim.n)] # Store the dates when people are vaccinated self.orig_rel_sus = sc.dcp(sim.people.rel_sus) # Keep a copy of pre-vaccination susceptibility self.orig_symp_prob = sc.dcp(sim.people.symp_prob) # ...and symptom probability self.mod_rel_sus = np.ones(sim.n, dtype=cvd.default_float) # Store the final modifiers self.mod_symp_prob = np.ones(sim.n, dtype=cvd.default_float) # Store the final modifiers + self.vacc_inds = None return @@ -1194,12 +1197,11 @@ def apply(self, sim): self.mod_symp_prob[vacc_inds] *= rel_symp_eff self.vaccinations[vacc_inds] += 1 for v_ind in vacc_inds: - self.date_vaccinated[v_ind].append(sim.t) + self.vaccination_dates[v_ind].append(sim.t) # Update vaccine attributes in sim sim.people.vaccinated[vacc_inds] = True sim.people.vaccinations[vacc_inds] += 1 - sim.people.vaccination_dates = self.date_vaccinated # TODO: refactor return @@ -1215,9 +1217,9 @@ class vaccinate(Intervention): - ``pars``: vaccine pars that are given to Vaccine() class Args: - days (int or array): the day or array of days to apply the interventions - prob (float): probability of being vaccinated (i.e., fraction of the population) - vaccine_pars (dict or label): passed to Vaccine() + vaccine (dict/str): which vaccine to use + days (int/arr): the day or array of days to apply the interventions + prob (float): probability of being vaccinated (i.e., fraction of the population) subtarget (dict): subtarget intervention to people with particular indices (see test_num() for details) kwargs (dict): passed to Intervention() @@ -1225,13 +1227,54 @@ class vaccinate(Intervention): interv = cv.vaccinate(days=50, prob=0.3, ) ''' - def __init__(self, days, prob=1.0, vaccine_pars=None, subtarget=None, **kwargs): + def __init__(self, vaccine, days, prob=1.0, subtarget=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object self.days = sc.dcp(days) self.prob = prob self.subtarget = subtarget - self.vaccine_pars = vaccine_pars + self.vaccine_pars = vaccine self.vaccine_ind = None + self.vaccine_strain_info = cvpar.get_vaccine_strain_pars() + self.parse_vaccine_pars(vaccine=vaccine) + return + + + def parse_vaccine_pars(self, vaccine=None): + ''' Unpack vaccine information, which may be given in different ways''' + + # Option 1: vaccines can be chosen from a list of pre-defined strains + if isinstance(vaccine, str): + + choices, mapping = cvpar.get_vaccine_choices() + strain_pars = cvpar.get_vaccine_strain_pars() + dose_pars = cvpar.get_vaccine_dose_pars() + choicestr = sc.newlinejoin(sc.mergelists(*choices.values())) + + normvacc = vaccine.lower() + for txt in ['.', ' ', '&', '-', 'vaccine']: + normvacc = normvacc.replace(txt, '') + + if normvacc in mapping: + normvacc = mapping[normvacc] + vaccine_pars = sc.mergedicts(strain_pars[normvacc], dose_pars[normvacc]) + else: # pragma: no cover + errormsg = f'The selected vaccine "{vaccine}" is not implemented; choices are:\n{choicestr}' + raise NotImplementedError(errormsg) + + if self.label is None: + self.label = normvacc + + # Option 2: strains can be specified as a dict of pars + elif isinstance(vaccine, dict): + vaccine_pars = vaccine + if self.label is None: + self.label = 'Custom vaccine' + + else: # pragma: no cover + errormsg = f'Could not understand {type(vaccine)}, please specify as a string indexing a predefined vaccine or a dict.' + raise ValueError(errormsg) + + self.p = sc.objdict(vaccine_pars) return From 79399bd80680a925734854df0d3fc7189693533c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 13 Apr 2021 00:34:40 -0700 Subject: [PATCH 488/569] getting close --- covasim/immunity.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index bfa7930ee..d5de5dbd7 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -243,33 +243,33 @@ def init_immunity(sim, create=False): if not sim['use_waning']: return - ts = sim['n_strains'] + ns = sim['n_strains'] immunity = {} # Pull out all of the circulating strains for cross-immunity - circulating_strains = ['wild'] + strain_labels = sim['strain_map'].values() rel_imms = dict() - for strain in sim['strains']: - circulating_strains.append(strain.label) - rel_imms[strain.label] = strain.p.rel_imm + for label in strain_labels: + rel_imms[label] = sim['strain_pars'][label]['rel_imm'] # If immunity values have been provided, process them if sim['immunity'] is None or create: # Initialize immunity for ax in cvd.immunity_axes: if ax == 'sus': # Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] - immunity[ax] = np.full((ts, ts), sim['cross_immunity'], dtype=cvd.default_float) # Default for off-diagnonals - np.fill_diagonal(immunity[ax], 1) # Default for own-immunity + immunity[ax] = np.full((ns, ns), sim['cross_immunity'], dtype=cvd.default_float) # Default for off-diagnonals + np.fill_diagonal(immunity[ax], 1.0) # Default for own-immunity else: # Progression and transmission are matrices of scalars of size sim['n_strains'] - immunity[ax] = np.ones(ts, dtype=cvd.default_float) + immunity[ax] = np.ones(ns, dtype=cvd.default_float) - cross_immunity = cvpar.get_cross_immunity() - known_strains = cross_immunity.keys() - for i in range(ts): - for j in range(ts): + default_cross_immunity = cvpar.get_cross_immunity() + for i in range(ns): + for j in range(ns): if i != j: - if circulating_strains[i] in known_strains and circulating_strains[j] in known_strains: - immunity['sus'][j][i] = cross_immunity[circulating_strains[j]][circulating_strains[i]] + label_i = sim['strain_map'][i] + label_j = sim['strain_map'][j] + if label_i in default_cross_immunity and label_j in default_cross_immunity: + immunity['sus'][j][i] = default_cross_immunity[label_j][label_i] sim['immunity'] = immunity # Next, precompute the NAb kinetics and store these for access during the sim From 869fd4665db12962933c66f82fe7365468907d7b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 13 Apr 2021 02:15:41 -0700 Subject: [PATCH 489/569] changing to allow multiple strains of the same name --- covasim/immunity.py | 53 ++++++++++++++++-------------- covasim/interventions.py | 58 +++++++++++++++++++++------------ covasim/parameters.py | 21 ++++++------ tests/devtests/test_variants.py | 8 ++--- 4 files changed, 81 insertions(+), 59 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index d5de5dbd7..72ab57fc0 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -21,11 +21,10 @@ class strain(sc.prettyobj): Args: strain (str/dict): name of strain, or dictionary of parameters specifying information about the strain + days (int/list): day(s) on which new variant is introduced label (str): if strain is supplied as a dict, the name of the strain - days (int/list): day(s) on which new variant is introduced. n_imports (int): the number of imports of the strain to be added rescale (bool): whether the number of imports should be rescaled with the population - kwargs (dict): passed to Intervention() **Example**:: @@ -36,16 +35,19 @@ class strain(sc.prettyobj): sim2 = cv.Sim(strains=cv.strain('b117', days=0, n_imports=20), pop_infected=0).run() # Replace default strain with b117 ''' - def __init__(self, strain=None, label=None, days=None, n_imports=1, rescale=True): + def __init__(self, strain, days, label=None, n_imports=1, rescale=True): self.days = days # Handle inputs - self.n_imports = cvd.default_int(n_imports) - self.parse_strain_pars(strain=strain, label=label) # Strains can be defined in different ways: process these here + self.n_imports = int(n_imports) + self.index = None # Index of the strain in the sim; set later + self.label = None # Strain label (used as a dict key) + self.p = None # This is where the parameters will be stored + self.parse(strain=strain, label=label) # Strains can be defined in different ways: process these here self.initialized = False return - def parse_strain_pars(self, strain=None, label=None): - ''' Unpack strain information, which may be given in different ways''' + def parse(self, strain=None, label=None): + ''' Unpack strain information, which may be given in different ways ''' # Option 1: strains can be chosen from a list of pre-defined strains if isinstance(strain, str): @@ -53,40 +55,49 @@ def parse_strain_pars(self, strain=None, label=None): choices, mapping = cvpar.get_strain_choices() known_strain_pars = cvpar.get_strain_pars() - normstrain = strain.lower() + label = strain.lower() for txt in ['.', ' ', 'strain', 'variant', 'voc']: - normstrain = normstrain.replace(txt, '') + label = label.replace(txt, '') - if normstrain in mapping: - normstrain = mapping[normstrain] - strain_pars = known_strain_pars[normstrain] + if label in mapping: + label = mapping[label] + strain_pars = known_strain_pars[label] else: errormsg = f'The selected variant "{strain}" is not implemented; choices are:\n{sc.pp(choices, doprint=False)}' raise NotImplementedError(errormsg) # Option 2: strains can be specified as a dict of pars elif isinstance(strain, dict): + + default_strain_pars = cvpar.get_strain_pars(default=True) + default_keys = list(default_strain_pars.keys()) + strain_pars = strain label = strain_pars.pop('label', label) # Allow including the label in the parameters if label is None: label = 'custom' - # Check that valid keys have been supplied: + # Check that valid keys have been supplied... invalid = [] for key in strain_pars.keys(): - if key not in cvd.strain_pars: + if key not in default_keys: invalid.append(key) if len(invalid): errormsg = f'Could not parse strain keys "{sc.strjoin(invalid)}"; valid keys are: "{sc.strjoin(cvd.strain_pars)}"' raise sc.KeyNotFoundError(errormsg) + # ...and populate any that are missing + for key in default_keys: + if key not in strain_pars: + strain_pars[key] = default_strain_pars[key] + else: errormsg = f'Could not understand {type(strain)}, please specify as a dict or a predefined strain:\n{sc.pp(choices, doprint=False)}' raise ValueError(errormsg) - # Set label - self.label = label if label else normstrain - self.p = sc.objdict(strain_pars) # Convert to an objdict and save + # Set label and parameters + self.label = label + self.p = sc.objdict(strain_pars) return @@ -111,13 +122,7 @@ def initialize(self, sim): sim['strain_map'][self.index] = self.label # Update strain info - defaults = cvpar.get_strain_pars(default=True) - sim['strain_pars'][self.label] = {} - for key in cvd.strain_pars: - if key not in self.p: - self.p[key] = defaults[key] - sim['strain_pars'][self.label][key] = self.p[key] - + sim['strain_pars'][self.label] = self.p self.initialized = True return diff --git a/covasim/interventions.py b/covasim/interventions.py index 604b6a138..ff89c5877 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1133,6 +1133,7 @@ class simple_vaccine(Intervention): kwargs (dict): passed to Intervention() Note: this intervention is still under development and should be used with caution. + It is intended for use with use_waning=False. **Examples**:: @@ -1218,6 +1219,7 @@ class vaccinate(Intervention): Args: vaccine (dict/str): which vaccine to use + label (str): if vaccine is supplied as a dict, the name of the vaccine days (int/arr): the day or array of days to apply the interventions prob (float): probability of being vaccinated (i.e., fraction of the population) subtarget (dict): subtarget intervention to people with particular indices (see test_num() for details) @@ -1225,44 +1227,46 @@ class vaccinate(Intervention): **Examples**:: - interv = cv.vaccinate(days=50, prob=0.3, ) + pfizer = cv.vaccinate(vaccine='pfizer', days=50, prob=0.3) + custom = cv.vaccinate(vaccine=) ''' - def __init__(self, vaccine, days, prob=1.0, subtarget=None, **kwargs): + def __init__(self, vaccine, days, label=None, prob=1.0, subtarget=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object self.days = sc.dcp(days) self.prob = prob self.subtarget = subtarget - self.vaccine_pars = vaccine - self.vaccine_ind = None - self.vaccine_strain_info = cvpar.get_vaccine_strain_pars() - self.parse_vaccine_pars(vaccine=vaccine) + self.index = None # Index of the vaccine in the sim; set later + self.label = None # Vacine label (used as a dict key) + self.p = None # Vaccine parameters + self.parse(vaccine=vaccine, label=label) # Populate return - def parse_vaccine_pars(self, vaccine=None): + def parse(self, vaccine=None, label=None): ''' Unpack vaccine information, which may be given in different ways''' + + # Option 1: vaccines can be chosen from a list of pre-defined strains if isinstance(vaccine, str): choices, mapping = cvpar.get_vaccine_choices() strain_pars = cvpar.get_vaccine_strain_pars() dose_pars = cvpar.get_vaccine_dose_pars() - choicestr = sc.newlinejoin(sc.mergelists(*choices.values())) - normvacc = vaccine.lower() + label = vaccine.lower() for txt in ['.', ' ', '&', '-', 'vaccine']: - normvacc = normvacc.replace(txt, '') + label = label.replace(txt, '') - if normvacc in mapping: - normvacc = mapping[normvacc] - vaccine_pars = sc.mergedicts(strain_pars[normvacc], dose_pars[normvacc]) + if label in mapping: + label = mapping[label] + vaccine_pars = sc.mergedicts(strain_pars[label], dose_pars[label]) else: # pragma: no cover - errormsg = f'The selected vaccine "{vaccine}" is not implemented; choices are:\n{choicestr}' + errormsg = f'The selected vaccine "{vaccine}" is not implemented; choices are:\n{sc.pp(choices, doprint=False)}' raise NotImplementedError(errormsg) if self.label is None: - self.label = normvacc + self.label = label # Option 2: strains can be specified as a dict of pars elif isinstance(vaccine, dict): @@ -1270,11 +1274,27 @@ def parse_vaccine_pars(self, vaccine=None): if self.label is None: self.label = 'Custom vaccine' + label = vaccine_pars.pop('label', label) # Allow including the label in the parameters + if label is None: + label = 'custom' + + # Check that valid keys have been supplied: + invalid = [] + for key in vaccine_pars.keys(): + if key not in list(strain_pars.keys()): + invalid.append(key) + if len(invalid): + errormsg = f'Could not parse strain keys "{sc.strjoin(invalid)}"; valid keys are: "{sc.strjoin(cvd.strain_pars)}"' + raise sc.KeyNotFoundError(errormsg) + else: # pragma: no cover errormsg = f'Could not understand {type(vaccine)}, please specify as a string indexing a predefined vaccine or a dict.' raise ValueError(errormsg) + # Set label and parameters + self.label = label self.p = sc.objdict(vaccine_pars) + return @@ -1286,12 +1306,10 @@ def initialize(self, sim): self.vaccinated = [None]*(sim['n_days']+1) # keep track of inds of people vaccinated on each day self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person self.vaccination_dates = np.full(sim.n, np.nan) # Store the dates when people are vaccinated - self.vaccine_ind = len(sim['vaccines']) - self.vaccine = cvi.Vaccine(self.vaccine_pars) + self.index = len(sim['vaccines']) # Set the index based on the current number included + # self.vaccine = cvi.Vaccine(self.vaccine_pars) # self.vaccine.initialize(sim) - sim['vaccines'].append(self.vaccine) - self.doses = self.vaccine.p.doses - self.interval = self.vaccine.p.interval + sim['vaccines'].append(self.vaccine) # Add into the sim return diff --git a/covasim/parameters.py b/covasim/parameters.py index 9154d99ec..0bbc5e22b 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -109,9 +109,6 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): # Events and interventions pars['interventions'] = [] # The interventions present in this simulation; populated by the user pars['analyzers'] = [] # Custom analysis functions; populated by the user - pars['strains'] = [] # Additional strains of the virus; populated by the user, see immunity.py - pars['vaccines'] = [] # Vaccines that are being used; populated by user - pars['timelimit'] = None # Time limit for the simulation (seconds) pars['stopping_func'] = None # A function to call to stop the sim partway through @@ -121,6 +118,16 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['no_hosp_factor'] = 2.0 # Multiplier for how much more likely severely ill people are to become critical if no hospital beds are available pars['no_icu_factor'] = 2.0 # Multiplier for how much more likely critically ill people are to die if no ICU beds are available + # Handle vaccine and strain parameters + pars['vaccine_pars'] = {} # Vaccines that are being used; populated during initialization + pars['vaccine_map'] = {} #Reverse mapping from number to vaccine key + pars['strains'] = [] # Additional strains of the virus; populated by the user, see immunity.py + pars['strain_map'] = {0:'wild'} # Reverse mapping from number to strain key + pars['strain_pars'] = dict(wild={}) # Populated just below + for sp in cvd.strain_pars: + if sp in pars.keys(): + pars['strain_pars']['wild'][sp] = pars[sp] + # Update with any supplied parameter values and generate things that need to be generated pars.update(kwargs) reset_layer_pars(pars) @@ -138,14 +145,6 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): if sc.compareversions(version, '2.1.0') == -1 and 'migrate_lognormal' not in pars: cvm.migrate_lognormal(pars, verbose=pars['verbose']) - # Handle strain pars - if 'strain_pars' not in pars: - pars['strain_map'] = {0:'wild'} # Reverse mapping from number to strain key - pars['strain_pars'] = dict(wild={}) # Populated just below - for sp in cvd.strain_pars: - if sp in pars.keys(): - pars['strain_pars']['wild'][sp] = pars[sp] - return pars diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index b902a7063..43065f934 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -98,7 +98,7 @@ def test_vaccine_1strain(do_plot=False, do_show=True, do_save=False): 'n_days': 120, }) - pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer') + pfizer = cv.vaccinate(days=[20], vaccine='pfizer') sim = cv.Sim( use_waning=True, pars=pars, @@ -127,7 +127,7 @@ def test_synthpops(): sim.vxsubtarg.age = [75, 65, 50, 18] sim.vxsubtarg.prob = [.05, .05, .05, .05] sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] - pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) + pfizer = cv.vaccinate(days=subtarg_days, vaccine='pfizer', subtarget=vacc_subtarg) sim['interventions'] += [pfizer] sim.run() @@ -149,7 +149,7 @@ def test_vaccine_1strain_scen(do_plot=False, do_show=True, do_save=False): base_sim.vxsubtarg.age = [75, 65, 50, 18] base_sim.vxsubtarg.prob = [.05, .05, .05, .05] base_sim.vxsubtarg.days = subtarg_days = [20, 40, 60, 80] - pfizer = cv.vaccinate(days=subtarg_days, vaccine_pars='pfizer', subtarget=vacc_subtarg) + pfizer = cv.vaccinate(days=subtarg_days, vaccine='pfizer', subtarget=vacc_subtarg) # Define the scenarios @@ -194,7 +194,7 @@ def test_vaccine_2strains_scen(do_plot=False, do_show=True, do_save=False): base_sim.vxsubtarg.age = [75, 65, 50, 18] base_sim.vxsubtarg.prob = [.01, .01, .01, .01] base_sim.vxsubtarg.days = subtarg_days = [60, 150, 200, 220] - jnj = cv.vaccinate(days=subtarg_days, vaccine_pars='j&j', subtarget=vacc_subtarg) + jnj = cv.vaccinate(days=subtarg_days, vaccine='j&j', subtarget=vacc_subtarg) b1351 = cv.strain('b1351', days=10, n_imports=20) p1 = cv.strain('p1', days=100, n_imports=100) From df36632f4e03407b0f79d9ccec49f1a9d5d123e3 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 13 Apr 2021 02:41:24 -0700 Subject: [PATCH 490/569] done --- covasim/immunity.py | 29 ++++++------------------ covasim/interventions.py | 48 ++++++++++++++++++++++++---------------- covasim/sim.py | 5 +++++ 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 72ab57fc0..43cf8abfa 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -47,7 +47,7 @@ def __init__(self, strain, days, label=None, n_imports=1, rescale=True): def parse(self, strain=None, label=None): - ''' Unpack strain information, which may be given in different ways ''' + ''' Unpack strain information, which may be given as either a string or a dict ''' # Option 1: strains can be chosen from a list of pre-defined strains if isinstance(strain, str): @@ -72,6 +72,7 @@ def parse(self, strain=None, label=None): default_strain_pars = cvpar.get_strain_pars(default=True) default_keys = list(default_strain_pars.keys()) + # Parse label strain_pars = strain label = strain_pars.pop('label', label) # Allow including the label in the parameters if label is None: @@ -103,32 +104,16 @@ def parse(self, strain=None, label=None): def initialize(self, sim): - - # Check we haven't already initialized - if self.initialized: - errormsg = 'Strains cannot be re-initialized since the change the state of the sim' - raise RuntimeError(errormsg) - - # Store the index of this strain, and increment the number of strains in the simulation - self.index = sim['n_strains'] - sim['n_strains'] += 1 - - # Store the mapping of this strain - existing = list(sim['strain_map'].values()) - if self.label in existing: - errormsg = f'Cannot add new strain with label "{self.label}", it already exists in the list of strains: {sc.strjoin(existing)}' - raise ValueError(errormsg) - else: - sim['strain_map'][self.index] = self.label - - # Update strain info - sim['strain_pars'][self.label] = self.p + ''' Update strain info in sim ''' + sim['strain_pars'][self.label] = self.p # Store the parameters + self.index = list(sim['strain_pars'].keys()).index(self.label) # Find where we are in the list + sim['strain_map'][self.index] = self.label # Use that to populate the reverse mapping self.initialized = True - return def apply(self, sim): + ''' Introduce new infections with this strain ''' for ind in cvi.find_day(self.days, sim.t, interv=self, sim=sim): # Time to introduce strain susceptible_inds = cvu.true(sim.people.susceptible) n_imports = sc.randround(self.n_imports/sim.rescale_vec[sim.t]) # Round stochastically to the nearest number of imports diff --git a/covasim/interventions.py b/covasim/interventions.py index ff89c5877..d99560c88 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1243,9 +1243,7 @@ def __init__(self, vaccine, days, label=None, prob=1.0, subtarget=None, **kwargs def parse(self, vaccine=None, label=None): - ''' Unpack vaccine information, which may be given in different ways''' - - + ''' Unpack vaccine information, which may be given as a string or dict ''' # Option 1: vaccines can be chosen from a list of pre-defined strains if isinstance(vaccine, str): @@ -1270,23 +1268,13 @@ def parse(self, vaccine=None, label=None): # Option 2: strains can be specified as a dict of pars elif isinstance(vaccine, dict): - vaccine_pars = vaccine - if self.label is None: - self.label = 'Custom vaccine' + # Parse label + vaccine_pars = vaccine label = vaccine_pars.pop('label', label) # Allow including the label in the parameters if label is None: label = 'custom' - # Check that valid keys have been supplied: - invalid = [] - for key in vaccine_pars.keys(): - if key not in list(strain_pars.keys()): - invalid.append(key) - if len(invalid): - errormsg = f'Could not parse strain keys "{sc.strjoin(invalid)}"; valid keys are: "{sc.strjoin(cvd.strain_pars)}"' - raise sc.KeyNotFoundError(errormsg) - else: # pragma: no cover errormsg = f'Could not understand {type(vaccine)}, please specify as a string indexing a predefined vaccine or a dict.' raise ValueError(errormsg) @@ -1301,15 +1289,37 @@ def parse(self, vaccine=None, label=None): def initialize(self, sim): ''' Fix the dates and store the vaccinations ''' super().initialize() + + # Populate any missing keys -- must be here, after strains are initialized + default_strain_pars = cvpar.get_vaccine_strain_pars(default=True) + default_dose_pars = cvpar.get_vaccine_dose_pars(default=True) + strain_labels = list(sim['strain_pars'].keys()) + dose_keys = list(default_dose_pars.keys()) + + # Handle dose keys + for key in dose_keys: + if key not in self.p: + self.p[key] = default_dose_pars[key] + + # Handle strains + for key in strain_labels: + if key not in self.p: + if key in default_strain_pars: + val = default_strain_pars[key] + else: + val = 1.0 + if sim['verbose']: print('Note: No cross-immunity specified for vaccine {self.label} and strain {key}, setting to 1.0') + self.p[key] = val + self.first_dose_eligible = process_days(sim, self.days) # days that group becomes eligible self.second_dose_days = [None]*(sim['n_days']+1) # inds who get second dose (if relevant) self.vaccinated = [None]*(sim['n_days']+1) # keep track of inds of people vaccinated on each day self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person self.vaccination_dates = np.full(sim.n, np.nan) # Store the dates when people are vaccinated - self.index = len(sim['vaccines']) # Set the index based on the current number included - # self.vaccine = cvi.Vaccine(self.vaccine_pars) - # self.vaccine.initialize(sim) - sim['vaccines'].append(self.vaccine) # Add into the sim + sim['vaccine_pars'][self.label] = self.p # Store the parameters + self.index = list(sim['vaccine_pars'].keys()).index(self.label) # Find where we are in the list + sim['vaccine_map'][self.index] = self.label # Use that to populate the reverse mapping + return diff --git a/covasim/sim.py b/covasim/sim.py index 150d7bff6..af7d5492d 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -503,6 +503,11 @@ def init_strains(self): errormsg = f'Strain {i} ({strain}) is not a cv.strain object; please create using cv.strain()' raise TypeError(errormsg) + len_pars = len(self['strain_pars']) + len_map = len(self['strain_map']) + assert len_pars == len_map, f"strain_pars and strain_map must be the same length, but they're not: {len_pars} ≠ {len_map}" + self['n_strains'] = len_pars # Each strain has an entry in strain_pars + return From 3a529aa54b0be2bb7a04b06483126c6586525cf6 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 13 Apr 2021 03:04:56 -0700 Subject: [PATCH 491/569] first pass --- covasim/immunity.py | 80 ++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 43cf8abfa..a6c19c572 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -126,7 +126,22 @@ def apply(self, sim): #%% Neutralizing antibody methods -def init_nab(people, inds, prior_inf=True, vacc_info=None): +def get_vaccine_pars(pars): + ''' + Temporary helper function to get vaccine parameters; to be refactored + + TODO: use people.vaccine_source to get the per-person specific NAb decay + ''' + try: + vaccine = pars['vaccine_map'][0] # For now, just use the first vaccine, if available + vaccine_pars = pars['vaccine_pars'][vaccine] + except: + vaccine_pars = pars # Otherwise, just use defaults for natural immunity + + return vaccine_pars + + +def init_nab(people, inds, prior_inf=True): ''' Draws an initial neutralizing antibody (NAb) level for individuals. Can come from a natural infection or vaccination and depends on if there is prior immunity: @@ -136,21 +151,18 @@ def init_nab(people, inds, prior_inf=True, vacc_info=None): depending upon vaccine source. If individual has existing NAb, multiply booster impact ''' - if vacc_info is None: - # print('Note: using default vaccine dosing information') - vacc_info = cvpar.get_vaccine_dose_pars()['default'] - nab_arrays = people.nab[inds] prior_nab_inds = cvu.idefined(nab_arrays, inds) # Find people with prior NAb no_prior_nab_inds = np.setdiff1d(inds, prior_nab_inds) # Find people without prior NAb peak_nab = people.init_nab[prior_nab_inds] + pars = people.pars # NAb from infection if prior_inf: - nab_boost = people.pars['nab_boost'] # Boosting factor for natural infection + nab_boost = pars['nab_boost'] # Boosting factor for natural infection # 1) No prior NAb: draw NAb from a distribution and compute if len(no_prior_nab_inds): - init_nab = cvu.sample(**people.pars['nab_init'], size=len(no_prior_nab_inds)) + init_nab = cvu.sample(**pars['nab_init'], size=len(no_prior_nab_inds)) prior_symp = people.prior_symptoms[no_prior_nab_inds] no_prior_nab = (2**init_nab) * prior_symp people.init_nab[no_prior_nab_inds] = no_prior_nab @@ -162,14 +174,16 @@ def init_nab(people, inds, prior_inf=True, vacc_info=None): # NAb from a vaccine else: - nab_boost = vacc_info['nab_boost'] # Boosting factor for vaccination + vaccine_pars = get_vaccine_pars(pars) + # 1) No prior NAb: draw NAb from a distribution and compute if len(no_prior_nab_inds): - init_nab = cvu.sample(**vacc_info['nab_init'], size=len(no_prior_nab_inds)) + init_nab = cvu.sample(**vaccine_pars['nab_init'], size=len(no_prior_nab_inds)) people.init_nab[no_prior_nab_inds] = 2**init_nab # 2) Prior nab (from natural or vaccine dose 1): multiply existing nab by boost factor if len(prior_nab_inds): + nab_boost = vaccine_pars['nab_boost'] # Boosting factor for vaccination init_nab = peak_nab * nab_boost people.init_nab[prior_nab_inds] = init_nab @@ -233,12 +247,11 @@ def init_immunity(sim, create=False): if not sim['use_waning']: return - ns = sim['n_strains'] - immunity = {} - # Pull out all of the circulating strains for cross-immunity + ns = sim['n_strains'] + immunity = {} + rel_imms = {} strain_labels = sim['strain_map'].values() - rel_imms = dict() for label in strain_labels: rel_imms[label] = sim['strain_pars'][label]['rel_imm'] @@ -268,7 +281,7 @@ def init_immunity(sim, create=False): return -def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): +def check_immunity(people, strain, sus=True, inds=None): ''' Calculate people's immunity on this timestep from prior infections + vaccination @@ -280,22 +293,21 @@ def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): Gets called from sim before computing trans_sus, sus=True, inds=None Gets called from people.infect() to calculate prog/trans, sus=False, inds= inds of people being infected ''' - if vacc_info is None: - # print('Note: using default vaccine dosing information') - vacc_info = cvpar.get_vaccine_dose_pars()['default'] - vacc_strain = cvpar.get_vaccine_strain_pars()['default'] - - was_inf = cvu.true(people.t >= people.date_recovered) # Had a previous exposure, now recovered - is_vacc = cvu.true(people.vaccinated) # Vaccinated + # Handle parameters and indices + pars = people.pars + vaccine_pars = get_vaccine_pars(pars) + was_inf = cvu.true(people.t >= people.date_recovered) # Had a previous exposure, now recovered + is_vacc = cvu.true(people.vaccinated) # Vaccinated date_rec = people.date_recovered # Date recovered - immunity = people.pars['immunity'] # cross-immunity/own-immunity scalars to be applied to NAb level before computing efficacy - nab_eff_pars = people.pars['nab_eff'] + immunity = pars['immunity'] # cross-immunity/own-immunity scalars to be applied to NAb level before computing efficacy + nab_eff = pars['nab_eff'] # If vaccines are present, extract relevant information about them vacc_present = len(is_vacc) if vacc_present: - vx_nab_eff_pars = vacc_info['nab_eff'] + vx_nab_eff_pars = vaccine_pars['nab_eff'] + vacc_mapping = np.array([vaccine_pars[label] for label in pars['strain_map'].values()]) # PART 1: Immunity to infection for susceptible individuals if sus: @@ -308,14 +320,14 @@ def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): is_sus_was_inf_diff = np.intersect1d(is_sus, was_inf_diff) # Susceptible and being challenged by a different strain if len(is_sus_vacc): - vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) - vaccine_scale = vacc_strain[strain] # TODO: handle this better + vaccine_source = cvd.default_int(people.vaccine_source[is_sus_vacc]) # TODO: use vaccine source + vaccine_scale = vacc_mapping[strain] current_nabs = people.nab[is_sus_vacc] people.sus_imm[strain, is_sus_vacc] = nab_to_efficacy(current_nabs * vaccine_scale, 'sus', vx_nab_eff_pars) if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain current_nabs = people.nab[is_sus_was_inf_same] - people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_nabs * immunity['sus'][strain, strain], 'sus', nab_eff_pars) + people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_nabs * immunity['sus'][strain, strain], 'sus', nab_eff) if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain prior_strains = people.recovered_strain[is_sus_was_inf_diff] @@ -323,7 +335,7 @@ def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] current_nabs = people.nab[unique_inds] - people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_nabs * immunity['sus'][strain, unique_strain], 'sus', nab_eff_pars) + people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_nabs * immunity['sus'][strain, unique_strain], 'sus', nab_eff) # PART 2: Immunity to disease for currently-infected people else: @@ -331,16 +343,16 @@ def check_immunity(people, strain, sus=True, inds=None, vacc_info=None): was_inf = np.intersect1d(inds, was_inf) if len(is_inf_vacc): # Immunity for infected people who've been vaccinated - vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) - vaccine_scale = vacc_strain[strain] # TODO: handle this better + vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) # TODO: use vaccine source + vaccine_scale = vacc_mapping[strain] current_nabs = people.nab[is_inf_vacc] - people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_nabs * vaccine_scale * immunity['symp'][strain], 'symp', nab_eff_pars) - people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_nabs * vaccine_scale * immunity['sev'][strain], 'sev', nab_eff_pars) + people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_nabs * vaccine_scale * immunity['symp'][strain], 'symp', nab_eff) + people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_nabs * vaccine_scale * immunity['sev'][strain], 'sev', nab_eff) if len(was_inf): # Immunity for reinfected people current_nabs = people.nab[was_inf] - people.symp_imm[strain, was_inf] = nab_to_efficacy(current_nabs * immunity['symp'][strain], 'symp', nab_eff_pars) - people.sev_imm[strain, was_inf] = nab_to_efficacy(current_nabs * immunity['sev'][strain], 'sev', nab_eff_pars) + people.symp_imm[strain, was_inf] = nab_to_efficacy(current_nabs * immunity['symp'][strain], 'symp', nab_eff) + people.sev_imm[strain, was_inf] = nab_to_efficacy(current_nabs * immunity['sev'][strain], 'sev', nab_eff) return From bf0abee3ce528481a79b16b4a0d08f8b1bd5fab2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 13 Apr 2021 03:11:52 -0700 Subject: [PATCH 492/569] fixed tests --- covasim/immunity.py | 2 +- covasim/interventions.py | 6 ++--- tests/devtests/test_variants.py | 47 +-------------------------------- tests/test_immunity.py | 2 +- 4 files changed, 6 insertions(+), 51 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index a6c19c572..746440f06 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -307,7 +307,7 @@ def check_immunity(people, strain, sus=True, inds=None): vacc_present = len(is_vacc) if vacc_present: vx_nab_eff_pars = vaccine_pars['nab_eff'] - vacc_mapping = np.array([vaccine_pars[label] for label in pars['strain_map'].values()]) + vacc_mapping = np.array([vaccine_pars.get(label, 1.0) for label in pars['strain_map'].values()]) # TODO: make more robust # PART 1: Immunity to infection for susceptible individuals if sus: diff --git a/covasim/interventions.py b/covasim/interventions.py index d99560c88..d573f16a8 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1342,8 +1342,8 @@ def apply(self, sim): self.vaccinated[sim.t] = vacc_inds sim.people.flows['new_vaccinations'] += len(vacc_inds) sim.people.flows['new_vaccinated'] += len(vacc_inds) - if self.interval is not None: - next_dose_day = sim.t + self.interval + if self.p.interval is not None: + next_dose_day = sim.t + self.p.interval if next_dose_day < sim['n_days']: self.second_dose_days[next_dose_day] = vacc_inds @@ -1354,7 +1354,7 @@ def apply(self, sim): # Update vaccine attributes in sim sim.people.vaccinated[vacc_inds] = True - sim.people.vaccine_source[vacc_inds] = self.vaccine_ind + sim.people.vaccine_source[vacc_inds] = self.index self.vaccinations[vacc_inds] += 1 self.vaccination_dates[vacc_inds] = sim.t diff --git a/tests/devtests/test_variants.py b/tests/devtests/test_variants.py index 43065f934..c759e10bc 100644 --- a/tests/devtests/test_variants.py +++ b/tests/devtests/test_variants.py @@ -58,7 +58,6 @@ def test_importstrain_longerdur(do_plot=False, do_show=True, do_save=False): strain_pars = { 'rel_beta': 1.5, - 'dur': {'exp2inf':dict(dist='lognormal_int', par1=6.0, par2=2.0)} } strain = cv.strain(strain=strain_pars, label='Custom strain', days=10, n_imports=30) @@ -239,49 +238,6 @@ def test_vaccine_2strains_scen(do_plot=False, do_show=True, do_save=False): return scens -def test_strainduration_scen(do_plot=False, do_show=True, do_save=False): - sc.heading('Run a sim with 2 strains, one of which has a much longer period before symptoms develop') - - strain_pars = {'dur':{'inf2sym': {'dist': 'lognormal_int', 'par1': 10.0, 'par2': 0.9}}} - strains = cv.strain(strain=strain_pars, label='10 days til symptoms', days=10, n_imports=30) - tp = cv.test_prob(symp_prob=0.2) # Add an efficient testing program - - pars = sc.mergedicts(base_pars, { - 'beta': 0.015, # Make beta higher than usual so people get infected quickly - 'n_days': 120, - 'interventions': tp - }) - n_runs = 1 - base_sim = cv.Sim(use_waning=True, pars=pars) - - # Define the scenarios - scenarios = { - 'baseline': { - 'name':'1 day to symptoms', - 'pars': {} - }, - 'slowsymp': { - 'name':'10 days to symptoms', - 'pars': {'strains': [strains]} - } - } - - metapars = {'n_runs': n_runs} - scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run(debug=debug) - - to_plot = sc.objdict({ - 'New infections': ['new_infections'], - 'Cumulative infections': ['cum_infections'], - 'New diagnoses': ['new_diagnoses'], - 'Cumulative diagnoses': ['cum_diagnoses'], - }) - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path='results/test_strainduration.png', to_plot=to_plot) - - return scens - - def test_msim(do_plot=False): sc.heading('Testing multisim...') @@ -453,14 +409,13 @@ def get_ind_of_min_value(list, time): # Run multisim and scenario tests scens0 = test_vaccine_1strain_scen() scens1 = test_vaccine_2strains_scen() - scens2 = test_strainduration_scen(**kw) msim0 = test_msim() # Run immunity tests sim_immunity0 = test_varyingimmunity(**kw) # Run test to compare sims with and without waning - scens3 = test_waning_vs_not(**kw) + scens2 = test_waning_vs_not(**kw) sc.toc() diff --git a/tests/test_immunity.py b/tests/test_immunity.py index 4f10f02f6..ba315b418 100644 --- a/tests/test_immunity.py +++ b/tests/test_immunity.py @@ -147,7 +147,7 @@ def test_vaccines(do_plot=False): sc.heading('Testing vaccines...') p1 = cv.strain('sa variant', days=20, n_imports=20) - pfizer = cv.vaccinate(days=30, vaccine_pars='pfizer') + pfizer = cv.vaccinate(vaccine='pfizer', days=30) sim = cv.Sim(base_pars, use_waning=True, strains=p1, interventions=pfizer) sim.run() From cab482cdd66ab25ccf1fbdb26f9861d9c46688e2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 13 Apr 2021 03:18:35 -0700 Subject: [PATCH 493/569] update export --- covasim/base.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 86214ba0e..4c1e6090f 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -563,23 +563,28 @@ def to_json(self, filename=None, keys=None, tostring=False, indent=2, verbose=Fa return output - def to_excel(self, filename=None): + def to_excel(self, filename=None, skip_pars=None): ''' - Export results as XLSX + Export parameters and results as Excel format Args: - filename (str): if None, return string; else, write to file + filename (str): if None, return string; else, write to file + skip_pars (list): if provided, a custom list parameters to exclude Returns: An sc.Spreadsheet with an Excel file, or writes the file to disk ''' + if skip_pars is None: + skip_pars = ['strain_map', 'vaccine_map'] # These include non-string keys so fail at sc.flattendict() + resdict = self.export_results(for_json=False) result_df = pd.DataFrame.from_dict(resdict) result_df.index = self.datevec result_df.index.name = 'date' - par_df = pd.DataFrame.from_dict(sc.flattendict(self.pars, sep='_'), orient='index', columns=['Value']) + pars = {k:v for k,v in self.pars.items() if k not in skip_pars} + par_df = pd.DataFrame.from_dict(sc.flattendict(pars, sep='_'), orient='index', columns=['Value']) par_df.index.name = 'Parameter' spreadsheet = sc.Spreadsheet() From b9ea63a4850b26971d8b5904a4267eca0f12d687 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 13 Apr 2021 03:37:42 -0700 Subject: [PATCH 494/569] working --- covasim/defaults.py | 9 +++++---- covasim/immunity.py | 1 + covasim/interventions.py | 6 +++--- docs/tutorials/tut_immunity.ipynb | 31 ++++++------------------------- 4 files changed, 15 insertions(+), 32 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index b84fd74a4..20e86c40d 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -363,13 +363,14 @@ def get_default_plots(which='default', kind='sim', sim=None): # Show default but with strains elif 'strain' in which: # pragma: no cover plots = sc.odict({ - 'Total counts': [ + 'Cumulative infections by strain': [ 'cum_infections_by_strain', - 'n_infectious_by_strain', - 'cum_diagnoses', ], - 'Daily counts': [ + 'New infections by strain': [ 'new_infections_by_strain', + ], + 'Diagnoses': [ + 'cum_diagnoses', 'new_diagnoses', ], 'Health outcomes': [ diff --git a/covasim/immunity.py b/covasim/immunity.py index 746440f06..a2ea36ba4 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -105,6 +105,7 @@ def parse(self, strain=None, label=None): def initialize(self, sim): ''' Update strain info in sim ''' + self.days = cvi.process_days(sim, self.days) # Convert days into correct format sim['strain_pars'][self.label] = self.p # Store the parameters self.index = list(sim['strain_pars'].keys()).index(self.label) # Find where we are in the list sim['strain_map'][self.index] = self.label # Use that to populate the reverse mapping diff --git a/covasim/interventions.py b/covasim/interventions.py index d573f16a8..6ad8a435c 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1311,7 +1311,7 @@ def initialize(self, sim): if sim['verbose']: print('Note: No cross-immunity specified for vaccine {self.label} and strain {key}, setting to 1.0') self.p[key] = val - self.first_dose_eligible = process_days(sim, self.days) # days that group becomes eligible + self.days = process_days(sim, self.days) # days that group becomes eligible self.second_dose_days = [None]*(sim['n_days']+1) # inds who get second dose (if relevant) self.vaccinated = [None]*(sim['n_days']+1) # keep track of inds of people vaccinated on each day self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person @@ -1326,14 +1326,14 @@ def initialize(self, sim): def apply(self, sim): ''' Perform vaccination ''' - if sim.t >= min(self.first_dose_eligible): + if sim.t >= np.min(self.days): # Determine who gets first dose of vaccine today vacc_probs = np.zeros(sim.n) if self.subtarget is not None: subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted else: - for _ in find_day(self.first_dose_eligible, sim.t): + for ind in find_day(self.days, sim.t, interv=self, sim=sim): unvacc_inds = sc.findinds(~sim.people.vaccinated) vacc_probs[unvacc_inds] = self.prob # Assign equal vaccination probability to everyone vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated diff --git a/docs/tutorials/tut_immunity.ipynb b/docs/tutorials/tut_immunity.ipynb index 5016b151f..08d3e8a1d 100644 --- a/docs/tutorials/tut_immunity.ipynb +++ b/docs/tutorials/tut_immunity.ipynb @@ -24,6 +24,7 @@ "outputs": [], "source": [ "import numpy as np\n", + "import sciris as sc\n", "import covasim as cv\n", "cv.options.set(dpi=100, show=False, close=True, verbose=0) # Standard options for Jupyter notebook\n", "\n", @@ -66,21 +67,17 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", - "import sciris as sc\n", - "import covasim as cv\n", - "\n", "# Define three new strains: B117, B1351, and a custom-defined strain\n", - "b117 = cv.Strain('b117', days=5, n_imports=10)\n", - "b1351 = cv.Strain('b1351', days=10, n_imports=10)\n", - "custom = cv.Strain(label='1.5x more transmissible', strain = {'rel_beta': 1.5}, days=15, n_imports=10)\n", + "b117 = cv.strain('b1351', days=10, n_imports=10)\n", + "b1351 = cv.strain('b117', days=10, n_imports=10)\n", + "custom = cv.strain(label='3x more transmissible', strain = {'rel_beta': 3.0}, days=20, n_imports=10)\n", "\n", "# Create the simulation\n", "sim = cv.Sim(use_waning=True, strains=[b117, b1351, custom])\n", "\n", "# Run and plot\n", "sim.run()\n", - "sim.plot()\n" + "sim.plot('strain')" ] }, { @@ -100,8 +97,6 @@ "metadata": {}, "outputs": [], "source": [ - "import covasim as cv\n", - "\n", "# Create some base parameters\n", "pars = {\n", " 'beta': 0.015,\n", @@ -109,7 +104,7 @@ "}\n", "\n", "# Define a Pfizer vaccine\n", - "pfizer = cv.vaccinate(days=[20], vaccine_pars='pfizer')\n", + "pfizer = cv.vaccinate(vaccine='pfizer', days=20)\n", "sim = cv.Sim(\n", " use_waning=True,\n", " pars=pars,\n", @@ -118,20 +113,6 @@ "sim.run()\n", "sim.plot()\n" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From b1857936994cf20b5b305bb1f214060552b41e3e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 13 Apr 2021 03:42:10 -0700 Subject: [PATCH 495/569] updated changelog --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1762558ad..8948bf036 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,7 +31,7 @@ This version introduces fully featured vaccines, variants, and immunity. **Note: Highlights ^^^^^^^^^^ - **Model structure**: The model now follows an "SEIS"-type structure, instead of the previous "SEIR" structure. This means that after recovering from an infection, agents return to the "susceptible" compartment. Each agent in the simulation has properties ``sus_imm``, ``trans_imm`` and ``prog_imm``, which respectively determine their immunity to acquiring an infection, transmitting an infection, or developing a more severe case of COVID-19. All these immunity levels are initially zero. They can be boosted by either natural infection or vaccination, and thereafter they can wane over time or remain permanently elevated. -- **Multi-strain modeling**: Model functionality has been extended to allow for modeling of multiple different co-circulating strains with different properties. This means you can now do e.g. ``b117 = cv.Strain('b117', days=1, n_imports=20)`` followed by ``sim = cv.Sim(strains=b117)`` to import strain B117. Further examples are contained in ``devtests/test_variants.py``. +- **Multi-strain modeling**: Model functionality has been extended to allow for modeling of multiple different co-circulating strains with different properties. This means you can now do e.g. ``b117 = cv.strain('b117', days=1, n_imports=20)`` followed by ``sim = cv.Sim(strains=b117)`` to import strain B117. Further examples are contained in ``tests/test_immunity.py`` and in Tutorial 8. - **New methods for vaccine modeling**: A new ``cv.vaccinate()`` intervention has been added, which allows more flexible modeling of vaccinations. Vaccines, like natural infections, are assumed to boost agents' immunity. - **Consistency**: By default, results from Covasim 3.0.0 should exactly match Covasim 2.1.2. To use the new features, you will need to manually specify ``cv.Sim(use_waning=True)``. @@ -63,7 +63,7 @@ Changes to results New functions, methods and classes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- The newly-added file ``immunity.py`` contains functions, methods, and classes related to calculating immunity. This includes the ``Strain`` class, the ``Vaccine`` class. +- The newly-added file ``immunity.py`` contains functions, methods, and classes related to calculating immunity. This includes the ``strain`` class (which uses lowercase convention like Covasim interventions, which are also technically classes). - A new ``cv.vaccinate()`` intervention has been added. Compared to the previous ``vaccine`` intervention (now renamed ``cv.simple_vaccine()``), this new intervention allows vaccination to boost agents' immunity against infection, transmission, and progression. - A new function ``cv.demo()`` has been added as a shortcut to ``cv.Sim().run().plot()``. - There is a new ``sim.people.make_nonnaive()`` method, as the opposite of ``sim.people.make_naive()``. From 9474e9a2e7f1179a99070c5d448d73ea8a5b8aa1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 13 Apr 2021 03:48:07 -0700 Subject: [PATCH 496/569] update readme --- CHANGELOG.rst | 21 +++++++++++---------- README.rst | 1 + 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8948bf036..0d8adc617 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,12 +35,8 @@ Highlights - **New methods for vaccine modeling**: A new ``cv.vaccinate()`` intervention has been added, which allows more flexible modeling of vaccinations. Vaccines, like natural infections, are assumed to boost agents' immunity. - **Consistency**: By default, results from Covasim 3.0.0 should exactly match Covasim 2.1.2. To use the new features, you will need to manually specify ``cv.Sim(use_waning=True)``. -State changes -^^^^^^^^^^^^^ -- Several new states have been added, such as ``people.naive``, which stores whether or not a person has ever been exposed to COVID before. - -Parameter changes -^^^^^^^^^^^^^^^^^ +Immunity-related parameter changes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - A new control parameter, ``use_waning``, has been added that controls whether to use new waning immunity dynamics ("SEIS" structure) or the old dynamics where post-infection immunity was perfect and did not wane ("SEIR" structure). By default, ``use_waning=False``. - A subset of existing parameters have been made strain-specific, meaning that they are allowed to differ by strain. These include: ``rel_beta``, which specifies the relative transmissibility of a new strain compared to the wild strain; ``rel_symp_prob``, ``rel_severe_prob``, ``rel_crit_prob``, and the newly-added immunity parameters ``rel_imm`` (see next point). The list of parameters that can vary by strain is specified in ``defaults.py``. - The parameter ``n_strains`` is an integer that specifies how many strains will be in circulation at some point during the course of the simulation. @@ -52,22 +48,27 @@ Parameter changes - The parameter ``cross_immunity``. By default, infection with one strain of SARS-CoV-2 is assumed to grant 50% immunity to infection with a different strain. This default assumption of 50% cross-immunity can be modified via this parameter (which will then apply to all strains in the simulation), or it can be modified on a per-strain basis using the ``immunity`` parameter described below. - The parameter ``immunity`` is a matrix of size ``total_strains`` by ``total_strains``. Row ``i`` specifies the immunity levels that people who have been infected with strain ``i`` have to other strains. - The parameter ``rel_imm`` is a dictionary with keys ``asymp``, ``mild`` and ``severe``. These contain scalars specifying the relative immunity levels for someone who had an asymptomatic, mild, or severe infection. By default, values of 0.98, 0.99, and 1.0 are used. -- The parameter ``strains`` contains information about any circulating strains that have been specified as additional to the default strain, and the parameter ``vaccines`` contains information about any vaccines in use. These are initialized as ``None`` and then populated by the user. +- The parameter ``strains`` contains information about any circulating strains that have been specified as additional to the default strain. This is initialized as an empty list and then populated by the user. + +Other parameter changes +^^^^^^^^^^^^^^^^^^^^^^^ - The parameter ``frac_susceptible`` will initialize the simulation with less than 100% of the population to be susceptible to COVID (to represent, for example, a baseline level of population immunity). Note that this is intended for quick explorations only, since people are selected at random, whereas in reality higher-risk people will typically be infected first and preferentially be immune. This is primarily designed for use with ``use_waning=False``. - The parameter ``scaled_pop``, if supplied, can be used in place of ``pop_scale`` or ``pop_size``. For example, if you specify ``cv.Sim(pop_size=100e3, scaled_pop=550e3)``, it will automatically calculate ``pop_scale=5.5``. - Aliases have been added for several parameters: ``pop_size`` can also be supplied as ``n_agents``, and ``pop_infected`` can also be supplied as ``init_infected``. This only applies when creating a sim; otherwise, the default names will be used for these parameters. -Changes to results -^^^^^^^^^^^^^^^^^^ +Changes to states and results +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +- Several new states have been added, such as ``people.naive``, which stores whether or not a person has ever been exposed to COVID before. - New results have been added to store information by strain, as well as population immunity levels. In addition to new entries in ``sim.results``, such as ``pop_nabs`` (population level neutralizing antibodies) and ``new_reinfections``, there is a new set of results ``sim.results.strain``: ``cum_infections_by_strain``, ``cum_infectious_by_strain``, ``new_infections_by_strain``, ``new_infectious_by_strain``, ``prevalence_by_strain``, ``incidence_by_strain``. New functions, methods and classes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - The newly-added file ``immunity.py`` contains functions, methods, and classes related to calculating immunity. This includes the ``strain`` class (which uses lowercase convention like Covasim interventions, which are also technically classes). - A new ``cv.vaccinate()`` intervention has been added. Compared to the previous ``vaccine`` intervention (now renamed ``cv.simple_vaccine()``), this new intervention allows vaccination to boost agents' immunity against infection, transmission, and progression. -- A new function ``cv.demo()`` has been added as a shortcut to ``cv.Sim().run().plot()``. - There is a new ``sim.people.make_nonnaive()`` method, as the opposite of ``sim.people.make_naive()``. - New functions ``cv.iundefined()`` and ``cv.iundefinedi()`` have been added for completeness. +- A new function ``cv.demo()`` has been added as a shortcut to ``cv.Sim().run().plot()``. +- There are now additional shortcut plotting methods, including ``sim.plot('strain')`` and ``sim.plot('all')``. Renamed functions and methods ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/README.rst b/README.rst index 2cd7503f5..002dcd3ee 100644 --- a/README.rst +++ b/README.rst @@ -139,6 +139,7 @@ The structure of the ``covasim`` folder is as follows, roughly in the order in w * ``people.py``: The ``People`` class, for handling updates of state for each person. * ``population.py``: Functions for creating populations of people, including age, contacts, etc. * ``interventions.py``: The ``Intervention`` class, for adding interventions and dynamically modifying parameters, and classes for each of the specific interventions derived from it. +* ``immunity.py``: The ``strain`` class, and functions for computing waning immunity and neutralizing antibodies. * ``sim.py``: The ``Sim`` class, which performs most of the heavy lifting: initializing the model, running, and plotting. * ``run.py``: Functions for running simulations (e.g. parallel runs and the ``Scenarios`` and ``MultiSim`` classes). * ``analysis.py``: The ``Analyzers`` class (for performing analyses on the sim while it's running), the ``Fit`` class (for calculating the fit between the model and the data), the ``TransTree`` class, and other classes and functions for analyzing simulations. From f53a5f5a374bcfebb3ea480ad20b039cd9133d43 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 13 Apr 2021 03:57:21 -0700 Subject: [PATCH 497/569] update baseline --- covasim/regression/pars_v3.0.0.json | 59 ++++++++++++++--------------- tests/benchmark.json | 6 +-- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/covasim/regression/pars_v3.0.0.json b/covasim/regression/pars_v3.0.0.json index c591b32e3..b2a65b7ae 100644 --- a/covasim/regression/pars_v3.0.0.json +++ b/covasim/regression/pars_v3.0.0.json @@ -9,9 +9,11 @@ "rand_seed": 1, "verbose": 0.1, "pop_scale": 1, + "scaled_pop": null, "rescale": true, "rescale_threshold": 0.05, "rescale_factor": 1.2, + "frac_susceptible": 1.0, "contacts": { "a": 20 }, @@ -35,24 +37,21 @@ "beta": 0.016, "n_imports": 0, "n_strains": 1, - "total_strains": 1, "use_waning": false, - "NAb_init": { + "nab_init": { "dist": "normal", "par1": 0, "par2": 2 }, - "NAb_decay": { + "nab_decay": { "form": "nab_decay", - "pars": { - "init_decay_rate": 0.007701635339554948, - "init_decay_time": 250, - "decay_decay_rate": 0.001 - } + "decay_rate1": 0.007701635339554948, + "decay_time1": 250, + "decay_rate2": 0.001 }, - "NAb_kin": null, - "NAb_boost": 1.5, - "NAb_eff": { + "nab_kin": null, + "nab_boost": 1.5, + "nab_eff": { "sus": { "slope": 2.7, "n_50": 0.03 @@ -62,12 +61,11 @@ }, "cross_immunity": 0.5, "rel_imm": { - "asymptomatic": 0.85, + "asymp": 0.85, "mild": 1, "severe": 1.5 }, "immunity": null, - "vaccine_info": null, "rel_beta": 1.0, "asymp_factor": 1.0, "dur": { @@ -229,29 +227,30 @@ "quar_period": 14, "interventions": [], "analyzers": [], - "strains": [], - "vaccines": [], "timelimit": null, "stopping_func": null, "n_beds_hosp": null, "n_beds_icu": null, "no_hosp_factor": 2.0, "no_icu_factor": 2.0, + "vaccine_pars": {}, + "vaccine_map": {}, + "strains": [], + "strain_map": { + "0": "wild" + }, "strain_pars": { - "rel_beta": [ - 1.0 - ], - "rel_symp_prob": [ - 1.0 - ], - "rel_severe_prob": [ - 1.0 - ], - "rel_crit_prob": [ - 1.0 - ], - "rel_death_prob": [ - 1.0 - ] + "wild": { + "rel_imm": { + "asymp": 0.85, + "mild": 1, + "severe": 1.5 + }, + "rel_beta": 1.0, + "rel_symp_prob": 1.0, + "rel_severe_prob": 1.0, + "rel_crit_prob": 1.0, + "rel_death_prob": 1.0 + } } } \ No newline at end of file diff --git a/tests/benchmark.json b/tests/benchmark.json index 975fa5f7a..f13df0422 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,12 +1,12 @@ { "time": { - "initialize": 0.411, - "run": 0.505 + "initialize": 0.413, + "run": 0.496 }, "parameters": { "pop_size": 20000, "pop_type": "hybrid", "n_days": 60 }, - "cpu_performance": 0.9815278999602399 + "cpu_performance": 0.9599341238861429 } \ No newline at end of file From 75d5986f85adc88f1a57be3da20a4fb999f7e7f8 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 14 Apr 2021 11:27:48 -0400 Subject: [PATCH 498/569] manaus updates --- covasim/immunity.py | 8 ++++---- covasim/interventions.py | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index ae03fb9f8..09852baaf 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -225,7 +225,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_init'] = dict(dist='normal', par1=2, par2= 2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 - vaccine_pars['NAb_boost'] = 2 + vaccine_pars['NAb_boost'] = 3 vaccine_pars['label'] = vaccine # Known parameters on moderna @@ -234,7 +234,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_init'] = dict(dist='normal', par1=2, par2=2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 29 - vaccine_pars['NAb_boost'] = 2 + vaccine_pars['NAb_boost'] = 3 vaccine_pars['label'] = vaccine # Known parameters on az @@ -243,7 +243,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_init'] = dict(dist='normal', par1=-1, par2=2) vaccine_pars['doses'] = 2 vaccine_pars['interval'] = 22 - vaccine_pars['NAb_boost'] = 2 + vaccine_pars['NAb_boost'] = 3 vaccine_pars['label'] = vaccine # Known parameters on j&j @@ -252,7 +252,7 @@ def parse_vaccine_pars(self, vaccine=None): vaccine_pars['NAb_init'] = dict(dist='normal', par1=-1, par2=2) vaccine_pars['doses'] = 1 vaccine_pars['interval'] = None - vaccine_pars['NAb_boost'] = 2 + vaccine_pars['NAb_boost'] = 3 vaccine_pars['label'] = vaccine else: diff --git a/covasim/interventions.py b/covasim/interventions.py index 60755dedf..eecadfe8e 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1180,7 +1180,8 @@ def apply(self, sim): vacc_probs = np.zeros(sim.n) if self.subtarget is not None: subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) - vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted + if len(subtarget_inds): + vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted else: for _ in find_day(self.first_dose_eligible, sim.t): unvacc_inds = sc.findinds(~sim.people.vaccinated) From d2ca31a55eff610ee38343be7b839a0275406be8 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 14 Apr 2021 12:53:56 -0400 Subject: [PATCH 499/569] confusion with n_strains and n_strains? --- covasim/people.py | 6 +++--- covasim/sim.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index a16b8a542..02fb5e34e 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -76,11 +76,11 @@ def __init__(self, pars, strict=True, **kwargs): for key in self.meta.strain_states: self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) for key in self.meta.by_strain_states: - self[key] = np.full((self.n_strains, self.pop_size), False, dtype=bool) + self[key] = np.full((self['n_strains'], self.pop_size), False, dtype=bool) # Set immunity and antibody states for key in self.meta.imm_states: # Everyone starts out with no immunity - self[key] = np.zeros((self.n_strains, self.pop_size), dtype=cvd.default_float) + self[key] = np.zeros((self['n_strains'], self.pop_size), dtype=cvd.default_float) for key in self.meta.nab_states: # Everyone starts out with no antibodies self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) for key in self.meta.vacc_states: @@ -223,7 +223,7 @@ def check_infectious(self): inds = self.check_inds(self.infectious, self.date_infectious, filter_inds=self.is_exp) self.infectious[inds] = True self.infectious_strain[inds] = self.exposed_strain[inds] - for strain in range(self.pars['n_strains']): + for strain in range(self.n_strains): this_strain_inds = cvu.itrue(self.infectious_strain[inds] == strain, inds) n_this_strain_inds = len(this_strain_inds) self.flows_strain['new_infectious_by_strain'][strain] += n_this_strain_inds diff --git a/covasim/sim.py b/covasim/sim.py index af7d5492d..5ff44798f 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -598,7 +598,8 @@ def step(self): viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) # Shorten useful parameters - ns = self['n_strains'] # Shorten number of strains + ns = self.people.n_strains + # ns = self['n_strains'] # Shorten number of strains sus = people.susceptible symp = people.symptomatic diag = people.diagnosed From 88cd8e359ef1ba94083168f568a4e6c0e846b283 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 14 Apr 2021 14:09:32 -0400 Subject: [PATCH 500/569] resizing strain-specific arrays in people --- covasim/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/covasim/base.py b/covasim/base.py index 4c1e6090f..7196fc088 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1029,11 +1029,17 @@ def validate(self, die=True, verbose=False): # Check that the length of each array is consistent expected_len = len(self) + expected_strains = self.pars['n_strains'] for key in self.keys(): actual_len = len(self[key]) # check if it's 2d if self[key].ndim > 1: - actual_len = len(self[key][0]) + actual_len = np.shape(self[key])[1] + actual_strains = np.shape(self[key])[0] + if actual_strains != expected_strains: + if verbose: + print(f'Resizing "{key}" from {actual_strains} to {expected_strains}') + self._resize_arrays(keys=key, pop_size=(expected_strains, expected_len)) if actual_len != expected_len: # pragma: no cover if die: errormsg = f'Length of key "{key}" did not match population size ({actual_len} vs. {expected_len})' From f92d6b6347f1349a7364f06994c466ff269f1ef5 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 14 Apr 2021 14:37:23 -0400 Subject: [PATCH 501/569] weird use of len(people) and self.n that was causing issues --- covasim/interventions.py | 2 +- covasim/people.py | 5 ++--- covasim/sim.py | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 6ad8a435c..c99610290 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -930,7 +930,7 @@ def apply(self, sim): diag_inds = cvu.true(sim.people.diagnosed) # Construct the testing probabilities piece by piece -- complicated, since need to do it in the right order - test_probs = np.zeros(sim.n) # Begin by assigning equal testing probability to everyone + test_probs = np.zeros(sim['pop_size']) # Begin by assigning equal testing probability to everyone test_probs[symp_inds] = symp_prob # People with symptoms (true positive) test_probs[ili_inds] = symp_prob # People with symptoms (false positive) test_probs[asymp_inds] = self.asymp_prob # People without symptoms diff --git a/covasim/people.py b/covasim/people.py index 02fb5e34e..f0ee00aee 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -49,7 +49,6 @@ def __init__(self, pars, strict=True, **kwargs): self.pars = pars # Equivalent to self.set_pars(pars) self.pop_size = int(pars['pop_size']) self.location = pars.get('location') # Try to get location, but set to None otherwise - self.n_strains = pars.get('n_strains', 1) # Assume 1 strain if not supplied self.version = cvv.__version__ # Store version info # Other initialization @@ -121,7 +120,7 @@ def init_flows(self): self.flows = {key:0 for key in cvd.new_result_flows} self.flows_strain = {} for key in cvd.new_result_flows_by_strain: - self.flows_strain[key] = np.zeros(self.n_strains, dtype=cvd.default_float) + self.flows_strain[key] = np.zeros(self.pars['n_strains'], dtype=cvd.default_float) return @@ -223,7 +222,7 @@ def check_infectious(self): inds = self.check_inds(self.infectious, self.date_infectious, filter_inds=self.is_exp) self.infectious[inds] = True self.infectious_strain[inds] = self.exposed_strain[inds] - for strain in range(self.n_strains): + for strain in range(self['n_strains']): this_strain_inds = cvu.itrue(self.infectious_strain[inds] == strain, inds) n_this_strain_inds = len(this_strain_inds) self.flows_strain['new_infectious_by_strain'][strain] += n_this_strain_inds diff --git a/covasim/sim.py b/covasim/sim.py index 5ff44798f..e3e5e6b4d 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -526,7 +526,7 @@ def rescale(self): if current_scale < pop_scale: # We have room to rescale not_naive_inds = self.people.false('naive') # Find everyone not naive n_not_naive = len(not_naive_inds) # Number of people who are not naive - n_people = len(self.people) # Number of people overall + n_people = self['pop_size'] # Number of people overall current_ratio = n_not_naive/n_people # Current proportion not naive threshold = self['rescale_threshold'] # Threshold to trigger rescaling if current_ratio > threshold: # Check if we've reached point when we want to rescale @@ -565,7 +565,7 @@ def step(self): if self['n_imports']: n_imports = cvu.poisson(self['n_imports']/self.rescale_vec[self.t]) # Imported cases if n_imports>0: - importation_inds = cvu.choose(max_n=len(people), n=n_imports) + importation_inds = cvu.choose(max_n=self['pop_size'], n=n_imports) people.infect(inds=importation_inds, hosp_max=hosp_max, icu_max=icu_max, layer='importation') # Add strains @@ -660,7 +660,7 @@ def step(self): self.results['strain'][key][strain][t] += count[strain] # Update nab and immunity for this time step - self.results['pop_nabs'][t] = np.sum(people.nab[cvu.defined(people.nab)])/len(people) + self.results['pop_nabs'][t] = np.sum(people.nab[cvu.defined(people.nab)])/self['pop_size'] self.results['pop_protection'][t] = np.nanmean(people.sus_imm) self.results['pop_symp_protection'][t] = np.nanmean(people.symp_imm) From 9e2a9f3afd16eb67a503d66b7f0854e06ff22fa8 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 14 Apr 2021 14:58:53 -0400 Subject: [PATCH 502/569] fixed ns issue i created! --- covasim/immunity.py | 3 ++- covasim/sim.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index a2ea36ba4..13d5899fc 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -117,7 +117,8 @@ def apply(self, sim): ''' Introduce new infections with this strain ''' for ind in cvi.find_day(self.days, sim.t, interv=self, sim=sim): # Time to introduce strain susceptible_inds = cvu.true(sim.people.susceptible) - n_imports = sc.randround(self.n_imports/sim.rescale_vec[sim.t]) # Round stochastically to the nearest number of imports + # n_imports = sc.randround(self.n_imports/sim.rescale_vec[sim.t]) # Round stochastically to the nearest number of imports + n_imports = sc.randround(self.n_imports) importation_inds = np.random.choice(susceptible_inds, n_imports) sim.people.infect(inds=importation_inds, layer='importation', strain=self.index) return diff --git a/covasim/sim.py b/covasim/sim.py index e3e5e6b4d..c718e8f4a 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -598,8 +598,7 @@ def step(self): viral_load = cvu.compute_viral_load(t, date_inf, date_rec, date_dead, frac_time, load_ratio, high_cap) # Shorten useful parameters - ns = self.people.n_strains - # ns = self['n_strains'] # Shorten number of strains + ns = self['n_strains'] # Shorten number of strains sus = people.susceptible symp = people.symptomatic diag = people.diagnosed From 36b644a191c05edb09b8448c49bff5671c48c707 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 14 Apr 2021 15:48:04 -0400 Subject: [PATCH 503/569] only counting nabs for people alive --- covasim/sim.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/covasim/sim.py b/covasim/sim.py index c718e8f4a..595911926 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -659,7 +659,8 @@ def step(self): self.results['strain'][key][strain][t] += count[strain] # Update nab and immunity for this time step - self.results['pop_nabs'][t] = np.sum(people.nab[cvu.defined(people.nab)])/self['pop_size'] + inds_alive = cvu.false(people.dead) + self.results['pop_nabs'][t] = np.sum(people.nab[inds_alive[cvu.defined(people.nab[inds_alive])]])/len(inds_alive) self.results['pop_protection'][t] = np.nanmean(people.sus_imm) self.results['pop_symp_protection'][t] = np.nanmean(people.symp_imm) From 46dd836518ef2ddbb7beec271c415675519cb022 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 14 Apr 2021 19:29:49 -0400 Subject: [PATCH 504/569] sim.n in vaccinate not working --- covasim/interventions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index c99610290..a8351f987 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1328,7 +1328,7 @@ def apply(self, sim): if sim.t >= np.min(self.days): # Determine who gets first dose of vaccine today - vacc_probs = np.zeros(sim.n) + vacc_probs = np.zeros(sim.pars['pop_size']) if self.subtarget is not None: subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted From 4b7ab97d5f0bf13e8bf46eba09e162c1320594e6 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 14 Apr 2021 19:42:25 -0400 Subject: [PATCH 505/569] another vaccine bug fix --- covasim/interventions.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index a8351f987..21a073200 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1351,17 +1351,17 @@ def apply(self, sim): if vacc_inds_dose2 is not None: vacc_inds = np.concatenate((vacc_inds, vacc_inds_dose2), axis=None) sim.people.flows['new_vaccinations'] += len(vacc_inds_dose2) - - # Update vaccine attributes in sim - sim.people.vaccinated[vacc_inds] = True - sim.people.vaccine_source[vacc_inds] = self.index - self.vaccinations[vacc_inds] += 1 - self.vaccination_dates[vacc_inds] = sim.t - - # Update vaccine attributes in sim - sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] - sim.people.date_vaccinated[vacc_inds] = self.vaccination_dates[vacc_inds] - cvi.init_nab(sim.people, vacc_inds, prior_inf=False) + if len(vacc_inds): + # Update vaccine attributes in sim + sim.people.vaccinated[vacc_inds] = True + sim.people.vaccine_source[vacc_inds] = self.index + self.vaccinations[vacc_inds] += 1 + self.vaccination_dates[vacc_inds] = sim.t + + # Update vaccine attributes in sim + sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] + sim.people.date_vaccinated[vacc_inds] = self.vaccination_dates[vacc_inds] + cvi.init_nab(sim.people, vacc_inds, prior_inf=False) return From eade5338f740e91e7818d2d1377dc401ff1cca5d Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 14 Apr 2021 19:55:30 -0400 Subject: [PATCH 506/569] another vaccine bug fix --- covasim/interventions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 21a073200..736fa3abe 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1314,8 +1314,8 @@ def initialize(self, sim): self.days = process_days(sim, self.days) # days that group becomes eligible self.second_dose_days = [None]*(sim['n_days']+1) # inds who get second dose (if relevant) self.vaccinated = [None]*(sim['n_days']+1) # keep track of inds of people vaccinated on each day - self.vaccinations = np.zeros(sim.n, dtype=cvd.default_int) # Number of doses given per person - self.vaccination_dates = np.full(sim.n, np.nan) # Store the dates when people are vaccinated + self.vaccinations = np.zeros(sim.pars['pop_size'], dtype=cvd.default_int) # Number of doses given per person + self.vaccination_dates = np.full(sim.pars['pop_size'], np.nan) # Store the dates when people are vaccinated sim['vaccine_pars'][self.label] = self.p # Store the parameters self.index = list(sim['vaccine_pars'].keys()).index(self.label) # Find where we are in the list sim['vaccine_map'][self.index] = self.label # Use that to populate the reverse mapping From 9c5431583cdf287d6b27081cba6933457ac68d13 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Wed, 14 Apr 2021 20:09:23 -0400 Subject: [PATCH 507/569] another vaccine bug fix --- covasim/interventions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 736fa3abe..1a55754bd 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1331,7 +1331,8 @@ def apply(self, sim): vacc_probs = np.zeros(sim.pars['pop_size']) if self.subtarget is not None: subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) - vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted + if len(subtarget_vals): + vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted else: for ind in find_day(self.days, sim.t, interv=self, sim=sim): unvacc_inds = sc.findinds(~sim.people.vaccinated) From fd874df915cd6d2a089242fe3c5cbe038d55f56e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 14 Apr 2021 23:49:03 -0700 Subject: [PATCH 508/569] update init --- covasim/base.py | 6 +++--- covasim/people.py | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 7196fc088..a5fd01dc3 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -858,7 +858,7 @@ def __setitem__(self, key, value): def __len__(self): ''' This is just a scalar, but validate() and _resize_arrays() make sure it's right ''' - return self.pop_size + return self.pars['pop_size'] def __iter__(self): @@ -883,7 +883,7 @@ def __add__(self, people2): raise NotImplementedError(errormsg) # Validate - newpeople.pop_size += people2.pop_size + newpeople.pars['pop_size'] += people2.pars['pop_size'] newpeople.validate() # Reassign UIDs so they're unique @@ -1060,7 +1060,7 @@ def _resize_arrays(self, pop_size=None, keys=None): ''' Resize arrays if any mismatches are found ''' if pop_size is None: pop_size = len(self) - self.pop_size = pop_size + self.pars['pop_size'] = pop_size if keys is None: keys = self.keys() keys = sc.promotetolist(keys) diff --git a/covasim/people.py b/covasim/people.py index f0ee00aee..09c8c6c32 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -46,10 +46,10 @@ def __init__(self, pars, strict=True, **kwargs): # Handle pars and population size if sc.isnumber(pars): # Interpret as a population size pars = {'pop_size':pars} # Ensure it's a dictionary - self.pars = pars # Equivalent to self.set_pars(pars) - self.pop_size = int(pars['pop_size']) - self.location = pars.get('location') # Try to get location, but set to None otherwise - self.version = cvv.__version__ # Store version info + self.pars = pars # Equivalent to self.set_pars(pars) + self.pars['pop_size'] = int(pars['pop_size']) + self.pars.setdefault('location', None) + self.version = cvv.__version__ # Store version info # Other initialization self.t = 0 # Keep current simulation time @@ -62,32 +62,32 @@ def __init__(self, pars, strict=True, **kwargs): # Set person properties -- all floats except for UID for key in self.meta.person: if key == 'uid': - self[key] = np.arange(self.pop_size, dtype=cvd.default_int) + self[key] = np.arange(self.pars['pop_size'], dtype=cvd.default_int) else: - self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) + self[key] = np.full(self.pars['pop_size'], np.nan, dtype=cvd.default_float) # Set health states -- only susceptible is true by default -- booleans except exposed by strain which should return the strain that ind is exposed to for key in self.meta.states: val = (key in ['susceptible', 'naive']) # Default value is True for susceptible and naive, false otherwise - self[key] = np.full(self.pop_size, val, dtype=bool) + self[key] = np.full(self.pars['pop_size'], val, dtype=bool) # Set strain states, which store info about which strain a person is exposed to for key in self.meta.strain_states: - self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) + self[key] = np.full(self.pars['pop_size'], np.nan, dtype=cvd.default_float) for key in self.meta.by_strain_states: - self[key] = np.full((self['n_strains'], self.pop_size), False, dtype=bool) + self[key] = np.full((self.pars['n_strains'], self.pars['pop_size']), False, dtype=bool) # Set immunity and antibody states for key in self.meta.imm_states: # Everyone starts out with no immunity - self[key] = np.zeros((self['n_strains'], self.pop_size), dtype=cvd.default_float) + self[key] = np.zeros((self.pars['n_strains'], self.pars['pop_size']), dtype=cvd.default_float) for key in self.meta.nab_states: # Everyone starts out with no antibodies - self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) + self[key] = np.full(self.pars['pop_size'], np.nan, dtype=cvd.default_float) for key in self.meta.vacc_states: - self[key] = np.zeros(self.pop_size, dtype=cvd.default_int) + self[key] = np.zeros(self.pars['pop_size'], dtype=cvd.default_int) # Set dates and durations -- both floats for key in self.meta.dates + self.meta.durs: - self[key] = np.full(self.pop_size, np.nan, dtype=cvd.default_float) + self[key] = np.full(self.pars['pop_size'], np.nan, dtype=cvd.default_float) # Store the dtypes used in a flat dict self._dtypes = {key:self[key].dtype for key in self.keys()} # Assign all to float by default @@ -222,7 +222,7 @@ def check_infectious(self): inds = self.check_inds(self.infectious, self.date_infectious, filter_inds=self.is_exp) self.infectious[inds] = True self.infectious_strain[inds] = self.exposed_strain[inds] - for strain in range(self['n_strains']): + for strain in range(self.pars['n_strains']): this_strain_inds = cvu.itrue(self.infectious_strain[inds] == strain, inds) n_this_strain_inds = len(this_strain_inds) self.flows_strain['new_infectious_by_strain'][strain] += n_this_strain_inds From 6b60c2e710c5ddad687ba9cf97f9ef81e34f69f9 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 14 Apr 2021 23:54:51 -0700 Subject: [PATCH 509/569] tests not passing yet --- covasim/people.py | 1 + 1 file changed, 1 insertion(+) diff --git a/covasim/people.py b/covasim/people.py index 09c8c6c32..b19be694e 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -48,6 +48,7 @@ def __init__(self, pars, strict=True, **kwargs): pars = {'pop_size':pars} # Ensure it's a dictionary self.pars = pars # Equivalent to self.set_pars(pars) self.pars['pop_size'] = int(pars['pop_size']) + self.pars.setdefault('n_strains', 1) self.pars.setdefault('location', None) self.version = cvv.__version__ # Store version info From dc6ce549220e85a17d9a9ac259d88a8e1943b481 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 15 Apr 2021 00:05:41 -0700 Subject: [PATCH 510/569] fix rescaling --- covasim/immunity.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 13d5899fc..d15d23f5d 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -38,6 +38,7 @@ class strain(sc.prettyobj): def __init__(self, strain, days, label=None, n_imports=1, rescale=True): self.days = days # Handle inputs self.n_imports = int(n_imports) + self.rescale = rescale self.index = None # Index of the strain in the sim; set later self.label = None # Strain label (used as a dict key) self.p = None # This is where the parameters will be stored @@ -117,8 +118,8 @@ def apply(self, sim): ''' Introduce new infections with this strain ''' for ind in cvi.find_day(self.days, sim.t, interv=self, sim=sim): # Time to introduce strain susceptible_inds = cvu.true(sim.people.susceptible) - # n_imports = sc.randround(self.n_imports/sim.rescale_vec[sim.t]) # Round stochastically to the nearest number of imports - n_imports = sc.randround(self.n_imports) + rescale_factor = sim.rescale_vec[sim.t] if self.rescale else 1.0 + n_imports = sc.randround(self.n_imports/rescale_factor) # Round stochastically to the nearest number of imports importation_inds = np.random.choice(susceptible_inds, n_imports) sim.people.infect(inds=importation_inds, layer='importation', strain=self.index) return From fe9d9bdadb80a202b794ddc738f59f1279f8e432 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 16 Apr 2021 11:14:37 -0400 Subject: [PATCH 511/569] immune updates --- covasim/parameters.py | 12 ++++++------ covasim/sim.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 0bbc5e22b..24dfa31af 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -70,7 +70,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['nab_decay'] = dict(form='nab_decay', decay_rate1=np.log(2)/90, decay_time1=250, decay_rate2=0.001) # Parameters describing the kinetics of decay of nabs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 pars['nab_kin'] = None # Constructed during sim initialization using the nab_decay parameters pars['nab_boost'] = 1.5 # Multiplicative factor applied to a person's nab levels if they get reinfected. # TODO: add source - pars['nab_eff'] = dict(sus=dict(slope=2.7, n_50=0.03), symp=0.1, sev=0.52) # Parameters to map nabs to efficacy + pars['nab_eff'] = dict(sus=dict(slope=1.6, n_50=0.05), symp=0.1, sev=0.52) # Parameters to map nabs to efficacy pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains pars['rel_imm'] = dict(asymp=0.85, mild=1, severe=1.5) # Relative immunity from natural infection varies by symptoms pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in immunity.py @@ -489,7 +489,7 @@ def get_vaccine_dose_pars(default=False): pars = dict( default = dict( - nab_eff = dict(sus=dict(slope=2.5, n_50=0.55)), + nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, doses = 1, @@ -497,7 +497,7 @@ def get_vaccine_dose_pars(default=False): ), pfizer = dict( - nab_eff = dict(sus=dict(slope=2.5, n_50=0.55)), + nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, doses = 2, @@ -505,7 +505,7 @@ def get_vaccine_dose_pars(default=False): ), moderna = dict( - nab_eff = dict(sus=dict(slope=2.5, n_50=0.55)), + nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, doses = 2, @@ -513,7 +513,7 @@ def get_vaccine_dose_pars(default=False): ), az = dict( - nab_eff = dict(sus=dict(slope=2.5, n_50=0.55)), + nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, doses = 2, @@ -521,7 +521,7 @@ def get_vaccine_dose_pars(default=False): ), jj = dict( - nab_eff = dict(sus=dict(slope=2.5, n_50=0.55)), + nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), nab_init = dict(dist='normal', par1=0.5, par2= 2), nab_boost = 2, doses = 1, diff --git a/covasim/sim.py b/covasim/sim.py index 595911926..4ebab2819 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -378,7 +378,7 @@ def load_population(self, popfile=None, **kwargs): elif isinstance(obj, cvb.BasePeople): self.people = obj self.people.set_pars(self.pars) # Replace the saved parameters with this simulation's - n_actual = len(self.people) + n_actual = self['pop_size'] layer_keys = self.people.layer_keys() else: # pragma: no cover errormsg = f'Cound not interpret input of {type(obj)} as a population file: must be a dict or People object' From b04c72a52dbed8d3a9010219269bda56b4017cd4 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 16 Apr 2021 12:22:33 -0400 Subject: [PATCH 512/569] immune updates --- covasim/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/covasim/base.py b/covasim/base.py index a5fd01dc3..3599b4194 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1060,7 +1060,8 @@ def _resize_arrays(self, pop_size=None, keys=None): ''' Resize arrays if any mismatches are found ''' if pop_size is None: pop_size = len(self) - self.pars['pop_size'] = pop_size + if not isinstance(pop_size, tuple): + self.pars['pop_size'] = pop_size if keys is None: keys = self.keys() keys = sc.promotetolist(keys) From f92c25c453e039dd8f45454caaa220d8dad3a393 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 16 Apr 2021 13:10:48 -0400 Subject: [PATCH 513/569] vaccine pars update --- covasim/parameters.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 24dfa31af..7ab1802fd 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -490,7 +490,7 @@ def get_vaccine_dose_pars(default=False): default = dict( nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), - nab_init = dict(dist='normal', par1=0.5, par2= 2), + nab_init = dict(dist='normal', par1=2, par2= 2), nab_boost = 2, doses = 1, interval = None, @@ -498,7 +498,7 @@ def get_vaccine_dose_pars(default=False): pfizer = dict( nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), - nab_init = dict(dist='normal', par1=0.5, par2= 2), + nab_init = dict(dist='normal', par1=2, par2= 2), nab_boost = 2, doses = 2, interval = 21, @@ -506,7 +506,7 @@ def get_vaccine_dose_pars(default=False): moderna = dict( nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), - nab_init = dict(dist='normal', par1=0.5, par2= 2), + nab_init = dict(dist='normal', par1=2, par2= 2), nab_boost = 2, doses = 2, interval = 28, @@ -514,7 +514,7 @@ def get_vaccine_dose_pars(default=False): az = dict( nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), - nab_init = dict(dist='normal', par1=0.5, par2= 2), + nab_init = dict(dist='normal', par1=-0.85, par2= 2), nab_boost = 2, doses = 2, interval = 21, @@ -522,7 +522,7 @@ def get_vaccine_dose_pars(default=False): jj = dict( nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), - nab_init = dict(dist='normal', par1=0.5, par2= 2), + nab_init = dict(dist='normal', par1=-1.1, par2= 2), nab_boost = 2, doses = 1, interval = None, From 87a84863f8362defbbae3b9f6f56f8cf8a64f1d1 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 16 Apr 2021 20:03:57 +0200 Subject: [PATCH 514/569] starting --- covasim/parameters.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 0bbc5e22b..99559df72 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -351,16 +351,16 @@ def get_strain_pars(default=False): pars = dict( wild = dict( - rel_imm = 1.0, - rel_beta = 1.0, - rel_symp_prob = 1.0, - rel_severe_prob = 1.0, - rel_crit_prob = 1.0, - rel_death_prob = 1.0, + rel_imm = 1.0, # Default values + rel_beta = 1.0, # Default values + rel_symp_prob = 1.0, # Default values + rel_severe_prob = 1.0, # Default values + rel_crit_prob = 1.0, # Default values + rel_death_prob = 1.0, # Default values ), b117 = dict( - rel_imm = 1.0, + rel_imm = 1.0, # Scaling factor applied to rel_beta = 1.5, # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 rel_symp_prob = 1.0, rel_severe_prob = 1.8, # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf From db9407c91e26abe6b8ccae5518cf5af726913390 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 16 Apr 2021 21:16:53 +0200 Subject: [PATCH 515/569] got it working ok now --- covasim/defaults.py | 2 +- covasim/immunity.py | 24 ++++++++++-------- covasim/parameters.py | 58 +++++++++++++++++++++---------------------- covasim/people.py | 6 ++--- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 20e86c40d..096732e95 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -190,7 +190,7 @@ def __init__(self): # Parameters that can vary by strain strain_pars = [ - 'rel_imm', + 'rel_imm_strain', 'rel_beta', 'rel_symp_prob', 'rel_severe_prob', diff --git a/covasim/immunity.py b/covasim/immunity.py index d15d23f5d..7ee05ab0b 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -253,29 +253,33 @@ def init_immunity(sim, create=False): # Pull out all of the circulating strains for cross-immunity ns = sim['n_strains'] immunity = {} - rel_imms = {} - strain_labels = sim['strain_map'].values() - for label in strain_labels: - rel_imms[label] = sim['strain_pars'][label]['rel_imm'] + # rel_imms = {} + # strain_labels = sim['strain_map'].values() + # for label in strain_labels: + # rel_imms[label] = sim['strain_pars'][label]['rel_imm'] # If immunity values have been provided, process them if sim['immunity'] is None or create: - # Initialize immunity + + # Firstly, initialize immunity matrix with defaults. These are then overwitten with strain-specific values below for ax in cvd.immunity_axes: if ax == 'sus': # Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] - immunity[ax] = np.full((ns, ns), sim['cross_immunity'], dtype=cvd.default_float) # Default for off-diagnonals - np.fill_diagonal(immunity[ax], 1.0) # Default for own-immunity + immunity[ax] = np.ones((ns, ns), dtype=cvd.default_float) # Fill with defaults else: # Progression and transmission are matrices of scalars of size sim['n_strains'] - immunity[ax] = np.ones(ns, dtype=cvd.default_float) + immunity[ax] = np.ones(ns, dtype=cvd.default_float) # Fill with defaults + # Next, overwrite these defaults with any known immunity values about specific strains default_cross_immunity = cvpar.get_cross_immunity() for i in range(ns): + label_i = sim['strain_map'][i] for j in range(ns): - if i != j: - label_i = sim['strain_map'][i] + if i != j: # Populate cross-immunity label_j = sim['strain_map'][j] if label_i in default_cross_immunity and label_j in default_cross_immunity: immunity['sus'][j][i] = default_cross_immunity[label_j][label_i] + else: # Populate own-immunity + immunity['sus'][i, i] = sim['strain_pars'][label_i]['rel_imm_strain'] + sim['immunity'] = immunity # Next, precompute the NAb kinetics and store these for access during the sim diff --git a/covasim/parameters.py b/covasim/parameters.py index 4f82f1b20..7e032af54 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -59,6 +59,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['beta_dist'] = dict(dist='neg_binomial', par1=1.0, par2=0.45, step=0.01) # Distribution to draw individual level transmissibility; dispersion from https://www.researchsquare.com/article/rs-29548/v1 pars['viral_dist'] = dict(frac_time=0.3, load_ratio=2, high_cap=4) # The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 pars['beta'] = 0.016 # Beta per symptomatic contact; absolute value, calibrated + pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 # Parameters that control settings and defaults for multi-strain runs pars['n_imports'] = 0 # Average daily number of imported cases (actual number is drawn from Poisson distribution) @@ -71,13 +72,12 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['nab_kin'] = None # Constructed during sim initialization using the nab_decay parameters pars['nab_boost'] = 1.5 # Multiplicative factor applied to a person's nab levels if they get reinfected. # TODO: add source pars['nab_eff'] = dict(sus=dict(slope=1.6, n_50=0.05), symp=0.1, sev=0.52) # Parameters to map nabs to efficacy - pars['cross_immunity'] = 0.5 # Default cross-immunity protection factor that applies across different strains - pars['rel_imm'] = dict(asymp=0.85, mild=1, severe=1.5) # Relative immunity from natural infection varies by symptoms + pars['rel_imm_symp'] = dict(asymp=0.85, mild=1, severe=1.5) # Relative immunity from natural infection varies by symptoms pars['immunity'] = None # Matrix of immunity and cross-immunity factors, set by init_immunity() in immunity.py # Strain-specific disease transmission parameters. By default, these are set up for a single strain, but can all be modified for multiple strains - pars['rel_beta'] = 1.0 - pars['asymp_factor'] = 1.0 # Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 + pars['rel_beta'] = 1.0 # Relative transmissibility varies by strain + pars['rel_imm_strain'] = 1.0 # Relative own-immmunity varies by strain # Duration parameters: time for disease progression pars['dur'] = {} @@ -351,7 +351,7 @@ def get_strain_pars(default=False): pars = dict( wild = dict( - rel_imm = 1.0, # Default values + rel_imm_strain = 1.0, # Default values rel_beta = 1.0, # Default values rel_symp_prob = 1.0, # Default values rel_severe_prob = 1.0, # Default values @@ -360,16 +360,16 @@ def get_strain_pars(default=False): ), b117 = dict( - rel_imm = 1.0, # Scaling factor applied to + rel_imm_strain = 1.0, # Immunity protection obtained from a natural infection with wild type, relative to wild type. No evidence yet for a difference with B117 rel_beta = 1.5, # Transmissibility estimates range from 40-80%, see https://cmmid.github.io/topics/covid19/uk-novel-variant.html, https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.26.1.2002106 - rel_symp_prob = 1.0, + rel_symp_prob = 1.0, # Inconclusive evidence on the likelihood of symptom development. See https://www.thelancet.com/journals/lanpub/article/PIIS2468-2667(21)00055-4/fulltext rel_severe_prob = 1.8, # From https://www.ssi.dk/aktuelt/nyheder/2021/b117-kan-fore-til-flere-indlaggelser and https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/961042/S1095_NERVTAG_update_note_on_B.1.1.7_severity_20210211.pdf - rel_crit_prob = 1.0, - rel_death_prob = 1.0, + rel_crit_prob = 1.0, # Various studies have found increased mortality for B117 (summary here: https://www.thelancet.com/journals/laninf/article/PIIS1473-3099(21)00201-2/fulltext#tbl1), but not necessarily when conditioned on having developed severe disease + rel_death_prob = 1.0, # See comment above. ), b1351 = dict( - rel_imm = 0.25, + rel_imm_strain = 0.25, # Immunity protection obtained from a natural infection with wild type, relative to wild type. TODO: add source rel_beta = 1.4, rel_symp_prob = 1.0, rel_severe_prob = 1.4, @@ -378,8 +378,8 @@ def get_strain_pars(default=False): ), p1 = dict( - rel_imm = 0.5, - rel_beta = 1.4, + rel_imm_strain = 0.5, # + rel_beta = 1.4, # Estimated to be 1.7–2.4-fold more transmissible than wild-type: https://science.sciencemag.org/content/early/2021/04/13/science.abh2644 rel_symp_prob = 1.0, rel_severe_prob = 1.4, rel_crit_prob = 1.0, @@ -395,36 +395,36 @@ def get_strain_pars(default=False): def get_cross_immunity(default=False): ''' - Get the cross immunity between each strain and each other strain + Get the cross immunity between each strain in a sim ''' pars = dict( wild = dict( - wild = 1.0, - b117 = 0.5, - b1351 = 0.5, - p1 = 0.5, + wild = 1.0, # Default for own-immunity + b117 = 0.5, # Assumption + b1351 = 0.5, # Assumption + p1 = 0.5, # Assumption ), b117 = dict( - wild = 0.5, - b117 = 1.0, - b1351 = 0.8, - p1 = 0.8, + wild = 0.5, # Assumption + b117 = 1.0, # Default for own-immunity + b1351 = 0.8, # Assumption + p1 = 0.8, # Assumption ), b1351 = dict( - wild = 0.066, - b117 = 0.1, - b1351 = 1.0, - p1 = 0.1, + wild = 0.066, # https://www.nature.com/articles/s41586-021-03471-w + b117 = 0.1, # Assumption + b1351 = 1.0, # Default for own-immunity + p1 = 0.1, # Assumption ), p1 = dict( - wild = 0.17, - b117 = 0.2, - b1351 = 0.2, - p1 = 1.0, + wild = 0.34, # Previous (non-P.1) infection provides 54–79% of the protection against infection with P.1 that it provides against non-P.1 lineages: https://science.sciencemag.org/content/early/2021/04/13/science.abh2644 + b117 = 0.4, # Assumption based on the above + b1351 = 0.4, # Assumption based on the above + p1 = 1.0, # Default for own-immunity ), ) diff --git a/covasim/people.py b/covasim/people.py index b19be694e..c2ccfee29 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -288,9 +288,9 @@ def check_recovery(self, inds=None, filter_inds='is_exp'): # Reset additional states self.susceptible[inds] = True - self.prior_symptoms[inds] = self.pars['rel_imm']['asymp'] - self.prior_symptoms[mild_inds] = self.pars['rel_imm']['mild'] - self.prior_symptoms[severe_inds] = self.pars['rel_imm']['severe'] + self.prior_symptoms[inds] = self.pars['rel_imm_symp']['asymp'] + self.prior_symptoms[mild_inds] = self.pars['rel_imm_symp']['mild'] + self.prior_symptoms[severe_inds] = self.pars['rel_imm_symp']['severe'] if len(inds): cvi.init_nab(self, inds, prior_inf=True) return len(inds) From b65bec1c4988f15f0dbe87f226fd4014421c56fe Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Fri, 16 Apr 2021 21:20:46 +0200 Subject: [PATCH 516/569] clean up --- covasim/immunity.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index 7ee05ab0b..b797822ee 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -253,10 +253,6 @@ def init_immunity(sim, create=False): # Pull out all of the circulating strains for cross-immunity ns = sim['n_strains'] immunity = {} - # rel_imms = {} - # strain_labels = sim['strain_map'].values() - # for label in strain_labels: - # rel_imms[label] = sim['strain_pars'][label]['rel_imm'] # If immunity values have been provided, process them if sim['immunity'] is None or create: From 7cccc0e1cad67fb492fc081ab7cd54091f8f0820 Mon Sep 17 00:00:00 2001 From: Katherine Rosenfeld Date: Fri, 16 Apr 2021 13:41:54 -0700 Subject: [PATCH 517/569] do not vaccinate dead people --- covasim/interventions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/covasim/interventions.py b/covasim/interventions.py index 1a55754bd..979ad9338 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1337,6 +1337,7 @@ def apply(self, sim): for ind in find_day(self.days, sim.t, interv=self, sim=sim): unvacc_inds = sc.findinds(~sim.people.vaccinated) vacc_probs[unvacc_inds] = self.prob # Assign equal vaccination probability to everyone + vacc_probs[cvu.true(sim.people.dead)] *= 0.0 # do not vaccinate dead people vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated if len(vacc_inds): From 22bf5527f4c693ca0aa0d27c055a64120b7f6c8f Mon Sep 17 00:00:00 2001 From: Katherine Rosenfeld Date: Fri, 16 Apr 2021 14:25:51 -0700 Subject: [PATCH 518/569] example for estimating vaccine efficiency --- tests/devtests/dev_test_vaccine_efficiency.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/devtests/dev_test_vaccine_efficiency.py diff --git a/tests/devtests/dev_test_vaccine_efficiency.py b/tests/devtests/dev_test_vaccine_efficiency.py new file mode 100644 index 000000000..cb3713550 --- /dev/null +++ b/tests/devtests/dev_test_vaccine_efficiency.py @@ -0,0 +1,71 @@ +''' +Calculate vaccine efficiency for protection against symptomatic covid after first dose +''' + +import covasim as cv +import numpy as np +cv.check_version('>=3.0.0') + +# construct analyzer to select placebo arm +class placebo_arm(cv.Analyzer): + def __init__(self, day, trial_size, **kwargs): + super().__init__(**kwargs) + self.day = day + self.trial_size = trial_size + return + + def initialize(self, sim=None): + self.placebo_inds = [] + self.initialized = True + return + + def apply(self, sim): + if sim.t == self.day: + eligible = cv.true(~np.isfinite(sim.people.date_exposed) & ~sim.people.vaccinated) + self.placebo_inds = eligible[cv.choose(len(eligible), min(self.trial_size, len(eligible)))] + return + +pars = { + 'pop_size': 20000, + 'beta': 0.015, + 'n_days': 120, +} + +# Define vaccine arm +trial_size = 500 +start_trial = 20 +def subtarget(sim): + # select people who are susceptible + if sim.t == start_trial: + eligible = cv.true(~np.isfinite(sim.people.date_exposed)) + inds = eligible[cv.choose(len(eligible), min(trial_size//2, len(eligible)))] + else: + inds = [] + return {'vals': [1.0 for ind in inds], 'inds': inds} + +pfizer = cv.vaccinate(vaccine='pfizer', days=[start_trial], prob=0.0, subtarget=subtarget) + +sim = cv.Sim( + use_waning=True, + pars=pars, + interventions=pfizer, + analyzers=placebo_arm(day=start_trial, trial_size=trial_size//2) +) +sim.run() + +# Find trial arm indices, those who were vaccinated +vacc_inds = cv.true(sim.people.vaccinated) +placebo_inds = sim['analyzers'][0].placebo_inds +# Check that there is no overlap +assert (len(set(vacc_inds).intersection(set(placebo_inds))) == 0) +# Calculate vaccine efficiency +VE = 1 - (np.isfinite(sim.people.date_symptomatic[vacc_inds]).sum() / + np.isfinite(sim.people.date_symptomatic[placebo_inds]).sum()) +print('Vaccine efficiency for symptomatic covid:', VE) + +# Plot +to_plot = cv.get_default_plots('default', 'sim') +to_plot['Health outcomes'] += ['cum_vaccinated'] +sim.plot(to_plot=to_plot) + +print('Done') \ No newline at end of file From 61ea4aaff762555983b23af4cd5d25f38fe78dfa Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 16 Apr 2021 16:43:39 -0700 Subject: [PATCH 519/569] update vietnam --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 002dcd3ee..87e82ac4e 100644 --- a/README.rst +++ b/README.rst @@ -36,13 +36,13 @@ Covasim has been used for analyses in over a dozen countries, both to inform pol 2. **Determining the optimal strategy for reopening schools, the impact of test and trace interventions, and the risk of occurrence of a second COVID-19 epidemic wave in the UK: a modelling study**. Panovska-Griffiths J, Kerr CC, Stuart RM, Mistry D, Klein DJ, Viner R, Bonnell C (2020-08-03). *Lancet Child and Adolescent Health* S2352-4642(20) 30250-9. doi: https://doi.org/10.1016/S2352-4642(20)30250-9. -3. **Modelling the impact of reducing control measures on the COVID-19 pandemic in a low transmission setting**. Scott N, Palmer A, Delport D, Abeysuriya RG, Stuart RM, Kerr CC, Mistry D, Klein DJ, Sacks-Davis R, Heath K, Hainsworth S, Pedrana A, Stoove M, Wilson DP, Hellard M (in press; accepted 2020-09-02). *Medical Journal of Australia* [`Preprint `__]; doi: https://doi.org/10.1101/2020.06.11.20127027. +3. **Estimating and mitigating the risk of COVID-19 epidemic rebound associated with reopening of international borders in Vietnam: a modelling study**. Pham QD, Stuart RM, Nguyen TV, Luong QC, Tran DQ, Phan LT, Dang TQ, Tran DN, Mistry D, Klein DJ, Abeysuriya RG, Oron AP, Kerr CC (2021-04-12). *Lancet Global Health* S2214-109X(21) 00103-0; doi: https://doi.org/10.1016/S2214-109X(21)00103-0. -4. **Lessons learned from Vietnam's COVID-19 response: the role of adaptive behaviour change and testing in epidemic control**. Pham QD, Stuart RM, Nguyen TV, Luong QC, Tran DQ, Phan LT, Dang TQ, Tran DN, Mistry D, Klein DJ, Abeysuriya RG, Oron AP, Kerr CC (in press; accepted 2021-02-25). *Lancet Global Health*; doi: https://doi.org/10.1101/2020.12.18.20248454. +4. **Modelling the impact of reducing control measures on the COVID-19 pandemic in a low transmission setting**. Scott N, Palmer A, Delport D, Abeysuriya RG, Stuart RM, Kerr CC, Mistry D, Klein DJ, Sacks-Davis R, Heath K, Hainsworth S, Pedrana A, Stoove M, Wilson DP, Hellard M (in press; accepted 2020-09-02). *Medical Journal of Australia* [`Preprint `__]; doi: https://doi.org/10.1101/2020.06.11.20127027. -5. **The role of masks, testing and contact tracing in preventing COVID-19 resurgences: a case study from New South Wales, Australia**. Stuart RM, Abeysuriya RG, Kerr CC, Mistry D, Klein DJ, Gray R, Hellard M, Scott N (in press; accepted 2021-03-19). *BMJ Open* doi: https://doi.org/10.1101/2020.09.02.20186742. +5. **The role of masks, testing and contact tracing in preventing COVID-19 resurgences: a case study from New South Wales, Australia**. Stuart RM, Abeysuriya RG, Kerr CC, Mistry D, Klein DJ, Gray R, Hellard M, Scott N (in press; accepted 2021-03-19). *BMJ Open*; doi: https://doi.org/10.1101/2020.09.02.20186742. -6. **The potential contribution of face coverings to the control of SARS-CoV-2 transmission in schools and broader society in the UK: a modelling study**. Panovska-Griffiths J, Kerr CC, Waites W, Stuart RM, Mistry D, Foster D, Klein DJ, Viner R, Bonnell C (in press; accepted 2021-04-08). *Nature Scientific Reports* doi: https://doi.org/10.1101/2020.09.28.20202937. +6. **The potential contribution of face coverings to the control of SARS-CoV-2 transmission in schools and broader society in the UK: a modelling study**. Panovska-Griffiths J, Kerr CC, Waites W, Stuart RM, Mistry D, Foster D, Klein DJ, Viner R, Bonnell C (in press; accepted 2021-04-08). *Nature Scientific Reports*; doi: https://doi.org/10.1101/2020.09.28.20202937. 7. **Schools are not islands: Balancing COVID-19 risk and educational benefits using structural and temporal countermeasures**. Cohen JA, Mistry D, Kerr CC, Klein DJ (under review; posted 2020-09-10). *medRxiv* 2020.09.08.20190942; doi: https://doi.org/10.1101/2020.09.08.20190942. From b3288e115145a3111c4b4486beb534e21c1410ba Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 16 Apr 2021 17:03:15 -0700 Subject: [PATCH 520/569] update changelog --- CHANGELOG.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0d8adc617..63815ef90 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,6 +34,22 @@ Highlights - **Multi-strain modeling**: Model functionality has been extended to allow for modeling of multiple different co-circulating strains with different properties. This means you can now do e.g. ``b117 = cv.strain('b117', days=1, n_imports=20)`` followed by ``sim = cv.Sim(strains=b117)`` to import strain B117. Further examples are contained in ``tests/test_immunity.py`` and in Tutorial 8. - **New methods for vaccine modeling**: A new ``cv.vaccinate()`` intervention has been added, which allows more flexible modeling of vaccinations. Vaccines, like natural infections, are assumed to boost agents' immunity. - **Consistency**: By default, results from Covasim 3.0.0 should exactly match Covasim 2.1.2. To use the new features, you will need to manually specify ``cv.Sim(use_waning=True)``. +- **Still TLDR?** Here's a quick showcase of the new features: + +.. code-block:: python + + import covasim as cv + + pars = dict( + use_waning = True, # Use the new immunity features + n_days = 180, # Set the days, as before + n_agents = 50e3, # New alias for pop_size + scaled_pop = 200e3, # New alternative to specifying pop_scale + strains = cv.strain('b117', days=20, n_imports=20), # Introduce B117 + interventions = cv.vaccinate('astrazeneca', days=80), # Create a vaccine + ) + + cv.Sim(pars).run().plot('strain') # Create, run, and plot strain results Immunity-related parameter changes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 80e801b01fa8d3bc23fdf026a22d377669bf49e6 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 16 Apr 2021 17:50:36 -0700 Subject: [PATCH 521/569] tests pass --- covasim/base.py | 36 +++++++++++++++++++++++------------- covasim/run.py | 8 +++++--- covasim/sim.py | 17 +++++++++++------ tests/test_other.py | 4 ++-- 4 files changed, 41 insertions(+), 24 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 3599b4194..d00ee217d 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -892,6 +892,12 @@ def __add__(self, people2): return newpeople + def __radd__(self, people2): + ''' Allows sum() to work correctly ''' + if not people2: return self + else: return self.__add__(people2) + + def _brief(self): ''' Return a one-line description of the people -- used internally and by repr(); @@ -1031,15 +1037,14 @@ def validate(self, die=True, verbose=False): expected_len = len(self) expected_strains = self.pars['n_strains'] for key in self.keys(): - actual_len = len(self[key]) - # check if it's 2d - if self[key].ndim > 1: - actual_len = np.shape(self[key])[1] - actual_strains = np.shape(self[key])[0] + if self[key].ndim == 1: + actual_len = len(self[key]) + else: # If it's 2D, strains need to be checked separately + actual_strains, actual_len = self[key].shape if actual_strains != expected_strains: if verbose: print(f'Resizing "{key}" from {actual_strains} to {expected_strains}') - self._resize_arrays(keys=key, pop_size=(expected_strains, expected_len)) + self._resize_arrays(keys=key, new_size=(expected_strains, expected_len)) if actual_len != expected_len: # pragma: no cover if die: errormsg = f'Length of key "{key}" did not match population size ({actual_len} vs. {expected_len})' @@ -1056,17 +1061,22 @@ def validate(self, die=True, verbose=False): return - def _resize_arrays(self, pop_size=None, keys=None): + def _resize_arrays(self, new_size=None, keys=None): ''' Resize arrays if any mismatches are found ''' - if pop_size is None: - pop_size = len(self) - if not isinstance(pop_size, tuple): - self.pars['pop_size'] = pop_size + + # Handle None or tuple input (representing strains and pop_size) + if new_size is None: + new_size = len(self) + pop_size = new_size if not isinstance(new_size, tuple) else new_size[1] + self.pars['pop_size'] = pop_size + + # Reset sizes if keys is None: keys = self.keys() keys = sc.promotetolist(keys) for key in keys: - self[key].resize(pop_size, refcheck=False) + self[key].resize(new_size, refcheck=False) # Don't worry about cross-references to the arrays + return @@ -1120,7 +1130,7 @@ def from_people(self, people, resize=True): # Handle population size pop_size = len(people) if resize: - self._resize_arrays(pop_size=pop_size) + self._resize_arrays(new_size=pop_size) # Iterate over people -- slow! for p,person in enumerate(people): diff --git a/covasim/run.py b/covasim/run.py index 2164efc5c..6a9f2d254 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -321,12 +321,14 @@ def combine(self, output=False): n_runs = len(self) combined_sim = sc.dcp(self.sims[0]) - combined_sim.parallelized = {'parallelized':True, 'combined':True, 'n_runs':n_runs} # Store how this was parallelized - combined_sim['pop_size'] *= n_runs # Record the number of people + combined_sim.parallelized = dict(parallelized=True, combined=True, n_runs=n_runs) # Store how this was parallelized for s,sim in enumerate(self.sims[1:]): # Skip the first one - if combined_sim.people: + if combined_sim.people: # If the people are there, add them and increment the population size accordingly combined_sim.people += sim.people + combined_sim['pop_size'] = combined_sim.people.pars['pop_size'] + else: # If not, manually update population size + combined_sim['pop_size'] += sim['pop_size'] # Record the number of people for key in sim.result_keys(): vals = sim.results[key].values if len(vals) != combined_sim.npts: diff --git a/covasim/sim.py b/covasim/sim.py index 4ebab2819..967de1e92 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -375,20 +375,25 @@ def load_population(self, popfile=None, **kwargs): self.popdict = obj n_actual = len(self.popdict['uid']) layer_keys = self.popdict['layer_keys'] + elif isinstance(obj, cvb.BasePeople): + n_actual = len(obj) self.people = obj self.people.set_pars(self.pars) # Replace the saved parameters with this simulation's - n_actual = self['pop_size'] layer_keys = self.people.layer_keys() + + # Perform validation + n_expected = self['pop_size'] + if n_actual != n_expected: # External consistency check + errormsg = f'Wrong number of people ({n_expected:n} requested, {n_actual:n} actual) -- please change "pop_size" to match or regenerate the file' + raise ValueError(errormsg) + self.people.validate() # Internal consistency check + else: # pragma: no cover errormsg = f'Cound not interpret input of {type(obj)} as a population file: must be a dict or People object' raise ValueError(errormsg) - # Perform validation - n_expected = self['pop_size'] - if n_actual != n_expected: - errormsg = f'Wrong number of people ({n_expected:n} requested, {n_actual:n} actual) -- please change "pop_size" to match or regenerate the file' - raise ValueError(errormsg) + self.reset_layer_pars(force=False, layer_keys=layer_keys) # Ensure that layer keys match the loaded population self.popfile = None # Once loaded, remove to save memory diff --git a/tests/test_other.py b/tests/test_other.py index 61f7a31e4..f24f54549 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -90,8 +90,8 @@ def test_basepeople(): ppl.date_keys() ppl.dur_keys() ppl.indices() - ppl._resize_arrays(pop_size=200) # This only resizes the arrays, not actually create new people - ppl._resize_arrays(pop_size=100) # Change back + ppl._resize_arrays(new_size=200) # This only resizes the arrays, not actually create new people + ppl._resize_arrays(new_size=100) # Change back ppl.to_df() ppl.to_arr() ppl.person(50) From b584576dc10de956f4e9cf141a4065f0d954ed2e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 16 Apr 2021 18:11:16 -0700 Subject: [PATCH 522/569] more updates --- covasim/parameters.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 7e032af54..5289fc7ce 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -369,7 +369,7 @@ def get_strain_pars(default=False): ), b1351 = dict( - rel_imm_strain = 0.25, # Immunity protection obtained from a natural infection with wild type, relative to wild type. TODO: add source + rel_imm_strain = 0.066, # Immunity protection obtained from a natural infection with wild type, relative to wild type. TODO: add source rel_beta = 1.4, rel_symp_prob = 1.0, rel_severe_prob = 1.4, @@ -378,7 +378,7 @@ def get_strain_pars(default=False): ), p1 = dict( - rel_imm_strain = 0.5, # + rel_imm_strain = 0.17, rel_beta = 1.4, # Estimated to be 1.7–2.4-fold more transmissible than wild-type: https://science.sciencemag.org/content/early/2021/04/13/science.abh2644 rel_symp_prob = 1.0, rel_severe_prob = 1.4, @@ -463,9 +463,9 @@ def get_vaccine_strain_pars(default=False): az = dict( wild = 1.0, - b117 = 1.0, - b1351 = 1/2, - p1 = 1/2, + b117 = 1/2.3, + b1351 = 1/9, + p1 = 1/2.9, ), jj = dict( @@ -490,7 +490,7 @@ def get_vaccine_dose_pars(default=False): default = dict( nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), - nab_init = dict(dist='normal', par1=2, par2= 2), + nab_init = dict(dist='normal', par1=2, par2=2), nab_boost = 2, doses = 1, interval = None, @@ -498,32 +498,32 @@ def get_vaccine_dose_pars(default=False): pfizer = dict( nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), - nab_init = dict(dist='normal', par1=2, par2= 2), - nab_boost = 2, + nab_init = dict(dist='normal', par1=2, par2=2), + nab_boost = 3, doses = 2, interval = 21, ), moderna = dict( nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), - nab_init = dict(dist='normal', par1=2, par2= 2), - nab_boost = 2, + nab_init = dict(dist='normal', par1=2, par2=2), + nab_boost = 3, doses = 2, interval = 28, ), az = dict( nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), - nab_init = dict(dist='normal', par1=-0.85, par2= 2), - nab_boost = 2, + nab_init = dict(dist='normal', par1=-0.85, par2=2), + nab_boost = 3, doses = 2, interval = 21, ), jj = dict( nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), - nab_init = dict(dist='normal', par1=-1.1, par2= 2), - nab_boost = 2, + nab_init = dict(dist='normal', par1=-1.1, par2=2), + nab_boost = 3, doses = 1, interval = None, ), From 6d2502ee0d358248e9baacca3676e869f9940caa Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 16 Apr 2021 18:21:43 -0700 Subject: [PATCH 523/569] update version --- covasim/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/version.py b/covasim/version.py index 055f08788..8ca4d74cd 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,6 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '3.0.0' -__versiondate__ = '2021-04-13' +__version__ = '3.0.1' +__versiondate__ = '2021-04-16' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From c23cbaa92f5d6166d14ca988205fe15827f9b943 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 16 Apr 2021 18:48:18 -0700 Subject: [PATCH 524/569] more updates --- covasim/interventions.py | 36 +++++++++++++++++++++++++++--------- covasim/parameters.py | 4 ++-- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 979ad9338..c69e4cb44 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1209,26 +1209,39 @@ def apply(self, sim): class vaccinate(Intervention): ''' - Apply a vaccine to a subset of the population. In addition to changing the - relative susceptibility and the probability of developing symptoms if still - infected, this intervention stores several types of data: + Apply a vaccine to a subset of the population. + + The main purpose of the intervention is to change the relative susceptibility + and the probability of developing symptoms if still infected. However, this intervention + also stores several types of data: + - ``vaccinated``: whether or not a person is vaccinated - ``vaccinations``: the number of vaccine doses per person - - ``vaccination_dates``: list of dates per person - - ``pars``: vaccine pars that are given to Vaccine() class + - ``vaccination_dates``: list of vaccination dates per person Args: - vaccine (dict/str): which vaccine to use + vaccine (dict/str): which vaccine to use; see below for dict parameters label (str): if vaccine is supplied as a dict, the name of the vaccine days (int/arr): the day or array of days to apply the interventions prob (float): probability of being vaccinated (i.e., fraction of the population) subtarget (dict): subtarget intervention to people with particular indices (see test_num() for details) kwargs (dict): passed to Intervention() - **Examples**:: + If ``vaccine`` is supplied as a dictionary, it must have the following parameters: - pfizer = cv.vaccinate(vaccine='pfizer', days=50, prob=0.3) - custom = cv.vaccinate(vaccine=) + - ``nab_eff``: the waning efficacy of neutralizing antibodies at preventing infection + - ``nab_init``: the initial antibody level (higher = more protection) + - ``nab_boost``: how much of a boost being vaccinated on top of a previous dose or natural infection provides + - ``doses``: the number of doses required to be fully vaccinated + - ``interval``: the interval between doses + - entries for efficacy against each of the strains (e.g. ``b117``) + + See ``parameters.py`` for additional examples of these parameters. + + **Example**:: + + pfizer = cv.vaccinate(vaccine='pfizer', days=30, prob=0.7) + cv.Sim(interventions=pfizer, use_waning=True).run().plot() ''' def __init__(self, vaccine, days, label=None, prob=1.0, subtarget=None, **kwargs): super().__init__(**kwargs) # Initialize the Intervention object @@ -1290,6 +1303,11 @@ def initialize(self, sim): ''' Fix the dates and store the vaccinations ''' super().initialize() + # Check that the simulation parameters are correct + if not sim['use_waning']: + errormsg = 'The cv.vaccinate() intervention requires use_waning=True. Please enable waning, or else use cv.simple_vaccine().' + raise RuntimeError(errormsg) + # Populate any missing keys -- must be here, after strains are initialized default_strain_pars = cvpar.get_vaccine_strain_pars(default=True) default_dose_pars = cvpar.get_vaccine_dose_pars(default=True) diff --git a/covasim/parameters.py b/covasim/parameters.py index 5289fc7ce..7c2200d53 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -416,7 +416,7 @@ def get_cross_immunity(default=False): b1351 = dict( wild = 0.066, # https://www.nature.com/articles/s41586-021-03471-w b117 = 0.1, # Assumption - b1351 = 1.0, # Default for own-immunity + b1351 = 1.0, # Default for own-immunity p1 = 0.1, # Assumption ), @@ -424,7 +424,7 @@ def get_cross_immunity(default=False): wild = 0.34, # Previous (non-P.1) infection provides 54–79% of the protection against infection with P.1 that it provides against non-P.1 lineages: https://science.sciencemag.org/content/early/2021/04/13/science.abh2644 b117 = 0.4, # Assumption based on the above b1351 = 0.4, # Assumption based on the above - p1 = 1.0, # Default for own-immunity + p1 = 1.0, # Default for own-immunity ), ) From 79dfb912ccf845e56757252473379eee55f5735a Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Fri, 16 Apr 2021 21:58:50 -0400 Subject: [PATCH 525/569] strain pars update --- covasim/parameters.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 7ab1802fd..430c86359 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -401,29 +401,29 @@ def get_cross_immunity(default=False): wild = dict( wild = 1.0, - b117 = 0.5, - b1351 = 0.5, - p1 = 0.5, + b117 = 1/1.1, + b1351 = 1/2.3, + p1 = 1/1.8, ), b117 = dict( - wild = 0.5, + wild = 1/1.9, b117 = 1.0, - b1351 = 0.8, - p1 = 0.8, + b1351 = 1.0, + p1 = 1.0, ), b1351 = dict( - wild = 0.066, - b117 = 0.1, + wild = 1/15.1, + b117 = 1.0, b1351 = 1.0, - p1 = 0.1, + p1 = 1.0, ), p1 = dict( - wild = 0.17, - b117 = 0.2, - b1351 = 0.2, + wild = 1/5.6, + b117 = 1.0, + b1351 = 1.0, p1 = 1.0, ), ) @@ -463,9 +463,9 @@ def get_vaccine_strain_pars(default=False): az = dict( wild = 1.0, - b117 = 1.0, - b1351 = 1/2, - p1 = 1/2, + b117 = 1/2.3, + b1351 = 1/9, + p1 = 1/2.9, ), jj = dict( From 53609faa688b01ab9aff3f6236b9236c9a545124 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 16 Apr 2021 18:59:53 -0700 Subject: [PATCH 526/569] update changelog --- CHANGELOG.rst | 12 ++++++++++++ covasim/interventions.py | 8 ++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 63815ef90..23a786e3f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,18 @@ These are the major improvements we are currently working on. If there is a spec Latest versions (3.0.x) ~~~~~~~~~~~~~~~~~~~~~~~ + +Version 3.0.1 (2021-04-16) +-------------------------- +- Immunity and vaccine parameters have been updated. +- The ``People`` class has been updated to remove parameters that were copied into attributes; thus there is no longer both ``people.pars['pop_size']`` and ``people.pop_size``; only the former. Recommended practice is to use ``len(people)`` to get the number of people. +- Loaded population files can now be used with more than one strain; arrays will be resized automatically. If there is a mismatch in the number of people, this will *not* be automatically resized. +- A bug was fixed with the ``rescale`` argument to ``cv.strain()`` not having any effect. +- Dead people are no longer eligible to be vaccinated. +- *Regression information*: Any user scripts that call ``sim.people.pop_size`` should be updated to call ``len(sim.people)`` (preferred), or ``sim.n``, ``sim['pop_size']``, or ``sim.people.pars['pop_size']``. +- *GitHub info*: PR `999 `__ + + Version 3.0.0 (2021-04-13) -------------------------- This version introduces fully featured vaccines, variants, and immunity. **Note:** These new features are still under development; please use with caution and email us at covasim@idmod.org if you have any questions or issues. We expect there to be several more releases over the next few weeks as we refine these new features. diff --git a/covasim/interventions.py b/covasim/interventions.py index c69e4cb44..dc3788a1c 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -997,7 +997,7 @@ def initialize(self, sim): if self.trace_time is None: self.trace_time = 0.0 if self.quar_period is None: - self.quar_period = sim.pars['quar_period'] + self.quar_period = sim['quar_period'] if sc.isnumber(self.trace_probs): val = self.trace_probs self.trace_probs = {k:val for k in sim.people.layer_keys()} @@ -1332,8 +1332,8 @@ def initialize(self, sim): self.days = process_days(sim, self.days) # days that group becomes eligible self.second_dose_days = [None]*(sim['n_days']+1) # inds who get second dose (if relevant) self.vaccinated = [None]*(sim['n_days']+1) # keep track of inds of people vaccinated on each day - self.vaccinations = np.zeros(sim.pars['pop_size'], dtype=cvd.default_int) # Number of doses given per person - self.vaccination_dates = np.full(sim.pars['pop_size'], np.nan) # Store the dates when people are vaccinated + self.vaccinations = np.zeros(sim['pop_size'], dtype=cvd.default_int) # Number of doses given per person + self.vaccination_dates = np.full(sim['pop_size'], np.nan) # Store the dates when people are vaccinated sim['vaccine_pars'][self.label] = self.p # Store the parameters self.index = list(sim['vaccine_pars'].keys()).index(self.label) # Find where we are in the list sim['vaccine_map'][self.index] = self.label # Use that to populate the reverse mapping @@ -1346,7 +1346,7 @@ def apply(self, sim): if sim.t >= np.min(self.days): # Determine who gets first dose of vaccine today - vacc_probs = np.zeros(sim.pars['pop_size']) + vacc_probs = np.zeros(sim['pop_size']) if self.subtarget is not None: subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) if len(subtarget_vals): From ad8583f4fb8a90467db8e353e4a128f5945b653a Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 19 Apr 2021 14:27:47 -0400 Subject: [PATCH 527/569] immunity matrix only for susceptibility --- covasim/immunity.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/covasim/immunity.py b/covasim/immunity.py index f0a1658fd..10eb29800 100644 --- a/covasim/immunity.py +++ b/covasim/immunity.py @@ -252,17 +252,13 @@ def init_immunity(sim, create=False): # Pull out all of the circulating strains for cross-immunity ns = sim['n_strains'] - immunity = {} # If immunity values have been provided, process them if sim['immunity'] is None or create: # Firstly, initialize immunity matrix with defaults. These are then overwitten with strain-specific values below - for ax in cvd.immunity_axes: - if ax == 'sus': # Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] - immunity[ax] = np.ones((ns, ns), dtype=cvd.default_float) # Fill with defaults - else: # Progression and transmission are matrices of scalars of size sim['n_strains'] - immunity[ax] = np.ones(ns, dtype=cvd.default_float) # Fill with defaults + # Susceptibility matrix is of size sim['n_strains']*sim['n_strains'] + immunity = np.ones((ns, ns), dtype=cvd.default_float) # Fill with defaults # Next, overwrite these defaults with any known immunity values about specific strains default_cross_immunity = cvpar.get_cross_immunity() @@ -272,9 +268,9 @@ def init_immunity(sim, create=False): if i != j: # Populate cross-immunity label_j = sim['strain_map'][j] if label_i in default_cross_immunity and label_j in default_cross_immunity: - immunity['sus'][j][i] = default_cross_immunity[label_j][label_i] + immunity[j][i] = default_cross_immunity[label_j][label_i] else: # Populate own-immunity - immunity['sus'][i, i] = sim['strain_pars'][label_i]['rel_imm_strain'] + immunity[i, i] = sim['strain_pars'][label_i]['rel_imm_strain'] sim['immunity'] = immunity @@ -330,7 +326,7 @@ def check_immunity(people, strain, sus=True, inds=None): if len(is_sus_was_inf_same): # Immunity for susceptibles with prior exposure to this strain current_nabs = people.nab[is_sus_was_inf_same] - people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_nabs * immunity['sus'][strain, strain], 'sus', nab_eff) + people.sus_imm[strain, is_sus_was_inf_same] = nab_to_efficacy(current_nabs * immunity[strain, strain], 'sus', nab_eff) if len(is_sus_was_inf_diff): # Cross-immunity for susceptibles with prior exposure to a different strain prior_strains = people.recovered_strain[is_sus_was_inf_diff] @@ -338,7 +334,7 @@ def check_immunity(people, strain, sus=True, inds=None): for unique_strain in prior_strains_unique: unique_inds = is_sus_was_inf_diff[cvu.true(prior_strains == unique_strain)] current_nabs = people.nab[unique_inds] - people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_nabs * immunity['sus'][strain, unique_strain], 'sus', nab_eff) + people.sus_imm[strain, unique_inds] = nab_to_efficacy(current_nabs * immunity[strain, unique_strain], 'sus', nab_eff) # PART 2: Immunity to disease for currently-infected people else: @@ -349,13 +345,13 @@ def check_immunity(people, strain, sus=True, inds=None): vaccine_source = cvd.default_int(people.vaccine_source[is_inf_vacc]) # TODO: use vaccine source vaccine_scale = vacc_mapping[strain] current_nabs = people.nab[is_inf_vacc] - people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_nabs * vaccine_scale * immunity['symp'][strain], 'symp', nab_eff) - people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_nabs * vaccine_scale * immunity['sev'][strain], 'sev', nab_eff) + people.symp_imm[strain, is_inf_vacc] = nab_to_efficacy(current_nabs * vaccine_scale, 'symp', nab_eff) + people.sev_imm[strain, is_inf_vacc] = nab_to_efficacy(current_nabs * vaccine_scale, 'sev', nab_eff) if len(was_inf): # Immunity for reinfected people current_nabs = people.nab[was_inf] - people.symp_imm[strain, was_inf] = nab_to_efficacy(current_nabs * immunity['symp'][strain], 'symp', nab_eff) - people.sev_imm[strain, was_inf] = nab_to_efficacy(current_nabs * immunity['sev'][strain], 'sev', nab_eff) + people.symp_imm[strain, was_inf] = nab_to_efficacy(current_nabs, 'symp', nab_eff) + people.sev_imm[strain, was_inf] = nab_to_efficacy(current_nabs, 'sev', nab_eff) return From 87f3fed38d699daf58ba8e5138d8937cb51e1d85 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 19 Apr 2021 15:20:01 -0400 Subject: [PATCH 528/569] attempt at setting vaccine nabs to be relative to the day the nabs kick in, not day vaccinated --- covasim/interventions.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index dc3788a1c..04cf31d97 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1258,7 +1258,7 @@ def __init__(self, vaccine, days, label=None, prob=1.0, subtarget=None, **kwargs def parse(self, vaccine=None, label=None): ''' Unpack vaccine information, which may be given as a string or dict ''' - # Option 1: vaccines can be chosen from a list of pre-defined strains + # Option 1: vaccines can be chosen from a list of pre-defined vaccines if isinstance(vaccine, str): choices, mapping = cvpar.get_vaccine_choices() @@ -1330,10 +1330,13 @@ def initialize(self, sim): self.p[key] = val self.days = process_days(sim, self.days) # days that group becomes eligible + self.first_dose_nab_days = [None]*(sim['n_days']+1) # inds who get nabs from first dose + self.second_dose_nab_days = [None]*(sim['n_days']+1) # inds who get nabs from second dose (if relevant) self.second_dose_days = [None]*(sim['n_days']+1) # inds who get second dose (if relevant) self.vaccinated = [None]*(sim['n_days']+1) # keep track of inds of people vaccinated on each day self.vaccinations = np.zeros(sim['pop_size'], dtype=cvd.default_int) # Number of doses given per person self.vaccination_dates = np.full(sim['pop_size'], np.nan) # Store the dates when people are vaccinated + self.vaccine_nab_dates = np.full(sim['pop_size'], np.nan) # Store the dates when people get their new vaccine nabs sim['vaccine_pars'][self.label] = self.p # Store the parameters self.index = list(sim['vaccine_pars'].keys()).index(self.label) # Find where we are in the list sim['vaccine_map'][self.index] = self.label # Use that to populate the reverse mapping @@ -1366,9 +1369,13 @@ def apply(self, sim): next_dose_day = sim.t + self.p.interval if next_dose_day < sim['n_days']: self.second_dose_days[next_dose_day] = vacc_inds + self.first_dose_nab_days[next_dose_day] = vacc_inds + else: + self.first_dose_nab_days[sim.t] = vacc_inds vacc_inds_dose2 = self.second_dose_days[sim.t] if vacc_inds_dose2 is not None: + self.second_dose_nab_days[sim.t + 14] = vacc_inds_dose2 vacc_inds = np.concatenate((vacc_inds, vacc_inds_dose2), axis=None) sim.people.flows['new_vaccinations'] += len(vacc_inds_dose2) if len(vacc_inds): @@ -1378,10 +1385,21 @@ def apply(self, sim): self.vaccinations[vacc_inds] += 1 self.vaccination_dates[vacc_inds] = sim.t + self.vaccine_nab_dates[vacc_inds] = (sim.t + self.p.interval) if self.p.interval else sim.t + # Update vaccine attributes in sim sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] - sim.people.date_vaccinated[vacc_inds] = self.vaccination_dates[vacc_inds] - cvi.init_nab(sim.people, vacc_inds, prior_inf=False) + + # check whose nabs kick in today and then init_nabs for them! + vaccine_nabs_first_dose_inds = self.first_dose_nab_days[sim.t] + vaccine_nabs_second_dose_inds = self.second_dose_nab_days[sim.t] + + vaccine_nabs_inds = [vaccine_nabs_first_dose_inds, vaccine_nabs_second_dose_inds] + + for vaccinees in vaccine_nabs_inds: + if vaccinees is not None: + sim.people.date_vaccinated[vaccinees] = sim.t + cvi.init_nab(sim.people, vaccinees, prior_inf=False) return From 3d063fe59266061141fcdee80d9fa1c0e42a8666 Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Mon, 19 Apr 2021 21:51:28 -0400 Subject: [PATCH 529/569] added in fraction reinfected for results --- covasim/sim.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/covasim/sim.py b/covasim/sim.py index 4ebab2819..f89255f7e 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -315,6 +315,7 @@ def init_res(*args, **kwargs): self.results['pop_nabs'] = init_res('Population nab levels', scale=False, color=dcols.pop_nabs) self.results['pop_protection'] = init_res('Population immunity protection', scale=False, color=dcols.pop_protection) self.results['pop_symp_protection'] = init_res('Population symptomatic protection', scale=False, color=dcols.pop_symp_protection) + self.results['frac_reinfected'] = init_res('Proportion reinfected', scale=False) # Handle strains ns = self['n_strains'] @@ -842,6 +843,7 @@ def compute_states(self): self.results['strain']['incidence_by_strain'][:] = np.einsum('ji,i->ji',res['strain']['new_infections_by_strain'][:], 1/res['n_susceptible'][:]) # Calculate the incidence self.results['strain']['prevalence_by_strain'][:] = np.einsum('ji,i->ji',res['strain']['new_infections_by_strain'][:], 1/res['n_alive'][:]) # Calculate the prevalence self.results['frac_vaccinated'][:] = res['n_vaccinated'][:]/res['n_alive'][:] # Calculate the share vaccinated + self.results['frac_reinfected'][:] = res['new_reinfections'][:]/res['new_infections'][:] return From d06f7453a8a48a12aa314f490c78496d0d2251aa Mon Sep 17 00:00:00 2001 From: Paula Sanz-Leon Date: Tue, 20 Apr 2021 14:37:06 +1000 Subject: [PATCH 530/569] Add new vaccine brand: novavax --- covasim/parameters.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/covasim/parameters.py b/covasim/parameters.py index 7c2200d53..e70285a0d 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -337,6 +337,7 @@ def get_vaccine_choices(): 'default': ['default', None], 'pfizer': ['pfizer', 'biontech', 'pfizer-biontech'], 'moderna': ['moderna'], + 'novavax': ['nvx', 'novavax', 'nova'], 'az': ['az', 'astrazeneca'], 'jj': ['jj', 'jnj', 'johnson & johnson', 'janssen'], } @@ -461,6 +462,13 @@ def get_vaccine_strain_pars(default=False): p1 = 1/8.6, ), + novavax = dict( # https://ir.novavax.com/news-releases/news-release-details/novavax-covid-19-vaccine-demonstrates-893-efficacy-uk-phase-3 + wild = 1.0, + b117 = 1/1.12, + b1351 = 1/4.7, + p1 = 1/8.6, # assumption, no data available yet + ), + az = dict( wild = 1.0, b117 = 1/2.3, @@ -512,6 +520,14 @@ def get_vaccine_dose_pars(default=False): interval = 28, ), + novavax = dict( + nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), + nab_init = dict(dist='normal', par1=-0.9, par2=2), + nab_boost = 3, + doses = 2, + interval = 21, + ), + az = dict( nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), nab_init = dict(dist='normal', par1=-0.85, par2=2), From e1954de12154cde077920c25ec80ca8c97651cde Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 20 Apr 2021 17:18:28 -0400 Subject: [PATCH 531/569] added in prior diagnoses and setting diagnoses = False once recovered --- covasim/defaults.py | 3 +++ covasim/people.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/covasim/defaults.py b/covasim/defaults.py index 20e86c40d..42d3bd1b9 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -63,6 +63,7 @@ def __init__(self): 'critical', 'tested', 'diagnosed', + 'prior_diagnosed', # a previous infection was diagnosed 'recovered', 'dead', 'known_contact', @@ -167,6 +168,7 @@ def __init__(self): 'deaths': 'deaths', 'tests': 'tests', 'diagnoses': 'diagnoses', + 'rediagnoses': 'rediagnoses', 'quarantined': 'quarantined people', 'vaccinations': 'vaccinations', 'vaccinated': 'vaccinated people' @@ -264,6 +266,7 @@ def get_default_colors(): c.pop_nabs = '#32733d' c.pop_protection = '#9e1149' c.pop_symp_protection = '#b86113' + c.rediagnoses = '#b86113' return c diff --git a/covasim/people.py b/covasim/people.py index b19be694e..ba1dbe87f 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -279,6 +279,7 @@ def check_recovery(self, inds=None, filter_inds='is_exp'): self.exposed_by_strain[:, inds] = False self.infectious_by_strain[:, inds] = False + # Handle immunity aspects if self.pars['use_waning']: @@ -286,6 +287,11 @@ def check_recovery(self, inds=None, filter_inds='is_exp'): mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=inds) severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=inds) + # determine who was diagnosed and set their prior diagnosis to true and current diagnosis to false + inds_diagnosed = cvu.true(self.diagnosed[inds]) + self.prior_diagnosed[inds[inds_diagnosed]] = True + self.diagnosed[inds] = False + # Reset additional states self.susceptible[inds] = True self.prior_symptoms[inds] = self.pars['rel_imm']['asymp'] @@ -335,6 +341,10 @@ def check_diagnosed(self): self.date_end_quarantine[quarantined] = self.t # Set end quarantine date to match when the person left quarantine (and entered isolation) self.quarantined[diag_inds] = False # If you are diagnosed, you are isolated, not in quarantine + # Determine how many people who are diagnosed today have a prior diagnosis + prior_diag_inds = cvu.true(self.prior_diagnosed[test_pos_inds]) + self.flows['new_rediagnoses'] += len(prior_diag_inds) + return len(test_pos_inds) From 634b1c3a64606a9b6a035cea076dd624e7c005fe Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 20 Apr 2021 17:26:26 -0400 Subject: [PATCH 532/569] fix to setting nab interval --- covasim/interventions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 04cf31d97..8f71f7f24 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1375,7 +1375,9 @@ def apply(self, sim): vacc_inds_dose2 = self.second_dose_days[sim.t] if vacc_inds_dose2 is not None: - self.second_dose_nab_days[sim.t + 14] = vacc_inds_dose2 + next_nab_day = sim.t + self.p.interval + if next_nab_day < sim['n_days']: + self.second_dose_nab_days[next_nab_day] = vacc_inds_dose2 vacc_inds = np.concatenate((vacc_inds, vacc_inds_dose2), axis=None) sim.people.flows['new_vaccinations'] += len(vacc_inds_dose2) if len(vacc_inds): From 24f5beeb93fab99e6a3341f7b430883121340b1a Mon Sep 17 00:00:00 2001 From: Jamie Cohen Date: Tue, 20 Apr 2021 17:44:57 -0400 Subject: [PATCH 533/569] getting an error here --- covasim/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/base.py b/covasim/base.py index d00ee217d..01262807c 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1034,7 +1034,7 @@ def validate(self, die=True, verbose=False): raise ValueError(errormsg) # Check that the length of each array is consistent - expected_len = len(self) + expected_len = len(self.age) expected_strains = self.pars['n_strains'] for key in self.keys(): if self[key].ndim == 1: From 0a443bac19d5270034b2eeb4d7687b61fd1a8cdc Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Thu, 22 Apr 2021 17:59:59 -0700 Subject: [PATCH 534/569] update version --- covasim/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/version.py b/covasim/version.py index 8ca4d74cd..051e2ebb5 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,6 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '3.0.1' -__versiondate__ = '2021-04-16' +__version__ = '3.0.2' +__versiondate__ = '2021-04-23' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From 10f0070ab6f20d2a5959de4bb5d3302a7d7abe94 Mon Sep 17 00:00:00 2001 From: Romesh Abeysuriya Date: Sun, 25 Apr 2021 09:39:25 +1000 Subject: [PATCH 535/569] More flexible loading of old parameter values --- covasim/misc.py | 57 +++++++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index 4d9ee1a87..e8a380fab 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -9,7 +9,7 @@ import scipy.stats as sps from pathlib import Path from . import version as cvv - +from distutils.version import LooseVersion #%% Convenience imports from Sciris @@ -502,6 +502,16 @@ def get_version_pars(version, verbose=True): ''' Function for loading parameters from the specified version. + Parameters will be loaded for Covasim 'as at' the requested version i.e. the + most recent set of parameters that is <= the requested version. Available + parameter values are stored in the regression folder. If parameters are available + for versions 1.3, and 1.4, then this function will return the following + + - If parameters for version '1.3' are requested, parameters will be returned from '1.3' + - If parameters for version '1.3.5' are requested, parameters will be returned from '1.3', since + Covasim at version 1.3.5 would have been using the parameters defined at version 1.3. + - If parameters for version '1.4' are requested, parameters will be returned from '1.4' + Args: version (str): the version to load parameters from @@ -509,42 +519,23 @@ def get_version_pars(version, verbose=True): Dictionary of parameters from that version ''' - # Define mappings for available sets of parameters -- note that this must be manually updated from the changelog - match_map = { - '0.30.4': ['0.30.4'], - '0.31.0': ['0.31.0'], - '0.32.0': ['0.32.0'], - '1.0.0': ['1.0.0'], - '1.0.1': [f'1.0.{i}' for i in range(1,4)], - '1.1.0': ['1.1.0'], - '1.1.1': [f'1.1.{i}' for i in range(1,3)], - '1.1.3': [f'1.1.{i}' for i in range(3,8)], - '1.2.0': [f'1.2.{i}' for i in range(4)], - '1.3.0': [f'1.3.{i}' for i in range(6)], - '1.4.0': [f'1.4.{i}' for i in range(9)], - '1.5.0': [f'1.5.{i}' for i in range(4)] + [f'1.6.{i}' for i in range(2)] + [f'1.7.{i}' for i in range(7)], - '2.0.0': [f'2.0.{i}' for i in range(5)] + ['2.1.0'], - '2.1.1': [f'2.1.{i}' for i in range(1,3)], - '3.0.0': ['3.0.0'], - } - - # Find and check the match - match = None - for ver,verlist in match_map.items(): - if version in verlist: - match = ver - break - if match is None: # pragma: no cover - options = '\n'.join(sum(match_map.values(), [])) - errormsg = f'Could not find version "{version}" among options:\n{options}' + # Construct a sorted list of available parameters based on the files in the regression folder + regression_folder = sc.thisdir(__file__, 'regression', as_path=True) + available_versions = [x.stem.replace('pars_v','') for x in regression_folder.iterdir() if x.suffix=='.json'] + available_versions = sorted(available_versions, key=LooseVersion) + + # Find the highest parameter version that is <= the requested version + version_comparison = [sc.compareversions(version, v)>=0 for v in available_versions] + try: + target_version = available_versions[sc.findlast(version_comparison)] + except IndexError: + errormsg = f"Could not find a parameter version that was less than or equal to '{version}'. Available versions are {available_versions}" raise ValueError(errormsg) # Load the parameters - filename = f'pars_v{match}.json' - regression_folder = sc.thisdir(__file__, 'regression') - pars = sc.loadjson(filename=filename, folder=regression_folder) + pars = sc.loadjson(filename=regression_folder/f'pars_v{target_version}.json', folder=regression_folder) if verbose: - print(f'Loaded parameters from {match}') + print(f'Loaded parameters from {target_version}') return pars From 1e541b0990b988e7133a2fbab1bc845c98a422fe Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 25 Apr 2021 18:51:42 -0700 Subject: [PATCH 536/569] update faq --- FAQ.rst | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/FAQ.rst b/FAQ.rst index be6e7b827..62555ff90 100644 --- a/FAQ.rst +++ b/FAQ.rst @@ -12,8 +12,14 @@ This document contains answers to frequently (and some not so frequently) asked Usage questions ^^^^^^^^^^^^^^^ +What are the system requirements for Covasim? +--------------------------------------------------------------------------------- + +If your system can run scientific Python (Numpy, SciPy, and Matplotlib), then you can probably run Covasim. Covasim requires 1 GB of RAM per 1 million people, and can simulate roughly 5-10 million person-days per second. A typical use case, such as a population of 100,000 agents running for 500 days, would require 100 MB of memory and take about 5-10 seconds to run. + + Can Covasim be run on HPC clusters? ---------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------- Yes. On a single-node setup, it is quite easy: in fact, ``MultiSim`` objects will automatically scale to the number of cores available. This can also be specified explicitly with e.g. ``msim.run(n_cpus=24)``. @@ -21,7 +27,7 @@ For more complex use cases (e.g. running across multiple virtual machines), we r What method is best for saving simulation objects? ---------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------- The recommended way to save a simulation is simply via ``sim.save(filename)``. By default, this does *not* save the people (``sim.people``), since they are very large (i.e., 7 KB without people vs. 7 MB with people for 100,000 agents). However, if you really want to save the people, pass ``keep_people=True``. @@ -37,7 +43,7 @@ Typically, parameters are held constant for the duration of the simulation. Howe How can you introduce new infections into a simulation? --------------------------------------------------------------------------------- -These are referred to as *importations*. You can set the ``n_imports`` parameter for a fixed number of importations each day (or make it time-varying with ``cv.dynamic_pars()``, as described above). Alternatively, you can infect people directly using ``sim.people.infect()``. +These are referred to as *importations*. You can set the ``n_imports`` parameter for a fixed number of importations each day (or make it time-varying with ``cv.dynamic_pars()``, as described above). Alternatively, you can infect people directly using ``sim.people.infect()``. Since version 3.0, you can also import specific strains on a given day: e.g., ``cv.Sim(strains=cv.strain('b117', days=50, n_imports=10)``. How do you set custom prognoses parameters (mortality rate, susceptibility etc.)? From 66abe78be1ede4fbbe01fc7ce56461bc0e404502 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 25 Apr 2021 20:37:46 -0700 Subject: [PATCH 537/569] added to_df --- covasim/base.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index d00ee217d..c3a37ea5f 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -510,7 +510,7 @@ def export_pars(self, filename=None, indent=2, *args, **kwargs): def to_json(self, filename=None, keys=None, tostring=False, indent=2, verbose=False, *args, **kwargs): ''' - Export results as JSON. + Export results and parameters as JSON. Args: filename (str): if None, return string; else, write to file @@ -563,6 +563,23 @@ def to_json(self, filename=None, keys=None, tostring=False, indent=2, verbose=Fa return output + def to_df(self, date_index=False): + ''' + Export results to a pandas dataframe + + Args: + date_index (bool): if True, use the date as the index + ''' + resdict = self.export_results(for_json=False) + df = pd.DataFrame.from_dict(resdict) + df['date'] = self.datevec + new_columns = ['t','date'] + df.columns[1:-1].tolist() # Get column order + df = df.reindex(columns=new_columns) # Reorder so 't' and 'date' are first + if date_index: + df = df.set_index('date') + return df + + def to_excel(self, filename=None, skip_pars=None): ''' Export parameters and results as Excel format @@ -573,20 +590,19 @@ def to_excel(self, filename=None, skip_pars=None): Returns: An sc.Spreadsheet with an Excel file, or writes the file to disk - ''' if skip_pars is None: skip_pars = ['strain_map', 'vaccine_map'] # These include non-string keys so fail at sc.flattendict() - resdict = self.export_results(for_json=False) - result_df = pd.DataFrame.from_dict(resdict) - result_df.index = self.datevec - result_df.index.name = 'date' + # Export results + result_df = self.to_df(date_index=True) + # Export parameters pars = {k:v for k,v in self.pars.items() if k not in skip_pars} par_df = pd.DataFrame.from_dict(sc.flattendict(pars, sep='_'), orient='index', columns=['Value']) par_df.index.name = 'Parameter' + # Convert to spreadsheet spreadsheet = sc.Spreadsheet() spreadsheet.freshbytes() with pd.ExcelWriter(spreadsheet.bytes, engine='xlsxwriter') as writer: From 81cfb6d469efc0163908e9713071dc5f24536d98 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 25 Apr 2021 22:24:02 -0700 Subject: [PATCH 538/569] fix docstring --- covasim/plotting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index d2e37a74a..bf9fc633f 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -244,12 +244,13 @@ def date_formatter(start_day=None, dateformat=None, interval=None, start=None, e **Examples**:: # Automatically configure the axis with default option - cv.date_formatter(sim=sim, ax=ax) s + cv.date_formatter(sim=sim, ax=ax) # Manually configure ax = pl.subplot(111) ax.plot(np.arange(60), np.random.random(60)) formatter = cv.date_formatter(start_day='2020-04-04', interval=7, start='2020-05-01', end=50, dateformat='%Y-%m-%d', ax=ax) + ax.xaxis.set_major_formatter(formatter) ''' # Set the default -- "Mar-01" From 236c66ed9e2ba19bb8d5e6fcfc541ece29c56411 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 25 Apr 2021 22:37:59 -0700 Subject: [PATCH 539/569] update code of conduct --- CODE_OF_CONDUCT.rst | 50 ++++++++++--------------------------------- docs/contributing.rst | 6 +----- docs/index.rst | 1 + 3 files changed, 13 insertions(+), 44 deletions(-) diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst index d8a566230..ad5fae8ef 100644 --- a/CODE_OF_CONDUCT.rst +++ b/CODE_OF_CONDUCT.rst @@ -5,18 +5,12 @@ Contributor covenant code of conduct Our pledge ========== -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. +We believe that a diverse, equitable, and inclusive environment is essential for producing the best quality software. In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in Covasim development and the Covasim community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. Our standards ============= -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences @@ -26,57 +20,35 @@ include: Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances +* The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting Our responsibilities ==================== -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Covasim maintainers are responsible for clarifying the standards of acceptable behavior and will take appropriate and fair corrective action in response to any instances of unacceptable behavior. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Covasim maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. Scope ===== -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing Covasim or its community. Examples of representing the Covasim project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Enforcement =========== -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at covasim@idmod.org. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at covasim@idmod.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The Covasim team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +Covasim maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of Covasim's leadership. Attribution =========== -This Code of Conduct is adapted from the `Contributor Covenant`_, version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. +This Code of Conduct is adapted from the `Contributor Covenant`_, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. .. _Contributor Covenant: https://www.contributor-covenant.org diff --git a/docs/contributing.rst b/docs/contributing.rst index 79a8c558a..3bdd7dc21 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -1,5 +1 @@ -.. include:: ../CONTRIBUTING.rst - -.. toctree:: - - conduct \ No newline at end of file +.. include:: ../CONTRIBUTING.rst \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 683ea57de..fd2454634 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,5 +24,6 @@ Full contents parameters data glossary + conduct contributing modules \ No newline at end of file From 7a3c5ff26a56346505732a6d58e356b3f6202577 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 25 Apr 2021 22:40:41 -0700 Subject: [PATCH 540/569] update title --- CODE_OF_CONDUCT.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst index ad5fae8ef..44c7f0e35 100644 --- a/CODE_OF_CONDUCT.rst +++ b/CODE_OF_CONDUCT.rst @@ -1,6 +1,6 @@ -==================================== -Contributor covenant code of conduct -==================================== +=============== +Code of conduct +=============== Our pledge ========== From 753460ee7a3db3fd22e159b231c6e6b9c95cf148 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 25 Apr 2021 22:55:18 -0700 Subject: [PATCH 541/569] updated parameters readme --- covasim/README.rst | 57 +++++++++++++++++++++++++++++++------------ covasim/parameters.py | 2 +- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/covasim/README.rst b/covasim/README.rst index b214604bd..d413c48eb 100644 --- a/covasim/README.rst +++ b/covasim/README.rst @@ -6,9 +6,9 @@ This file describes each of the input parameters in Covasim. Note: the overall i Population parameters --------------------- -* ``pop_size`` = Number ultimately susceptible to CoV +* ``pop_size`` = Number of agents, i.e., people susceptible to SARS-CoV-2 * ``pop_infected`` = Number of initial infections -* ``pop_type`` = What type of population data to use -- random (fastest), synthpops (best), hybrid (compromise), or clustered (not recommended) +* ``pop_type`` = What type of population data to use -- 'random' (fastest), 'synthpops' (best), 'hybrid' (compromise) * ``location`` = What location to load data from -- default Seattle Simulation parameters @@ -17,31 +17,50 @@ Simulation parameters * ``end_day`` = End day of the simulation * ``n_days`` = Number of days to run, if end_day isn't specified * ``rand_seed`` = Random seed, if None, don't reset -* ``verbose`` = Whether or not to display information during the run -- options are 0 (silent), 1 (default), 2 (everything) +* ``verbose`` = Whether or not to display information during the run -- options are 0 (silent), 0.1 (some; default), 1 (more), 2 (everything) Rescaling parameters -------------------- * ``pop_scale`` = Factor by which to scale the population -- e.g. 1000 with pop_size = 10e3 means a population of 10m +* ``scaled_pop`` = The total scaled population, i.e. the number of agents times the scale factor; alternative to pop_scale * ``rescale`` = Enable dynamic rescaling of the population * ``rescale_threshold`` = Fraction susceptible population that will trigger rescaling if rescaling * ``rescale_factor`` = Factor by which we rescale the population Basic disease transmission -------------------------- -* ``beta`` = Beta per symptomatic contact; absolute -* ``contacts`` = The number of contacts per layer; set below -* ``dynam_layer`` = Which layers are dynamic; set below -* ``beta_layer`` = Transmissibility per layer; set below -* ``n_imports`` = Average daily number of imported cases (actual number is drawn from Poisson distribution) -* ``beta_dist`` = Distribution to draw individual level transmissibility; see https://wellcomeopenresearch.org/articles/5-67 -* ``viral_dist`` = The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 - -Efficacy of protection measures -------------------------------- +* ``beta`` = Beta per symptomatic contact; absolute +* ``n_imports`` = Average daily number of imported cases (actual number is drawn from Poisson distribution) +* ``beta_dist`` = Distribution to draw individual level transmissibility; see https://wellcomeopenresearch.org/articles/5-67 +* ``viral_dist`` = The time varying viral load (transmissibility); estimated from Lescure 2020, Lancet, https://doi.org/10.1016/S1473-3099(20)30200-0 * ``asymp_factor`` = Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 -* ``iso_factor`` = Multiply beta by this factor for diganosed cases to represent isolation; set below -* ``quar_factor`` = Quarantine multiplier on transmissibility and susceptibility; set below -* ``quar_period`` = Number of days to quarantine for; assumption based on standard policies + +Network parameters +------------------ +* ``contacts`` = The number of contacts per layer +* ``dynam_layer`` = Which layers are dynamic +* ``beta_layer`` = Transmissibility per layer + +Multi-strain parameters +----------------------- +* ``n_imports`` = Average daily number of imported cases (actual number is drawn from Poisson distribution) +* ``n_strains`` = The number of strains circulating in the population + +Immunity parameters +------------------- +* ``use_waning`` = Whether to use dynamically calculated immunity +* ``nab_init`` = Parameters for the distribution of the initial level of log2(nab) following natural infection, taken from fig1b of https://doi.org/10.1101/2021.03.09.21252641 +* ``nab_decay`` = Parameters describing the kinetics of decay of nabs over time, taken from fig3b of https://doi.org/10.1101/2021.03.09.21252641 +* ``nab_kin`` = Constructed during sim initialization using the nab_decay parameters +* ``nab_boost`` = Multiplicative factor applied to a person's nab levels if they get reinfected. # TODO: add source +* ``nab_eff`` = Parameters to map nabs to efficacy +* ``rel_imm_symp`` = Relative immunity from natural infection varies by symptoms +* ``immunity`` = Matrix of immunity and cross-immunity factors, set by init_immunity() in immunity.py + +Strain-specific parameters +-------------------------- +* ``rel_beta`` = Relative transmissibility varies by strain +* ``rel_imm_strain`` = Relative own-immunity varies by strain Time for disease progression ---------------------------- @@ -66,6 +85,12 @@ Severity parameters * ``prog_by_age`` = Whether to set disease progression based on the person's age * ``prognoses`` = The actual arrays of prognoses by age; this is populated later +Efficacy of protection measures +------------------------------- +* ``iso_factor`` = Multiply beta by this factor for diganosed cases to represent isolation; set below +* ``quar_factor`` = Quarantine multiplier on transmissibility and susceptibility; set below +* ``quar_period`` = Number of days to quarantine for; assumption based on standard policies + Events and interventions ------------------------ * ``interventions`` = The interventions present in this simulation; populated by the user diff --git a/covasim/parameters.py b/covasim/parameters.py index 7c2200d53..06b837b44 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -40,7 +40,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): pars['end_day'] = None # End day of the simulation pars['n_days'] = 60 # Number of days to run, if end_day isn't specified pars['rand_seed'] = 1 # Random seed, if None, don't reset - pars['verbose'] = cvo.verbose # Whether or not to display information during the run -- options are 0 (silent), 1 (default), 2 (everything) + pars['verbose'] = cvo.verbose # Whether or not to display information during the run -- options are 0 (silent), 0.1 (some; default), 1 (default), 2 (everything) # Rescaling parameters pars['pop_scale'] = 1 # Factor by which to scale the population -- e.g. pop_scale=10 with pop_size=100e3 means a population of 1 million From 3f868dd7d18337323f80fe863369b364ae1f8edf Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 25 Apr 2021 23:01:38 -0700 Subject: [PATCH 542/569] update kwarg --- covasim/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/misc.py b/covasim/misc.py index e8a380fab..3b34e7f79 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -520,7 +520,7 @@ def get_version_pars(version, verbose=True): ''' # Construct a sorted list of available parameters based on the files in the regression folder - regression_folder = sc.thisdir(__file__, 'regression', as_path=True) + regression_folder = sc.thisdir(__file__, 'regression', aspath=True) available_versions = [x.stem.replace('pars_v','') for x in regression_folder.iterdir() if x.suffix=='.json'] available_versions = sorted(available_versions, key=LooseVersion) From b798e25ea8a2c38bf9f3a7da6af04e0764f1487e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 25 Apr 2021 23:22:43 -0700 Subject: [PATCH 543/569] fix sequence regression --- covasim/interventions.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index dc3788a1c..137ad2af8 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -484,11 +484,8 @@ def __init__(self, days, interventions, **kwargs): def initialize(self, sim): ''' Fix the dates ''' super().initialize() - self.days = process_days(sim, self.days) - if isinstance(self.days, list): # Normal use case - self.days_arr = np.array(self.days + [sim.npts]) - else: # If a function is supplied - self.days_arr = self.days + self.days = [sim.day(day) for day in self.days] + self.days_arr = np.array(self.days + [sim.npts]) for intervention in self.interventions: intervention.initialize(sim) return @@ -496,11 +493,7 @@ def initialize(self, sim): def apply(self, sim): ''' Find the matching day, and see which intervention to activate ''' - if isinstance(self.days_arr, list): # Normal use case - days_arr = np.array([get_day(d, interv=self, sim=sim) for d in self.days_arr]) <= sim.t - else: - days_arr = self.days - inds = find_day(days_arr, interv=self, sim=sim, which='last') + inds = find_day(self.days_arr <= sim.t, which='last') if len(inds): return self.interventions[inds[0]].apply(sim) From eda0888ab7cd7f97226db1b96d06af35296e3eac Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 00:05:04 -0700 Subject: [PATCH 544/569] update population size handling --- covasim/base.py | 46 ++++++++++++++++++++++++++++++++----------- covasim/people.py | 10 +++------- covasim/population.py | 6 +++--- covasim/sim.py | 8 ++++++-- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index c3a37ea5f..bb29a1b97 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -853,28 +853,40 @@ def __getitem__(self, key): If the key is an integer, alias `people.person()` to return a `Person` instance ''' - if isinstance(key, int): - return self.person(key) - try: return self.__dict__[key] except: # pragma: no cover - errormsg = f'Key "{key}" is not a valid attribute of people' - raise AttributeError(errormsg) + if isinstance(key, int): + return self.person(key) + else: + errormsg = f'Key "{key}" is not a valid attribute of people' + raise AttributeError(errormsg) def __setitem__(self, key, value): ''' Ditto ''' if self._lock and key not in self.__dict__: # pragma: no cover - errormsg = f'Key "{key}" is not a valid attribute of people' + errormsg = f'Key "{key}" is not a current attribute of people, and the people object is locked; see people.unlock()' raise AttributeError(errormsg) self.__dict__[key] = value return + def lock(self): + ''' Lock the people object to prevent keys from being added ''' + self._lock = True + return + + + def unlock(self): + ''' Unlock the people object to allow keys to be added ''' + self._lock = False + return + + def __len__(self): ''' This is just a scalar, but validate() and _resize_arrays() make sure it's right ''' - return self.pars['pop_size'] + return int(self.pars['pop_size']) def __iter__(self): @@ -989,12 +1001,24 @@ def count_not(self, key): return (self[key]==0).sum() - def set_pars(self, pars): + def set_pars(self, pars=None): ''' - Very simple method to re-link the parameters stored in the people object - to the sim containing it: included simply for the sake of being explicit. + Re-link the parameters stored in the people object to the sim containing it, + and perform some basic validation. ''' - self.pars = pars + if pars is None: + pars = {} + elif sc.isnumber(pars): # Interpret as a population size + pars = {'pop_size':pars} # Ensure it's a dictionary + orig_pars = self.__dict__.get('pars') # Get the current parameters using dict's get method + pars = sc.mergedicts(orig_pars, pars) + if 'pop_size' not in pars: + errormsg = f'The parameter "pop_size" must be included in a population; keys supplied were:\n{sc.newlinejoin(pars.keys())}' + raise sc.KeyNotFoundError(errormsg) + pars['pop_size'] = int(pars['pop_size']) + pars.setdefault('n_strains', 1) + pars.setdefault('location', None) + self.pars = pars # Actually store the pars return diff --git a/covasim/people.py b/covasim/people.py index c2ccfee29..4959fa8b8 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -44,12 +44,7 @@ class People(cvb.BasePeople): def __init__(self, pars, strict=True, **kwargs): # Handle pars and population size - if sc.isnumber(pars): # Interpret as a population size - pars = {'pop_size':pars} # Ensure it's a dictionary - self.pars = pars # Equivalent to self.set_pars(pars) - self.pars['pop_size'] = int(pars['pop_size']) - self.pars.setdefault('n_strains', 1) - self.pars.setdefault('location', None) + self.set_pars(pars) self.version = cvv.__version__ # Store version info # Other initialization @@ -92,7 +87,8 @@ def __init__(self, pars, strict=True, **kwargs): # Store the dtypes used in a flat dict self._dtypes = {key:self[key].dtype for key in self.keys()} # Assign all to float by default - self._lock = strict # If strict is true, stop further keys from being set (does not affect attributes) + if strict: + self.lock() # If strict is true, stop further keys from being set (does not affect attributes) # Store flows to be computed during simulation self.init_flows() diff --git a/covasim/population.py b/covasim/population.py index d106cc7ed..dc2529d73 100644 --- a/covasim/population.py +++ b/covasim/population.py @@ -24,11 +24,11 @@ def make_people(sim, popdict=None, save_pop=False, popfile=None, die=True, reset=False, verbose=None, **kwargs): ''' Make the actual people for the simulation. Usually called via sim.initialize(), - not directly by the user. + but can be called directly by the user. Args: - sim (Sim) : the simulation object - popdict (dict) : if supplied, use this population dictionary rather than generate a new one + sim (Sim) : the simulation object; population parameters are taken from the sim object + popdict (dict) : if supplied, use this population dictionary instead of generating a new one save_pop (bool) : whether to save the population to disk popfile (bool) : if so, the filename to save to die (bool) : whether or not to fail if synthetic populations are requested but not available diff --git a/covasim/sim.py b/covasim/sim.py index 967de1e92..0646806ea 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -183,8 +183,12 @@ def validate_layer_pars(self): if self.people is not None: pop_keys = set(self.people.contacts.keys()) if pop_keys != set(layer_keys): # pragma: no cover - errormsg = f'Please update your parameter keys {layer_keys} to match population keys {pop_keys}. You may find sim.reset_layer_pars() helpful.' - raise sc.KeyNotFoundError(errormsg) + if not len(pop_keys): + errormsg = f'Your population does not have any layer keys, but your simulation does {layer_keys}. If you called cv.People() directly, you probably need cv.make_people() instead.' + raise sc.KeyNotFoundError(errormsg) + else: + errormsg = f'Please update your parameter keys {layer_keys} to match population keys {pop_keys}. You may find sim.reset_layer_pars() helpful.' + raise sc.KeyNotFoundError(errormsg) return From b2a19fef31993c150ec5203b273e70ab3aaa7b78 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 01:02:49 -0700 Subject: [PATCH 545/569] update test --- covasim/defaults.py | 22 ++++++- covasim/interventions.py | 50 ++++++++------- covasim/run.py | 2 +- tests/devtests/dev_test_vaccine_efficiency.py | 62 ++++++++++++------- 4 files changed, 87 insertions(+), 49 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 096732e95..7b75a9080 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -188,6 +188,20 @@ def __init__(self): new_result_flows_by_strain = [f'new_{key}' for key in result_flows_by_strain.keys()] cum_result_flows_by_strain = [f'cum_{key}' for key in result_flows_by_strain.keys()] +# Define results that are floats (not numbers of people) +float_results = [ + 'prevalence', + 'incidence', + 'r_eff', + 'doubling_time', + 'test_yield', + 'rel_test_yield', + 'frac_vaccinated', + 'pop_nabs', + 'pop_protection', + 'pop_symp_protection', +] + # Parameters that can vary by strain strain_pars = [ 'rel_imm_strain', @@ -317,7 +331,7 @@ def get_default_plots(which='default', kind='sim', sim=None): # Default plots -- different for sims and scenarios if which in [None, 'default']: - if kind == 'sim': + if 'sim' in kind: plots = sc.odict({ 'Total counts': [ 'cum_infections', @@ -335,7 +349,7 @@ def get_default_plots(which='default', kind='sim', sim=None): ], }) - elif kind == 'scens': # pragma: no cover + elif 'scen' in kind: # pragma: no cover plots = sc.odict({ 'Cumulative infections': [ 'cum_infections', @@ -348,6 +362,10 @@ def get_default_plots(which='default', kind='sim', sim=None): ], }) + else: + errormsg = f'Expecting "sim" or "scens", not "{kind}"' + raise ValueError(errormsg) + # Show an overview elif which == 'overview': # pragma: no cover plots = sc.dcp(overview_plots) diff --git a/covasim/interventions.py b/covasim/interventions.py index 137ad2af8..dbbac72b4 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1338,40 +1338,44 @@ def apply(self, sim): ''' Perform vaccination ''' if sim.t >= np.min(self.days): - # Determine who gets first dose of vaccine today + + # Initialize vacc_probs = np.zeros(sim['pop_size']) - if self.subtarget is not None: - subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) - if len(subtarget_vals): - vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted - else: - for ind in find_day(self.days, sim.t, interv=self, sim=sim): - unvacc_inds = sc.findinds(~sim.people.vaccinated) + vacc_inds = np.array([], dtype=int) + + # Vaccinate people with their first dose + for ind in find_day(self.days, sim.t, interv=self, sim=sim): + unvacc_inds = sc.findinds(~sim.people.vaccinated) + if self.subtarget is not None: + subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) + if len(subtarget_vals): + vacc_probs[subtarget_inds] = subtarget_vals # People being explicitly subtargeted + else: vacc_probs[unvacc_inds] = self.prob # Assign equal vaccination probability to everyone - vacc_probs[cvu.true(sim.people.dead)] *= 0.0 # do not vaccinate dead people - vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated - - if len(vacc_inds): - self.vaccinated[sim.t] = vacc_inds - sim.people.flows['new_vaccinations'] += len(vacc_inds) - sim.people.flows['new_vaccinated'] += len(vacc_inds) - if self.p.interval is not None: - next_dose_day = sim.t + self.p.interval - if next_dose_day < sim['n_days']: - self.second_dose_days[next_dose_day] = vacc_inds - + vacc_probs[cvu.true(sim.people.dead)] *= 0.0 # Do not vaccinate dead people + vacc_inds = cvu.true(cvu.binomial_arr(vacc_probs)) # Calculate who actually gets vaccinated + + if len(vacc_inds): + self.vaccinated[sim.t] = vacc_inds + sim.people.flows['new_vaccinations'] += len(vacc_inds) + sim.people.flows['new_vaccinated'] += len(vacc_inds) + if self.p.interval is not None: + next_dose_day = sim.t + self.p.interval + if next_dose_day < sim['n_days']: + self.second_dose_days[next_dose_day] = vacc_inds + + # Also, if appropriate, vaccinate people with their second dose vacc_inds_dose2 = self.second_dose_days[sim.t] if vacc_inds_dose2 is not None: vacc_inds = np.concatenate((vacc_inds, vacc_inds_dose2), axis=None) sim.people.flows['new_vaccinations'] += len(vacc_inds_dose2) + + # Update vaccine attributes in sim if len(vacc_inds): - # Update vaccine attributes in sim sim.people.vaccinated[vacc_inds] = True sim.people.vaccine_source[vacc_inds] = self.index self.vaccinations[vacc_inds] += 1 self.vaccination_dates[vacc_inds] = sim.t - - # Update vaccine attributes in sim sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] sim.people.date_vaccinated[vacc_inds] = self.vaccination_dates[vacc_inds] cvi.init_nab(sim.people, vacc_inds, prior_inf=False) diff --git a/covasim/run.py b/covasim/run.py index 6a9f2d254..b56ae1d98 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -394,7 +394,7 @@ def compare(self, t=None, sim_inds=None, output=False, do_plot=False, **kwargs): label += f' ({i})' for reskey in sim.result_keys(): val = sim.results[reskey].values[day] - if reskey not in ['r_eff', 'doubling_time']: + if reskey not in cvd.float_results: val = int(val) resdict[label][reskey] = val diff --git a/tests/devtests/dev_test_vaccine_efficiency.py b/tests/devtests/dev_test_vaccine_efficiency.py index cb3713550..022f3a634 100644 --- a/tests/devtests/dev_test_vaccine_efficiency.py +++ b/tests/devtests/dev_test_vaccine_efficiency.py @@ -2,10 +2,14 @@ Calculate vaccine efficiency for protection against symptomatic covid after first dose ''' -import covasim as cv import numpy as np +import sciris as sc +import covasim as cv + cv.check_version('>=3.0.0') +vaccines = ['pfizer', 'moderna', 'az', 'j&j'] + # construct analyzer to select placebo arm class placebo_arm(cv.Analyzer): def __init__(self, day, trial_size, **kwargs): @@ -29,13 +33,15 @@ def apply(self, sim): 'pop_size': 20000, 'beta': 0.015, 'n_days': 120, + 'verbose': -1, } # Define vaccine arm -trial_size = 500 +trial_size = 2000 start_trial = 20 + def subtarget(sim): - # select people who are susceptible + ''' Select people who are susceptible ''' if sim.t == start_trial: eligible = cv.true(~np.isfinite(sim.people.date_exposed)) inds = eligible[cv.choose(len(eligible), min(trial_size//2, len(eligible)))] @@ -43,29 +49,39 @@ def subtarget(sim): inds = [] return {'vals': [1.0 for ind in inds], 'inds': inds} -pfizer = cv.vaccinate(vaccine='pfizer', days=[start_trial], prob=0.0, subtarget=subtarget) +# Initialize +sims = [] +for vaccine in vaccines: + vx = cv.vaccinate(vaccine=vaccine, days=[start_trial], prob=0.0, subtarget=subtarget) + sim = cv.Sim( + label=vaccine, + use_waning=True, + pars=pars, + interventions=vx, + analyzers=placebo_arm(day=start_trial, trial_size=trial_size//2) + ) + sims.append(sim) -sim = cv.Sim( - use_waning=True, - pars=pars, - interventions=pfizer, - analyzers=placebo_arm(day=start_trial, trial_size=trial_size//2) -) -sim.run() +# Run +msim = cv.MultiSim(sims) +msim.run(keep_people=True) -# Find trial arm indices, those who were vaccinated -vacc_inds = cv.true(sim.people.vaccinated) -placebo_inds = sim['analyzers'][0].placebo_inds -# Check that there is no overlap -assert (len(set(vacc_inds).intersection(set(placebo_inds))) == 0) -# Calculate vaccine efficiency -VE = 1 - (np.isfinite(sim.people.date_symptomatic[vacc_inds]).sum() / - np.isfinite(sim.people.date_symptomatic[placebo_inds]).sum()) -print('Vaccine efficiency for symptomatic covid:', VE) +results = sc.objdict() +print('Vaccine efficiency for symptomatic covid:') +for sim in msim.sims: + vaccine = sim.label + vacc_inds = cv.true(sim.people.vaccinated) # Find trial arm indices, those who were vaccinated + placebo_inds = sim['analyzers'][0].placebo_inds + assert (len(set(vacc_inds).intersection(set(placebo_inds))) == 0) # Check that there is no overlap + # Calculate vaccine efficiency + VE = 1 - (np.isfinite(sim.people.date_symptomatic[vacc_inds]).sum() / + np.isfinite(sim.people.date_symptomatic[placebo_inds]).sum()) + results[vaccine] = VE + print(f' {vaccine:10s}: {VE*100}%') # Plot -to_plot = cv.get_default_plots('default', 'sim') -to_plot['Health outcomes'] += ['cum_vaccinated'] -sim.plot(to_plot=to_plot) +to_plot = cv.get_default_plots('default', 'scen') +to_plot['Vaccinations'] = ['cum_vaccinated'] +msim.plot(to_plot=to_plot) print('Done') \ No newline at end of file From fe262ed8c04415fe71bb8e691d99e8a1c38c8bec Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 01:03:43 -0700 Subject: [PATCH 546/569] update printing --- tests/devtests/dev_test_vaccine_efficiency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/devtests/dev_test_vaccine_efficiency.py b/tests/devtests/dev_test_vaccine_efficiency.py index 022f3a634..0e180d2fd 100644 --- a/tests/devtests/dev_test_vaccine_efficiency.py +++ b/tests/devtests/dev_test_vaccine_efficiency.py @@ -77,7 +77,7 @@ def subtarget(sim): VE = 1 - (np.isfinite(sim.people.date_symptomatic[vacc_inds]).sum() / np.isfinite(sim.people.date_symptomatic[placebo_inds]).sum()) results[vaccine] = VE - print(f' {vaccine:10s}: {VE*100}%') + print(f' {vaccine:8s}: {VE*100:0.2f}%') # Plot to_plot = cv.get_default_plots('default', 'scen') From 3e4027cff6807f3ec1e5ea32b1d8045156e36f1a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 01:12:54 -0700 Subject: [PATCH 547/569] update vaccine names --- covasim/parameters.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 93ba1eb4b..d5bd4b752 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -335,11 +335,11 @@ def get_vaccine_choices(): # List of choices currently available: new ones can be added to the list along with their aliases choices = { 'default': ['default', None], - 'pfizer': ['pfizer', 'biontech', 'pfizer-biontech'], - 'moderna': ['moderna'], - 'novavax': ['nvx', 'novavax', 'nova'], - 'az': ['az', 'astrazeneca'], - 'jj': ['jj', 'jnj', 'johnson & johnson', 'janssen'], + 'pfizer': ['pfizer', 'biontech', 'pfizer-biontech', 'pf', 'pfz', 'pz'], + 'moderna': ['moderna', 'md'], + 'novavax': ['novavax', 'nova', 'covovax', 'nvx', 'nv'], + 'az': ['astrazeneca', 'oxford', 'vaxzevria', 'az'], + 'jj': ['jnj', 'johnson & johnson', 'janssen', 'jj'], } mapping = {name:key for key,synonyms in choices.items() for name in synonyms} # Flip from key:value to value:key return choices, mapping @@ -462,13 +462,6 @@ def get_vaccine_strain_pars(default=False): p1 = 1/8.6, ), - novavax = dict( # https://ir.novavax.com/news-releases/news-release-details/novavax-covid-19-vaccine-demonstrates-893-efficacy-uk-phase-3 - wild = 1.0, - b117 = 1/1.12, - b1351 = 1/4.7, - p1 = 1/8.6, # assumption, no data available yet - ), - az = dict( wild = 1.0, b117 = 1/2.3, @@ -482,6 +475,13 @@ def get_vaccine_strain_pars(default=False): b1351 = 1/6.7, p1 = 1/8.6, ), + + novavax = dict( # https://ir.novavax.com/news-releases/news-release-details/novavax-covid-19-vaccine-demonstrates-893-efficacy-uk-phase-3 + wild = 1.0, + b117 = 1/1.12, + b1351 = 1/4.7, + p1 = 1/8.6, # assumption, no data available yet + ), ) if default: @@ -520,14 +520,6 @@ def get_vaccine_dose_pars(default=False): interval = 28, ), - novavax = dict( - nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), - nab_init = dict(dist='normal', par1=-0.9, par2=2), - nab_boost = 3, - doses = 2, - interval = 21, - ), - az = dict( nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), nab_init = dict(dist='normal', par1=-0.85, par2=2), @@ -543,6 +535,14 @@ def get_vaccine_dose_pars(default=False): doses = 1, interval = None, ), + + novavax = dict( + nab_eff = dict(sus=dict(slope=1.6, n_50=0.05)), + nab_init = dict(dist='normal', par1=-0.9, par2=2), + nab_boost = 3, + doses = 2, + interval = 21, + ), ) if default: From f18ef2d177e976a3ba40c02f5203c908624cbaab Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 01:20:33 -0700 Subject: [PATCH 548/569] fix merge --- covasim/interventions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/covasim/interventions.py b/covasim/interventions.py index 54ce3db11..e3e334ba7 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1323,10 +1323,13 @@ def initialize(self, sim): self.p[key] = val self.days = process_days(sim, self.days) # days that group becomes eligible + self.first_dose_nab_days = [None]*(sim['n_days']+1) # inds who get nabs from first dose + self.second_dose_nab_days = [None]*(sim['n_days']+1) # inds who get nabs from second dose (if relevant) self.second_dose_days = [None]*(sim['n_days']+1) # inds who get second dose (if relevant) self.vaccinated = [None]*(sim['n_days']+1) # keep track of inds of people vaccinated on each day self.vaccinations = np.zeros(sim['pop_size'], dtype=cvd.default_int) # Number of doses given per person self.vaccination_dates = np.full(sim['pop_size'], np.nan) # Store the dates when people are vaccinated + self.vaccine_nab_dates = np.full(sim['pop_size'], np.nan) # Store the dates when people get their new vaccine nabs sim['vaccine_pars'][self.label] = self.p # Store the parameters self.index = list(sim['vaccine_pars'].keys()).index(self.label) # Find where we are in the list sim['vaccine_map'][self.index] = self.label # Use that to populate the reverse mapping From 1bf665a0dac42ab7132d075263031d5e643f07cc Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 01:29:33 -0700 Subject: [PATCH 549/569] working on fixing tests --- covasim/run.py | 5 +++-- covasim/sim.py | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index b56ae1d98..fa25fa0f9 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -393,8 +393,9 @@ def compare(self, t=None, sim_inds=None, output=False, do_plot=False, **kwargs): if label in resdict: # Avoid duplicates label += f' ({i})' for reskey in sim.result_keys(): - val = sim.results[reskey].values[day] - if reskey not in cvd.float_results: + res = sim.results[reskey] + val = res.values[day] + if res.scale: # Results that are scaled by population are ints val = int(val) resdict[label][reskey] = val diff --git a/covasim/sim.py b/covasim/sim.py index ee040e642..b1b68c877 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -305,10 +305,10 @@ def init_res(*args, **kwargs): self.results[f'n_{key}'] = init_res(label, color=dcols[key]) # Other variables - self.results['n_alive'] = init_res('Number alive', scale=False) - self.results['n_naive'] = init_res('Number never infected', scale=False) - self.results['n_preinfectious'] = init_res('Number preinfectious', scale=False, color=dcols.exposed) - self.results['n_removed'] = init_res('Number removed', scale=False, color=dcols.recovered) + self.results['n_alive'] = init_res('Number alive', scale=True) + self.results['n_naive'] = init_res('Number never infected', scale=True) + self.results['n_preinfectious'] = init_res('Number preinfectious', scale=True, color=dcols.exposed) + self.results['n_removed'] = init_res('Number removed', scale=True, color=dcols.recovered) self.results['prevalence'] = init_res('Prevalence', scale=False) self.results['incidence'] = init_res('Incidence', scale=False) self.results['r_eff'] = init_res('Effective reproduction number', scale=False) From eb8ea0797d3e5e3fd47fdbfe2765e10307c3df98 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 01:36:26 -0700 Subject: [PATCH 550/569] more updates --- covasim/base.py | 2 +- covasim/defaults.py | 2 +- covasim/sim.py | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 84858a743..bb29a1b97 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1074,7 +1074,7 @@ def validate(self, die=True, verbose=False): raise ValueError(errormsg) # Check that the length of each array is consistent - expected_len = len(self.age) + expected_len = len(self) expected_strains = self.pars['n_strains'] for key in self.keys(): if self[key].ndim == 1: diff --git a/covasim/defaults.py b/covasim/defaults.py index 2bf635a32..df56d19ae 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -266,6 +266,7 @@ def get_default_colors(): c.tests = '#aaa8ff' c.diagnoses = '#5f5cd2' c.diagnosed = c.diagnoses + c.rediagnoses = c.diagnoses c.quarantined = '#5c399c' c.vaccinations = c.quarantined # TODO: new color c.vaccinated = c.quarantined @@ -280,7 +281,6 @@ def get_default_colors(): c.pop_nabs = '#32733d' c.pop_protection = '#9e1149' c.pop_symp_protection = '#b86113' - c.rediagnoses = '#b86113' return c diff --git a/covasim/sim.py b/covasim/sim.py index b1b68c877..7ad5c4b14 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -319,7 +319,6 @@ def init_res(*args, **kwargs): self.results['pop_nabs'] = init_res('Population nab levels', scale=False, color=dcols.pop_nabs) self.results['pop_protection'] = init_res('Population immunity protection', scale=False, color=dcols.pop_protection) self.results['pop_symp_protection'] = init_res('Population symptomatic protection', scale=False, color=dcols.pop_symp_protection) - self.results['frac_reinfected'] = init_res('Proportion reinfected', scale=False) # Handle strains ns = self['n_strains'] @@ -848,11 +847,11 @@ def compute_states(self): self.results['n_removed'][:] = count_recov*res['cum_recoveries'][:] + res['cum_deaths'][:] # Calculate the number removed: recovered + dead self.results['prevalence'][:] = res['n_exposed'][:]/res['n_alive'][:] # Calculate the prevalence self.results['incidence'][:] = res['new_infections'][:]/res['n_susceptible'][:] # Calculate the incidence + self.results['frac_vaccinated'][:] = res['n_vaccinated'][:]/res['n_alive'][:] # Calculate the fraction vaccinated self.results['strain']['incidence_by_strain'][:] = np.einsum('ji,i->ji',res['strain']['new_infections_by_strain'][:], 1/res['n_susceptible'][:]) # Calculate the incidence self.results['strain']['prevalence_by_strain'][:] = np.einsum('ji,i->ji',res['strain']['new_infections_by_strain'][:], 1/res['n_alive'][:]) # Calculate the prevalence - self.results['frac_vaccinated'][:] = res['n_vaccinated'][:]/res['n_alive'][:] # Calculate the share vaccinated - self.results['frac_reinfected'][:] = res['new_reinfections'][:]/res['new_infections'][:] + return From 87418471fccfae0e4de57178b11409db5e45aab8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 01:37:39 -0700 Subject: [PATCH 551/569] remove rediagnoses for now --- covasim/defaults.py | 1 - 1 file changed, 1 deletion(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index df56d19ae..6fe38ddb7 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -63,7 +63,6 @@ def __init__(self): 'critical', 'tested', 'diagnosed', - 'prior_diagnosed', # a previous infection was diagnosed 'recovered', 'dead', 'known_contact', From 247754dc30c56251849a27e86856068db28f1dc1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 01:47:38 -0700 Subject: [PATCH 552/569] update states --- covasim/defaults.py | 2 -- covasim/people.py | 11 ++--------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 6fe38ddb7..7b75a9080 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -167,7 +167,6 @@ def __init__(self): 'deaths': 'deaths', 'tests': 'tests', 'diagnoses': 'diagnoses', - 'rediagnoses': 'rediagnoses', 'quarantined': 'quarantined people', 'vaccinations': 'vaccinations', 'vaccinated': 'vaccinated people' @@ -265,7 +264,6 @@ def get_default_colors(): c.tests = '#aaa8ff' c.diagnoses = '#5f5cd2' c.diagnosed = c.diagnoses - c.rediagnoses = c.diagnoses c.quarantined = '#5c399c' c.vaccinations = c.quarantined # TODO: new color c.vaccinated = c.quarantined diff --git a/covasim/people.py b/covasim/people.py index f98bf3738..e19c729c4 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -283,18 +283,15 @@ def check_recovery(self, inds=None, filter_inds='is_exp'): mild_inds = self.check_inds(self.susceptible, self.date_symptomatic, filter_inds=inds) severe_inds = self.check_inds(self.susceptible, self.date_severe, filter_inds=inds) - # determine who was diagnosed and set their prior diagnosis to true and current diagnosis to false - inds_diagnosed = cvu.true(self.diagnosed[inds]) - self.prior_diagnosed[inds[inds_diagnosed]] = True - self.diagnosed[inds] = False - # Reset additional states self.susceptible[inds] = True + self.diagnosed[inds] = False # Reset their diagnosis state because they might be reinfected self.prior_symptoms[inds] = self.pars['rel_imm_symp']['asymp'] self.prior_symptoms[mild_inds] = self.pars['rel_imm_symp']['mild'] self.prior_symptoms[severe_inds] = self.pars['rel_imm_symp']['severe'] if len(inds): cvi.init_nab(self, inds, prior_inf=True) + return len(inds) @@ -337,10 +334,6 @@ def check_diagnosed(self): self.date_end_quarantine[quarantined] = self.t # Set end quarantine date to match when the person left quarantine (and entered isolation) self.quarantined[diag_inds] = False # If you are diagnosed, you are isolated, not in quarantine - # Determine how many people who are diagnosed today have a prior diagnosis - prior_diag_inds = cvu.true(self.prior_diagnosed[test_pos_inds]) - self.flows['new_rediagnoses'] += len(prior_diag_inds) - return len(test_pos_inds) From 072ca1574db85205f45d7675c494cc67074a1c4a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 01:51:55 -0700 Subject: [PATCH 553/569] tidying --- covasim/interventions.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index e3e334ba7..20a01a61b 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1322,14 +1322,14 @@ def initialize(self, sim): if sim['verbose']: print('Note: No cross-immunity specified for vaccine {self.label} and strain {key}, setting to 1.0') self.p[key] = val + self.days = process_days(sim, self.days) # days that group becomes eligible - self.first_dose_nab_days = [None]*(sim['n_days']+1) # inds who get nabs from first dose - self.second_dose_nab_days = [None]*(sim['n_days']+1) # inds who get nabs from second dose (if relevant) - self.second_dose_days = [None]*(sim['n_days']+1) # inds who get second dose (if relevant) - self.vaccinated = [None]*(sim['n_days']+1) # keep track of inds of people vaccinated on each day - self.vaccinations = np.zeros(sim['pop_size'], dtype=cvd.default_int) # Number of doses given per person - self.vaccination_dates = np.full(sim['pop_size'], np.nan) # Store the dates when people are vaccinated - self.vaccine_nab_dates = np.full(sim['pop_size'], np.nan) # Store the dates when people get their new vaccine nabs + self.first_dose_nab_days = [None]*sim.npts # People who get nabs from first dose + self.second_dose_nab_days = [None]*sim.npts # People who get nabs from second dose (if relevant) + self.second_dose_days = [None]*sim.npts # People who get second dose (if relevant) + self.vaccinated = [None]*sim.npts # Keep track of inds of people vaccinated on each day + self.vaccinations = np.zeros(sim['pop_size'], dtype=cvd.default_int) # Number of doses given per person + self.vaccination_dates = np.full(sim['pop_size'], np.nan) # Store the dates when people are vaccinated sim['vaccine_pars'][self.label] = self.p # Store the parameters self.index = list(sim['vaccine_pars'].keys()).index(self.label) # Find where we are in the list sim['vaccine_map'][self.index] = self.label # Use that to populate the reverse mapping @@ -1386,8 +1386,6 @@ def apply(self, sim): self.vaccinations[vacc_inds] += 1 self.vaccination_dates[vacc_inds] = sim.t - self.vaccine_nab_dates[vacc_inds] = (sim.t + self.p.interval) if self.p.interval else sim.t - # Update vaccine attributes in sim sim.people.vaccinations[vacc_inds] = self.vaccinations[vacc_inds] From 3466ca47e9465826a35eb8f909da3aaaa6e6811e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 01:54:02 -0700 Subject: [PATCH 554/569] tidying --- covasim/interventions.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 20a01a61b..041427b5d 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1342,12 +1342,10 @@ def apply(self, sim): if sim.t >= np.min(self.days): - # Initialize - vacc_probs = np.zeros(sim['pop_size']) - vacc_inds = np.array([], dtype=int) - # Vaccinate people with their first dose + vacc_inds = np.array([], dtype=int) # Initialize in case no one gets their first dose for ind in find_day(self.days, sim.t, interv=self, sim=sim): + vacc_probs = np.zeros(sim['pop_size']) unvacc_inds = sc.findinds(~sim.people.vaccinated) if self.subtarget is not None: subtarget_inds, subtarget_vals = get_subtargets(self.subtarget, sim) @@ -1361,7 +1359,7 @@ def apply(self, sim): if len(vacc_inds): self.vaccinated[sim.t] = vacc_inds sim.people.flows['new_vaccinations'] += len(vacc_inds) - sim.people.flows['new_vaccinated'] += len(vacc_inds) + sim.people.flows['new_vaccinated'] += len(vacc_inds) if self.p.interval is not None: next_dose_day = sim.t + self.p.interval if next_dose_day < sim['n_days']: From 08d004209a959ce32a5323dfe7a8cdc001034f44 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 02:01:59 -0700 Subject: [PATCH 555/569] update changelog --- CHANGELOG.rst | 8 ++++++++ covasim/defaults.py | 14 -------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 23a786e3f..a79243997 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,14 @@ Latest versions (3.0.x) ~~~~~~~~~~~~~~~~~~~~~~~ +Version 3.0.2 (2021-04-26) +-------------------------- +- Added a new method, ``sim.to_df()``, that exports results to a pandas dataframe. +- Added ``people.lock()`` and ``people.unlock()`` methods, so you do not need to set ``people._lock`` manually. +- Added extra parameter checking to ``people.set_pars(pars)``, so ``pop_size`` is guaranteed to be an integer. +- *GitHub info*: PR `1020 `__ + + Version 3.0.1 (2021-04-16) -------------------------- - Immunity and vaccine parameters have been updated. diff --git a/covasim/defaults.py b/covasim/defaults.py index 7b75a9080..76e6a9f1c 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -188,20 +188,6 @@ def __init__(self): new_result_flows_by_strain = [f'new_{key}' for key in result_flows_by_strain.keys()] cum_result_flows_by_strain = [f'cum_{key}' for key in result_flows_by_strain.keys()] -# Define results that are floats (not numbers of people) -float_results = [ - 'prevalence', - 'incidence', - 'r_eff', - 'doubling_time', - 'test_yield', - 'rel_test_yield', - 'frac_vaccinated', - 'pop_nabs', - 'pop_protection', - 'pop_symp_protection', -] - # Parameters that can vary by strain strain_pars = [ 'rel_imm_strain', From 8437c031d29b022aac0e5118b5fc02daf7c82210 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 26 Apr 2021 02:10:11 -0700 Subject: [PATCH 556/569] update changelog and version --- CHANGELOG.rst | 7 +++++++ covasim/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a79243997..aae4978ef 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,9 +27,16 @@ Latest versions (3.0.x) Version 3.0.2 (2021-04-26) -------------------------- +- Added Novavax as one of the default vaccines. +- If ``use_waning=True``, people will now become *undiagnosed* when they recover (so they are not incorrectly marked as diagnosed if they become reinfected). - Added a new method, ``sim.to_df()``, that exports results to a pandas dataframe. - Added ``people.lock()`` and ``people.unlock()`` methods, so you do not need to set ``people._lock`` manually. - Added extra parameter checking to ``people.set_pars(pars)``, so ``pop_size`` is guaranteed to be an integer. +- Flattened ``sim['immunity']`` to no longer have separate axes for susceptible, symptomatic, and severe. +- Fixed a bug in ``cv.sequence()``, introduced in version 2.1.2, that meant it would only ever trigger the last intervention. +- Fixed a bug where if subtargeting was used with ``cv.vaccinate()``, it would trigger on every day. +- Fixed ``msim.compare()`` to be more careful about not converting all results to integers. +- *Regression information*: If you are using waning, ``sim.people.diagnosed`` no longer refers to everyone who has ever been diagnosed, only those still infectious. You can use ``sim.people.defined('date_diagnosed')`` in place of ``sim.people.true('diagnosed')`` (before these were identical). - *GitHub info*: PR `1020 `__ diff --git a/covasim/version.py b/covasim/version.py index 051e2ebb5..7fcae3a69 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -5,5 +5,5 @@ __all__ = ['__version__', '__versiondate__', '__license__'] __version__ = '3.0.2' -__versiondate__ = '2021-04-23' +__versiondate__ = '2021-04-26' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' From 3728196602aabcd611b21921b45c5d1cec098bd6 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Thu, 29 Apr 2021 09:06:42 +0200 Subject: [PATCH 557/569] first pass --- covasim/defaults.py | 8 ++++++++ covasim/people.py | 41 +++++++++++++++++++++++------------------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index 76e6a9f1c..cd431603f 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -64,6 +64,7 @@ def __init__(self): 'tested', 'diagnosed', 'recovered', + 'known_dead', 'dead', 'known_contact', 'quarantined', @@ -146,6 +147,7 @@ def __init__(self): 'recovered': 'Number recovered', 'dead': 'Number dead', 'diagnosed': 'Number of confirmed cases', + 'known_dead': 'Number of confirmed deaths', 'quarantined': 'Number in quarantine', 'vaccinated': 'Number of people vaccinated', } @@ -167,6 +169,7 @@ def __init__(self): 'deaths': 'deaths', 'tests': 'tests', 'diagnoses': 'diagnoses', + 'known_deaths': 'known deaths', 'quarantined': 'quarantined people', 'vaccinations': 'vaccinations', 'vaccinated': 'vaccinated people' @@ -260,6 +263,8 @@ def get_default_colors(): c.critical = '#b86113' c.deaths = '#000000' c.dead = c.deaths + c.known_dead = c.deaths + c.known_deaths = c.deaths c.default = '#000000' c.pop_nabs = '#32733d' c.pop_protection = '#9e1149' @@ -273,6 +278,7 @@ def get_default_colors(): 'cum_severe', 'cum_critical', 'cum_deaths', + 'cum_known_deaths', 'cum_diagnoses', 'new_infections', 'new_severe', @@ -332,6 +338,7 @@ def get_default_plots(which='default', kind='sim', sim=None): 'cum_severe', 'cum_critical', 'cum_deaths', + 'cum_known_deaths', ], }) @@ -345,6 +352,7 @@ def get_default_plots(which='default', kind='sim', sim=None): ], 'Cumulative deaths': [ 'cum_deaths', + 'cum_known_deaths', ], }) diff --git a/covasim/people.py b/covasim/people.py index e19c729c4..500866661 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -168,16 +168,18 @@ def update_states_pre(self, t): # Initialize self.t = t - self.is_exp = self.true('exposed') # For storing the interim values since used in every subsequent calculation + self.is_exp = self.true('exposed') # For storing the interim values since used in every subsequent calculation # Perform updates self.init_flows() - self.flows['new_infectious'] += self.check_infectious() # For people who are exposed and not infectious, check if they begin being infectious - self.flows['new_symptomatic'] += self.check_symptomatic() - self.flows['new_severe'] += self.check_severe() - self.flows['new_critical'] += self.check_critical() - self.flows['new_deaths'] += self.check_death() - self.flows['new_recoveries'] += self.check_recovery() # TODO: check logic here + self.flows['new_infectious'] += self.check_infectious() # For people who are exposed and not infectious, check if they begin being infectious + self.flows['new_symptomatic'] += self.check_symptomatic() + self.flows['new_severe'] += self.check_severe() + self.flows['new_critical'] += self.check_critical() + self.flows['new_recoveries'] += self.check_recovery() + new_deaths, new_known_deaths = self.check_death() + self.flows['new_deaths'] += new_deaths + self.flows['new_known_deaths'] += new_known_deaths return @@ -298,20 +300,23 @@ def check_recovery(self, inds=None, filter_inds='is_exp'): def check_death(self): ''' Check whether or not this person died on this timestep ''' inds = self.check_inds(self.dead, self.date_dead, filter_inds=self.is_exp) - self.susceptible[inds] = False - self.exposed[inds] = False - self.infectious[inds] = False - self.symptomatic[inds] = False - self.severe[inds] = False - self.critical[inds] = False - self.known_contact[inds] = False - self.quarantined[inds] = False - self.recovered[inds] = False - self.dead[inds] = True + self.dead[inds] = True + # Check whether the person was diagnosed before dying + diag_inds = self.check_inds(self.dead, self.date_dead, filter_inds=self.true('diagnosed') ) + self.known_dead[diag_inds] = True + self.susceptible[inds] = False + self.exposed[inds] = False + self.infectious[inds] = False + self.symptomatic[inds] = False + self.severe[inds] = False + self.critical[inds] = False + self.known_contact[inds] = False + self.quarantined[inds] = False + self.recovered[inds] = False self.infectious_strain[inds] = np.nan self.exposed_strain[inds] = np.nan self.recovered_strain[inds] = np.nan - return len(inds) + return len(inds), len(diag_inds) def check_diagnosed(self): From 4849e584bb6e3c4eb8e6e2176d4fb64448bcef41 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Thu, 29 Apr 2021 17:01:25 +0200 Subject: [PATCH 558/569] seems ok --- covasim/people.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index 500866661..33aee057a 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -301,8 +301,7 @@ def check_death(self): ''' Check whether or not this person died on this timestep ''' inds = self.check_inds(self.dead, self.date_dead, filter_inds=self.is_exp) self.dead[inds] = True - # Check whether the person was diagnosed before dying - diag_inds = self.check_inds(self.dead, self.date_dead, filter_inds=self.true('diagnosed') ) + diag_inds = inds[self.diagnosed[inds]] # Check whether the person was diagnosed before dying self.known_dead[diag_inds] = True self.susceptible[inds] = False self.exposed[inds] = False From 9ac047ffda1b4f272bab8904aa4cd71e40821a85 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 3 May 2021 15:37:40 +0200 Subject: [PATCH 559/569] change immunity assumption --- covasim/parameters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index d5bd4b752..33adf2a43 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -370,7 +370,7 @@ def get_strain_pars(default=False): ), b1351 = dict( - rel_imm_strain = 0.066, # Immunity protection obtained from a natural infection with wild type, relative to wild type. TODO: add source + rel_imm_strain = 1.0, # Immunity protection obtained from a natural infection with wild type, relative to wild type. TODO: add source rel_beta = 1.4, rel_symp_prob = 1.0, rel_severe_prob = 1.4, @@ -416,9 +416,9 @@ def get_cross_immunity(default=False): b1351 = dict( wild = 0.066, # https://www.nature.com/articles/s41586-021-03471-w - b117 = 0.1, # Assumption + b117 = 0.5, # Assumption b1351 = 1.0, # Default for own-immunity - p1 = 0.1, # Assumption + p1 = 0.5, # Assumption ), p1 = dict( From ba8081fed251352b1279f6ea012d8b23b0c0ee76 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 14 May 2021 12:01:18 -0700 Subject: [PATCH 560/569] first implementation of calibration --- covasim/analysis.py | 144 +++++++++++++++++++++++++++++++++++++++++++- covasim/sim.py | 11 +--- 2 files changed, 146 insertions(+), 9 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index bd8ba5c1d..b7bd52a03 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -3,6 +3,7 @@ but which are useful for particular investigations. ''' +import os import numpy as np import pylab as pl import pandas as pd @@ -12,9 +13,15 @@ from . import interventions as cvi from . import settings as cvset from . import plotting as cvpl +from . import run as cvr +try: + import optuna as op +except ImportError as E: # pragma: no cover + errormsg = f'Optuna import failed ({str(E)}), please install first (pip install optuna)' + op = ImportError(errormsg) -__all__ = ['Analyzer', 'snapshot', 'age_histogram', 'daily_age_stats', 'daily_stats', 'Fit', 'TransTree'] +__all__ = ['Analyzer', 'snapshot', 'age_histogram', 'daily_age_stats', 'daily_stats', 'Fit', 'Calibration', 'TransTree'] class Analyzer(sc.prettyobj): @@ -1223,6 +1230,141 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No return fig + +class Calibration(Analyzer): + ''' + A class to handle calibration of Covasim simulations. + ''' + + def __init__(self, sim, calib_pars=None, label=None, **kwargs): + super().__init__(label=label) # Initialize the Analyzer object + if isinstance(op, Exception): raise op # If Optuna failed to import, raise that exception now + self.sim = sim + self.calib_pars = calib_pars + self.results = None + self.init_optuna(**kwargs) + return + + + def init_optuna(self, **kwargs): + ''' Create a (mutable) dictionary for global settings ''' + g = sc.objdict() # g for "global" -- probably should rename + g.name = kwargs.pop('name', 'covasim') + g.db_name = kwargs.pop('db_name', f'{g.name}.db') + g.storage = kwargs.pop('storage', f'sqlite:///{g.db_name}') + g.n_workers = kwargs.pop('n_workers', 4) # Define how many workers to run in parallel + g.n_trials = kwargs.pop('n_trials', 100) # Define the number of trials, i.e. sim runs, per worker + self.g = g + if len(kwargs): + errormsg = f'Did not recognize keys {sc.strjoin(kwargs.keys())}' + raise ValueError(errormsg) + return + + + def run_sim(self, calib_pars, label=None, return_sim=False): + ''' Create and run a simulation ''' + sim = self.sim.copy() + if label: + sim.label = label + sim.update_pars(calib_pars) + sim.run() + sim.compute_fit() + if return_sim: + return sim + else: + return sim.fit.mismatch + + + def run_trial(self, trial): + ''' Define the objective for Optuna ''' + pars = {} + for key, (best,low,high) in self.calib_pars.items(): + pars[key] = trial.suggest_uniform(key, low, high) # Sample from beta values within this range + mismatch = self.run_sim(pars) + return mismatch + + + def worker(self): + ''' Run a single worker ''' + study = op.load_study(storage=self.g.storage, study_name=self.g.name) + output = study.optimize(self.run_trial, n_trials=self.g.n_trials) + return output + + + def run_workers(self): + ''' Run multiple workers in parallel ''' + output = sc.parallelize(self.worker, self.g.n_workers) + return output + + + def make_study(self): + ''' Make a study, deleting one if it already exists ''' + if os.path.exists(self.g.db_name): + os.remove(self.g.db_name) + print(f'Removed existing calibration {self.g.db_name}') + output = op.create_study(storage=self.g.storage, study_name=self.g.name) + return output + + + def calibrate(self, calib_pars=None, **kwargs): + ''' Actually perform calibration ''' + + # Load and validate calibration parameters + if calib_pars is not None: + self.calib_pars = calib_pars + if self.calib_pars is None: + errormsg = 'You must supply calibration parameters either when creating the calibration object or when calling calibrate().' + raise ValueError(errormsg) + + # Update optuna settings + to_pop = [] + for k,v in kwargs.items(): + if k in self.g: + self.g[k] = v + to_pop.append(k) + for k in to_pop: + kwargs.pop(k) + + # Run the optimization + t0 = sc.tic() + self.make_study() + self.run_workers() + study = op.load_study(storage=self.g.storage, study_name=self.g.name) + self.best_pars = study.best_params + T = sc.toc(t0, output=True) + print(f'Output: {self.best_pars}, time: {T}') + + # Compare the results + self.initial_pars = {k:v[0] for k,v in self.calib_pars.items()} + self.before = self.run_sim(calib_pars=self.initial_pars, label='Before calibration', return_sim=True) + self.after = self.run_sim(calib_pars=self.best_pars, label='After calibration', return_sim=True) + return + + + def summarize(self): + try: + before = self.before.fit.mismatch + after = self.after.fit.mismatch + print('Initial parameter values:') + print(self.initial_pars) + print('Best parameter values:') + print(self.best_pars) + print(f'Mismatch before calibration: {before:n}') + print(f'Mismatch after calibration: {after:n}') + print(f'Percent improvement: {(1-(before-after)/before)*100:0.1f}%') + return before, after + except Exception as E: + errormsg = 'Could not get summary, have you run the calibration?' + raise RuntimeError(errormsg) from E + + + def plot(self, **kwargs): + msim = cvr.MultiSim([self.before, self.after]) + fig = msim.plot(**kwargs) + return fig + + + class TransTree(Analyzer): ''' A class for holding a transmission tree. There are several different representations diff --git a/covasim/sim.py b/covasim/sim.py index 7ad5c4b14..58e38c1d8 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -1147,13 +1147,12 @@ def brief(self, output=False): return string - def compute_fit(self, output=True, *args, **kwargs): + def compute_fit(self, *args, **kwargs): ''' Compute the fit between the model and the data. See cv.Fit() for more information. Args: - output (bool): whether or not to return the TransTree; if not, store in sim.results args (list): passed to cv.Fit() kwargs (dict): passed to cv.Fit() @@ -1164,12 +1163,8 @@ def compute_fit(self, output=True, *args, **kwargs): fit = sim.compute_fit() fit.plot() ''' - fit = cva.Fit(self, *args, **kwargs) - if output: - return fit - else: - self.results.fit = fit - return + self.fit = cva.Fit(self, *args, **kwargs) + return self.fit def make_age_histogram(self, *args, output=True, **kwargs): From 0f15ab48a824bc20eab5c76c5ccfc6d57f4494d4 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 14 May 2021 12:52:18 -0700 Subject: [PATCH 561/569] update version and baseline --- covasim/sim.py | 16 ++++++++++++++++ covasim/version.py | 4 ++-- tests/baseline.json | 3 +++ tests/benchmark.json | 6 +++--- tests/requirements_test.txt | 3 ++- tests/test_analysis.py | 38 +++++++++++++++++++++++++++++++------ 6 files changed, 58 insertions(+), 12 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 58e38c1d8..9bb55b398 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -1167,6 +1167,22 @@ def compute_fit(self, *args, **kwargs): return self.fit + def calibrate(self, calib_pars, **kwargs): + ''' + Automtaically calibrate the simulation, returning a Calibration object. + + Args: + calib_pars (dict): a dictionary of the parameters to calibrate of the format dict(key1=[best, low, high]) + kwargs (dict): passed to cv.Calibration() + + Returns: + calib: a Calibration object + ''' + calib = cva.Calibration(sim=self, calib_pars=calib_pars, **kwargs) + calib.calibrate() + return calib + + def make_age_histogram(self, *args, output=True, **kwargs): ''' Calculate the age histograms of infections, deaths, diagnoses, etc. See diff --git a/covasim/version.py b/covasim/version.py index 7fcae3a69..c1b113cfb 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,6 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '3.0.2' -__versiondate__ = '2021-04-26' +__version__ = '3.0.3' +__versiondate__ = '2021-05-14' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' diff --git a/tests/baseline.json b/tests/baseline.json index 33736bfd6..812ba1ca6 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -10,6 +10,7 @@ "cum_deaths": 30.0, "cum_tests": 10783.0, "cum_diagnoses": 3867.0, + "cum_known_deaths": 23.0, "cum_quarantined": 4092.0, "cum_vaccinations": 0.0, "cum_vaccinated": 0.0, @@ -23,6 +24,7 @@ "new_deaths": 3.0, "new_tests": 195.0, "new_diagnoses": 45.0, + "new_known_deaths": 3.0, "new_quarantined": 153.0, "new_vaccinations": 0.0, "new_vaccinated": 0.0, @@ -35,6 +37,7 @@ "n_recovered": 8551.0, "n_dead": 30.0, "n_diagnosed": 3867.0, + "n_known_dead": 23.0, "n_quarantined": 3938.0, "n_vaccinated": 0.0, "n_alive": 19970.0, diff --git a/tests/benchmark.json b/tests/benchmark.json index f13df0422..f20bef0fe 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,12 +1,12 @@ { "time": { - "initialize": 0.413, - "run": 0.496 + "initialize": 0.402, + "run": 0.491 }, "parameters": { "pop_size": 20000, "pop_type": "hybrid", "n_days": 60 }, - "cpu_performance": 0.9599341238861429 + "cpu_performance": 0.8012151421258092 } \ No newline at end of file diff --git a/tests/requirements_test.txt b/tests/requirements_test.txt index f44ca7f56..0a408d037 100644 --- a/tests/requirements_test.txt +++ b/tests/requirements_test.txt @@ -1,3 +1,4 @@ pytest pytest-cov -pytest-parallel \ No newline at end of file +pytest-parallel +optuna \ No newline at end of file diff --git a/tests/test_analysis.py b/tests/test_analysis.py index f00904fb9..a2e4a5682 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -129,6 +129,31 @@ def simple(actual, predicted, scale=2): return fit1 +def test_calibration(): + sc.heading('Testing calibration') + + pars = dict( + verbose = 0, + start_day = '2020-02-05', + pop_size = 1e3, + pop_scale = 4, + interventions = cv.test_prob(0.1), + ) + + sim = cv.Sim(pars, datafile='example_data.csv') + + calib_pars = dict( + beta = [0.016, 0.005, 0.020], + rel_death_prob = [1.0, 0.5, 2.0], + ) + + calib = cv.Calibration(sim, calib_pars, n_trials=10) + calib.calibrate() + calib.plot(to_plot=['cum_deaths', 'cum_diagnoses']) + + return calib + + def test_transtree(): sc.heading('Testing transmission tree') @@ -159,12 +184,13 @@ def test_transtree(): cv.options.set(interactive=do_plot) T = sc.tic() - snapshot = test_snapshot() - agehist = test_age_hist() - daily_age = test_daily_age() - daily = test_daily_stats() - fit = test_fit() - transtree = test_transtree() + # snapshot = test_snapshot() + # agehist = test_age_hist() + # daily_age = test_daily_age() + # daily = test_daily_stats() + # fit = test_fit() + calib = test_calibration() + # transtree = test_transtree() print('\n'*2) sc.toc(T) From 6efee2594c181036323bcd9bb82fe898ce96d1d1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 14 May 2021 13:39:04 -0700 Subject: [PATCH 562/569] update calibration object --- CHANGELOG.rst | 7 ++++ covasim/analysis.py | 76 ++++++++++++++++++++++++++---------------- covasim/sim.py | 16 +++++++-- tests/test_analysis.py | 28 ++++++++++------ 4 files changed, 84 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index aae4978ef..3669056a3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,13 @@ Latest versions (3.0.x) ~~~~~~~~~~~~~~~~~~~~~~~ +Version 3.0.3 (2021-05-14) +-------------------------- +- Added a new class, ``cv.Calibration``, that can perform automatic calibration. Simplest usage is ``sim.calibrate(calib_pars)``. +- *Regression information*: If you are using waning, ``sim.people.diagnosed`` no longer refers to everyone who has ever been diagnosed, only those still infectious. You can use ``sim.people.defined('date_diagnosed')`` in place of ``sim.people.true('diagnosed')`` (before these were identical). +- *GitHub info*: PR `1047 `__ + + Version 3.0.2 (2021-04-26) -------------------------- - Added Novavax as one of the default vaccines. diff --git a/covasim/analysis.py b/covasim/analysis.py index b7bd52a03..37c1f6e6a 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1234,39 +1234,57 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No class Calibration(Analyzer): ''' A class to handle calibration of Covasim simulations. + + Args: + sim (Sim): the simulation to calibrate + calib_pars (dict): a dictionary of the parameters to calibrate of the format dict(key1=[best, low, high]) + custom_fn (function): a custom function for modifying the simulation; receives the sim and calib_pars as inputs, should return the modified sim + label (str): a label for this calibration object + + kwargs (dict): passed to cv.Calibration() + + Returns: + A Calibration object + + **Example**:: + + sim = cv.Sim(datafile='data.csv') + calib_pars = dict(beta=[0.015, 0.010, 0.020]) + calib = sim.calibrate(calib_pars) + calib.plot() ''' - def __init__(self, sim, calib_pars=None, label=None, **kwargs): + def __init__(self, sim, calib_pars=None, custom_fn=None, label=None, name=None, db_name=None, storage=None, n_workers=None, n_trials=None): super().__init__(label=label) # Initialize the Analyzer object if isinstance(op, Exception): raise op # If Optuna failed to import, raise that exception now - self.sim = sim - self.calib_pars = calib_pars - self.results = None - self.init_optuna(**kwargs) - return + # Handle arguments + if name is None: name = 'covasim_calibration' + if db_name is None: db_name = f'{name}.db' + if storage is None: storage = f'sqlite:///{db_name}' + if n_workers is None: n_workers = 4 + if n_trials is None: n_trials = 20 + self.sim = sim + self.calib_pars = calib_pars + self.custom_fn = custom_fn + self.run_args = sc.objdict(name=name, db_name=db_name, storage=storage, n_workers=n_workers, n_trials=n_trials) - def init_optuna(self, **kwargs): - ''' Create a (mutable) dictionary for global settings ''' - g = sc.objdict() # g for "global" -- probably should rename - g.name = kwargs.pop('name', 'covasim') - g.db_name = kwargs.pop('db_name', f'{g.name}.db') - g.storage = kwargs.pop('storage', f'sqlite:///{g.db_name}') - g.n_workers = kwargs.pop('n_workers', 4) # Define how many workers to run in parallel - g.n_trials = kwargs.pop('n_trials', 100) # Define the number of trials, i.e. sim runs, per worker - self.g = g - if len(kwargs): - errormsg = f'Did not recognize keys {sc.strjoin(kwargs.keys())}' - raise ValueError(errormsg) return def run_sim(self, calib_pars, label=None, return_sim=False): ''' Create and run a simulation ''' sim = self.sim.copy() - if label: - sim.label = label - sim.update_pars(calib_pars) + if label: sim.label = label + valid_pars = {k:v for k,v in calib_pars.items() if k in sim.pars} + sim.update_pars(valid_pars) + if self.custom_fn: + sim = self.custom_fn(sim, calib_pars) + else: + if len(valid_pars) != len(calib_pars): + extra = set(calib_pars.keys()) - set(valid_pars.keys()) + errormsg = f'The following parameters are not part of the sim, nor is a custom function specified to use them: {sc.strjoin(extra)}' + raise ValueError(errormsg) sim.run() sim.compute_fit() if return_sim: @@ -1286,23 +1304,23 @@ def run_trial(self, trial): def worker(self): ''' Run a single worker ''' - study = op.load_study(storage=self.g.storage, study_name=self.g.name) - output = study.optimize(self.run_trial, n_trials=self.g.n_trials) + study = op.load_study(storage=self.run_args.storage, study_name=self.run_args.name) + output = study.optimize(self.run_trial, n_trials=self.run_args.n_trials) return output def run_workers(self): ''' Run multiple workers in parallel ''' - output = sc.parallelize(self.worker, self.g.n_workers) + output = sc.parallelize(self.worker, iterarg=self.run_args.n_workers) return output def make_study(self): ''' Make a study, deleting one if it already exists ''' - if os.path.exists(self.g.db_name): - os.remove(self.g.db_name) - print(f'Removed existing calibration {self.g.db_name}') - output = op.create_study(storage=self.g.storage, study_name=self.g.name) + if os.path.exists(self.run_args.db_name): + os.remove(self.run_args.db_name) + print(f'Removed existing calibration {self.run_args.db_name}') + output = op.create_study(storage=self.run_args.storage, study_name=self.run_args.name) return output @@ -1329,7 +1347,7 @@ def calibrate(self, calib_pars=None, **kwargs): t0 = sc.tic() self.make_study() self.run_workers() - study = op.load_study(storage=self.g.storage, study_name=self.g.name) + study = op.load_study(storage=self.run_args.storage, study_name=self.run_args.name) self.best_pars = study.best_params T = sc.toc(t0, output=True) print(f'Output: {self.best_pars}, time: {T}') diff --git a/covasim/sim.py b/covasim/sim.py index 9bb55b398..a9683635d 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -1156,9 +1156,12 @@ def compute_fit(self, *args, **kwargs): args (list): passed to cv.Fit() kwargs (dict): passed to cv.Fit() + Returns: + A Fit object + **Example**:: - sim = cv.Sim(datafile=data.csv) + sim = cv.Sim(datafile='data.csv') sim.run() fit = sim.compute_fit() fit.plot() @@ -1169,14 +1172,21 @@ def compute_fit(self, *args, **kwargs): def calibrate(self, calib_pars, **kwargs): ''' - Automtaically calibrate the simulation, returning a Calibration object. + Automatically calibrate the simulation, returning a Calibration object. Args: calib_pars (dict): a dictionary of the parameters to calibrate of the format dict(key1=[best, low, high]) kwargs (dict): passed to cv.Calibration() Returns: - calib: a Calibration object + A Calibration object + + **Example**:: + + sim = cv.Sim(datafile='data.csv') + calib_pars = dict(beta=[0.015, 0.010, 0.020]) + calib = sim.calibrate(calib_pars) + calib.plot() ''' calib = cva.Calibration(sim=self, calib_pars=calib_pars, **kwargs) calib.calibrate() diff --git a/tests/test_analysis.py b/tests/test_analysis.py index a2e4a5682..2559b76de 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -137,20 +137,26 @@ def test_calibration(): start_day = '2020-02-05', pop_size = 1e3, pop_scale = 4, - interventions = cv.test_prob(0.1), + interventions = [cv.test_prob(symp_prob=0.1)], ) sim = cv.Sim(pars, datafile='example_data.csv') calib_pars = dict( - beta = [0.016, 0.005, 0.020], - rel_death_prob = [1.0, 0.5, 2.0], + beta = [0.013, 0.005, 0.020], + test_prob = [0.01, 0.00, 0.30] ) - calib = cv.Calibration(sim, calib_pars, n_trials=10) - calib.calibrate() + def set_test_prob(sim, calib_pars): + tp = sim.get_intervention(cv.test_prob) + tp.symp_prob = calib_pars['test_prob'] + return sim + + calib = sim.calibrate(calib_pars=calib_pars, custom_fn=set_test_prob, n_trials=5) calib.plot(to_plot=['cum_deaths', 'cum_diagnoses']) + assert calib.after.fit.mismatch < calib.before.fit.mismatch + return calib @@ -184,13 +190,13 @@ def test_transtree(): cv.options.set(interactive=do_plot) T = sc.tic() - # snapshot = test_snapshot() - # agehist = test_age_hist() - # daily_age = test_daily_age() - # daily = test_daily_stats() - # fit = test_fit() + snapshot = test_snapshot() + agehist = test_age_hist() + daily_age = test_daily_age() + daily = test_daily_stats() + fit = test_fit() calib = test_calibration() - # transtree = test_transtree() + transtree = test_transtree() print('\n'*2) sc.toc(T) From 6fa92e6e7aa7421ae5acbf9ab2ac83d5f600664a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 14 May 2021 14:05:38 -0700 Subject: [PATCH 563/569] update changelog --- CHANGELOG.rst | 6 ++- covasim/analysis.py | 65 ++++++++++++++++------------ docs/tutorials/tut_calibration.ipynb | 2 +- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3669056a3..1340c5606 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,8 +27,10 @@ Latest versions (3.0.x) Version 3.0.3 (2021-05-14) -------------------------- -- Added a new class, ``cv.Calibration``, that can perform automatic calibration. Simplest usage is ``sim.calibrate(calib_pars)``. -- *Regression information*: If you are using waning, ``sim.people.diagnosed`` no longer refers to everyone who has ever been diagnosed, only those still infectious. You can use ``sim.people.defined('date_diagnosed')`` in place of ``sim.people.true('diagnosed')`` (before these were identical). +- Added a new class, ``cv.Calibration``, that can perform automatic calibration. Simplest usage is ``sim.calibrate(calib_pars)``. Note: this requires Optuna, which is not installed by default; please install separately via ``pip install optuna``. +- Added a new result, ``known_deaths``, which counts only deaths among people who have been diagnosed. +- ``sim.compute_fit()`` now returns the fit by default, and creates ``sim.fit`` (previously, this was stored in ``sim.results.fit``). +- *Regression information*: Calls to ``sim.results.fit`` should be replaced with ``sim.fit``. The ``output`` parameter for ``sim.compute_fit()`` has been removed since it now always outputs the ``Fit`` object. - *GitHub info*: PR `1047 `__ diff --git a/covasim/analysis.py b/covasim/analysis.py index 37c1f6e6a..ab4355baf 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1240,6 +1240,7 @@ class Calibration(Analyzer): calib_pars (dict): a dictionary of the parameters to calibrate of the format dict(key1=[best, low, high]) custom_fn (function): a custom function for modifying the simulation; receives the sim and calib_pars as inputs, should return the modified sim label (str): a label for this calibration object + verbose (bool): whether to print details of the calibration kwargs (dict): passed to cv.Calibration() @@ -1250,11 +1251,12 @@ class Calibration(Analyzer): sim = cv.Sim(datafile='data.csv') calib_pars = dict(beta=[0.015, 0.010, 0.020]) - calib = sim.calibrate(calib_pars) + calib = cv.calibrate(sim, calib_pars) + calib.calibrate() calib.plot() ''' - def __init__(self, sim, calib_pars=None, custom_fn=None, label=None, name=None, db_name=None, storage=None, n_workers=None, n_trials=None): + def __init__(self, sim, calib_pars=None, custom_fn=None, name=None, db_name=None, storage=None, n_workers=None, n_trials=None, label=None, verbose=True): super().__init__(label=label) # Initialize the Analyzer object if isinstance(op, Exception): raise op # If Optuna failed to import, raise that exception now @@ -1268,7 +1270,8 @@ def __init__(self, sim, calib_pars=None, custom_fn=None, label=None, name=None, self.calib_pars = calib_pars self.custom_fn = custom_fn self.run_args = sc.objdict(name=name, db_name=db_name, storage=storage, n_workers=n_workers, n_trials=n_trials) - + self.verbose = verbose + self.calibrated = False return @@ -1304,6 +1307,10 @@ def run_trial(self, trial): def worker(self): ''' Run a single worker ''' + if self.verbose: + op.logging.set_verbosity(op.logging.DEBUG) + else: + op.logging.set_verbosity(op.logging.ERROR) study = op.load_study(storage=self.run_args.storage, study_name=self.run_args.name) output = study.optimize(self.run_trial, n_trials=self.run_args.n_trials) return output @@ -1324,8 +1331,14 @@ def make_study(self): return output - def calibrate(self, calib_pars=None, **kwargs): - ''' Actually perform calibration ''' + def calibrate(self, calib_pars=None, verbose=True, **kwargs): + ''' + Actually perform calibration. + + Args: + calib_pars (dict): if supplied, overwrite stored calib_pars + kwargs (dict): if supplied, overwrite stored run_args (n_trials, n_workers, etc.) + ''' # Load and validate calibration parameters if calib_pars is not None: @@ -1333,47 +1346,45 @@ def calibrate(self, calib_pars=None, **kwargs): if self.calib_pars is None: errormsg = 'You must supply calibration parameters either when creating the calibration object or when calling calibrate().' raise ValueError(errormsg) - - # Update optuna settings - to_pop = [] - for k,v in kwargs.items(): - if k in self.g: - self.g[k] = v - to_pop.append(k) - for k in to_pop: - kwargs.pop(k) + self.run_args.update(kwargs) # Update optuna settings # Run the optimization t0 = sc.tic() self.make_study() self.run_workers() study = op.load_study(storage=self.run_args.storage, study_name=self.run_args.name) - self.best_pars = study.best_params - T = sc.toc(t0, output=True) - print(f'Output: {self.best_pars}, time: {T}') + self.best_pars = sc.objdict(study.best_params) + self.elapsed = sc.toc(t0, output=True) # Compare the results - self.initial_pars = {k:v[0] for k,v in self.calib_pars.items()} + self.initial_pars = sc.objdict({k:v[0] for k,v in self.calib_pars.items()}) self.before = self.run_sim(calib_pars=self.initial_pars, label='Before calibration', return_sim=True) - self.after = self.run_sim(calib_pars=self.best_pars, label='After calibration', return_sim=True) + self.after = self.run_sim(calib_pars=self.best_pars, label='After calibration', return_sim=True) + + # Tidy up + self.calibrated = True + if verbose: + self.summarize() + return def summarize(self): - try: + if self.calibrated: + print(f'Calibration for {self.run_args.n_workers*self.run_args.n_trials} total trials completed in {self.elapsed:0.1f} s.') before = self.before.fit.mismatch after = self.after.fit.mismatch - print('Initial parameter values:') + print('\nInitial parameter values:') print(self.initial_pars) - print('Best parameter values:') + print('\nBest parameter values:') print(self.best_pars) - print(f'Mismatch before calibration: {before:n}') + print(f'\nMismatch before calibration: {before:n}') print(f'Mismatch after calibration: {after:n}') - print(f'Percent improvement: {(1-(before-after)/before)*100:0.1f}%') + print(f'Percent improvement: {((before-after)/before)*100:0.1f}%') return before, after - except Exception as E: - errormsg = 'Could not get summary, have you run the calibration?' - raise RuntimeError(errormsg) from E + else: + print('Calibration not yet run; please run calib.calibrate()') + return def plot(self, **kwargs): diff --git a/docs/tutorials/tut_calibration.ipynb b/docs/tutorials/tut_calibration.ipynb index e46a33fb6..9d6c050e6 100644 --- a/docs/tutorials/tut_calibration.ipynb +++ b/docs/tutorials/tut_calibration.ipynb @@ -67,8 +67,8 @@ "metadata": {}, "outputs": [], "source": [ - "sim.initialize(reset=True) # Reinitialize the sim\n", "sim['rel_death_prob'] = 2 # Double the death rate since deaths were too low\n", + "sim.initialize(reset=True) # Reinitialize the sim\n", "\n", "# Rerun and compute fit\n", "sim.run()\n", From e6dd30adc081a3d1c3575e2795fe23d5e9c4d986 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 14 May 2021 14:11:41 -0700 Subject: [PATCH 564/569] update how tests are installed --- .github/workflows/tests.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a90fb0a88..83919a6c4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -21,10 +21,10 @@ jobs: - name: Install Covasim run: pip install -e . - name: Install tests - run: pip install pytest + run: pip install tests/requirements_test.txt - name: Run integration tests working-directory: ./tests - run: pytest -v test_*.py --durations=0 + run: pytest -v test_*.py --workers auto --durations=0 - name: Run unit tests working-directory: ./tests/unittests - run: pytest -v test_*.py --durations=0 + run: pytest -v test_*.py --workers auto --durations=0 From 6a9a59ab5a26251100d44625d3c6f9c3b71e0351 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 14 May 2021 14:14:49 -0700 Subject: [PATCH 565/569] fix paths --- .github/workflows/tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 83919a6c4..5d5797c30 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -21,7 +21,8 @@ jobs: - name: Install Covasim run: pip install -e . - name: Install tests - run: pip install tests/requirements_test.txt + working-directory: ./tests + run: pip install -r requirements_test.txt - name: Run integration tests working-directory: ./tests run: pytest -v test_*.py --workers auto --durations=0 From 960156f476422e70bd8689644b310d389270a48a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 17 May 2021 22:16:24 -0700 Subject: [PATCH 566/569] update docstrings --- covasim/analysis.py | 32 ++++++++++++++++++++++++-------- covasim/sim.py | 5 +++-- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index ab4355baf..37572c21a 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1233,15 +1233,27 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No class Calibration(Analyzer): ''' - A class to handle calibration of Covasim simulations. + A class to handle calibration of Covasim simulations. Uses the Optuna hyperparameter + optimization library (optuna.org), which must be installed separately (via + pip install optuna). + + Note: running a calibration does not guarantee a good fit! You must ensure that + you run for a sufficient number of iterations, have enough free parameters, and + that the parameters have wide enough bounds. Please see the tutorial on calibration + for more information. Args: sim (Sim): the simulation to calibrate calib_pars (dict): a dictionary of the parameters to calibrate of the format dict(key1=[best, low, high]) custom_fn (function): a custom function for modifying the simulation; receives the sim and calib_pars as inputs, should return the modified sim + n_trials (int): the number of trials per worker + n_workers (int): the number of parallel workers (default: maximum + total_trials (int): if n_trials is not supplied, calculate by dividing this number by n_workers) + name (str): the name of the database (default: 'covasim_calibration') + db_name (str): the name of the database file (default: 'covasim_calibration.db') + storage (str): the location of the database (default: sqlite) label (str): a label for this calibration object verbose (bool): whether to print details of the calibration - kwargs (dict): passed to cv.Calibration() Returns: @@ -1251,25 +1263,29 @@ class Calibration(Analyzer): sim = cv.Sim(datafile='data.csv') calib_pars = dict(beta=[0.015, 0.010, 0.020]) - calib = cv.calibrate(sim, calib_pars) + calib = cv.Calibration(sim, calib_pars, total_trials=100) calib.calibrate() calib.plot() ''' - def __init__(self, sim, calib_pars=None, custom_fn=None, name=None, db_name=None, storage=None, n_workers=None, n_trials=None, label=None, verbose=True): + def __init__(self, sim, calib_pars=None, custom_fn=None, n_trials=None, n_workers=None, total_trials=None, name=None, db_name=None, storage=None, label=None, verbose=True): super().__init__(label=label) # Initialize the Analyzer object if isinstance(op, Exception): raise op # If Optuna failed to import, raise that exception now + import multiprocessing as mp - # Handle arguments + # Handle run arguments + if n_trials is None: n_trials = 20 + if n_workers is None: n_workers = mp.cpu_count() if name is None: name = 'covasim_calibration' if db_name is None: db_name = f'{name}.db' if storage is None: storage = f'sqlite:///{db_name}' - if n_workers is None: n_workers = 4 - if n_trials is None: n_trials = 20 + if total_trials is not None: n_trials = total_trials/n_workers + self.run_args = sc.objdict(n_trials=int(n_trials), n_workers=int(n_workers), name=name, db_name=db_name, storage=storage) + + # Handle other inputs self.sim = sim self.calib_pars = calib_pars self.custom_fn = custom_fn - self.run_args = sc.objdict(name=name, db_name=db_name, storage=storage, n_workers=n_workers, n_trials=n_trials) self.verbose = verbose self.calibrated = False return diff --git a/covasim/sim.py b/covasim/sim.py index a9683635d..3b9846faa 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -1172,7 +1172,8 @@ def compute_fit(self, *args, **kwargs): def calibrate(self, calib_pars, **kwargs): ''' - Automatically calibrate the simulation, returning a Calibration object. + Automatically calibrate the simulation, returning a Calibration object + (a type of analyzer). See the documentation on that class for more information. Args: calib_pars (dict): a dictionary of the parameters to calibrate of the format dict(key1=[best, low, high]) @@ -1185,7 +1186,7 @@ def calibrate(self, calib_pars, **kwargs): sim = cv.Sim(datafile='data.csv') calib_pars = dict(beta=[0.015, 0.010, 0.020]) - calib = sim.calibrate(calib_pars) + calib = sim.calibrate(calib_pars, n_trials=50) calib.plot() ''' calib = cva.Calibration(sim=self, calib_pars=calib_pars, **kwargs) From f5c64850afbd029414c01b4c71c2d3615acbfb2b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 17 May 2021 22:49:11 -0700 Subject: [PATCH 567/569] update tutorial --- CHANGELOG.rst | 6 +- covasim/version.py | 2 +- docs/tutorials/clean_outputs | 3 +- docs/tutorials/tut_calibration.ipynb | 121 ++++++++++----------------- examples/t07_optuna_calibration.py | 102 ++++++---------------- 5 files changed, 75 insertions(+), 159 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1340c5606..c721194d4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,7 +15,7 @@ Coming soon These are the major improvements we are currently working on. If there is a specific bugfix or feature you would like to see, please `create an issue `__. -- Expanded tutorials (health care workers, calibration, exercises, etc.) +- Continued updates to vaccine and variant parameters and workflows - Multi-region and geographical support - Economics and costing analysis @@ -25,9 +25,9 @@ Latest versions (3.0.x) ~~~~~~~~~~~~~~~~~~~~~~~ -Version 3.0.3 (2021-05-14) +Version 3.0.3 (2021-05-17) -------------------------- -- Added a new class, ``cv.Calibration``, that can perform automatic calibration. Simplest usage is ``sim.calibrate(calib_pars)``. Note: this requires Optuna, which is not installed by default; please install separately via ``pip install optuna``. +- Added a new class, ``cv.Calibration``, that can perform automatic calibration. Simplest usage is ``sim.calibrate(calib_pars)``. Note: this requires Optuna, which is not installed by default; please install separately via ``pip install optuna``. See the updated calibration tutorial for more information. - Added a new result, ``known_deaths``, which counts only deaths among people who have been diagnosed. - ``sim.compute_fit()`` now returns the fit by default, and creates ``sim.fit`` (previously, this was stored in ``sim.results.fit``). - *Regression information*: Calls to ``sim.results.fit`` should be replaced with ``sim.fit``. The ``output`` parameter for ``sim.compute_fit()`` has been removed since it now always outputs the ``Fit`` object. diff --git a/covasim/version.py b/covasim/version.py index c1b113cfb..e8e828a8c 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -5,5 +5,5 @@ __all__ = ['__version__', '__versiondate__', '__license__'] __version__ = '3.0.3' -__versiondate__ = '2021-05-14' +__versiondate__ = '2021-05-17' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' diff --git a/docs/tutorials/clean_outputs b/docs/tutorials/clean_outputs index 60492a75a..313378771 100755 --- a/docs/tutorials/clean_outputs +++ b/docs/tutorials/clean_outputs @@ -4,4 +4,5 @@ echo 'Deleting:' echo `ls -1 ./my-*.* 2> /dev/null` echo '...in 2 seconds' sleep 2 -rm -vf ./my-*.* \ No newline at end of file +rm -vf ./my-*.* +rm -vf ./covasim_calibration.db \ No newline at end of file diff --git a/docs/tutorials/tut_calibration.ipynb b/docs/tutorials/tut_calibration.ipynb index 9d6c050e6..c43e1e734 100644 --- a/docs/tutorials/tut_calibration.ipynb +++ b/docs/tutorials/tut_calibration.ipynb @@ -25,6 +25,7 @@ "cv.options.set(dpi=100, show=False, close=True, verbose=0) # Standard options for Jupyter notebook\n", "\n", "pars = dict(\n", + " pop_size = 10_000,\n", " start_day = '2020-02-01',\n", " end_day = '2020-04-11',\n", " beta = 0.015,\n", @@ -113,6 +114,7 @@ "def objective(x, n_runs=10):\n", " print(f'Running sim for beta={x[0]}, rel_death_prob={x[1]}')\n", " pars = dict(\n", + " pop_size = 10_000,\n", " start_day = '2020-02-01',\n", " end_day = '2020-04-11',\n", " beta = x[0],\n", @@ -129,7 +131,7 @@ " mismatch = np.mean(mismatches)\n", " return mismatch\n", "\n", - "guess = [0.015, 1] # Initial guess of parameters -- beta and relative death probability\n", + "guess = [0.01, 1] # Initial guess of parameters -- beta and relative death probability\n", "pars = scipy.optimize.minimize(objective, x0=guess, method='nelder-mead') # Run the optimization\n", "```" ] @@ -140,9 +142,16 @@ "source": [ "This should converge after roughly 3-10 minutes, although you will likely find that the improvement is minimal.\n", "\n", - "What's happening here? Trying to overcome the limitations of an algorithm that expects deterministic results simply by running more sims is fairly futile – if you run *N* sims and average them together, you've only reduced noise by √*N*, i.e. you have to average together 100 sims to reduce noise by a factor of 10, and even that might not be enough. Clearly, we need a more powerful approach.\n", + "What's happening here? Trying to overcome the limitations of an algorithm that expects deterministic results simply by running more sims is fairly futile – if you run *N* sims and average them together, you've only reduced noise by √*N*, i.e. you have to average together 100 sims to reduce noise by a factor of 10, and even that might not be enough. Clearly, we need a more powerful approach." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Built-in calibration\n", "\n", - "One such package we have found works reasonably well is called [Optuna](https://optuna.org/). You are strongly encouraged to read its documentation, but below is a full example to help get you started. You may wish to copy this example into a separate .py file and run it outside of the Jupyter notebook environment." + "One such package we have found works reasonably well is called [Optuna](https://optuna.org/). It is built into Covasim as `sim.calibrate()` (it's not installed by default, so please install it first with `pip install optuna`). Do not expect this to be a magic bullet solution: you will likely still need to try out multiple different parameter sets for calibration, manually update the values of uncalibrated parameters, check if the data actually make sense, etc. Even once all these things are in place, it still needs to be run for enough iterations, which might be a few hundred iterations for 3-4 calibrated (free) parameters or tens of thousands of iterations for 10 or more free parameters. The example below should get you started, but best to expect that it will _not_ work for your particular use case without significant modification!" ] }, { @@ -152,84 +161,43 @@ "outputs": [], "source": [ "'''\n", - "Example for running Optuna\n", + "Example for running built-in calibration with Optuna\n", "'''\n", "\n", - "import os\n", "import sciris as sc\n", "import covasim as cv\n", - "import optuna as op\n", - "\n", - "# Create a (mutable) dictionary for global settings\n", - "g = sc.objdict()\n", - "g.name = 'my-example-calibration'\n", - "g.db_name = f'{g.name}.db'\n", - "g.storage = f'sqlite:///{g.db_name}'\n", - "g.n_workers = 4 # Define how many workers to run in parallel\n", - "g.n_trials = 25 # Define the number of trials, i.e. sim runs, per worker\n", "\n", + "# Create default simulation\n", + "pars = sc.objdict(\n", + " pop_size = 10_000,\n", + " start_day = '2020-02-01',\n", + " end_day = '2020-04-11',\n", + " beta = 0.010,\n", + " rel_death_prob = 1.0,\n", + " interventions = cv.test_num(daily_tests='data'),\n", + " verbose = 0,\n", + ")\n", + "sim = cv.Sim(pars=pars, datafile='example_data.csv')\n", "\n", - "def run_sim(pars, label=None, return_sim=False):\n", - " ''' Create and run a simulation '''\n", - " pars = dict(\n", - " pop_size = 10_000,\n", - " start_day = '2020-02-01',\n", - " end_day = '2020-04-11',\n", - " beta = pars[\"beta\"],\n", - " rel_death_prob = pars[\"rel_death_prob\"],\n", - " interventions = cv.test_num(daily_tests='data'),\n", - " verbose = 0,\n", - " )\n", - " sim = cv.Sim(pars=pars, datafile='example_data.csv', label=label)\n", - " sim.run()\n", - " fit = sim.compute_fit()\n", - " if return_sim:\n", - " return sim\n", - " else:\n", - " return fit.mismatch\n", - "\n", - "\n", - "def run_trial(trial):\n", - " ''' Define the objective for Optuna '''\n", - " pars = {}\n", - " pars[\"beta\"] = trial.suggest_uniform('beta', 0.005, 0.020) # Sample from beta values within this range\n", - " pars[\"rel_death_prob\"] = trial.suggest_uniform('rel_death_prob', 0.5, 3.0) # Sample from beta values within this range\n", - " mismatch = run_sim(pars)\n", - " return mismatch\n", - "\n", - "\n", - "def worker():\n", - " ''' Run a single worker '''\n", - " study = op.load_study(storage=g.storage, study_name=g.name)\n", - " output = study.optimize(run_trial, n_trials=g.n_trials)\n", - " return output\n", - "\n", - "\n", - "def run_workers():\n", - " ''' Run multiple workers in parallel '''\n", - " output = sc.parallelize(worker, g.n_workers)\n", - " return output\n", - "\n", + "# Parameters to calibrate -- format is best, low, high\n", + "calib_pars = dict(\n", + " beta = [pars.beta, 0.005, 0.20],\n", + " rel_death_prob = [pars.rel_death_prob, 0.5, 3.0],\n", + ")\n", "\n", - "def make_study():\n", - " ''' Make a study, deleting one if it already exists '''\n", - " if os.path.exists(g.db_name):\n", - " os.remove(g.db_name)\n", - " print(f'Removed existing calibration {g.db_name}')\n", - " output = op.create_study(storage=g.storage, study_name=g.name)\n", - " return output\n", + "if __name__ == '__main__':\n", "\n", + " # Run the calibration\n", + " n_trials = 25\n", + " n_workers = 4\n", + " calib = sim.calibrate(calib_pars=calib_pars, n_trials=n_trials, n_workers=n_workers)\n", "\n", "if __name__ == '__main__':\n", "\n", - " # Run the optimization\n", - " t0 = sc.tic()\n", - " make_study()\n", - " run_workers()\n", - " study = op.load_study(storage=g.storage, study_name=g.name)\n", - " best_pars = study.best_params\n", - " T = sc.toc(t0, output=True)\n", - " print(f'\\n\\nOutput: {best_pars}, time: {T:0.1f} s')" + " # Run the calibration\n", + " n_trials = 25\n", + " n_workers = 4\n", + " calib = sim.calibrate(calib_pars=calib_pars, n_trials=n_trials, n_workers=n_workers)" ] }, { @@ -242,22 +210,21 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# Plot the results\n", - "initial_pars = dict(beta=0.015, rel_death_prob=1.0)\n", - "before = run_sim(pars=initial_pars, label='Before calibration', return_sim=True)\n", - "after = run_sim(pars=best_pars, label='After calibration', return_sim=True)\n", - "msim = cv.MultiSim([before, after])\n", - "msim.plot(to_plot=['cum_tests', 'cum_diagnoses', 'cum_deaths'])" + "calib.summarize()\n", + "calib.plot(to_plot=['cum_tests', 'cum_diagnoses', 'cum_deaths'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Compared to `scipy.optimize.minimize()`, Optuna took less time and produced a much better fit." + "Compared to `scipy.optimize.minimize()`, Optuna took less time and produced a much better fit. However, it's still far from perfect -- more iterations, and calibrating more parameters beyond just these two, would still be required before the model could be considered \"calibrated\"." ] } ], diff --git a/examples/t07_optuna_calibration.py b/examples/t07_optuna_calibration.py index 81f2674cc..6768272f5 100644 --- a/examples/t07_optuna_calibration.py +++ b/examples/t07_optuna_calibration.py @@ -1,87 +1,35 @@ ''' -Example for running Optuna +Example for running built-in calibration with Optuna ''' -import os import sciris as sc import covasim as cv -import optuna as op - -# Create a (mutable) dictionary for global settings -g = sc.objdict() -g.name = 'my-example-calibration' -g.db_name = f'{g.name}.db' -g.storage = f'sqlite:///{g.db_name}' -g.n_workers = 4 # Define how many workers to run in parallel -g.n_trials = 25 # Define the number of trials, i.e. sim runs, per worker - - -def run_sim(pars, label=None, return_sim=False): - ''' Create and run a simulation ''' - print(f'Running sim for beta={pars["beta"]}, rel_death_prob={pars["rel_death_prob"]}') - pars = dict( - pop_size = 10_000, - start_day = '2020-02-01', - end_day = '2020-04-11', - beta = pars["beta"], - rel_death_prob = pars["rel_death_prob"], - interventions = cv.test_num(daily_tests='data'), - verbose = 0, - ) - sim = cv.Sim(pars=pars, datafile='example_data.csv', label=label) - sim.run() - fit = sim.compute_fit() - if return_sim: - return sim - else: - return fit.mismatch - - -def run_trial(trial): - ''' Define the objective for Optuna ''' - pars = {} - pars["beta"] = trial.suggest_uniform('beta', 0.005, 0.020) # Sample from beta values within this range - pars["rel_death_prob"] = trial.suggest_uniform('rel_death_prob', 0.5, 3.0) # Sample from beta values within this range - mismatch = run_sim(pars) - return mismatch - - -def worker(): - ''' Run a single worker ''' - study = op.load_study(storage=g.storage, study_name=g.name) - output = study.optimize(run_trial, n_trials=g.n_trials) - return output - - -def run_workers(): - ''' Run multiple workers in parallel ''' - output = sc.parallelize(worker, g.n_workers) - return output - - -def make_study(): - ''' Make a study, deleting one if it already exists ''' - if os.path.exists(g.db_name): - os.remove(g.db_name) - print(f'Removed existing calibration {g.db_name}') - output = op.create_study(storage=g.storage, study_name=g.name) - return output +# Create default simulation +pars = sc.objdict( + pop_size = 10_000, + start_day = '2020-02-01', + end_day = '2020-04-11', + beta = 0.015, + rel_death_prob = 1.0, + interventions = cv.test_num(daily_tests='data'), + verbose = 0, +) +sim = cv.Sim(pars=pars, datafile='example_data.csv') + +# Parameters to calibrate -- format is best, low, high +calib_pars = dict( + beta = [pars.beta, 0.005, 0.20], + rel_death_prob = [pars.rel_death_prob, 0.5, 3.0], +) if __name__ == '__main__': - # Run the optimization - t0 = sc.tic() - make_study() - run_workers() - study = op.load_study(storage=g.storage, study_name=g.name) - best_pars = study.best_params - T = sc.toc(t0, output=True) - print(f'Output: {best_pars}, time: {T}') + # Run the calibration + n_trials = 25 + n_workers = 4 + calib = sim.calibrate(calib_pars=calib_pars, n_trials=n_trials, n_workers=n_workers) - # Plot the results - initial_pars = dict(beta=0.015, rel_death_prob=1.0) - before = run_sim(pars=initial_pars, label='Before calibration', return_sim=True) - after = run_sim(pars=best_pars, label='After calibration', return_sim=True) - msim = cv.MultiSim([before, after]) - msim.plot(to_plot=['cum_tests', 'cum_diagnoses', 'cum_deaths']) \ No newline at end of file + # Summarize and plot the results + calib.summarize() + calib.plot(to_plot=['cum_tests', 'cum_diagnoses', 'cum_deaths']) \ No newline at end of file From 56ab243f6726052149d92913e93557b8e37d26f1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 17 May 2021 23:02:22 -0700 Subject: [PATCH 568/569] update docs --- CHANGELOG.rst | 1 + covasim/analysis.py | 14 +++++++++++--- docs/tutorials/tut_calibration.ipynb | 3 +-- examples/t07_optuna_calibration.py | 3 +-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c721194d4..04a97ebef 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -29,6 +29,7 @@ Version 3.0.3 (2021-05-17) -------------------------- - Added a new class, ``cv.Calibration``, that can perform automatic calibration. Simplest usage is ``sim.calibrate(calib_pars)``. Note: this requires Optuna, which is not installed by default; please install separately via ``pip install optuna``. See the updated calibration tutorial for more information. - Added a new result, ``known_deaths``, which counts only deaths among people who have been diagnosed. +- Updated several vaccine and variant parameters (e.g., B1.351 and B117 cross-immunity). - ``sim.compute_fit()`` now returns the fit by default, and creates ``sim.fit`` (previously, this was stored in ``sim.results.fit``). - *Regression information*: Calls to ``sim.results.fit`` should be replaced with ``sim.fit``. The ``output`` parameter for ``sim.compute_fit()`` has been removed since it now always outputs the ``Fit`` object. - *GitHub info*: PR `1047 `__ diff --git a/covasim/analysis.py b/covasim/analysis.py index 37572c21a..624fea5f1 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1288,6 +1288,13 @@ def __init__(self, sim, calib_pars=None, custom_fn=None, n_trials=None, n_worker self.custom_fn = custom_fn self.verbose = verbose self.calibrated = False + + # Handle if the sim has already been run + if self.sim.complete: + print('Warning: sim has already been run; re-initializing, but in future, use a sim that has not been run') + self.sim = self.sim.copy() + self.sim.initialize() + return @@ -1316,7 +1323,7 @@ def run_trial(self, trial): ''' Define the objective for Optuna ''' pars = {} for key, (best,low,high) in self.calib_pars.items(): - pars[key] = trial.suggest_uniform(key, low, high) # Sample from beta values within this range + pars[key] = trial.suggest_uniform(key, low, high) # Sample from values within this range mismatch = self.run_sim(pars) return mismatch @@ -1353,6 +1360,7 @@ def calibrate(self, calib_pars=None, verbose=True, **kwargs): Args: calib_pars (dict): if supplied, overwrite stored calib_pars + verbose (bool): whether to print output from each trial kwargs (dict): if supplied, overwrite stored run_args (n_trials, n_workers, etc.) ''' @@ -1368,8 +1376,8 @@ def calibrate(self, calib_pars=None, verbose=True, **kwargs): t0 = sc.tic() self.make_study() self.run_workers() - study = op.load_study(storage=self.run_args.storage, study_name=self.run_args.name) - self.best_pars = sc.objdict(study.best_params) + self.study = op.load_study(storage=self.run_args.storage, study_name=self.run_args.name) + self.best_pars = sc.objdict(self.study.best_params) self.elapsed = sc.toc(t0, output=True) # Compare the results diff --git a/docs/tutorials/tut_calibration.ipynb b/docs/tutorials/tut_calibration.ipynb index c43e1e734..e50f70e5c 100644 --- a/docs/tutorials/tut_calibration.ipynb +++ b/docs/tutorials/tut_calibration.ipynb @@ -131,7 +131,7 @@ " mismatch = np.mean(mismatches)\n", " return mismatch\n", "\n", - "guess = [0.01, 1] # Initial guess of parameters -- beta and relative death probability\n", + "guess = [0.015, 1] # Initial guess of parameters -- beta and relative death probability\n", "pars = scipy.optimize.minimize(objective, x0=guess, method='nelder-mead') # Run the optimization\n", "```" ] @@ -216,7 +216,6 @@ "outputs": [], "source": [ "# Plot the results\n", - "calib.summarize()\n", "calib.plot(to_plot=['cum_tests', 'cum_diagnoses', 'cum_deaths'])" ] }, diff --git a/examples/t07_optuna_calibration.py b/examples/t07_optuna_calibration.py index 6768272f5..82cc39979 100644 --- a/examples/t07_optuna_calibration.py +++ b/examples/t07_optuna_calibration.py @@ -30,6 +30,5 @@ n_workers = 4 calib = sim.calibrate(calib_pars=calib_pars, n_trials=n_trials, n_workers=n_workers) - # Summarize and plot the results - calib.summarize() + # Plot the results calib.plot(to_plot=['cum_tests', 'cum_diagnoses', 'cum_deaths']) \ No newline at end of file From 531c449271bb40020d384b31b7484daa14d73035 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 17 May 2021 23:13:53 -0700 Subject: [PATCH 569/569] update tutorial --- docs/tutorials/tut_calibration.ipynb | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/tutorials/tut_calibration.ipynb b/docs/tutorials/tut_calibration.ipynb index e50f70e5c..f1cfffbad 100644 --- a/docs/tutorials/tut_calibration.ipynb +++ b/docs/tutorials/tut_calibration.ipynb @@ -190,13 +190,6 @@ " # Run the calibration\n", " n_trials = 25\n", " n_workers = 4\n", - " calib = sim.calibrate(calib_pars=calib_pars, n_trials=n_trials, n_workers=n_workers)\n", - "\n", - "if __name__ == '__main__':\n", - "\n", - " # Run the calibration\n", - " n_trials = 25\n", - " n_workers = 4\n", " calib = sim.calibrate(calib_pars=calib_pars, n_trials=n_trials, n_workers=n_workers)" ] }, @@ -204,7 +197,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's see how well it did:" + "So it improved the fit (see above), but let's visualize this as a plot:" ] }, {