From 7dfb7659d737c0985aba2dc11896111ad7357ef5 Mon Sep 17 00:00:00 2001 From: Robyn Stuart Date: Mon, 27 Apr 2020 14:18:58 +1000 Subject: [PATCH 001/194] change default prognoses --- covasim/parameters.py | 7 ++++--- covasim/people.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index c4ee81b7a..6e866213a 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -158,13 +158,14 @@ def get_prognoses(by_age=True): else: prognoses = dict( age_cutoffs = np.array([10, 20, 30, 40, 50, 60, 70, 80, max_age]), # Age cutoffs + sus_ORs = np.array([1., 1., 1., 1., 1., 1., 2., 2., 2.]), # Odds ratios for relative susceptibility - from https://www.medrxiv.org/content/10.1101/2020.03.03.20028423v3.full.pdf, no significant differences by age symp_probs = np.array([0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90]), # Overall probability of developing symptoms (based on https://www.medrxiv.org/content/10.1101/2020.03.24.20043018v1.full.pdf, scaled for overall symptomaticity) - severe_probs = np.array([0.00100, 0.00100, 0.01100, 0.03400, 0.04300, 0.08200, 0.11800, 0.16600, 0.18400]), # Overall probability of developing severe symptoms (https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf) - crit_probs = np.array([0.00004, 0.00011, 0.00050, 0.00123, 0.00214, 0.00800, 0.02750, 0.06000, 0.10333]), # Overall probability of developing critical symptoms (derived from https://www.cdc.gov/mmwr/volumes/69/wr/mm6912e2.htm) + severe_probs = np.array([0.00050, 0.00165, 0.00720, 0.02080, 0.03430, 0.07650, 0.13280, 0.20655, 0.24570]), # Overall probability of developing severe symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) + crit_probs = np.array([0.00003, 0.00008, 0.00036, 0.00104, 0.00216, 0.00933, 0.03639, 0.08923, 0.17420]), # Overall probability of developing critical symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) death_probs = np.array([0.00002, 0.00006, 0.00030, 0.00080, 0.00150, 0.00600, 0.02200, 0.05100, 0.09300]), # Overall probability of dying (https://www.imperial.ac.uk/media/imperial-college/medicine/sph/ide/gida-fellowships/Imperial-College-COVID19-NPI-modelling-16-03-2020.pdf) ) - prognoses['death_probs'] /= prognoses['crit_probs'] # Conditional probability of dying, given severe symptoms + prognoses['death_probs'] /= prognoses['crit_probs'] # Conditional probability of dying, given critical symptoms prognoses['crit_probs'] /= prognoses['severe_probs'] # Conditional probability of symptoms becoming critical, given severe prognoses['severe_probs'] /= prognoses['symp_probs'] # Conditional probability of symptoms becoming severe, given symptomatic diff --git a/covasim/people.py b/covasim/people.py index 5b2466b3c..7e5fd0474 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -74,7 +74,7 @@ def find_cutoff(age_cutoffs, age): self.severe_prob[:] = prognoses['severe_probs'][inds] self.crit_prob[:] = prognoses['crit_probs'][inds] self.death_prob[:] = prognoses['death_probs'][inds] - self.rel_sus[:] = 1.0 # By default: is susceptible + self.rel_sus[:] = prognoses['sus_ORs'][inds] # Default susceptibilities self.rel_trans[:] = 0.0 # By default: cannot transmit return From 2744cd072b60759c514b274715954e68784c78c6 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 29 Apr 2020 10:39:30 -0700 Subject: [PATCH 002/194] added plotly test --- covasim/plotting.py | 13 +++++++++---- tests/devtests/plotly_tests.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100755 tests/devtests/plotly_tests.py diff --git a/covasim/plotting.py b/covasim/plotting.py index ab707dc24..5f9f666eb 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -5,7 +5,6 @@ import covasim as cv import numpy as np -import sciris as sc import plotly.graph_objects as go @@ -88,7 +87,7 @@ def standard_plots(sim): return plots -def plot_people(sim): +def plot_people(sim, do_show=False): ''' Plot a "cascade" of people moving through different states ''' z, states = get_individual_states(sim) @@ -115,10 +114,13 @@ def plot_people(sim): fig.update_layout(yaxis_range=(0, sim.n)) fig.update_layout(title={'text': 'Numbers of people by health state'}, xaxis_title='Day', yaxis_title='People', autosize=True) + if do_show: + fig.show() + return fig -def animate_people(sim): +def animate_people(sim, do_show=False): ''' Plot an animation of each person in the sim ''' z, states = get_individual_states(sim) @@ -127,7 +129,7 @@ def animate_people(sim): max_color = max(states, key=lambda x: x['value'])['value'] colorscale = [[x['value'] / max_color, x['color']] for x in states] - aspect = 3 + aspect = 1 y_size = int(np.ceil((z.shape[0] / aspect) ** 0.5)) x_size = int(np.ceil(aspect * y_size)) @@ -243,4 +245,7 @@ def animate_people(sim): fig.update_layout(title={'text': 'Epidemic over time'}) + if do_show: + fig.show() + return fig \ No newline at end of file diff --git a/tests/devtests/plotly_tests.py b/tests/devtests/plotly_tests.py new file mode 100755 index 000000000..01e7b3d7f --- /dev/null +++ b/tests/devtests/plotly_tests.py @@ -0,0 +1,10 @@ +import plotly.io as pio +import covasim as cv + + +pio.renderers.default = "browser" + +sim = cv.Sim(pop_size=10e3) + +sim.run() +fig = cv.animate_people(sim, do_show=True) From 0078287acee6e96760bcb08bd6f6f6fd537e7500 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 1 May 2020 00:55:35 -0700 Subject: [PATCH 003/194] population sizes test --- tests/devtests/pop_sizes.py | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100755 tests/devtests/pop_sizes.py diff --git a/tests/devtests/pop_sizes.py b/tests/devtests/pop_sizes.py new file mode 100755 index 000000000..828e1551a --- /dev/null +++ b/tests/devtests/pop_sizes.py @@ -0,0 +1,55 @@ +''' +Explore how infections change as a function of population size +''' + +#%% Run + +import covasim as cv +import pylab as pl +import sciris as sc + +sims = sc.objdict() + +popsizes = [10e3, 20e3, 30e3, 40e3, 50e3, 75e3, 100e3, 125e3, 150e3, 200e3, 300e3, 400e3, 500e3] + +results = [] +keys = [] +for psize in popsizes: + print(f'Running {psize}...') + key = f'p{psize}' + keys.append(key) + sims[key] = cv.Sim(pop_size=psize, + n_days=60, + rand_seed=1, + pop_type = 'random', + verbose=0, + ) + sims[key].run() + results.append(sims[key].results['cum_infections'].values) + + +#%% Ploting + +pl.figure(dpi=200) +# pl.rcParams['font.size'] = 18 + +pl.subplot(1,2,1) +colors = sc.vectocolor(pl.log10(popsizes), cmap='parula') +for k,key in enumerate(keys): + label = f'{int(float(key[1:]))/1000}k: {results[k][-1]:0.0f}' + pl.plot(results[k], label=label, lw=3, color=colors[k]) + print(label) +pl.legend() +pl.title('Total number of infections') +pl.xlabel('Day') +pl.ylabel('Number of infections') + +pl.subplot(1,2,2) +for k,key in enumerate(keys): + label = f'{int(float(key[1:]))/1000}k: {results[k][-1]/popsizes[k]*100:0.1f}' + pl.plot(results[k]/popsizes[k]*100, label=label, lw=3, color=colors[k]) + print(label) +pl.legend() +pl.title('Final prevalence') +pl.xlabel('Day') +pl.ylabel('Prevalence (%)') \ No newline at end of file From 0df26eec3612017cdcf30f328c3f34e5185cc02f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 1 May 2020 01:13:30 -0700 Subject: [PATCH 004/194] added test script --- tests/devtests/pop_sizes.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/devtests/pop_sizes.py b/tests/devtests/pop_sizes.py index 828e1551a..290389220 100755 --- a/tests/devtests/pop_sizes.py +++ b/tests/devtests/pop_sizes.py @@ -14,13 +14,13 @@ results = [] keys = [] -for psize in popsizes: +for p,psize in enumerate(popsizes): print(f'Running {psize}...') key = f'p{psize}' keys.append(key) sims[key] = cv.Sim(pop_size=psize, - n_days=60, - rand_seed=1, + n_days=30, + rand_seed=3059,#, 29837*(p+298), pop_type = 'random', verbose=0, ) @@ -28,12 +28,12 @@ results.append(sims[key].results['cum_infections'].values) -#%% Ploting +#%% Plotting pl.figure(dpi=200) # pl.rcParams['font.size'] = 18 -pl.subplot(1,2,1) +pl.subplot(1,3,1) colors = sc.vectocolor(pl.log10(popsizes), cmap='parula') for k,key in enumerate(keys): label = f'{int(float(key[1:]))/1000}k: {results[k][-1]:0.0f}' @@ -44,7 +44,7 @@ pl.xlabel('Day') pl.ylabel('Number of infections') -pl.subplot(1,2,2) +pl.subplot(1,3,2) for k,key in enumerate(keys): label = f'{int(float(key[1:]))/1000}k: {results[k][-1]/popsizes[k]*100:0.1f}' pl.plot(results[k]/popsizes[k]*100, label=label, lw=3, color=colors[k]) @@ -52,4 +52,11 @@ pl.legend() pl.title('Final prevalence') pl.xlabel('Day') -pl.ylabel('Prevalence (%)') \ No newline at end of file +pl.ylabel('Prevalence (%)') + +pl.subplot(1,3,3) +fres = [res[-1] for res in results] +pl.scatter(popsizes, fres, s=150, c=colors) +pl.title('Correlation') +pl.xlabel('Population size') +pl.ylabel('Number of infections') \ No newline at end of file From 25975be8c4fe3e1eb64b9e12cd6b9b6821a4037f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 1 May 2020 01:19:20 -0700 Subject: [PATCH 005/194] some correlation, even with replicates --- tests/devtests/pop_sizes.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/devtests/pop_sizes.py b/tests/devtests/pop_sizes.py index 290389220..15b15db4e 100755 --- a/tests/devtests/pop_sizes.py +++ b/tests/devtests/pop_sizes.py @@ -10,7 +10,9 @@ sims = sc.objdict() -popsizes = [10e3, 20e3, 30e3, 40e3, 50e3, 75e3, 100e3, 125e3, 150e3, 200e3, 300e3, 400e3, 500e3] +popsizes = [1e3, 2e3, 5e3, 10e3, 20e3, 30e3, 40e3, 50e3, 75e3, 100e3] +popsizes += [p+1e3 for p in popsizes] +popsizes += [p+2e3 for p in popsizes] results = [] keys = [] @@ -20,7 +22,7 @@ keys.append(key) sims[key] = cv.Sim(pop_size=psize, n_days=30, - rand_seed=3059,#, 29837*(p+298), + rand_seed=2585+p,#, 29837*(p+298), pop_type = 'random', verbose=0, ) @@ -30,7 +32,7 @@ #%% Plotting -pl.figure(dpi=200) +pl.figure(figsize=(12,6), dpi=200) # pl.rcParams['font.size'] = 18 pl.subplot(1,3,1) @@ -59,4 +61,6 @@ pl.scatter(popsizes, fres, s=150, c=colors) pl.title('Correlation') pl.xlabel('Population size') -pl.ylabel('Number of infections') \ No newline at end of file +pl.ylabel('Number of infections') + +print(pl.corrcoef(popsizes, fres)) \ No newline at end of file From 0e664853cfa686850a62160691a4590212dc1659 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 1 May 2020 01:33:40 -0700 Subject: [PATCH 006/194] population sizes script --- tests/devtests/pop_sizes.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/devtests/pop_sizes.py b/tests/devtests/pop_sizes.py index 15b15db4e..0e5b2e272 100755 --- a/tests/devtests/pop_sizes.py +++ b/tests/devtests/pop_sizes.py @@ -10,19 +10,19 @@ sims = sc.objdict() -popsizes = [1e3, 2e3, 5e3, 10e3, 20e3, 30e3, 40e3, 50e3, 75e3, 100e3] -popsizes += [p+1e3 for p in popsizes] -popsizes += [p+2e3 for p in popsizes] +popsizes = [10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3, 80e3, 90e3, 100e3] +popsizes += [p+ex for ex in [1e3, 2e3, 3e3, 4e3, 5e3] for p in popsizes] results = [] keys = [] for p,psize in enumerate(popsizes): - print(f'Running {psize}...') + print(f'Running {psize} ({p+1}/{len(popsizes)}...') key = f'p{psize}' keys.append(key) sims[key] = cv.Sim(pop_size=psize, n_days=30, - rand_seed=2585+p,#, 29837*(p+298), + pop_infected=100, + rand_seed=25857+p*241,#, 29837*(p+298), pop_type = 'random', verbose=0, ) @@ -32,7 +32,7 @@ #%% Plotting -pl.figure(figsize=(12,6), dpi=200) +pl.figure(figsize=(18,6), dpi=200) # pl.rcParams['font.size'] = 18 pl.subplot(1,3,1) @@ -41,20 +41,21 @@ label = f'{int(float(key[1:]))/1000}k: {results[k][-1]:0.0f}' pl.plot(results[k], label=label, lw=3, color=colors[k]) print(label) -pl.legend() +# pl.legend() pl.title('Total number of infections') pl.xlabel('Day') pl.ylabel('Number of infections') +sc.commaticks(axis='y') pl.subplot(1,3,2) for k,key in enumerate(keys): label = f'{int(float(key[1:]))/1000}k: {results[k][-1]/popsizes[k]*100:0.1f}' pl.plot(results[k]/popsizes[k]*100, label=label, lw=3, color=colors[k]) print(label) -pl.legend() -pl.title('Final prevalence') +# pl.legend() +pl.title('Attack rate') pl.xlabel('Day') -pl.ylabel('Prevalence (%)') +pl.ylabel('Attack rate (%)') pl.subplot(1,3,3) fres = [res[-1] for res in results] @@ -62,5 +63,7 @@ pl.title('Correlation') pl.xlabel('Population size') pl.ylabel('Number of infections') +sc.commaticks(axis='x') +sc.commaticks(axis='y') print(pl.corrcoef(popsizes, fres)) \ No newline at end of file From 69ced38424b4fb70ecb10acf5cc4eea1469708b3 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 1 May 2020 19:18:00 -0700 Subject: [PATCH 007/194] chmod and rename --- tests/devtests/archive/intervention_by_date.py | 0 tests/devtests/{ => archive}/pop_sizes.py | 0 tests/devtests/archive/pop_type_scaling.py | 0 tests/devtests/dev_test_popchoices2.py | 0 tests/devtests/draw_networks.py | 0 tests/devtests/intervention_export.py | 0 tests/devtests/testing_delay.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 tests/devtests/archive/intervention_by_date.py rename tests/devtests/{ => archive}/pop_sizes.py (100%) mode change 100755 => 100644 mode change 100755 => 100644 tests/devtests/archive/pop_type_scaling.py mode change 100755 => 100644 tests/devtests/dev_test_popchoices2.py mode change 100755 => 100644 tests/devtests/draw_networks.py mode change 100755 => 100644 tests/devtests/intervention_export.py mode change 100755 => 100644 tests/devtests/testing_delay.py diff --git a/tests/devtests/archive/intervention_by_date.py b/tests/devtests/archive/intervention_by_date.py old mode 100755 new mode 100644 diff --git a/tests/devtests/pop_sizes.py b/tests/devtests/archive/pop_sizes.py old mode 100755 new mode 100644 similarity index 100% rename from tests/devtests/pop_sizes.py rename to tests/devtests/archive/pop_sizes.py diff --git a/tests/devtests/archive/pop_type_scaling.py b/tests/devtests/archive/pop_type_scaling.py old mode 100755 new mode 100644 diff --git a/tests/devtests/dev_test_popchoices2.py b/tests/devtests/dev_test_popchoices2.py old mode 100755 new mode 100644 diff --git a/tests/devtests/draw_networks.py b/tests/devtests/draw_networks.py old mode 100755 new mode 100644 diff --git a/tests/devtests/intervention_export.py b/tests/devtests/intervention_export.py old mode 100755 new mode 100644 diff --git a/tests/devtests/testing_delay.py b/tests/devtests/testing_delay.py old mode 100755 new mode 100644 From 963df94ab589346a1baf081c8ce7c458db1fa49c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 1 May 2020 19:19:51 -0700 Subject: [PATCH 008/194] change aspect ratio back --- covasim/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 5f9f666eb..15b15d6cd 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -129,7 +129,7 @@ def animate_people(sim, do_show=False): max_color = max(states, key=lambda x: x['value'])['value'] colorscale = [[x['value'] / max_color, x['color']] for x in states] - aspect = 1 + aspect = 3 y_size = int(np.ceil((z.shape[0] / aspect) ** 0.5)) x_size = int(np.ceil(aspect * y_size)) From e79fbd398f2019ebeac912164c530a992a523cae Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 1 May 2020 20:13:48 -0700 Subject: [PATCH 009/194] implementing multisim class --- covasim/run.py | 384 +++++++++++++++++++++++++++++++++++++++++++--- tests/test_run.py | 23 +-- 2 files changed, 374 insertions(+), 33 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index b286b8fdf..535ca3514 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -17,7 +17,7 @@ # Specify all externally visible functions this file defines -__all__ = ['make_metapars', 'Scenarios', 'single_run', 'multi_run'] +__all__ = ['make_metapars', 'MultiSim', 'Scenarios', 'single_run', 'multi_run'] @@ -34,6 +34,364 @@ def make_metapars(): return metapars +class MultiSim(sc.prettyobj): + ''' + Class for running multiple copies of a simulation. + + Args: + sims (Sim or list): a single sim or a list of sims + base_sim (Sim): the sim used for shared properties; if not supplied, the first of the sims provided + + Returns: + msim: a MultiSim object + + **Examples**:: + + sim = cv.Sim() # Create the sim + msim = cv.MultiSim(sim) # Create the multisim + msim.replicate(5) # Create 5 replicates + msim.run() # Run them in parallel + msim.reduce() # Calculate statistics + msim.plot() # Plot results + + sims = [cv.Sim(beta=0.015*[1+0.1*i]) for i in range(5)] # Create sims + for sim in sims: sim.run() # Run sims in serial + msim = cv.MultiSim(sims) # Convert to multisim + msim.reduce('add') # Add results together + msim.plot() # Plot as single sim + ''' + + def __init__(self, sims=None, base_sim=None): + + # Handle inputs + if base_sim is None: + if isinstance(sims, cvs.Sim): + base_sim = sims + sims = [] + elif isinstance(sims, list): + base_sim = sims[0] + else: + errormsg = f'If base_sim is not supplied, sims must be either a sims or a list of sims, not {type(sims)}' + raise TypeError(errormsg) + if sims is None: + sims = [] + + self.sims = sims + self.base_sim = base_sim + self.results = None + self.which = None # Whether the multisim is to be reduced, combined, etc. + + return + + + def __len__(self): + try: + return len(self.sims) + except: + return 0 + + + def result_keys(self): + ''' Attempt to retrieve the results keys from the base sim ''' + try: + keys = self.base_sim.result_keys() + except Exception as E: + errormsg = f'Could not retrieve result keys since base sim not accessible: {str(E)}' + raise ValueError(errormsg) + return keys + + + def run(self, *args, **kwargs): + ''' + Run the actual scenarios + + Args: + debug (bool): if True, runs a single run instead of multiple, which makes debugging easier + verbose (int): level of detail to print, passed to sim.run() + kwargs (dict): passed to multi_run() and thence to sim.run() + + Returns: + None (modifies Scenarios object in place) + ''' + + self.sims = multi_run(self.sims, *args, **kwargs) + return + + + def combine(self, keep_people=False, output=False): + ''' Combine multiple sims into a single sim with scaled results ''' + + 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 + + for s,sim in enumerate(self.sims[1:]): # Skip the first one + if keep_people: + combined_sim.people += sim.people + for key in sim.result_keys(): + this_res = sim.results[key] + combined_sim.results[key].values += this_res.values + + # For non-count results (scale=False), rescale them + for key in combined_sim.result_keys(): + if not combined_sim.results[key].scale: + combined_sim.results[key].values /= n_runs + + self.combined_sim = combined_sim + self.results = self.combined_sim.results + self.which = 'combined' + + if output: + return combined_sim + else: + return + + + def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, + axis_args=None, fill_args=None, legend_args=None, as_dates=True, dateformat=None, + interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, + do_show=True, sep_figs=False, verbose=None): + ''' + Plot the results -- can supply arguments for both the figure and the plots. + + 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() + 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() + 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 + do_show (bool): Whether or not to show the figure + sep_figs (bool): Whether to show separate figures for different results instead of subplots + verbose (bool): Display a bit of extra information + + Returns: + fig: Figure handle + ''' + + if verbose is None: + verbose = self['verbose'] + sc.printv('Plotting...', 1, verbose) + + if to_plot is None: + to_plot = cvd.get_scen_plots() + to_plot = sc.dcp(to_plot) # In case it's supplied as a dict + + # Handle input arguments -- merge user input with defaults + fig_args = sc.mergedicts({'figsize': (16, 14)}, fig_args) + plot_args = sc.mergedicts({'lw': 3, 'alpha': 0.7}, plot_args) + axis_args = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.90, 'wspace': 0.25, 'hspace': 0.25}, axis_args) + fill_args = sc.mergedicts({'alpha': 0.2}, fill_args) + legend_args = sc.mergedicts({'loc': 'best'}, legend_args) + + if sep_figs: + figs = [] + else: + fig = pl.figure(**fig_args) + pl.subplots_adjust(**axis_args) + pl.rcParams['font.size'] = font_size + if font_family: + pl.rcParams['font.family'] = font_family + + n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have + for rk,reskey in enumerate(to_plot): + title = self.base_sim.results[reskey].name # Get the name of this result from the base simulation + if sep_figs: + figs.append(pl.figure(**fig_args)) + ax = pl.subplot(111) + else: + if rk == 0: + ax = pl.subplot(n_rows, n_cols, rk + 1) + else: + ax = pl.subplot(n_rows, n_cols, rk + 1, sharex=ax) + + resdata = self.results[reskey] + + for scenkey, scendata in resdata.items(): + + pl.fill_between(self.tvec, scendata.low, scendata.high, **fill_args) + pl.plot(self.tvec, scendata.best, label=scendata.name, **plot_args) + pl.title(title) + if rk == 0: + pl.legend(**legend_args) + + pl.grid(grid) + if commaticks: + sc.commaticks() + + if self.base_sim.data is not None and reskey in self.base_sim.data: + data_t = np.array((self.base_sim.data.index-self.base_sim['start_day'])/np.timedelta64(1,'D')) + pl.plot(data_t, self.base_sim.data[reskey], 'sk', **plot_args) + + # Optionally reset tick marks (useful for e.g. plotting weeks/months) + if interval: + xmin,xmax = ax.get_xlim() + ax.set_xticks(pl.arange(xmin, xmax+1, interval)) + + # Set xticks as dates + if as_dates: + @ticker.FuncFormatter + def date_formatter(x, pos): + return (self.base_sim['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') + ax.xaxis.set_major_formatter(date_formatter) + if not interval: + ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) + + # Ensure the figure actually renders or saves + if do_save: + if fig_path is None: # No figpath provided - see whether do_save is a figpath + fig_path = 'covasim_scenarios.png' # Just give it a default name + fig_path = sc.makefilepath(fig_path) # Ensure it's valid, including creating the folder + pl.savefig(fig_path) + + if do_show: + pl.show() + else: + pl.close(fig) + + return fig + + + def to_json(self, filename=None, tostring=True, indent=2, verbose=False, *args, **kwargs): + ''' + Export results as JSON. + + Args: + filename (str): if None, return string; else, write to file + + Returns: + A unicode string containing a JSON representation of the results, + or writes the JSON file to disk + + ''' + d = {'t':self.tvec, + 'results': self.results, + 'basepars': self.basepars, + 'metapars': self.metapars, + 'simpars': self.base_sim.export_pars(), + 'scenarios': self.scenarios + } + if filename is None: + output = sc.jsonify(d, tostring=tostring, indent=indent, verbose=verbose, *args, **kwargs) + else: + output = sc.savejson(filename=filename, obj=d, indent=indent, *args, **kwargs) + + return output + + + def to_excel(self, filename=None): + ''' + Export results as XLSX + + Args: + filename (str): if None, return string; else, write to file + + Returns: + An sc.Spreadsheet with an Excel file, or writes the file to disk + + ''' + spreadsheet = sc.Spreadsheet() + spreadsheet.freshbytes() + with pd.ExcelWriter(spreadsheet.bytes, engine='xlsxwriter') as writer: + for key in self.result_keys(): + result_df = pd.DataFrame.from_dict(sc.flattendict(self.results[key], sep='_')) + result_df.to_excel(writer, sheet_name=key) + spreadsheet.load() + + if filename is None: + output = spreadsheet + else: + output = spreadsheet.save(filename) + + return output + + + def save(self, scenfile=None, keep_sims=True, keep_people=False, **kwargs): + ''' + Save to disk as a gzipped pickle. + + Args: + scenfile (str or None): the name or path of the file to save to; if None, uses stored + keep_sims (bool): whether or not to store the actual Sim objects in the Scenarios object + keep_people (bool): whether or not to store the population in the Sim objects (NB, very large) + keywords: passed to makefilepath() + + Returns: + scenfile (str): the validated absolute path to the saved file + + **Example**:: + + scens.save() # Saves to a .scens file with the date and time of creation by default + + ''' + if scenfile is None: + scenfile = self.scenfile + scenfile = sc.makefilepath(filename=scenfile, **kwargs) + self.scenfile = scenfile # Store the actual saved filename + + # Store sims seperately + sims = self.sims + self.sims = None # Remove for now + + obj = sc.dcp(self) # This should be quick once we've removed the sims + if not keep_people: + obj.base_sim.shrink(in_place=True) + + if keep_sims or keep_people: + if keep_people: + if not obj._kept_people: + print('Warning: there are no people because they were not saved during the run. ' + 'If you want people, please rerun with keep_people=True.') + obj.sims = sims # Just restore the object in full + print('Note: saving people, which may produce a large file!') + else: + obj.sims = sc.objdict() + for key in sims.keys(): + obj.sims[key] = [] + for sim in sims[key]: + obj.sims[key].append(sim.shrink(in_place=False)) + + sc.saveobj(filename=scenfile, obj=obj) # Actually save + + self.sims = sims # Restore + return scenfile + + + @staticmethod + def load(scenfile, *args, **kwargs): + ''' + Load from disk from a gzipped pickle. + + Args: + scenfile (str): the name or path of the file to save to + kwargs: passed to sc.loadobj() + + Returns: + scens (Scenarios): the loaded scenarios object + + **Example**:: + + scens = cv.Scenarios.load('my-scenarios.scens') + ''' + scens = cvm.load(scenfile, *args, **kwargs) + if not isinstance(scens, Scenarios): + errormsg = f'Cannot load object of {type(scens)} as a Scenarios object' + raise TypeError(errormsg) + return scens + + + class Scenarios(cvb.ParsObj): ''' Class for running multiple sets of multiple simulations -- e.g., scenarios. @@ -577,26 +935,4 @@ def multi_run(sim, n_runs=4, noise=0.0, noisepar=None, iterpars=None, verbose=No errormsg = f'Must be Sim object or list, not {type(sim)}' raise TypeError(errormsg) - # Usual case -- return a list of sims - if not combine: - return sims - - # Or, combine them into a single sim with scaled results - else: - output_sim = sc.dcp(sims[0]) - output_sim.parallelized = {'parallelized':True, 'combined':True, 'n_runs':n_runs} # Store how this was parallelized - output_sim['pop_size'] *= n_runs # Record the number of people - - for s,sim in enumerate(sims[1:]): # Skip the first one - if keep_people: - output_sim.people += sim.people - for key in sim.result_keys(): - this_res = sim.results[key] - output_sim.results[key].values += this_res.values - - # For non-count results (scale=False), rescale them - for key in output_sim.result_keys(): - if not output_sim.results[key].scale: - output_sim.results[key].values /= len(sims) - - return output_sim + return sims diff --git a/tests/test_run.py b/tests/test_run.py index 67771e127..a3764b982 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -75,7 +75,7 @@ def test_scenarios(do_plot=False): return scens -def test_combine(do_plot=False): # If being run via pytest, turn off +def test_multisim_combine(do_plot=False): # If being run via pytest, turn off sc.heading('Combine results test') n_runs = 3 @@ -83,19 +83,22 @@ def test_combine(do_plot=False): # If being run via pytest, turn off pop_infected = 10 print('Running first sim...') - sim = cv.Sim({'pop_size':pop_size, 'pop_infected':pop_infected}) - sim = cv.multi_run(sim=sim, n_runs=n_runs, combine=True, keep_people=True) - assert sim['pop_size'] == pop_size*n_runs + sim = cv.Sim(pop_size=pop_size, pop_infected=pop_infected) + msim = cv.MultiSim(sim) + msim.replicate(n_runs) + msim.run() + sim1 = msim.combine(output=True, keep_people=True) + assert sim1['pop_size'] == pop_size*n_runs print('Running second sim, results should be similar but not identical (stochastic differences)...') - sim2 = cv.Sim({'pop_size':pop_size*n_runs, 'pop_infected':pop_infected*n_runs}) + sim2 = cv.Sim(pop_size=pop_size*n_runs, pop_infected=pop_infected*n_runs) sim2.run() if do_plot: - sim.plot() + msim.plot() sim2.plot() - return sim + return msim #%% Run as a script @@ -103,8 +106,10 @@ def test_combine(do_plot=False): # If being run via pytest, turn off T = sc.tic() # sim1 = test_singlerun() - sims2 = test_multirun(do_plot=do_plot) - # sims3 = test_combine(do_plot=do_plot) + # sims2 = test_multirun(do_plot=do_plot) + # sims3 = test_multisim_stats(do_plot=do_plot) + sims4 = test_multisim_combine(do_plot=do_plot) + # scens = test_scenarios(do_plot=do_plot) sc.toc(T) From a6ea28bb6ae2ade7ea94c280558f0500ec7d0719 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 1 May 2020 21:13:54 -0700 Subject: [PATCH 010/194] working on multirun --- covasim/run.py | 27 ++++++++++++++++----------- tests/test_run.py | 5 ++--- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index 535ca3514..0e818134d 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -67,14 +67,12 @@ def __init__(self, sims=None, base_sim=None): if base_sim is None: if isinstance(sims, cvs.Sim): base_sim = sims - sims = [] + sims = None elif isinstance(sims, list): base_sim = sims[0] else: errormsg = f'If base_sim is not supplied, sims must be either a sims or a list of sims, not {type(sims)}' raise TypeError(errormsg) - if sims is None: - sims = [] self.sims = sims self.base_sim = base_sim @@ -113,12 +111,16 @@ def run(self, *args, **kwargs): Returns: None (modifies Scenarios object in place) ''' + if self.sims is None: + sims = self.base_sim + else: + sims = self.sims - self.sims = multi_run(self.sims, *args, **kwargs) + self.sims = multi_run(sims, *args, **kwargs) return - def combine(self, keep_people=False, output=False): + def combine(self, output=False): ''' Combine multiple sims into a single sim with scaled results ''' n_runs = len(self) @@ -127,7 +129,7 @@ def combine(self, keep_people=False, output=False): combined_sim['pop_size'] *= n_runs # Record the number of people for s,sim in enumerate(self.sims[1:]): # Skip the first one - if keep_people: + if combined_sim.people: combined_sim.people += sim.people for key in sim.result_keys(): this_res = sim.results[key] @@ -803,7 +805,7 @@ def load(scenfile, *args, **kwargs): return scens -def single_run(sim, ind=0, noise=0.0, noisepar=None, verbose=None, keep_people=False, run_args=None, sim_args=None, **kwargs): +def single_run(sim, ind=0, reseed=True, noise=0.0, noisepar=None, verbose=None, keep_people=False, run_args=None, sim_args=None, **kwargs): ''' Convenience function to perform a single simulation run. Mostly used for parallelization, but can also be used directly. @@ -811,6 +813,7 @@ def single_run(sim, ind=0, noise=0.0, noisepar=None, verbose=None, keep_people=F Args: sim (Sim): the sim instance to be run ind (int): the index of this sim + reseed (bool): whether or not to generate a fresh seed for each run noise (float): the amount of noise to add to each run noisepar (string): the name of the parameter to add noise to verbose (int): detail to print @@ -836,8 +839,9 @@ def single_run(sim, ind=0, noise=0.0, noisepar=None, verbose=None, keep_people=F sim_args = sc.mergedicts(sim_args, kwargs) run_args = sc.mergedicts({'verbose':verbose}, run_args) - new_sim['rand_seed'] += ind # Reset the seed, otherwise no point of parallel runs - new_sim.set_seed() + if reseed: + new_sim['rand_seed'] += ind # Reset the seed, otherwise no point of parallel runs + new_sim.set_seed() # If the noise parameter is not found, guess what it should be if noisepar is None: @@ -876,7 +880,7 @@ def single_run(sim, ind=0, noise=0.0, noisepar=None, verbose=None, keep_people=F return new_sim -def multi_run(sim, n_runs=4, noise=0.0, noisepar=None, iterpars=None, verbose=None, combine=False, keep_people=None, run_args=None, sim_args=None, **kwargs): +def multi_run(sim, n_runs=4, reseed=True, noise=0.0, noisepar=None, iterpars=None, verbose=None, combine=False, keep_people=None, run_args=None, sim_args=None, **kwargs): ''' For running multiple runs in parallel. If the first argument is a list of sims, exactly these will be run and most other arguments will be ignored. @@ -884,6 +888,7 @@ def multi_run(sim, n_runs=4, noise=0.0, noisepar=None, iterpars=None, verbose=No Args: sim (Sim or list): the sim instance to be run, or a list of sims. n_runs (int): the number of parallel runs + reseed (bool): whether or not to generate a fresh seed for each run noise (float): the amount of noise to add to each run noisepar (string): the name of the parameter to add noise to iterpars (dict): any other parameters to iterate over the runs; see sc.parallelize() for syntax @@ -925,7 +930,7 @@ def multi_run(sim, n_runs=4, noise=0.0, noisepar=None, iterpars=None, verbose=No if isinstance(sim, cvs.Sim): # Normal case: one sim iterkwargs = {'ind':np.arange(n_runs)} iterkwargs.update(iterpars) - kwargs = {'sim':sim, 'noise':noise, 'noisepar':noisepar, 'verbose':verbose, 'keep_people':keep_people, 'sim_args':sim_args, 'run_args':run_args} + kwargs = dict(sim=sim, reseed=reseed, noise=noise, noisepar=noisepar, verbose=verbose, keep_people=keep_people, sim_args=sim_args, run_args=run_args) sims = sc.parallelize(single_run, iterkwargs=iterkwargs, kwargs=kwargs) elif isinstance(sim, list): # List of sims iterkwargs = {'sim':sim} diff --git a/tests/test_run.py b/tests/test_run.py index a3764b982..b84eed61f 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -85,9 +85,8 @@ def test_multisim_combine(do_plot=False): # If being run via pytest, turn off print('Running first sim...') sim = cv.Sim(pop_size=pop_size, pop_infected=pop_infected) msim = cv.MultiSim(sim) - msim.replicate(n_runs) - msim.run() - sim1 = msim.combine(output=True, keep_people=True) + msim.run(n_runs=n_runs, keep_people=True) + sim1 = msim.combine(output=True) assert sim1['pop_size'] == pop_size*n_runs print('Running second sim, results should be similar but not identical (stochastic differences)...') From 1d80edbf5f050e8d5162eaa4a68f90298bcc1521 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 1 May 2020 21:25:00 -0700 Subject: [PATCH 011/194] combine is working --- tests/test_run.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/test_run.py b/tests/test_run.py index b84eed61f..93743bf79 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -75,6 +75,25 @@ def test_scenarios(do_plot=False): return scens +def test_multisim_reduce(do_plot=False): # If being run via pytest, turn off + sc.heading('Combine results test') + + n_runs = 3 + pop_size = 1000 + pop_infected = 10 + + print('Running first sim...') + sim = cv.Sim(pop_size=pop_size, pop_infected=pop_infected) + msim = cv.MultiSim(sim) + msim.run(n_runs=n_runs, keep_people=True) + # msim.reduce() + + # if do_plot: + # msim.plot() + + return msim + + def test_multisim_combine(do_plot=False): # If being run via pytest, turn off sc.heading('Combine results test') @@ -94,7 +113,7 @@ def test_multisim_combine(do_plot=False): # If being run via pytest, turn off sim2.run() if do_plot: - msim.plot() + sim1.plot() sim2.plot() return msim @@ -106,8 +125,8 @@ def test_multisim_combine(do_plot=False): # If being run via pytest, turn off # sim1 = test_singlerun() # sims2 = test_multirun(do_plot=do_plot) - # sims3 = test_multisim_stats(do_plot=do_plot) - sims4 = test_multisim_combine(do_plot=do_plot) + msim1 = test_multisim_reduce(do_plot=do_plot) + # msim2 = test_multisim_combine(do_plot=do_plot) # scens = test_scenarios(do_plot=do_plot) From 98e288e69d7264c25eb471771e3fdba57f0cdc57 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 1 May 2020 23:36:32 -0700 Subject: [PATCH 012/194] refactored sim plotting into plotting.py --- covasim/plotting.py | 178 ++++++++++++++++++++++++++++++++++--- covasim/run.py | 9 +- covasim/sim.py | 147 ++++++------------------------ covasim/webapp/cova_app.py | 6 +- 4 files changed, 203 insertions(+), 137 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 15b15d6cd..4fcae75d7 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -3,12 +3,167 @@ part of the Sim and Scenarios objects. Intended mostly for use with the webapp. ''' -import covasim as cv import numpy as np +import sciris as sc +import pylab as pl +import datetime as dt +import matplotlib.ticker as ticker import plotly.graph_objects as go +from . import defaults as cvd + + +__all__ = ['plot', 'plot_result', 'plotly_sim', 'plotly_people', 'plotly_animate'] + + +def plot(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, as_dates=True, dateformat=None, + interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, + log_scale=False, do_show=True, sep_figs=False, verbose=None): + ''' + Plot the results -- can supply arguments for both the figure and the plots. + + Args: + to_plot (dict): Dict of results to plot; see get_sim_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() + 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 + 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 + sep_figs (bool): Whether to show separate figures for different results instead of subplots + verbose (bool): Display a bit of extra information + + Returns: + fig: Figure handle + ''' + + if verbose is None: + verbose = sim['verbose'] + sc.printv('Plotting...', 1, verbose) + + if to_plot is None: + to_plot = cvd.get_sim_plots() + to_plot = sc.odict(to_plot) # In case it's supplied as a dict + + # Handle input arguments -- merge user input with defaults + fig_args = sc.mergedicts({'figsize': (16, 14)}, fig_args) + plot_args = sc.mergedicts({'lw': 3, 'alpha': 0.7}, plot_args) + scatter_args = sc.mergedicts({'s':70, 'marker':'s'}, scatter_args) + axis_args = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.97, 'wspace': 0.25, 'hspace': 0.25}, axis_args) + fill_args = sc.mergedicts({'alpha': 0.2}, fill_args) + legend_args = sc.mergedicts({'loc': 'best'}, legend_args) + + if sep_figs: + figs = [] + else: + fig = pl.figure(**fig_args) + pl.subplots_adjust(**axis_args) + pl.rcParams['font.size'] = font_size + if font_family: + pl.rcParams['font.family'] = font_family + + res = sim.results # Shorten since heavily used + + # Plot everything + n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have + for p,title,keylabels in to_plot.enumitems(): + if p == 0: + ax = pl.subplot(n_rows, n_cols, p+1) + else: + ax = pl.subplot(n_rows, n_cols, p + 1, sharex=ax) + if log_scale: + if isinstance(log_scale, list): + if title in log_scale: + ax.set_yscale('log') + else: + ax.set_yscale('log') + for key in keylabels: + label = res[key].name + this_color = res[key].color + y = res[key].values + pl.plot(res['t'], y, label=label, **plot_args, c=this_color) + if sim.data is not None and key in sim.data: + data_t = (sim.data.index-sim['start_day'])/np.timedelta64(1,'D') # Convert from data date to model output index based on model start date + pl.scatter(data_t, sim.data[key], c=[this_color], **scatter_args) + if sim.data is not None and len(sim.data): + pl.scatter(pl.nan, pl.nan, c=[(0,0,0)], label='Data', **scatter_args) + + pl.legend(**legend_args) + pl.grid(grid) + sc.setylim() + if commaticks: + sc.commaticks() + pl.title(title) + + # Optionally reset tick marks (useful for e.g. plotting weeks/months) + if interval: + xmin,xmax = ax.get_xlim() + ax.set_xticks(pl.arange(xmin, xmax+1, interval)) + + # Set xticks as dates + if as_dates: + @ticker.FuncFormatter + def date_formatter(x, pos): + return (sim['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') + ax.xaxis.set_major_formatter(date_formatter) + if not interval: + ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) + + # Plot interventions + for intervention in sim['interventions']: + intervention.plot(sim, ax) + + # Ensure the figure actually renders or saves + if do_save: + if fig_path is None: # No figpath provided - see whether do_save is a figpath + fig_path = 'covasim_sim.png' # Just give it a default name + fig_path = sc.makefilepath(fig_path) # Ensure it's valid, including creating the folder + pl.savefig(fig_path) + + if do_show: + pl.show() + else: + pl.close(fig) + + return fig -__all__ = ['standard_plots', 'plot_people', 'animate_people'] +def plot_result(sim, key, fig_args=None, plot_args=None): + ''' + Simple method to plot a single result. Useful for results that aren't + standard outputs. + + Args: + key (str): the key of the result to plot + fig_args (dict): passed to pl.figure() + plot_args (dict): passed to pl.plot() + + **Examples**:: + + sim.plot_result('doubling_time') + ''' + fig_args = sc.mergedicts({'figsize':(16,10)}, fig_args) + plot_args = sc.mergedicts({'lw':3, 'alpha':0.7}, plot_args) + fig = pl.figure(**fig_args) + pl.subplot(111) + tvec = sim.results['t'] + res = sim.results[key] + y = res.values + color = res.color + pl.plot(tvec, y, c=color, **plot_args) + return fig def get_individual_states(sim): @@ -48,18 +203,18 @@ def get_individual_states(sim): for state in states: date = state['quantity'] if date is not None: - inds = cv.defined(people[date]) + inds = sim.people.defined(date) for ind in inds: z[ind, int(people[date][ind]):] = state['value'] return z, states -def standard_plots(sim): +def plotly_sim(sim): ''' Main simulation results -- parallel of sim.plot() ''' plots = [] - to_plot = cv.get_sim_plots() + to_plot = cvd.get_sim_plots() for p,title,keylabels in to_plot.enumitems(): fig = go.Figure() for key in keylabels: @@ -87,7 +242,7 @@ def standard_plots(sim): return plots -def plot_people(sim, do_show=False): +def plotly_people(sim, do_show=False): ''' Plot a "cascade" of people moving through different states ''' z, states = get_individual_states(sim) @@ -106,10 +261,11 @@ def plot_people(sim, do_show=False): if sim['interventions']: for interv in sim['interventions']: if hasattr(interv, 'days'): - for interv_day in interv.days: - if interv_day > 0 and interv_day < sim['n_days']: - fig.add_shape(dict(type="line", xref="x", yref="paper", x0=interv_day, x1=interv_day, y0=0, y1=1, name='Intervention', line=dict(width=0.5, dash='dash'))) - fig.update_layout(annotations=[dict(x=interv_day, y=1.07, xref="x", yref="paper", text="Intervention change", showarrow=False)]) + if interv.do_plot: + for interv_day in interv.days: + if interv_day > 0 and interv_day < sim['n_days']: + fig.add_shape(dict(type="line", xref="x", yref="paper", x0=interv_day, x1=interv_day, y0=0, y1=1, name='Intervention', line=dict(width=0.5, dash='dash'))) + fig.update_layout(annotations=[dict(x=interv_day, y=1.07, xref="x", yref="paper", text="Intervention change", showarrow=False)]) fig.update_layout(yaxis_range=(0, sim.n)) fig.update_layout(title={'text': 'Numbers of people by health state'}, xaxis_title='Day', yaxis_title='People', autosize=True) @@ -120,7 +276,7 @@ def plot_people(sim, do_show=False): return fig -def animate_people(sim, do_show=False): +def plotly_animate(sim, do_show=False): ''' Plot an animation of each person in the sim ''' z, states = get_individual_states(sim) diff --git a/covasim/run.py b/covasim/run.py index 0e818134d..07408747f 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -563,9 +563,9 @@ def print_heading(string): def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, - axis_args=None, fill_args=None, legend_args=None, as_dates=True, dateformat=None, + scatter_args=None, axis_args=None, fill_args=None, legend_args=None, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, - do_show=True, sep_figs=False, verbose=None): + log_scale=False, do_show=True, sep_figs=False, verbose=None): ''' Plot the results -- can supply arguments for both the figure and the plots. @@ -575,6 +575,7 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar 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() @@ -586,6 +587,7 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar font_family (str): Font face grid (bool): Whether or not to plot gridlines commaticks (bool): Plot y-axis with commas rather than scientific notation + log_scale (bool or list): 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 sep_figs (bool): Whether to show separate figures for different results instead of subplots verbose (bool): Display a bit of extra information @@ -605,7 +607,8 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar # Handle input arguments -- merge user input with defaults fig_args = sc.mergedicts({'figsize': (16, 14)}, fig_args) plot_args = sc.mergedicts({'lw': 3, 'alpha': 0.7}, plot_args) - axis_args = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.90, 'wspace': 0.25, 'hspace': 0.25}, axis_args) + scatter_args = sc.mergedicts({'s':70, 'marker':'s'}, scatter_args) + axis_args = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.97, 'wspace': 0.25, 'hspace': 0.25}, axis_args) fill_args = sc.mergedicts({'alpha': 0.2}, fill_args) legend_args = sc.mergedicts({'loc': 'best'}, legend_args) diff --git a/covasim/sim.py b/covasim/sim.py index 069505494..ca2fa5aba 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -6,8 +6,6 @@ import numpy as np import pylab as pl import sciris as sc -import datetime as dt -import matplotlib.ticker as ticker from . import version as cvv from . import utils as cvu from . import misc as cvm @@ -16,6 +14,7 @@ from . import parameters as cvpar from . import population as cvpop from . import interventions as cvi +from . import plotting as cvplt # Specify all externally visible things this file defines __all__ = ['Sim'] @@ -652,124 +651,40 @@ def summary_stats(self, verbose=None): def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, - scatter_args=None, axis_args=None, legend_args=None, as_dates=True, dateformat=None, - interval=None, n_cols=1, font_size=18, font_family=None, use_grid=True, use_commaticks=True, - log_scale=False, do_show=True, verbose=None): + scatter_args=None, axis_args=None, fill_args=None, legend_args=None, as_dates=True, dateformat=None, + interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, + log_scale=False, do_show=True, sep_figs=False, verbose=None): ''' Plot the results -- can supply arguments for both the figure and the plots. Args: - to_plot (dict): Nested dict of results to plot; see get_sim_plots() for structure - do_save (bool or str): Whether or not to save the figure. If a string, save to that filename. - 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() + to_plot (dict): Dict of results to plot; see get_sim_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() - legend_args (dict): Dictionary of kwargs to be passed to pl.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 - use_grid (bool): Whether or not to plot gridlines - use_commaticks (bool): Plot y-axis with commas rather than scientific notation - log_scale (bool or list): 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 - verbose (bool): Display a bit of extra information + 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() + 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 + 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 + verbose (bool): Display a bit of extra information Returns: fig: Figure handle ''' - - if verbose is None: - verbose = self['verbose'] - sc.printv('Plotting...', 1, verbose) - - if to_plot is None: - to_plot = cvd.get_sim_plots() - to_plot = sc.odict(to_plot) # In case it's supplied as a dict - - # Handle input arguments -- merge user input with defaults - fig_args = sc.mergedicts({'figsize':(16,14)}, fig_args) - plot_args = sc.mergedicts({'lw':3, 'alpha':0.7}, plot_args) - scatter_args = sc.mergedicts({'s':70, 'marker':'s'}, scatter_args) - axis_args = sc.mergedicts({'left':0.1, 'bottom':0.05, 'right':0.9, 'top':0.97, 'wspace':0.2, 'hspace':0.25}, axis_args) - legend_args = sc.mergedicts({'loc': 'best'}, legend_args) - - fig = pl.figure(**fig_args) - pl.subplots_adjust(**axis_args) - pl.rcParams['font.size'] = font_size - if font_family: - pl.rcParams['font.family'] = font_family - - res = self.results # Shorten since heavily used - - # Plot everything - n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have - for p,title,keylabels in to_plot.enumitems(): - if p == 0: - ax = pl.subplot(n_rows, n_cols, p+1) - else: - ax = pl.subplot(n_rows, n_cols, p + 1, sharex=ax) - if log_scale: - if isinstance(log_scale, list): - if title in log_scale: - ax.set_yscale('log') - else: - ax.set_yscale('log') - for key in keylabels: - label = res[key].name - this_color = res[key].color - y = res[key].values - pl.plot(res['t'], y, label=label, **plot_args, c=this_color) - if self.data is not None and key in self.data: - data_t = (self.data.index-self['start_day'])/np.timedelta64(1,'D') # Convert from data date to model output index based on model start date - pl.scatter(data_t, self.data[key], c=[this_color], **scatter_args) - if self.data is not None and len(self.data): - pl.scatter(pl.nan, pl.nan, c=[(0,0,0)], label='Data', **scatter_args) - - pl.legend(**legend_args) - pl.grid(use_grid) - sc.setylim() - if use_commaticks: - sc.commaticks() - pl.title(title) - - # Optionally reset tick marks (useful for e.g. plotting weeks/months) - if interval: - xmin,xmax = ax.get_xlim() - ax.set_xticks(pl.arange(xmin, xmax+1, interval)) - - # Set xticks as dates - if as_dates: - @ticker.FuncFormatter - def date_formatter(x, pos): - return (self['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') - ax.xaxis.set_major_formatter(date_formatter) - if not interval: - ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) - - # Plot interventions - for intervention in self['interventions']: - intervention.plot(self, ax) - - # Ensure the figure actually renders or saves - if do_save: - if fig_path is None: # No figpath provided - see whether do_save is a figpath - if isinstance(do_save, str) : - fig_path = do_save # It's a string, assume it's a filename - else: - fig_path = 'covasim.png' # Just give it a default name - fig_path = sc.makefilepath(fig_path) # Ensure it's valid, including creating the folder - pl.savefig(fig_path) - - if do_show: - pl.show() - else: - pl.close(fig) - + fig = cvplt.plot(sim=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, + scatter_args=scatter_args, axis_args=axis_args, legend_args=legend_args, as_dates=as_dates, dateformat=dateformat, + interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, + log_scale=log_scale, do_show=do_show, verbose=verbose) return fig @@ -787,13 +702,5 @@ def plot_result(self, key, fig_args=None, plot_args=None): sim.plot_result('doubling_time') ''' - fig_args = sc.mergedicts({'figsize':(16,10)}, fig_args) - plot_args = sc.mergedicts({'lw':3, 'alpha':0.7}, plot_args) - fig = pl.figure(**fig_args) - pl.subplot(111) - tvec = self.results['t'] - res = self.results[key] - y = res.values - color = res.color - pl.plot(tvec, y, c=color, **plot_args) + fig = cvplt.plot_result(sim=self, key=key, fig_args=fig_args, plot_args=plot_args) return fig diff --git a/covasim/webapp/cova_app.py b/covasim/webapp/cova_app.py index 69054e10f..8fda752ed 100644 --- a/covasim/webapp/cova_app.py +++ b/covasim/webapp/cova_app.py @@ -369,10 +369,10 @@ def process_graphs(figs): graphs = [] try: - graphs += process_graphs(cv.standard_plots(sim)) - graphs += process_graphs(cv.plot_people(sim)) + graphs += process_graphs(cv.plotly_sim(sim)) + graphs += process_graphs(cv.plotly_people(sim)) if show_animation: - graphs += process_graphs(cv.animate_people(sim)) + graphs += process_graphs(cv.plotly_animate(sim)) except Exception as E: errs.append(log_err('Plotting failed!', E)) if die: raise From 62e62b429d3e166c67a88c35560b4222e83286fb Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 1 May 2020 23:56:53 -0700 Subject: [PATCH 013/194] working --- covasim/defaults.py | 10 +++++-- covasim/run.py | 69 +++++++++++++++++++++++---------------------- 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/covasim/defaults.py b/covasim/defaults.py index b946ef343..605b8d935 100644 --- a/covasim/defaults.py +++ b/covasim/defaults.py @@ -178,10 +178,16 @@ def get_sim_plots(): def get_scen_plots(): ''' Default scenario plots -- used in run.py ''' - plots = [ + plots = sc.odict({ + 'Cumulative infections': [ 'cum_infections', + ], + 'Number of people currently infectious': [ 'n_infectious', + ], + 'Number of people requiring hospitalization': [ 'n_severe', - ] + ] + }) return plots diff --git a/covasim/run.py b/covasim/run.py index 07408747f..29ec5f81b 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -622,48 +622,49 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar pl.rcParams['font.family'] = font_family n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have - for rk,reskey in enumerate(to_plot): - title = self.base_sim.results[reskey].name # Get the name of this result from the base simulation + for rk,title,reskeys in to_plot.enumitems(): if sep_figs: figs.append(pl.figure(**fig_args)) ax = pl.subplot(111) else: if rk == 0: - ax = pl.subplot(n_rows, n_cols, rk + 1) + ax = pl.subplot(n_rows, n_cols, rk+1) else: - ax = pl.subplot(n_rows, n_cols, rk + 1, sharex=ax) - - resdata = self.results[reskey] - - for scenkey, scendata in resdata.items(): - - pl.fill_between(self.tvec, scendata.low, scendata.high, **fill_args) - pl.plot(self.tvec, scendata.best, label=scendata.name, **plot_args) - pl.title(title) - if rk == 0: - pl.legend(**legend_args) - - pl.grid(grid) - if commaticks: - sc.commaticks() + ax = pl.subplot(n_rows, n_cols, rk+1, sharex=ax) - if self.base_sim.data is not None and reskey in self.base_sim.data: - data_t = np.array((self.base_sim.data.index-self.base_sim['start_day'])/np.timedelta64(1,'D')) - pl.plot(data_t, self.base_sim.data[reskey], 'sk', **plot_args) - - # Optionally reset tick marks (useful for e.g. plotting weeks/months) - if interval: - xmin,xmax = ax.get_xlim() - ax.set_xticks(pl.arange(xmin, xmax+1, interval)) + for reskey in reskeys: - # Set xticks as dates - if as_dates: - @ticker.FuncFormatter - def date_formatter(x, pos): - return (self.base_sim['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') - ax.xaxis.set_major_formatter(date_formatter) - if not interval: - ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) + resdata = self.results[reskey] + + for scenkey, scendata in resdata.items(): + + pl.fill_between(self.tvec, scendata.low, scendata.high, **fill_args) + pl.plot(self.tvec, scendata.best, label=scendata.name, **plot_args) + pl.title(title) + if rk == 0: + pl.legend(**legend_args) + + pl.grid(grid) + if commaticks: + sc.commaticks() + + if self.base_sim.data is not None and reskey in self.base_sim.data: + data_t = np.array((self.base_sim.data.index-self.base_sim['start_day'])/np.timedelta64(1,'D')) + pl.plot(data_t, self.base_sim.data[reskey], 'sk', **plot_args) + + # Optionally reset tick marks (useful for e.g. plotting weeks/months) + if interval: + xmin,xmax = ax.get_xlim() + ax.set_xticks(pl.arange(xmin, xmax+1, interval)) + + # Set xticks as dates + if as_dates: + @ticker.FuncFormatter + def date_formatter(x, pos): + return (self.base_sim['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') + ax.xaxis.set_major_formatter(date_formatter) + if not interval: + ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) # Ensure the figure actually renders or saves if do_save: From 94f61447e7b175eb68d8d32f83e9aab8536cbb16 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 00:08:35 -0700 Subject: [PATCH 014/194] moved scenarios plotting into shared library --- covasim/plotting.py | 123 ++++++++++++++++ covasim/run.py | 334 +------------------------------------------- 2 files changed, 128 insertions(+), 329 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 4fcae75d7..a2b8d9f24 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -140,6 +140,129 @@ def date_formatter(x, pos): return fig + +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, as_dates=True, dateformat=None, + interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, + log_scale=False, do_show=True, sep_figs=False, verbose=None): + ''' + Plot the results -- can supply arguments for both the figure and the plots. + + Args: + scens (Scens): The Scenarios object being plotted + 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() + 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 + 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 + sep_figs (bool): Whether to show separate figures for different results instead of subplots + verbose (bool): Display a bit of extra information + + Returns: + fig: Figure handle + ''' + + if verbose is None: + verbose = scens['verbose'] + sc.printv('Plotting...', 1, verbose) + + if to_plot is None: + to_plot = cvd.get_scen_plots() + to_plot = sc.dcp(to_plot) # In case it's supplied as a dict + + # Handle input arguments -- merge user input with defaults + fig_args = sc.mergedicts({'figsize': (16, 14)}, fig_args) + plot_args = sc.mergedicts({'lw': 3, 'alpha': 0.7}, plot_args) + scatter_args = sc.mergedicts({'s':70, 'marker':'s'}, scatter_args) + axis_args = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.97, 'wspace': 0.25, 'hspace': 0.25}, axis_args) + fill_args = sc.mergedicts({'alpha': 0.2}, fill_args) + legend_args = sc.mergedicts({'loc': 'best'}, legend_args) + + sim = scens.base_sim + + if sep_figs: + figs = [] + else: + fig = pl.figure(**fig_args) + pl.subplots_adjust(**axis_args) + pl.rcParams['font.size'] = font_size + if font_family: + pl.rcParams['font.family'] = font_family + + n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have + for rk,title,reskeys in to_plot.enumitems(): + if sep_figs: + figs.append(pl.figure(**fig_args)) + ax = pl.subplot(111) + else: + if rk == 0: + ax = pl.subplot(n_rows, n_cols, rk+1) + else: + ax = pl.subplot(n_rows, n_cols, rk+1, sharex=ax) + + for reskey in reskeys: + + resdata = scens.results[reskey] + + for scenkey, scendata in resdata.items(): + + pl.fill_between(scens.tvec, scendata.low, scendata.high, **fill_args) + pl.plot(scens.tvec, scendata.best, label=scendata.name, **plot_args) + pl.title(title) + if rk == 0: + pl.legend(**legend_args) + + pl.grid(grid) + if commaticks: + sc.commaticks() + + if sim.data is not None and reskey in sim.data: + data_t = np.array((sim.data.index-sim['start_day'])/np.timedelta64(1,'D')) + pl.plot(data_t, sim.data[reskey], 'sk', **plot_args) + + # Optionally reset tick marks (useful for e.g. plotting weeks/months) + if interval: + xmin,xmax = ax.get_xlim() + ax.set_xticks(pl.arange(xmin, xmax+1, interval)) + + # Set xticks as dates + if as_dates: + @ticker.FuncFormatter + def date_formatter(x, pos): + return (sim['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') + ax.xaxis.set_major_formatter(date_formatter) + if not interval: + ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) + + # Ensure the figure actually renders or saves + if do_save: + if fig_path is None: # No figpath provided - see whether do_save is a figpath + fig_path = 'covasim_scenarios.png' # Just give it a default name + fig_path = sc.makefilepath(fig_path) # Ensure it's valid, including creating the folder + pl.savefig(fig_path) + + if do_show: + pl.show() + else: + pl.close(fig) + + return fig + + def plot_result(sim, key, fig_args=None, plot_args=None): ''' Simple method to plot a single result. Useful for results that aren't diff --git a/covasim/run.py b/covasim/run.py index 29ec5f81b..8a7cc5f52 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -7,13 +7,11 @@ import pylab as pl import pandas as pd import sciris as sc -import datetime as dt from collections import defaultdict -import matplotlib.ticker as ticker from . import misc as cvm -from . import defaults as cvd from . import base as cvb from . import sim as cvs +from . import plotting as cvplt # Specify all externally visible functions this file defines @@ -150,250 +148,6 @@ def combine(self, output=False): return - def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, - axis_args=None, fill_args=None, legend_args=None, as_dates=True, dateformat=None, - interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, - do_show=True, sep_figs=False, verbose=None): - ''' - Plot the results -- can supply arguments for both the figure and the plots. - - 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() - 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() - 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 - do_show (bool): Whether or not to show the figure - sep_figs (bool): Whether to show separate figures for different results instead of subplots - verbose (bool): Display a bit of extra information - - Returns: - fig: Figure handle - ''' - - if verbose is None: - verbose = self['verbose'] - sc.printv('Plotting...', 1, verbose) - - if to_plot is None: - to_plot = cvd.get_scen_plots() - to_plot = sc.dcp(to_plot) # In case it's supplied as a dict - - # Handle input arguments -- merge user input with defaults - fig_args = sc.mergedicts({'figsize': (16, 14)}, fig_args) - plot_args = sc.mergedicts({'lw': 3, 'alpha': 0.7}, plot_args) - axis_args = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.90, 'wspace': 0.25, 'hspace': 0.25}, axis_args) - fill_args = sc.mergedicts({'alpha': 0.2}, fill_args) - legend_args = sc.mergedicts({'loc': 'best'}, legend_args) - - if sep_figs: - figs = [] - else: - fig = pl.figure(**fig_args) - pl.subplots_adjust(**axis_args) - pl.rcParams['font.size'] = font_size - if font_family: - pl.rcParams['font.family'] = font_family - - n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have - for rk,reskey in enumerate(to_plot): - title = self.base_sim.results[reskey].name # Get the name of this result from the base simulation - if sep_figs: - figs.append(pl.figure(**fig_args)) - ax = pl.subplot(111) - else: - if rk == 0: - ax = pl.subplot(n_rows, n_cols, rk + 1) - else: - ax = pl.subplot(n_rows, n_cols, rk + 1, sharex=ax) - - resdata = self.results[reskey] - - for scenkey, scendata in resdata.items(): - - pl.fill_between(self.tvec, scendata.low, scendata.high, **fill_args) - pl.plot(self.tvec, scendata.best, label=scendata.name, **plot_args) - pl.title(title) - if rk == 0: - pl.legend(**legend_args) - - pl.grid(grid) - if commaticks: - sc.commaticks() - - if self.base_sim.data is not None and reskey in self.base_sim.data: - data_t = np.array((self.base_sim.data.index-self.base_sim['start_day'])/np.timedelta64(1,'D')) - pl.plot(data_t, self.base_sim.data[reskey], 'sk', **plot_args) - - # Optionally reset tick marks (useful for e.g. plotting weeks/months) - if interval: - xmin,xmax = ax.get_xlim() - ax.set_xticks(pl.arange(xmin, xmax+1, interval)) - - # Set xticks as dates - if as_dates: - @ticker.FuncFormatter - def date_formatter(x, pos): - return (self.base_sim['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') - ax.xaxis.set_major_formatter(date_formatter) - if not interval: - ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) - - # Ensure the figure actually renders or saves - if do_save: - if fig_path is None: # No figpath provided - see whether do_save is a figpath - fig_path = 'covasim_scenarios.png' # Just give it a default name - fig_path = sc.makefilepath(fig_path) # Ensure it's valid, including creating the folder - pl.savefig(fig_path) - - if do_show: - pl.show() - else: - pl.close(fig) - - return fig - - - def to_json(self, filename=None, tostring=True, indent=2, verbose=False, *args, **kwargs): - ''' - Export results as JSON. - - Args: - filename (str): if None, return string; else, write to file - - Returns: - A unicode string containing a JSON representation of the results, - or writes the JSON file to disk - - ''' - d = {'t':self.tvec, - 'results': self.results, - 'basepars': self.basepars, - 'metapars': self.metapars, - 'simpars': self.base_sim.export_pars(), - 'scenarios': self.scenarios - } - if filename is None: - output = sc.jsonify(d, tostring=tostring, indent=indent, verbose=verbose, *args, **kwargs) - else: - output = sc.savejson(filename=filename, obj=d, indent=indent, *args, **kwargs) - - return output - - - def to_excel(self, filename=None): - ''' - Export results as XLSX - - Args: - filename (str): if None, return string; else, write to file - - Returns: - An sc.Spreadsheet with an Excel file, or writes the file to disk - - ''' - spreadsheet = sc.Spreadsheet() - spreadsheet.freshbytes() - with pd.ExcelWriter(spreadsheet.bytes, engine='xlsxwriter') as writer: - for key in self.result_keys(): - result_df = pd.DataFrame.from_dict(sc.flattendict(self.results[key], sep='_')) - result_df.to_excel(writer, sheet_name=key) - spreadsheet.load() - - if filename is None: - output = spreadsheet - else: - output = spreadsheet.save(filename) - - return output - - - def save(self, scenfile=None, keep_sims=True, keep_people=False, **kwargs): - ''' - Save to disk as a gzipped pickle. - - Args: - scenfile (str or None): the name or path of the file to save to; if None, uses stored - keep_sims (bool): whether or not to store the actual Sim objects in the Scenarios object - keep_people (bool): whether or not to store the population in the Sim objects (NB, very large) - keywords: passed to makefilepath() - - Returns: - scenfile (str): the validated absolute path to the saved file - - **Example**:: - - scens.save() # Saves to a .scens file with the date and time of creation by default - - ''' - if scenfile is None: - scenfile = self.scenfile - scenfile = sc.makefilepath(filename=scenfile, **kwargs) - self.scenfile = scenfile # Store the actual saved filename - - # Store sims seperately - sims = self.sims - self.sims = None # Remove for now - - obj = sc.dcp(self) # This should be quick once we've removed the sims - if not keep_people: - obj.base_sim.shrink(in_place=True) - - if keep_sims or keep_people: - if keep_people: - if not obj._kept_people: - print('Warning: there are no people because they were not saved during the run. ' - 'If you want people, please rerun with keep_people=True.') - obj.sims = sims # Just restore the object in full - print('Note: saving people, which may produce a large file!') - else: - obj.sims = sc.objdict() - for key in sims.keys(): - obj.sims[key] = [] - for sim in sims[key]: - obj.sims[key].append(sim.shrink(in_place=False)) - - sc.saveobj(filename=scenfile, obj=obj) # Actually save - - self.sims = sims # Restore - return scenfile - - - @staticmethod - def load(scenfile, *args, **kwargs): - ''' - Load from disk from a gzipped pickle. - - Args: - scenfile (str): the name or path of the file to save to - kwargs: passed to sc.loadobj() - - Returns: - scens (Scenarios): the loaded scenarios object - - **Example**:: - - scens = cv.Scenarios.load('my-scenarios.scens') - ''' - scens = cvm.load(scenfile, *args, **kwargs) - if not isinstance(scens, Scenarios): - errormsg = f'Cannot load object of {type(scens)} as a Scenarios object' - raise TypeError(errormsg) - return scens - - - class Scenarios(cvb.ParsObj): ''' Class for running multiple sets of multiple simulations -- e.g., scenarios. @@ -595,88 +349,10 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar Returns: fig: Figure handle ''' - - if verbose is None: - verbose = self['verbose'] - sc.printv('Plotting...', 1, verbose) - - if to_plot is None: - to_plot = cvd.get_scen_plots() - to_plot = sc.dcp(to_plot) # In case it's supplied as a dict - - # Handle input arguments -- merge user input with defaults - fig_args = sc.mergedicts({'figsize': (16, 14)}, fig_args) - plot_args = sc.mergedicts({'lw': 3, 'alpha': 0.7}, plot_args) - scatter_args = sc.mergedicts({'s':70, 'marker':'s'}, scatter_args) - axis_args = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.97, 'wspace': 0.25, 'hspace': 0.25}, axis_args) - fill_args = sc.mergedicts({'alpha': 0.2}, fill_args) - legend_args = sc.mergedicts({'loc': 'best'}, legend_args) - - if sep_figs: - figs = [] - else: - fig = pl.figure(**fig_args) - pl.subplots_adjust(**axis_args) - pl.rcParams['font.size'] = font_size - if font_family: - pl.rcParams['font.family'] = font_family - - n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have - for rk,title,reskeys in to_plot.enumitems(): - if sep_figs: - figs.append(pl.figure(**fig_args)) - ax = pl.subplot(111) - else: - if rk == 0: - ax = pl.subplot(n_rows, n_cols, rk+1) - else: - ax = pl.subplot(n_rows, n_cols, rk+1, sharex=ax) - - for reskey in reskeys: - - resdata = self.results[reskey] - - for scenkey, scendata in resdata.items(): - - pl.fill_between(self.tvec, scendata.low, scendata.high, **fill_args) - pl.plot(self.tvec, scendata.best, label=scendata.name, **plot_args) - pl.title(title) - if rk == 0: - pl.legend(**legend_args) - - pl.grid(grid) - if commaticks: - sc.commaticks() - - if self.base_sim.data is not None and reskey in self.base_sim.data: - data_t = np.array((self.base_sim.data.index-self.base_sim['start_day'])/np.timedelta64(1,'D')) - pl.plot(data_t, self.base_sim.data[reskey], 'sk', **plot_args) - - # Optionally reset tick marks (useful for e.g. plotting weeks/months) - if interval: - xmin,xmax = ax.get_xlim() - ax.set_xticks(pl.arange(xmin, xmax+1, interval)) - - # Set xticks as dates - if as_dates: - @ticker.FuncFormatter - def date_formatter(x, pos): - return (self.base_sim['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') - ax.xaxis.set_major_formatter(date_formatter) - if not interval: - ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) - - # Ensure the figure actually renders or saves - if do_save: - if fig_path is None: # No figpath provided - see whether do_save is a figpath - fig_path = 'covasim_scenarios.png' # Just give it a default name - fig_path = sc.makefilepath(fig_path) # Ensure it's valid, including creating the folder - pl.savefig(fig_path) - - if do_show: - pl.show() - else: - pl.close(fig) + fig = cvplt.plot_scens(scens=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, + scatter_args=scatter_args, axis_args=axis_args, fill_args=fill_args, legend_args=legend_args, as_dates=as_dates, dateformat=dateformat, + interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, + log_scale=log_scale, do_show=do_show, sep_figs=sep_figs, verbose=verbose) return fig From 8604a342809c1b933e7f6dd2eb1c115fab7c1d1e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 00:42:16 -0700 Subject: [PATCH 015/194] split into functions --- covasim/plotting.py | 114 ++++++++++++++++++++++++++------------------ 1 file changed, 67 insertions(+), 47 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index a2b8d9f24..02f6c09e3 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -140,6 +140,68 @@ def date_formatter(x, pos): return fig +def set_figs(figs, n_rows, n_cols, rk, fig_args, sep_figs): + if sep_figs: + figs.append(pl.figure(**fig_args)) + ax = pl.subplot(111) + else: + if rk == 0: + ax = pl.subplot(n_rows, n_cols, rk+1) + else: + ax = pl.subplot(n_rows, n_cols, rk+1, sharex=ax) + return ax + + +def title_grid_ticks_legend(title, grid, commaticks, legend_args, rk): + pl.title(title) + if rk == 0: + pl.legend(**legend_args) + + pl.grid(grid) + if commaticks: + sc.commaticks() + return + + +def plot_data(sim, reskey, plot_args): + if sim.data is not None and reskey in sim.data: + data_t = np.array((sim.data.index-sim['start_day'])/np.timedelta64(1,'D')) + pl.plot(data_t, sim.data[reskey], 'sk', **plot_args) + return + + +def reset_ticks(ax, sim, interval, as_dates): + if interval: + xmin,xmax = ax.get_xlim() + ax.set_xticks(pl.arange(xmin, xmax+1, interval)) + + # Set xticks as dates + if as_dates: + + @ticker.FuncFormatter + def date_formatter(x, pos): + return (sim['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') + + ax.xaxis.set_major_formatter(date_formatter) + if not interval: + ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) + + return + +def tidy_up(fig, do_save, fig_path, do_show, default_name): + if do_save: + if fig_path is None: # No figpath provided - see whether do_save is a figpath + fig_path = default_name # Just give it a default name + fig_path = sc.makefilepath(fig_path) # Ensure it's valid, including creating the folder + pl.savefig(fig_path) + + if do_show: + pl.show() + else: + pl.close(fig) + + return + 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, as_dates=True, dateformat=None, @@ -205,60 +267,18 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have for rk,title,reskeys in to_plot.enumitems(): - if sep_figs: - figs.append(pl.figure(**fig_args)) - ax = pl.subplot(111) - else: - if rk == 0: - ax = pl.subplot(n_rows, n_cols, rk+1) - else: - ax = pl.subplot(n_rows, n_cols, rk+1, sharex=ax) - + ax = set_figs(figs, n_rows, n_cols, rk, fig_args, sep_figs) for reskey in reskeys: - resdata = scens.results[reskey] - for scenkey, scendata in resdata.items(): - pl.fill_between(scens.tvec, scendata.low, scendata.high, **fill_args) pl.plot(scens.tvec, scendata.best, label=scendata.name, **plot_args) - pl.title(title) - if rk == 0: - pl.legend(**legend_args) - - pl.grid(grid) - if commaticks: - sc.commaticks() - - if sim.data is not None and reskey in sim.data: - data_t = np.array((sim.data.index-sim['start_day'])/np.timedelta64(1,'D')) - pl.plot(data_t, sim.data[reskey], 'sk', **plot_args) - - # Optionally reset tick marks (useful for e.g. plotting weeks/months) - if interval: - xmin,xmax = ax.get_xlim() - ax.set_xticks(pl.arange(xmin, xmax+1, interval)) - - # Set xticks as dates - if as_dates: - @ticker.FuncFormatter - def date_formatter(x, pos): - return (sim['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') - ax.xaxis.set_major_formatter(date_formatter) - if not interval: - ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) + title_grid_ticks_legend(title, grid, commaticks, legend_args, rk) + plot_data(sim, reskey) + reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) # Ensure the figure actually renders or saves - if do_save: - if fig_path is None: # No figpath provided - see whether do_save is a figpath - fig_path = 'covasim_scenarios.png' # Just give it a default name - fig_path = sc.makefilepath(fig_path) # Ensure it's valid, including creating the folder - pl.savefig(fig_path) - - if do_show: - pl.show() - else: - pl.close(fig) + tidy_up(fig, do_save, fig_path, do_show, 'covasim_scenarios.png') return fig From 1d9d0376c85e3e7e9c7bf2a7b5f2456d3ecec545 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 12:13:08 -0700 Subject: [PATCH 016/194] more refactoring --- covasim/plotting.py | 74 ++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 02f6c09e3..27e90833b 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -140,7 +140,38 @@ def date_formatter(x, pos): return fig -def set_figs(figs, n_rows, n_cols, rk, fig_args, sep_figs): +def handle_args(to_plot, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args): + ''' Handle input arguments -- merge user input with defaults ''' + args = sc.objdict() + args.fig = sc.mergedicts({'figsize': (16, 14)}, fig_args) + args.plot = sc.mergedicts({'lw': 3, 'alpha': 0.7}, plot_args) + args.scatter = sc.mergedicts({'s':70, 'marker':'s'}, scatter_args) + args.axis = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.97, 'wspace': 0.25, 'hspace': 0.25}, axis_args) + args.fill = sc.mergedicts({'alpha': 0.2}, fill_args) + args.legend = sc.mergedicts({'loc': 'best'}, legend_args) + + if to_plot is None: + to_plot = cvd.get_scen_plots() + to_plot = sc.dcp(to_plot) # In case it's supplied as a dict + + return to_plot, args + + +def create_figs(sep_figs, args, font_size, font_family): + if sep_figs: + fig = None + figs = [] + else: + fig = pl.figure(**args.fig) + figs = None + pl.subplots_adjust(**args.axis) + pl.rcParams['font.size'] = font_size + if font_family: + pl.rcParams['font.family'] = font_family + return fig, figs + + +def create_subplots(figs, n_rows, n_cols, rk, fig_args, sep_figs): if sep_figs: figs.append(pl.figure(**fig_args)) ax = pl.subplot(111) @@ -206,7 +237,7 @@ def tidy_up(fig, do_save, fig_path, do_show, default_name): 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, - log_scale=False, do_show=True, sep_figs=False, verbose=None): + log_scale=False, do_show=True, sep_figs=False): ''' Plot the results -- can supply arguments for both the figure and the plots. @@ -232,55 +263,36 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, 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 sep_figs (bool): Whether to show separate figures for different results instead of subplots - verbose (bool): Display a bit of extra information Returns: fig: Figure handle ''' - if verbose is None: - verbose = scens['verbose'] - sc.printv('Plotting...', 1, verbose) - - if to_plot is None: - to_plot = cvd.get_scen_plots() - to_plot = sc.dcp(to_plot) # In case it's supplied as a dict - - # Handle input arguments -- merge user input with defaults - fig_args = sc.mergedicts({'figsize': (16, 14)}, fig_args) - plot_args = sc.mergedicts({'lw': 3, 'alpha': 0.7}, plot_args) - scatter_args = sc.mergedicts({'s':70, 'marker':'s'}, scatter_args) - axis_args = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.97, 'wspace': 0.25, 'hspace': 0.25}, axis_args) - fill_args = sc.mergedicts({'alpha': 0.2}, fill_args) - legend_args = sc.mergedicts({'loc': 'best'}, legend_args) + to_plot, args = handle_args(to_plot, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) sim = scens.base_sim - if sep_figs: - figs = [] - else: - fig = pl.figure(**fig_args) - pl.subplots_adjust(**axis_args) - pl.rcParams['font.size'] = font_size - if font_family: - pl.rcParams['font.family'] = font_family + fig, figs = create_figs(sep_figs, args, font_size, font_family) n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have for rk,title,reskeys in to_plot.enumitems(): - ax = set_figs(figs, n_rows, n_cols, rk, fig_args, sep_figs) + ax = create_subplots(figs, n_rows, n_cols, rk, args.fig, sep_figs) for reskey in reskeys: resdata = scens.results[reskey] for scenkey, scendata in resdata.items(): - pl.fill_between(scens.tvec, scendata.low, scendata.high, **fill_args) - pl.plot(scens.tvec, scendata.best, label=scendata.name, **plot_args) - title_grid_ticks_legend(title, grid, commaticks, legend_args, rk) + pl.fill_between(scens.tvec, scendata.low, scendata.high, **args.fill) + pl.plot(scens.tvec, scendata.best, label=scendata.name, **args.plot) + title_grid_ticks_legend(title, grid, commaticks, args.legend, rk) plot_data(sim, reskey) reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) # Ensure the figure actually renders or saves tidy_up(fig, do_save, fig_path, do_show, 'covasim_scenarios.png') - return fig + if sep_figs: + return figs + else: + return fig def plot_result(sim, key, fig_args=None, plot_args=None): From 7465bb795a01b369365104475b715d36733addfd Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 12:22:15 -0700 Subject: [PATCH 017/194] about to convert sim plotting --- covasim/plotting.py | 203 +++++++++++++++++++++++--------------------- covasim/run.py | 3 +- covasim/sim.py | 5 +- 3 files changed, 108 insertions(+), 103 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 27e90833b..47a8122b7 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -12,10 +12,110 @@ from . import defaults as cvd -__all__ = ['plot', 'plot_result', 'plotly_sim', 'plotly_people', 'plotly_animate'] +__all__ = ['plot_sim', 'plot_scens', 'plot_result', 'plotly_sim', 'plotly_people', 'plotly_animate'] -def plot(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, + + + +def handle_args(to_plot, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args): + ''' Handle input arguments -- merge user input with defaults ''' + args = sc.objdict() + args.fig = sc.mergedicts({'figsize': (16, 14)}, fig_args) + args.plot = sc.mergedicts({'lw': 3, 'alpha': 0.7}, plot_args) + args.scatter = sc.mergedicts({'s':70, 'marker':'s'}, scatter_args) + args.axis = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.97, 'wspace': 0.25, 'hspace': 0.25}, axis_args) + args.fill = sc.mergedicts({'alpha': 0.2}, fill_args) + args.legend = sc.mergedicts({'loc': 'best'}, legend_args) + + if to_plot is None: + to_plot = cvd.get_scen_plots() + to_plot = sc.dcp(to_plot) # In case it's supplied as a dict + + return to_plot, args + + +def create_figs(sep_figs, args, font_size, font_family): + if sep_figs: + fig = None + figs = [] + else: + fig = pl.figure(**args.fig) + figs = None + pl.subplots_adjust(**args.axis) + pl.rcParams['font.size'] = font_size + if font_family: + pl.rcParams['font.family'] = font_family + return fig, figs + + +def create_subplots(figs, ax, n_rows, n_cols, rk, fig_args, sep_figs): + if sep_figs: + figs.append(pl.figure(**fig_args)) + ax = pl.subplot(111) + else: + if rk == 0: + ax = pl.subplot(n_rows, n_cols, rk+1) + else: + ax = pl.subplot(n_rows, n_cols, rk+1, sharex=ax) + return ax + + +def title_grid_ticks_legend(title, grid, commaticks, legend_args, rk): + pl.title(title) + if rk == 0: + pl.legend(**legend_args) + + pl.grid(grid) + if commaticks: + sc.commaticks() + return + + +def plot_data(sim, key, scatter_args): + if sim.data is not None and key in sim.data and len(sim.data[key]): + this_color = sim.results[key].color + data_t = (sim.data.index-sim['start_day'])/np.timedelta64(1,'D') # Convert from data date to model output index based on model start date + pl.scatter(data_t, sim.data[key], c=[this_color], **scatter_args) + pl.scatter(pl.nan, pl.nan, c=[(0,0,0)], label='Data', **scatter_args) + return + + +def reset_ticks(ax, sim, interval, as_dates): + if interval: + xmin,xmax = ax.get_xlim() + ax.set_xticks(pl.arange(xmin, xmax+1, interval)) + + # Set xticks as dates + if as_dates: + + @ticker.FuncFormatter + def date_formatter(x, pos): + return (sim['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') + + ax.xaxis.set_major_formatter(date_formatter) + if not interval: + ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) + + return + + +def tidy_up(fig, do_save, fig_path, do_show, default_name): + if do_save: + if fig_path is None: # No figpath provided - see whether do_save is a figpath + fig_path = default_name # Just give it a default name + fig_path = sc.makefilepath(fig_path) # Ensure it's valid, including creating the folder + pl.savefig(fig_path) + + if do_show: + pl.show() + else: + pl.close(fig) + + return + + +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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, log_scale=False, do_show=True, sep_figs=False, verbose=None): @@ -140,100 +240,6 @@ def date_formatter(x, pos): return fig -def handle_args(to_plot, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args): - ''' Handle input arguments -- merge user input with defaults ''' - args = sc.objdict() - args.fig = sc.mergedicts({'figsize': (16, 14)}, fig_args) - args.plot = sc.mergedicts({'lw': 3, 'alpha': 0.7}, plot_args) - args.scatter = sc.mergedicts({'s':70, 'marker':'s'}, scatter_args) - args.axis = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.97, 'wspace': 0.25, 'hspace': 0.25}, axis_args) - args.fill = sc.mergedicts({'alpha': 0.2}, fill_args) - args.legend = sc.mergedicts({'loc': 'best'}, legend_args) - - if to_plot is None: - to_plot = cvd.get_scen_plots() - to_plot = sc.dcp(to_plot) # In case it's supplied as a dict - - return to_plot, args - - -def create_figs(sep_figs, args, font_size, font_family): - if sep_figs: - fig = None - figs = [] - else: - fig = pl.figure(**args.fig) - figs = None - pl.subplots_adjust(**args.axis) - pl.rcParams['font.size'] = font_size - if font_family: - pl.rcParams['font.family'] = font_family - return fig, figs - - -def create_subplots(figs, n_rows, n_cols, rk, fig_args, sep_figs): - if sep_figs: - figs.append(pl.figure(**fig_args)) - ax = pl.subplot(111) - else: - if rk == 0: - ax = pl.subplot(n_rows, n_cols, rk+1) - else: - ax = pl.subplot(n_rows, n_cols, rk+1, sharex=ax) - return ax - - -def title_grid_ticks_legend(title, grid, commaticks, legend_args, rk): - pl.title(title) - if rk == 0: - pl.legend(**legend_args) - - pl.grid(grid) - if commaticks: - sc.commaticks() - return - - -def plot_data(sim, reskey, plot_args): - if sim.data is not None and reskey in sim.data: - data_t = np.array((sim.data.index-sim['start_day'])/np.timedelta64(1,'D')) - pl.plot(data_t, sim.data[reskey], 'sk', **plot_args) - return - - -def reset_ticks(ax, sim, interval, as_dates): - if interval: - xmin,xmax = ax.get_xlim() - ax.set_xticks(pl.arange(xmin, xmax+1, interval)) - - # Set xticks as dates - if as_dates: - - @ticker.FuncFormatter - def date_formatter(x, pos): - return (sim['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') - - ax.xaxis.set_major_formatter(date_formatter) - if not interval: - ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) - - return - -def tidy_up(fig, do_save, fig_path, do_show, default_name): - if do_save: - if fig_path is None: # No figpath provided - see whether do_save is a figpath - fig_path = default_name # Just give it a default name - fig_path = sc.makefilepath(fig_path) # Ensure it's valid, including creating the folder - pl.savefig(fig_path) - - if do_show: - pl.show() - else: - pl.close(fig) - - return - - 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, @@ -275,15 +281,16 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, fig, figs = create_figs(sep_figs, args, font_size, font_family) n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have + ax = None for rk,title,reskeys in to_plot.enumitems(): - ax = create_subplots(figs, n_rows, n_cols, rk, args.fig, sep_figs) + ax = create_subplots(figs, ax, n_rows, n_cols, rk, args.fig, sep_figs) for reskey in reskeys: resdata = scens.results[reskey] for scenkey, scendata in resdata.items(): pl.fill_between(scens.tvec, scendata.low, scendata.high, **args.fill) pl.plot(scens.tvec, scendata.best, label=scendata.name, **args.plot) title_grid_ticks_legend(title, grid, commaticks, args.legend, rk) - plot_data(sim, reskey) + plot_data(sim, reskey, args.scatter) reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) # Ensure the figure actually renders or saves diff --git a/covasim/run.py b/covasim/run.py index 8a7cc5f52..0ed160f69 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -344,7 +344,6 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar log_scale (bool or list): 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 sep_figs (bool): Whether to show separate figures for different results instead of subplots - verbose (bool): Display a bit of extra information Returns: fig: Figure handle @@ -352,7 +351,7 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar fig = cvplt.plot_scens(scens=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, fill_args=fill_args, legend_args=legend_args, as_dates=as_dates, dateformat=dateformat, interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, - log_scale=log_scale, do_show=do_show, sep_figs=sep_figs, verbose=verbose) + log_scale=log_scale, do_show=do_show, sep_figs=sep_figs) return fig diff --git a/covasim/sim.py b/covasim/sim.py index ca2fa5aba..74a2d40fc 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -676,15 +676,14 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar commaticks (bool): Plot y-axis with commas rather than scientific notation 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 - verbose (bool): Display a bit of extra information Returns: fig: Figure handle ''' - fig = cvplt.plot(sim=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, + fig = cvplt.plot_sim(sim=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, legend_args=legend_args, as_dates=as_dates, dateformat=dateformat, interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, - log_scale=log_scale, do_show=do_show, verbose=verbose) + log_scale=log_scale, do_show=do_show) return fig From b3607f493e01b905d0768742beea895395aecb47 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 12:36:14 -0700 Subject: [PATCH 018/194] starting to refactor sim plotting --- covasim/plotting.py | 58 +++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 47a8122b7..c3e484bf1 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -46,7 +46,7 @@ def create_figs(sep_figs, args, font_size, font_family): pl.rcParams['font.size'] = font_size if font_family: pl.rcParams['font.family'] = font_family - return fig, figs + return fig, figs, None # Initialize axis to be None def create_subplots(figs, ax, n_rows, n_cols, rk, fig_args, sep_figs): @@ -61,17 +61,6 @@ def create_subplots(figs, ax, n_rows, n_cols, rk, fig_args, sep_figs): return ax -def title_grid_ticks_legend(title, grid, commaticks, legend_args, rk): - pl.title(title) - if rk == 0: - pl.legend(**legend_args) - - pl.grid(grid) - if commaticks: - sc.commaticks() - return - - def plot_data(sim, key, scatter_args): if sim.data is not None and key in sim.data and len(sim.data[key]): this_color = sim.results[key].color @@ -81,7 +70,19 @@ def plot_data(sim, key, scatter_args): return -def reset_ticks(ax, sim, interval, as_dates): +def title_grid_legend(title, grid, legend_args, subplot_num): + pl.title(title) + if subplot_num == 0: # Only show the + pl.legend(**legend_args) + pl.grid(grid) + return + + +def reset_ticks(ax, sim, commaticks, interval, as_dates): + + if commaticks: + sc.commaticks() + if interval: xmin,xmax = ax.get_xlim() ax.set_xticks(pl.arange(xmin, xmax+1, interval)) @@ -100,7 +101,7 @@ def date_formatter(x, pos): return -def tidy_up(fig, do_save, fig_path, do_show, default_name): +def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name): if do_save: if fig_path is None: # No figpath provided - see whether do_save is a figpath fig_path = default_name # Just give it a default name @@ -112,7 +113,10 @@ def tidy_up(fig, do_save, fig_path, do_show, default_name): else: pl.close(fig) - return + if sep_figs: + return figs + else: + return fig def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, @@ -274,32 +278,24 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, fig: Figure handle ''' + # Handle inputs to_plot, args = handle_args(to_plot, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) - - sim = scens.base_sim - - fig, figs = create_figs(sep_figs, args, font_size, font_family) + fig, figs, ax = create_figs(sep_figs, args, font_size, font_family) n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have - ax = None for rk,title,reskeys in to_plot.enumitems(): ax = create_subplots(figs, ax, n_rows, n_cols, rk, args.fig, sep_figs) for reskey in reskeys: resdata = scens.results[reskey] for scenkey, scendata in resdata.items(): - pl.fill_between(scens.tvec, scendata.low, scendata.high, **args.fill) - pl.plot(scens.tvec, scendata.best, label=scendata.name, **args.plot) - title_grid_ticks_legend(title, grid, commaticks, args.legend, rk) - plot_data(sim, reskey, args.scatter) - reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + pl.fill_between(scens.tvec, scendata.low, scendata.high, **args.fill) # Create the uncertainty bound + pl.plot(scens.tvec, scendata.best, label=scendata.name, **args.plot) # Plot the actual line + plot_data(scens.base_sim, reskey, args.scatter) # Plot the data + title_grid_legend(title, grid, args.legend, rk) # Configure the title, grid, and legend + reset_ticks(ax, scens.base_sim, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) # Ensure the figure actually renders or saves - tidy_up(fig, do_save, fig_path, do_show, 'covasim_scenarios.png') - - if sep_figs: - return figs - else: - return fig + return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, 'covasim_scenarios.png') def plot_result(sim, key, fig_args=None, plot_args=None): From fd422e01006f5f84ca1e26cd8b30760fc1cae51d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 13:04:36 -0700 Subject: [PATCH 019/194] now changing plot_sim --- CHANGELOG.rst | 6 ++++++ covasim/plotting.py | 19 ++++++++++--------- covasim/version.py | 4 ++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1ee80b859..baef63871 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,12 @@ changes from multiple patch versions are grouped together, so numbering will not strictly consecutive. +Version 0.30.0 (2020-05-02) +--------------------------- +- Added new ``MultiSim`` class for plotting a single simulation with uncertainty. +- Refactored plotting to increase consistency between ``sim.plot()``, ``sim.plot_result()``, ``scens.plot()``, and ``multisim.plot()``. + + Version 0.29.9 (2020-04-28) --------------------------- - Fixed bug in which people who had been tested and since recovered were not being diagnosed. diff --git a/covasim/plotting.py b/covasim/plotting.py index c3e484bf1..a047bfab7 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -18,7 +18,7 @@ -def handle_args(to_plot, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args): +def handle_args(to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args): ''' Handle input arguments -- merge user input with defaults ''' args = sc.objdict() args.fig = sc.mergedicts({'figsize': (16, 14)}, fig_args) @@ -32,7 +32,9 @@ def handle_args(to_plot, fig_args, plot_args, scatter_args, axis_args, fill_args to_plot = cvd.get_scen_plots() to_plot = sc.dcp(to_plot) # In case it's supplied as a dict - return to_plot, args + n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have + + return to_plot, n_rows, args def create_figs(sep_figs, args, font_size, font_family): @@ -275,27 +277,26 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, sep_figs (bool): Whether to show separate figures for different results instead of subplots Returns: - fig: Figure handle + fig: Figure handle, or list of figure handles if sep_figs is used ''' # Handle inputs - to_plot, args = handle_args(to_plot, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) + to_plot, n_rows, args = handle_args(to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) fig, figs, ax = create_figs(sep_figs, args, font_size, font_family) - n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have + # Do the plotting for rk,title,reskeys in to_plot.enumitems(): ax = create_subplots(figs, ax, n_rows, n_cols, rk, args.fig, sep_figs) for reskey in reskeys: resdata = scens.results[reskey] for scenkey, scendata in resdata.items(): - pl.fill_between(scens.tvec, scendata.low, scendata.high, **args.fill) # Create the uncertainty bound - pl.plot(scens.tvec, scendata.best, label=scendata.name, **args.plot) # Plot the actual line + ax.fill_between(scens.tvec, scendata.low, scendata.high, **args.fill) # Create the uncertainty bound + ax.plot(scens.tvec, scendata.best, label=scendata.name, **args.plot) # Plot the actual line plot_data(scens.base_sim, reskey, args.scatter) # Plot the data title_grid_legend(title, grid, args.legend, rk) # Configure the title, grid, and legend reset_ticks(ax, scens.base_sim, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) - # Ensure the figure actually renders or saves - return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, 'covasim_scenarios.png') + return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name='covasim_scenarios.png') def plot_result(sim, key, fig_args=None, plot_args=None): diff --git a/covasim/version.py b/covasim/version.py index d2fd1fa01..97c5876c3 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -2,6 +2,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '0.29.9' -__versiondate__ = '2020-04-28' +__version__ = '0.30.0' +__versiondate__ = '2020-05-02' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2020 by IDM' From e3f4f74ee5be991e8fa206cbd8a316758473c019 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 13:34:33 -0700 Subject: [PATCH 020/194] refactored plot_sim --- covasim/interventions.py | 19 +---- covasim/plotting.py | 148 +++++++++++++-------------------------- 2 files changed, 51 insertions(+), 116 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 2c93f9df6..87a6db9a4 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -121,9 +121,9 @@ def plot(self, sim, ax=None): if self.do_plot or self.do_plot is None: if ax is None: ax = pl.gca() - ylims = ax.get_ylim() for day in self.days: - pl.plot([day]*2, ylims, '--', c=[0,0,0]) + if day is not None: + ax.axvline(day, linestyle='--', c=[0,0,0]) return @@ -312,14 +312,6 @@ def apply(self, sim): return - def plot(self, sim, ax): - ''' Plot vertical lines for when changes in beta ''' - ylims = ax.get_ylim() - for day in self.days: - pl.plot([day]*2, ylims, '--', c=[0,0,0]) - return - - class clip_edges(Intervention): ''' Isolate contacts by moving them from the simulation to this intervention. @@ -408,13 +400,6 @@ def apply(self, sim): return - def plot(self, sim, ax): - ''' Plot vertical lines for when changes in beta ''' - ylims = ax.get_ylim() - for day in self.days: - pl.plot([day]*2, ylims, '--', c=[0,0,0]) - return - #%% Testing interventions diff --git a/covasim/plotting.py b/covasim/plotting.py index a047bfab7..d84a95c01 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -18,7 +18,7 @@ -def handle_args(to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args): +def handle_args(which, to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args): ''' Handle input arguments -- merge user input with defaults ''' args = sc.objdict() args.fig = sc.mergedicts({'figsize': (16, 14)}, fig_args) @@ -29,7 +29,13 @@ def handle_args(to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, f args.legend = sc.mergedicts({'loc': 'best'}, legend_args) if to_plot is None: - to_plot = cvd.get_scen_plots() + if which == 'sim': + to_plot = cvd.get_sim_plots() + elif which =='scens': + to_plot = cvd.get_scen_plots() + else: + errormsg = f'Which must be "sim" or "scens", not {which}' + raise NotImplementedError(errormsg) to_plot = sc.dcp(to_plot) # In case it's supplied as a dict n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have @@ -51,7 +57,7 @@ def create_figs(sep_figs, args, font_size, font_family): return fig, figs, None # Initialize axis to be None -def create_subplots(figs, ax, n_rows, n_cols, rk, fig_args, sep_figs): +def create_subplots(figs, ax, n_rows, n_cols, rk, fig_args, sep_figs, log_scale, title): if sep_figs: figs.append(pl.figure(**fig_args)) ax = pl.subplot(111) @@ -60,6 +66,14 @@ def create_subplots(figs, ax, n_rows, n_cols, rk, fig_args, sep_figs): ax = pl.subplot(n_rows, n_cols, rk+1) else: ax = pl.subplot(n_rows, n_cols, rk+1, sharex=ax) + + if log_scale: + if isinstance(log_scale, list): + if title in log_scale: + ax.set_yscale('log') + else: + ax.set_yscale('log') + return ax @@ -72,11 +86,18 @@ def plot_data(sim, key, scatter_args): return -def title_grid_legend(title, grid, legend_args, subplot_num): +def plot_interventions(sim, ax): # Plot the interventions + for intervention in sim['interventions']: + intervention.plot(sim, ax) + return + + +def title_grid_legend(title, grid, legend_args, show=True): pl.title(title) - if subplot_num == 0: # Only show the + if show: # Only show the legend for some subplots pl.legend(**legend_args) pl.grid(grid) + sc.setylim() return @@ -149,101 +170,28 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot 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 sep_figs (bool): Whether to show separate figures for different results instead of subplots - verbose (bool): Display a bit of extra information Returns: fig: Figure handle ''' - if verbose is None: - verbose = sim['verbose'] - sc.printv('Plotting...', 1, verbose) - - if to_plot is None: - to_plot = cvd.get_sim_plots() - to_plot = sc.odict(to_plot) # In case it's supplied as a dict - - # Handle input arguments -- merge user input with defaults - fig_args = sc.mergedicts({'figsize': (16, 14)}, fig_args) - plot_args = sc.mergedicts({'lw': 3, 'alpha': 0.7}, plot_args) - scatter_args = sc.mergedicts({'s':70, 'marker':'s'}, scatter_args) - axis_args = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.97, 'wspace': 0.25, 'hspace': 0.25}, axis_args) - fill_args = sc.mergedicts({'alpha': 0.2}, fill_args) - legend_args = sc.mergedicts({'loc': 'best'}, legend_args) - - if sep_figs: - figs = [] - else: - fig = pl.figure(**fig_args) - pl.subplots_adjust(**axis_args) - pl.rcParams['font.size'] = font_size - if font_family: - pl.rcParams['font.family'] = font_family - - res = sim.results # Shorten since heavily used - - # Plot everything - n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have - for p,title,keylabels in to_plot.enumitems(): - if p == 0: - ax = pl.subplot(n_rows, n_cols, p+1) - else: - ax = pl.subplot(n_rows, n_cols, p + 1, sharex=ax) - if log_scale: - if isinstance(log_scale, list): - if title in log_scale: - ax.set_yscale('log') - else: - ax.set_yscale('log') - for key in keylabels: - label = res[key].name - this_color = res[key].color - y = res[key].values - pl.plot(res['t'], y, label=label, **plot_args, c=this_color) - if sim.data is not None and key in sim.data: - data_t = (sim.data.index-sim['start_day'])/np.timedelta64(1,'D') # Convert from data date to model output index based on model start date - pl.scatter(data_t, sim.data[key], c=[this_color], **scatter_args) - if sim.data is not None and len(sim.data): - pl.scatter(pl.nan, pl.nan, c=[(0,0,0)], label='Data', **scatter_args) - - pl.legend(**legend_args) - pl.grid(grid) - sc.setylim() - if commaticks: - sc.commaticks() - pl.title(title) - - # Optionally reset tick marks (useful for e.g. plotting weeks/months) - if interval: - xmin,xmax = ax.get_xlim() - ax.set_xticks(pl.arange(xmin, xmax+1, interval)) - - # Set xticks as dates - if as_dates: - @ticker.FuncFormatter - def date_formatter(x, pos): - return (sim['start_day'] + dt.timedelta(days=x)).strftime('%b-%d') - ax.xaxis.set_major_formatter(date_formatter) - if not interval: - ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) - - # Plot interventions - for intervention in sim['interventions']: - intervention.plot(sim, ax) - - # Ensure the figure actually renders or saves - if do_save: - if fig_path is None: # No figpath provided - see whether do_save is a figpath - fig_path = 'covasim_sim.png' # Just give it a default name - fig_path = sc.makefilepath(fig_path) # Ensure it's valid, including creating the folder - pl.savefig(fig_path) + # Handle inputs + to_plot, n_rows, args = handle_args('sim', to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) + fig, figs, ax = create_figs(sep_figs, args, font_size, font_family) - if do_show: - pl.show() - else: - pl.close(fig) + # Do the plotting + for pnum,title,keylabels in to_plot.enumitems(): + ax = create_subplots(figs, ax, n_rows, n_cols, pnum, args.fig, sep_figs, log_scale, title) + for reskey in keylabels: + res = sim.results[reskey] + res_t = sim.results['t'] + pl.plot(res_t, res.values, label=res.name, **args.plot, c=res.color) + plot_data(sim, reskey, args.scatter) # Plot the data + plot_interventions(sim, ax) # Plot the interventions + title_grid_legend(title, grid, args.legend) # Configure the title, grid, and legend + reset_ticks(ax, sim, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) - return fig + return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name='covasim.png') def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, @@ -281,20 +229,22 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, ''' # Handle inputs - to_plot, n_rows, args = handle_args(to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) + to_plot, n_rows, args = handle_args('scens', to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) fig, figs, ax = create_figs(sep_figs, args, font_size, font_family) # Do the plotting - for rk,title,reskeys in to_plot.enumitems(): - ax = create_subplots(figs, ax, n_rows, n_cols, rk, args.fig, sep_figs) + for pnum,title,reskeys in to_plot.enumitems(): + ax = create_subplots(figs, ax, n_rows, n_cols, pnum, args.fig, sep_figs, log_scale, title) for reskey in reskeys: resdata = scens.results[reskey] for scenkey, scendata in resdata.items(): + sim = scens.sims[scenkey][0] # Pull out the first sim in the list for this scenario ax.fill_between(scens.tvec, scendata.low, scendata.high, **args.fill) # Create the uncertainty bound ax.plot(scens.tvec, scendata.best, label=scendata.name, **args.plot) # Plot the actual line - plot_data(scens.base_sim, reskey, args.scatter) # Plot the data - title_grid_legend(title, grid, args.legend, rk) # Configure the title, grid, and legend - reset_ticks(ax, scens.base_sim, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + plot_data(sim, reskey, args.scatter) # Plot the data + plot_interventions(sim, ax) # Plot the interventions + title_grid_legend(title, grid, args.legend, pnum==0) # Configure the title, grid, and legend -- only show legend for first + reset_ticks(ax, sim, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name='covasim_scenarios.png') From fd7fe28d4e16db2f96f05ca02c7b0413b783b034 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 13:39:57 -0700 Subject: [PATCH 021/194] simplify docstrings --- covasim/plotting.py | 73 ++------------------------------------------- 1 file changed, 3 insertions(+), 70 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index d84a95c01..04cb7f72e 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -146,34 +146,7 @@ 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, log_scale=False, do_show=True, sep_figs=False, verbose=None): - ''' - Plot the results -- can supply arguments for both the figure and the plots. - - Args: - to_plot (dict): Dict of results to plot; see get_sim_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() - 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 - 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 - sep_figs (bool): Whether to show separate figures for different results instead of subplots - - Returns: - fig: Figure handle - ''' + ''' Plot the results of a sim -- see Sim.plot() for documentation. ''' # Handle inputs to_plot, n_rows, args = handle_args('sim', to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) @@ -198,35 +171,7 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, log_scale=False, do_show=True, sep_figs=False): - ''' - Plot the results -- can supply arguments for both the figure and the plots. - - Args: - scens (Scens): The Scenarios object being plotted - 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() - 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 - 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 - sep_figs (bool): Whether to show separate figures for different results instead of subplots - - Returns: - fig: Figure handle, or list of figure handles if sep_figs is used - ''' + ''' Plot the results of a scenario -- see Scenarios.plot() for documentation. ''' # Handle inputs to_plot, n_rows, args = handle_args('scens', to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) @@ -250,19 +195,7 @@ 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): - ''' - Simple method to plot a single result. Useful for results that aren't - standard outputs. - - Args: - key (str): the key of the result to plot - fig_args (dict): passed to pl.figure() - plot_args (dict): passed to pl.plot() - - **Examples**:: - - sim.plot_result('doubling_time') - ''' + ''' Plot a single result -- see Sim.plot_result() for documentation. ''' fig_args = sc.mergedicts({'figsize':(16,10)}, fig_args) plot_args = sc.mergedicts({'lw':3, 'alpha':0.7}, plot_args) fig = pl.figure(**fig_args) From 3cead81faeb41584d0c958762c8ce9bc63507e64 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 13:46:21 -0700 Subject: [PATCH 022/194] added docstrings --- covasim/plotting.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 04cb7f72e..189705376 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -15,9 +15,6 @@ __all__ = ['plot_sim', 'plot_scens', 'plot_result', 'plotly_sim', 'plotly_people', 'plotly_animate'] - - - def handle_args(which, to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args): ''' Handle input arguments -- merge user input with defaults ''' args = sc.objdict() @@ -44,6 +41,7 @@ def handle_args(which, to_plot, n_cols, fig_args, plot_args, scatter_args, axis_ def create_figs(sep_figs, args, font_size, font_family): + ''' Create the figures and set overall figure properties ''' if sep_figs: fig = None figs = [] @@ -58,6 +56,7 @@ def create_figs(sep_figs, args, font_size, font_family): def create_subplots(figs, ax, n_rows, n_cols, rk, fig_args, sep_figs, log_scale, title): + ''' Create subplots and set logarithmic scale ''' if sep_figs: figs.append(pl.figure(**fig_args)) ax = pl.subplot(111) @@ -78,6 +77,7 @@ def create_subplots(figs, ax, n_rows, n_cols, rk, fig_args, sep_figs, log_scale, def plot_data(sim, key, scatter_args): + ''' Add data to the plot ''' if sim.data is not None and key in sim.data and len(sim.data[key]): this_color = sim.results[key].color data_t = (sim.data.index-sim['start_day'])/np.timedelta64(1,'D') # Convert from data date to model output index based on model start date @@ -86,15 +86,17 @@ def plot_data(sim, key, scatter_args): return -def plot_interventions(sim, ax): # Plot the interventions +def plot_interventions(sim, ax): + ''' Add interventions to the plot ''' for intervention in sim['interventions']: intervention.plot(sim, ax) return -def title_grid_legend(title, grid, legend_args, show=True): +def title_grid_legend(title, grid, legend_args, show_legend=True): + ''' Set the plot title, add a legend, optionally add gridlines, and set the tickmarks ''' pl.title(title) - if show: # Only show the legend for some subplots + if show_legend: # Only show the legend for some subplots pl.legend(**legend_args) pl.grid(grid) sc.setylim() @@ -102,6 +104,7 @@ def title_grid_legend(title, grid, legend_args, show=True): def reset_ticks(ax, sim, commaticks, interval, as_dates): + ''' Set the tick marks, using dates by default ''' if commaticks: sc.commaticks() @@ -125,6 +128,7 @@ def date_formatter(x, pos): def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name): + ''' Handle saving, figure showing, and what value to return ''' if do_save: if fig_path is None: # No figpath provided - see whether do_save is a figpath fig_path = default_name # Just give it a default name From f3a5950a0f7b2d712b6f2ad93db1902501e0ae14 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 15:05:22 -0700 Subject: [PATCH 023/194] more improvements to plotting --- CHANGELOG.rst | 1 + covasim/plotting.py | 69 +++++++++++++++++++++++++++++---------------- covasim/run.py | 2 +- covasim/sim.py | 4 +-- examples/run_sim.py | 3 +- 5 files changed, 51 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index baef63871..3e3c345e7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,7 @@ Version 0.30.0 (2020-05-02) --------------------------- - Added new ``MultiSim`` class for plotting a single simulation with uncertainty. - Refactored plotting to increase consistency between ``sim.plot()``, ``sim.plot_result()``, ``scens.plot()``, and ``multisim.plot()``. +- Doubling time calculation defaults have been updated to use a window of 3 days and a maximum of 30 days. Version 0.29.9 (2020-04-28) diff --git a/covasim/plotting.py b/covasim/plotting.py index 189705376..5fc0ceaa0 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -15,7 +15,7 @@ __all__ = ['plot_sim', 'plot_scens', 'plot_result', 'plotly_sim', 'plotly_people', 'plotly_animate'] -def handle_args(which, to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args): +def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None): ''' Handle input arguments -- merge user input with defaults ''' args = sc.objdict() args.fig = sc.mergedicts({'figsize': (16, 14)}, fig_args) @@ -24,6 +24,11 @@ def handle_args(which, to_plot, n_cols, fig_args, plot_args, scatter_args, axis_ args.axis = sc.mergedicts({'left': 0.10, 'bottom': 0.05, 'right': 0.95, 'top': 0.97, 'wspace': 0.25, 'hspace': 0.25}, axis_args) args.fill = sc.mergedicts({'alpha': 0.2}, fill_args) args.legend = sc.mergedicts({'loc': 'best'}, legend_args) + return args + + +def handle_to_plot(which, to_plot, n_cols): + ''' Handle which quantities to plot ''' if to_plot is None: if which == 'sim': @@ -37,10 +42,10 @@ def handle_args(which, to_plot, n_cols, fig_args, plot_args, scatter_args, axis_ n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have - return to_plot, n_rows, args + return to_plot, n_rows -def create_figs(sep_figs, args, font_size, font_family): +def create_figs(args, font_size, font_family, sep_figs): ''' Create the figures and set overall figure properties ''' if sep_figs: fig = None @@ -103,11 +108,12 @@ def title_grid_legend(title, grid, legend_args, show_legend=True): return -def reset_ticks(ax, sim, commaticks, interval, as_dates): +def reset_ticks(ax, sim, y, commaticks, interval, as_dates): ''' Set the tick marks, using dates by default ''' if commaticks: - sc.commaticks() + if y.max() >= 1000: + sc.commaticks() if interval: xmin,xmax = ax.get_xlim() @@ -148,13 +154,14 @@ def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name): 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, as_dates=True, dateformat=None, - interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, + interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, log_scale=False, do_show=True, sep_figs=False, verbose=None): ''' Plot the results of a sim -- see Sim.plot() for documentation. ''' # Handle inputs - to_plot, n_rows, args = handle_args('sim', to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) - fig, figs, ax = create_figs(sep_figs, args, font_size, font_family) + args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) + to_plot, n_rows = handle_to_plot('sim', to_plot, n_cols) + fig, figs, ax = create_figs(args, font_size, font_family, sep_figs) # Do the plotting for pnum,title,keylabels in to_plot.enumitems(): @@ -162,24 +169,26 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot for reskey in keylabels: res = sim.results[reskey] res_t = sim.results['t'] - pl.plot(res_t, res.values, label=res.name, **args.plot, c=res.color) + res_y = res.values + ax.plot(res_t, res_y, label=res.name, **args.plot, c=res.color) plot_data(sim, reskey, args.scatter) # Plot the data plot_interventions(sim, ax) # Plot the interventions title_grid_legend(title, grid, args.legend) # Configure the title, grid, and legend - reset_ticks(ax, sim, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + reset_ticks(ax, sim, res_y, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name='covasim.png') 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, as_dates=True, dateformat=None, - interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, + interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, log_scale=False, do_show=True, sep_figs=False): ''' Plot the results of a scenario -- see Scenarios.plot() for documentation. ''' # Handle inputs - to_plot, n_rows, args = handle_args('scens', to_plot, n_cols, fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) - fig, figs, ax = create_figs(sep_figs, args, font_size, font_family) + args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) + to_plot, n_rows = handle_to_plot('scens', to_plot, n_cols) + fig, figs, ax = create_figs(args, font_size, font_family, sep_figs) # Do the plotting for pnum,title,reskeys in to_plot.enumitems(): @@ -188,27 +197,39 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, resdata = scens.results[reskey] for scenkey, scendata in resdata.items(): sim = scens.sims[scenkey][0] # Pull out the first sim in the list for this scenario + res_y = scendata.best ax.fill_between(scens.tvec, scendata.low, scendata.high, **args.fill) # Create the uncertainty bound - ax.plot(scens.tvec, scendata.best, label=scendata.name, **args.plot) # Plot the actual line + ax.plot(scens.tvec, res_y, label=scendata.name, **args.plot) # Plot the actual line plot_data(sim, reskey, args.scatter) # Plot the data plot_interventions(sim, ax) # Plot the interventions title_grid_legend(title, grid, args.legend, pnum==0) # Configure the title, grid, and legend -- only show legend for first - reset_ticks(ax, sim, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + reset_ticks(ax, sim, res_y, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name='covasim_scenarios.png') -def plot_result(sim, key, fig_args=None, plot_args=None): +def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, + font_size=18, font_family=None, grid=False, commaticks=True, as_dates=True, + dateformat=None, interval=None): ''' Plot a single result -- see Sim.plot_result() for documentation. ''' - fig_args = sc.mergedicts({'figsize':(16,10)}, fig_args) - plot_args = sc.mergedicts({'lw':3, 'alpha':0.7}, plot_args) - fig = pl.figure(**fig_args) - pl.subplot(111) - tvec = sim.results['t'] + + # Handle inputs + fig_args = sc.mergedicts({'figsize':(16,8)}, fig_args) + axis_args = sc.mergedicts({'top': 0.95}, axis_args) + args = handle_args(fig_args, plot_args, scatter_args, axis_args) + fig, figs, ax = create_figs(args, font_size, font_family, sep_figs=False) + + # Do the plotting + ax = pl.subplot(111) res = sim.results[key] - y = res.values - color = res.color - pl.plot(tvec, y, c=color, **plot_args) + res_t = sim.results['t'] + res_y = res.values + ax.plot(res_t, res_y, c=res.color, **args.plot) + plot_data(sim, key, args.scatter) # Plot the data + plot_interventions(sim, ax) # Plot the interventions + title_grid_legend(res.name, grid, args.legend, show_legend=False) # Configure the title, grid, and legend + reset_ticks(ax, sim, res_y, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + return fig diff --git a/covasim/run.py b/covasim/run.py index 0ed160f69..0a91cb6f6 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -318,7 +318,7 @@ def print_heading(string): def plot(self, 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, as_dates=True, dateformat=None, - interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, + interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, log_scale=False, do_show=True, sep_figs=False, verbose=None): ''' Plot the results -- can supply arguments for both the figure and the plots. diff --git a/covasim/sim.py b/covasim/sim.py index 74a2d40fc..12832f48b 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -485,7 +485,7 @@ def finalize(self, verbose=None): return - def compute_doubling(self, window=7, max_doubling_time=50): + def compute_doubling(self, window=3, max_doubling_time=30): ''' Calculate doubling time using exponential approximation -- a more detailed approach is in utils.py. Compares infections at time t to infections at time @@ -652,7 +652,7 @@ def summary_stats(self, verbose=None): def plot(self, 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, as_dates=True, dateformat=None, - interval=None, n_cols=1, font_size=18, font_family=None, grid=True, commaticks=True, + interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, log_scale=False, do_show=True, sep_figs=False, verbose=None): ''' Plot the results -- can supply arguments for both the figure and the plots. diff --git a/examples/run_sim.py b/examples/run_sim.py index 500fabd54..6cc42abf3 100644 --- a/examples/run_sim.py +++ b/examples/run_sim.py @@ -43,4 +43,5 @@ if do_plot: print('Plotting...') - fig = sim.plot(do_save=do_save, do_show=do_show, fig_path=fig_path) \ No newline at end of file + fig = sim.plot(do_save=do_save, do_show=do_show, fig_path=fig_path) + sim.plot_result('r_eff') \ No newline at end of file From 9879ada99ba4669c7d5b37732d36f579b8c1efa1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 15:21:21 -0700 Subject: [PATCH 024/194] working, but movie is broken --- covasim/plotting.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 5fc0ceaa0..060af07d5 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -277,6 +277,9 @@ def get_individual_states(sim): return z, states +# Default settings for the Plotly legend +plotly_legend = dict(legend_orientation="h", legend=dict(x=0.0, y=1.18)) + def plotly_sim(sim): ''' Main simulation results -- parallel of sim.plot() ''' @@ -287,13 +290,13 @@ def plotly_sim(sim): for key in keylabels: label = sim.results[key].name this_color = sim.results[key].color + x = sim.results['date'][:] y = sim.results[key][:] - fig.add_trace(go.Scatter(x=sim.results['t'][:], y=y, mode='lines', name=label, line_color=this_color)) + fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name=label, line_color=this_color)) if sim.data is not None and key in sim.data: - data_t = (sim.data.index-sim['start_day'])/np.timedelta64(1,'D') - print(sim.data.index, sim['start_day'], np.timedelta64(1,'D'), data_t) + xdata = sim.data['date'] ydata = sim.data[key] - fig.add_trace(go.Scatter(x=data_t, y=ydata, mode='markers', name=label + ' (data)', line_color=this_color)) + fig.add_trace(go.Scatter(x=xdata, y=ydata, mode='markers', name=label + ' (data)', line_color=this_color)) if sim['interventions']: for interv in sim['interventions']: @@ -303,7 +306,7 @@ def plotly_sim(sim): fig.add_shape(dict(type="line", xref="x", yref="paper", x0=interv_day, x1=interv_day, y0=0, y1=1, name='Intervention', line=dict(width=0.5, dash='dash'))) fig.update_layout(annotations=[dict(x=interv_day, y=1.07, xref="x", yref="paper", text="Intervention change", showarrow=False)]) - fig.update_layout(title={'text':title}, xaxis_title='Day', yaxis_title='Count', autosize=True) + fig.update_layout(title={'text':title}, yaxis_title='Count', autosize=True, **plotly_legend) plots.append(fig) return plots @@ -316,8 +319,10 @@ def plotly_people(sim, do_show=False): fig = go.Figure() for state in states[::-1]: # Reverse order for plotting + x = sim.results['date'][:] + y = (z == state['value']).sum(axis=0) fig.add_trace(go.Scatter( - x=sim.tvec, y=(z == state['value']).sum(axis=0), + x=x, y=y, stackgroup='one', line=dict(width=0.5, color=state['color']), fillcolor=state['color'], @@ -335,7 +340,7 @@ def plotly_people(sim, do_show=False): fig.update_layout(annotations=[dict(x=interv_day, y=1.07, xref="x", yref="paper", text="Intervention change", showarrow=False)]) fig.update_layout(yaxis_range=(0, sim.n)) - fig.update_layout(title={'text': 'Numbers of people by health state'}, xaxis_title='Day', yaxis_title='People', autosize=True) + fig.update_layout(title={'text': 'Numbers of people by health state'}, yaxis_title='People', autosize=True, **plotly_legend) if do_show: fig.show() @@ -466,7 +471,7 @@ def plotly_animate(sim, do_show=False): ) - fig.update_layout(title={'text': 'Epidemic over time'}) + fig.update_layout(title={'text': 'Epidemic over time'}, **plotly_legend) if do_show: fig.show() From 43bfbb15c6f09383da94b7d36dc876a31d06fff0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 15:26:05 -0700 Subject: [PATCH 025/194] movie is bigger --- covasim/plotting.py | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 060af07d5..5d1555b85 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -406,7 +406,7 @@ def plotly_animate(sim, do_show=False): "currentvalue": { "font": {"size": 20}, "prefix": "Day: ", - "visible": True, + "visible": False, "xanchor": "right" }, "transition": {"duration": 200}, @@ -448,27 +448,27 @@ def plotly_animate(sim, do_show=False): fig = go.Figure(fig_dict) - fig.update_layout( - autosize=True, - xaxis=dict( - automargin=True, - range=[-0.5, x_size + 0.5], - constrain="domain", - showgrid=False, - showline=False, - showticklabels=False, - ), - yaxis=dict( - automargin=True, - range=[-0.5, y_size + 0.5], - constrain="domain", - scaleanchor="x", - scaleratio=1, - showgrid=False, - showline=False, - showticklabels=False, - ), - ) + # fig.update_layout( + # autosize=True, + # xaxis=dict( + # automargin=True, + # range=[-0.5, x_size + 0.5], + # constrain="domain", + # showgrid=False, + # showline=False, + # showticklabels=False, + # ), + # yaxis=dict( + # automargin=True, + # range=[-0.5, y_size + 0.5], + # constrain="domain", + # scaleanchor="x", + # scaleratio=1, + # showgrid=False, + # showline=False, + # showticklabels=False, + # ), + # ) fig.update_layout(title={'text': 'Epidemic over time'}, **plotly_legend) From fd012560cf3b44699cf90de13fe3658d59e458ec Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 15:53:16 -0700 Subject: [PATCH 026/194] improved animation --- covasim/plotting.py | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 5d1555b85..a24690922 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -357,7 +357,7 @@ def plotly_animate(sim, do_show=False): max_color = max(states, key=lambda x: x['value'])['value'] colorscale = [[x['value'] / max_color, x['color']] for x in states] - aspect = 3 + aspect = 5 y_size = int(np.ceil((z.shape[0] / aspect) ** 0.5)) x_size = int(np.ceil(aspect * y_size)) @@ -448,27 +448,20 @@ def plotly_animate(sim, do_show=False): fig = go.Figure(fig_dict) - # fig.update_layout( - # autosize=True, - # xaxis=dict( - # automargin=True, - # range=[-0.5, x_size + 0.5], - # constrain="domain", - # showgrid=False, - # showline=False, - # showticklabels=False, - # ), - # yaxis=dict( - # automargin=True, - # range=[-0.5, y_size + 0.5], - # constrain="domain", - # scaleanchor="x", - # scaleratio=1, - # showgrid=False, - # showline=False, - # showticklabels=False, - # ), - # ) + fig.update_layout( + autosize=True, + xaxis=dict( + showgrid=False, + showline=False, + showticklabels=False, + ), + yaxis=dict( + automargin=True, + showgrid=False, + showline=False, + showticklabels=False, + ), + ) fig.update_layout(title={'text': 'Epidemic over time'}, **plotly_legend) From cc022247c63ade38d74e20498df1aa17d045e057 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 16:05:56 -0700 Subject: [PATCH 027/194] simplified run_sim --- examples/run_sim.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/examples/run_sim.py b/examples/run_sim.py index 6cc42abf3..d7079f677 100644 --- a/examples/run_sim.py +++ b/examples/run_sim.py @@ -1,13 +1,9 @@ ''' -Simple script for running the Covid-19 agent-based model +Simple script for running Covasim ''' -print('Importing...') -import sciris as sc import covasim as cv -print('Configuring...') - # Run options do_plot = 1 do_save = 0 @@ -15,15 +11,8 @@ verbose = 1 interv = 1 -# Set filename if saving -version = 'v0' -date = '2020apr25' -folder = 'results' -basename = f'{folder}/covasim_run_{date}_{version}' -fig_path = f'{basename}.png' - # Configure the sim -- can also just use a normal dictionary -pars = sc.objdict( +pars = dict( pop_size = 10000, # Population size pop_infected = 10, # Number of initial infections n_days = 120, # Number of days to simulate @@ -35,13 +24,10 @@ if interv: pars.interventions = cv.change_beta(days=45, changes=0.5) # Optionally add an intervention -print('Making sim...') +# Make, run, and plot the sim sim = cv.Sim(pars=pars) - -print('Running...') sim.run(verbose=verbose) if do_plot: print('Plotting...') - fig = sim.plot(do_save=do_save, do_show=do_show, fig_path=fig_path) - sim.plot_result('r_eff') \ No newline at end of file + fig = sim.plot(do_save=do_save, do_show=do_show) From 0d1bac2714291055b3ac308f0fd9e191b396ecb4 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 16:06:26 -0700 Subject: [PATCH 028/194] updated baseline --- tests/baseline.json | 2 +- tests/benchmark.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/baseline.json b/tests/baseline.json index 1be5dfecf..a5d1aa889 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -30,6 +30,6 @@ "new_quarantined": 0.0, "cum_quarantined": 0.0, "r_eff": 1.7936786654960493, - "doubling_time": 8.74834337181837 + "doubling_time": 9.29296551635779 } } \ No newline at end of file diff --git a/tests/benchmark.json b/tests/benchmark.json index 7497af5d0..681d7e45b 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,7 +1,7 @@ { "time": { - "initialize": 0.211, - "run": 0.172 + "initialize": 0.228, + "run": 0.174 }, "parameters": { "pop_size": 20000, From 87a76b62e3663f6c4a3b093d4a3b6ce187ba182d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 16:24:02 -0700 Subject: [PATCH 029/194] added low, high to results --- covasim/base.py | 24 ++++++++++++------------ covasim/plotting.py | 7 ++++--- tests/test_run.py | 6 +++--- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 862efd533..7586aa2b4 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -75,7 +75,6 @@ class Result(object): Args: name (str): name of this result, e.g. new_infections - values (array): array of values corresponding to this result npts (int): if values is None, precreate it to be of this length scale (str): whether or not the value scales by population size; options are "dynamic", "static", or False color (str or array): default color for plotting (hex or RGB notation) @@ -85,29 +84,30 @@ class Result(object): import covasim as cv r1 = cv.Result(name='test1', npts=10) r1[:5] = 20 - print(r2.values) - r2 = cv.Result(name='test2', values=range(10)) - print(r2) + print(r1.values) ''' - def __init__(self, name=None, values=None, npts=None, scale='dynamic', color=None): + def __init__(self, name=None, npts=None, scale='dynamic', 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' self.color = color # Default color - if values is None: - if npts is not None: - values = np.zeros(int(npts)) # If length is known, use zeros - else: - values = [] # Otherwise, empty - self.values = np.array(values, dtype=cvd.result_float) # Ensure it's an array + if npts is None: + npts = 0 + self.values = np.array(np.zeros(int(npts)), dtype=cvd.result_float) + self.low = None + self.high = None return def __repr__(self, *args, **kwargs): ''' Use pretty repr, like sc.prettyobj, but displaying full values ''' - output = sc.prepr(self, skip='values') + output = sc.prepr(self, skip=['values', 'low', 'high']) output += 'values:\n' + repr(self.values) + if self.low is not None: + output += '\nlow:\n' + repr(self.low) + if self.high is not None: + output += '\nhigh:\n' + repr(self.high) return output def __getitem__(self, *args, **kwargs): diff --git a/covasim/plotting.py b/covasim/plotting.py index a24690922..a7db06cca 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -169,12 +169,13 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot for reskey in keylabels: res = sim.results[reskey] res_t = sim.results['t'] - res_y = res.values - ax.plot(res_t, res_y, label=res.name, **args.plot, c=res.color) + if res.low is not None and res.high is not None: + ax.fill_between(res_t, res.low, res.hig, **args.fill) # Create the uncertainty bound + ax.plot(res_t, res.values, label=res.name, **args.plot, c=res.color) plot_data(sim, reskey, args.scatter) # Plot the data plot_interventions(sim, ax) # Plot the interventions title_grid_legend(title, grid, args.legend) # Configure the title, grid, and legend - reset_ticks(ax, sim, res_y, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + reset_ticks(ax, sim, res.values, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name='covasim.png') diff --git a/tests/test_run.py b/tests/test_run.py index 93743bf79..f626a4602 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -86,10 +86,10 @@ def test_multisim_reduce(do_plot=False): # If being run via pytest, turn off sim = cv.Sim(pop_size=pop_size, pop_infected=pop_infected) msim = cv.MultiSim(sim) msim.run(n_runs=n_runs, keep_people=True) - # msim.reduce() + msim.reduce() - # if do_plot: - # msim.plot() + if do_plot: + msim.plot() return msim From f43f1758a37b56bf9050851e802ce5782777a492 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 17:12:31 -0700 Subject: [PATCH 030/194] tests look reasonable --- covasim/plotting.py | 2 +- covasim/run.py | 95 ++++++++++++++++++++++++++++++++++++++++++--- covasim/sim.py | 4 +- tests/test_run.py | 18 ++++----- 4 files changed, 100 insertions(+), 19 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index a7db06cca..529108ec4 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -170,7 +170,7 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot res = sim.results[reskey] res_t = sim.results['t'] if res.low is not None and res.high is not None: - ax.fill_between(res_t, res.low, res.hig, **args.fill) # Create the uncertainty bound + ax.fill_between(res_t, res.low, res.high, **args.fill) # Create the uncertainty bound ax.plot(res_t, res.values, label=res.name, **args.plot, c=res.color) plot_data(sim, reskey, args.scatter) # Plot the data plot_interventions(sim, ax) # Plot the interventions diff --git a/covasim/run.py b/covasim/run.py index 0a91cb6f6..a76e83950 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -1,5 +1,5 @@ ''' -Functions for running multiple Covasim runs. +Functions and classes for running multiple Covasim runs. ''' #%% Imports @@ -39,6 +39,8 @@ class MultiSim(sc.prettyobj): Args: sims (Sim or list): a single sim or a list of sims base_sim (Sim): the sim used for shared properties; if not supplied, the first of the sims provided + make_metapars()['quantiles'] + kwargs (dict): stored in run_args and passed to run() Returns: msim: a MultiSim object @@ -59,7 +61,7 @@ class MultiSim(sc.prettyobj): msim.plot() # Plot as single sim ''' - def __init__(self, sims=None, base_sim=None): + def __init__(self, sims=None, base_sim=None, quantiles=None, **kwargs): # Handle inputs if base_sim is None: @@ -72,8 +74,13 @@ def __init__(self, sims=None, base_sim=None): errormsg = f'If base_sim is not supplied, sims must be either a sims or a list of sims, not {type(sims)}' raise TypeError(errormsg) + if quantiles is None: + quantiles = make_metapars()['quantiles'] + self.sims = sims self.base_sim = base_sim + self.quantiles = quantiles + self.run_args = sc.mergedicts(kwargs) self.results = None self.which = None # Whether the multisim is to be reduced, combined, etc. @@ -107,13 +114,15 @@ def run(self, *args, **kwargs): kwargs (dict): passed to multi_run() and thence to sim.run() Returns: - None (modifies Scenarios object in place) + None (modifies MultiSim object in place) ''' if self.sims is None: sims = self.base_sim else: sims = self.sims + kwargs = sc.mergedicts(self.run_args, kwargs) + print(kwargs) self.sims = multi_run(sims, *args, **kwargs) return @@ -138,16 +147,88 @@ def combine(self, output=False): if not combined_sim.results[key].scale: combined_sim.results[key].values /= n_runs - self.combined_sim = combined_sim - self.results = self.combined_sim.results + self.base_sim = combined_sim + self.results = combined_sim.results self.which = 'combined' if output: - return combined_sim + return self.base_sim else: return + def reduce(self, quantiles=None, output=False): + ''' Combine multiple sims into a single sim with scaled results ''' + + if quantiles is None: + quantiles = self.quantiles + if not isinstance(quantiles, dict): + try: + quantiles = {'low':quantiles[0], 'high':quantiles[1]} + except Exception as E: + errormsg = f'Could not figure out how to convert {quantiles} into a quantiles object: must be a dict with keys low, high or a 2-element array ({str(E)})' + raise ValueError(errormsg) + + # Store information on the sims + n_runs = len(self) + reduced_sim = sc.dcp(self.sims[0]) + reduced_sim.parallelized = {'parallelized':True, 'combined':False, 'n_runs':n_runs} # Store how this was parallelized + + # Perform the statistics + raw = {} + reskeys = reduced_sim.result_keys() + for reskey in reskeys: + raw[reskey] = np.zeros((reduced_sim.npts, len(self.sims))) + for s,sim in enumerate(self.sims): + raw[reskey][:,s] = sim.results[reskey].values + + for reskey in reskeys: + reduced_sim.results[reskey].values[:] = np.median(raw[reskey], axis=1) # Changed from median to mean for smoother plots + 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.summary_stats(verbose=False) # Recalculate the summary stats + + self.base_sim = reduced_sim + self.results = reduced_sim.results + self.which = 'reduced' + + if output: + return self.base_sim + else: + return + + + def plot(self, *args, **kwargs): + ''' Convenience mthod for plotting ''' + fig = self.base_sim.plot(*args, **kwargs) + return fig + + + def save(self, filename=None, **kwargs): + ''' + Save to disk as a gzipped pickle. Load with cv.load(filename). + + Args: + filename (str or None): the name or path of the file to save to; if None, uses stored + kwargs: passed to makefilepath() + + Returns: + filename (str): the validated absolute path to the saved file + + **Example**:: + + msim.save() # Saves to a .sim file with the date and time of creation by default + ''' + if filename is None: + filename = self.simfile + filename = sc.makefilepath(filename=filename, **kwargs) + self.filename = filename # Store the actual saved filename + sc.saveobj(filename=filename, obj=self) + return filename + + + + class Scenarios(cvb.ParsObj): ''' Class for running multiple sets of multiple simulations -- e.g., scenarios. @@ -517,6 +598,8 @@ def single_run(sim, ind=0, reseed=True, noise=0.0, noisepar=None, verbose=None, verbose = new_sim['verbose'] sim_args = sc.mergedicts(sim_args, kwargs) run_args = sc.mergedicts({'verbose':verbose}, run_args) + if not new_sim.label: + new_sim.label = f'Sim {ind:d}' if reseed: new_sim['rand_seed'] += ind # Reset the seed, otherwise no point of parallel runs diff --git a/covasim/sim.py b/covasim/sim.py index 12832f48b..5bbfa4b3a 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -469,7 +469,7 @@ def finalize(self, verbose=None): # Calculate cumulative results for key in cvd.result_flows.keys(): - self.results[f'cum_{key}'].values = np.cumsum(self.results[f'new_{key}'].values) + self.results[f'cum_{key}'].values[:] = np.cumsum(self.results[f'new_{key}'].values) self.results['cum_infections'].values += self['pop_infected']*self.rescale_vec[0] # Include initially infected people # Perform calculations on results @@ -555,7 +555,7 @@ def compute_r_eff(self, window=7): ind = den > 0 values[ind] = num[ind]/den[ind] - self.results['r_eff'].values = values + self.results['r_eff'].values[:] = values return diff --git a/tests/test_run.py b/tests/test_run.py index f626a4602..fc3eef0b6 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -78,16 +78,15 @@ def test_scenarios(do_plot=False): def test_multisim_reduce(do_plot=False): # If being run via pytest, turn off sc.heading('Combine results test') - n_runs = 3 + n_runs = 11 pop_size = 1000 pop_infected = 10 print('Running first sim...') sim = cv.Sim(pop_size=pop_size, pop_infected=pop_infected) - msim = cv.MultiSim(sim) - msim.run(n_runs=n_runs, keep_people=True) + msim = cv.MultiSim(sim, n_runs=n_runs, noise=0.1) + msim.run() msim.reduce() - if do_plot: msim.plot() @@ -113,7 +112,7 @@ def test_multisim_combine(do_plot=False): # If being run via pytest, turn off sim2.run() if do_plot: - sim1.plot() + msim.plot() sim2.plot() return msim @@ -123,12 +122,11 @@ def test_multisim_combine(do_plot=False): # If being run via pytest, turn off if __name__ == '__main__': T = sc.tic() - # sim1 = test_singlerun() - # sims2 = test_multirun(do_plot=do_plot) + sim1 = test_singlerun() + sims2 = test_multirun(do_plot=do_plot) msim1 = test_multisim_reduce(do_plot=do_plot) - # msim2 = test_multisim_combine(do_plot=do_plot) - - # scens = test_scenarios(do_plot=do_plot) + msim2 = test_multisim_combine(do_plot=do_plot) + scens = test_scenarios(do_plot=do_plot) sc.toc(T) From 606323b8d32282b667885c0a298403c729934f7b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 17:13:17 -0700 Subject: [PATCH 031/194] reorder --- tests/test_run.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/test_run.py b/tests/test_run.py index fc3eef0b6..bff5c9f6b 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -55,30 +55,10 @@ def test_multirun(do_plot=False): # If being run via pytest, turn off return sims -def test_scenarios(do_plot=False): - sc.heading('Scenarios test') - basepars = {'pop_size':1000} - - json_path = 'scen_test.json' - xlsx_path = 'scen_test.xlsx' - - scens = cv.Scenarios(basepars=basepars) - scens.run() - if do_plot: - scens.plot() - scens.to_json(json_path) - scens.to_excel(xlsx_path) - - for path in [json_path, xlsx_path]: - print(f'Removing {path}') - os.remove(path) - return scens - - def test_multisim_reduce(do_plot=False): # If being run via pytest, turn off sc.heading('Combine results test') - n_runs = 11 + n_runs = 3 pop_size = 1000 pop_infected = 10 @@ -118,6 +98,26 @@ def test_multisim_combine(do_plot=False): # If being run via pytest, turn off return msim +def test_scenarios(do_plot=False): + sc.heading('Scenarios test') + basepars = {'pop_size':1000} + + json_path = 'scen_test.json' + xlsx_path = 'scen_test.xlsx' + + scens = cv.Scenarios(basepars=basepars) + scens.run() + if do_plot: + scens.plot() + scens.to_json(json_path) + scens.to_excel(xlsx_path) + + for path in [json_path, xlsx_path]: + print(f'Removing {path}') + os.remove(path) + return scens + + #%% Run as a script if __name__ == '__main__': T = sc.tic() From 32bb4ecc0057d6139e2ea6e736d5c0e6e938484a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 17:22:44 -0700 Subject: [PATCH 032/194] fix run_sim --- examples/run_sim.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/examples/run_sim.py b/examples/run_sim.py index d7079f677..dac5ecdd5 100644 --- a/examples/run_sim.py +++ b/examples/run_sim.py @@ -2,32 +2,29 @@ Simple script for running Covasim ''' +import sciris as sc import covasim as cv # Run options do_plot = 1 -do_save = 0 -do_show = 1 verbose = 1 interv = 1 # Configure the sim -- can also just use a normal dictionary -pars = dict( - pop_size = 10000, # Population size - pop_infected = 10, # Number of initial infections - n_days = 120, # Number of days to simulate - rand_seed = 1, # Random seed - pop_type = 'hybrid', +pars = sc.objdict( + pop_size = 10000, # Population size + pop_infected = 10, # Number of initial infections + n_days = 120, # Number of days to simulate + rand_seed = 1, # Random seed + pop_type = 'hybrid', # Population to use -- "hybrid" is random with household, school,and work structure ) # Optionally add an intervention if interv: - pars.interventions = cv.change_beta(days=45, changes=0.5) # Optionally add an intervention + pars.interventions = cv.change_beta(days=45, changes=0.5) # Make, run, and plot the sim sim = cv.Sim(pars=pars) sim.run(verbose=verbose) - if do_plot: - print('Plotting...') - fig = sim.plot(do_save=do_save, do_show=do_show) + sim.plot() From 7906b4d3f1300585f3dfb06e3dbe4d1d2d37f90e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 17:28:34 -0700 Subject: [PATCH 033/194] added likelihood calculation --- covasim/run.py | 1 + 1 file changed, 1 insertion(+) diff --git a/covasim/run.py b/covasim/run.py index a76e83950..2d0f809d5 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -186,6 +186,7 @@ def reduce(self, quantiles=None, output=False): reduced_sim.results[reskey].values[:] = np.median(raw[reskey], axis=1) # Changed from median to mean for smoother plots 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.likelihood() # Recompute the likelihood for the average sim reduced_sim.summary_stats(verbose=False) # Recalculate the summary stats self.base_sim = reduced_sim From bdf9ed9122fed78bc9b6b8282b84bf22b68b675d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 18:05:54 -0700 Subject: [PATCH 034/194] added ability to plot multiple sims on one graph --- covasim/plotting.py | 38 ++++++++++++++++---------- covasim/run.py | 66 ++++++++++++++++++++++++--------------------- covasim/sim.py | 20 ++++++++------ 3 files changed, 72 insertions(+), 52 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 529108ec4..16d5c45c6 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -1,6 +1,9 @@ ''' -Plotly-based plotting functions to supplement the Matplotlib based ones that are -part of the Sim and Scenarios objects. Intended mostly for use with the webapp. +Core plotting functions for simulations, multisims, and scenarios. + +Also includes Plotly-based plotting functions to supplement the Matplotlib based +ones that are of the Sim and Scenarios objects. Intended mostly for use with the +webapp. ''' import numpy as np @@ -36,7 +39,7 @@ def handle_to_plot(which, to_plot, n_cols): elif which =='scens': to_plot = cvd.get_scen_plots() else: - errormsg = f'Which must be "sim" or "scens", not {which}' + errormsg = f'"which" must be "sim" or "scens", not "{which}"' raise NotImplementedError(errormsg) to_plot = sc.dcp(to_plot) # In case it's supplied as a dict @@ -45,13 +48,14 @@ def handle_to_plot(which, to_plot, n_cols): return to_plot, n_rows -def create_figs(args, font_size, font_family, sep_figs): +def create_figs(args, font_size, font_family, sep_figs, fig=None): ''' Create the figures and set overall figure properties ''' if sep_figs: fig = None figs = [] else: - fig = pl.figure(**args.fig) + if fig is None: + fig = pl.figure(**args.fig) # Create the figure if none is supplied figs = None pl.subplots_adjust(**args.axis) pl.rcParams['font.size'] = font_size @@ -99,22 +103,24 @@ def plot_interventions(sim, ax): def title_grid_legend(title, grid, legend_args, show_legend=True): - ''' Set the plot title, add a legend, optionally add gridlines, and set the tickmarks ''' + ''' Plot styling -- set the plot title, add a legend, and optionally add gridlines''' pl.title(title) if show_legend: # Only show the legend for some subplots pl.legend(**legend_args) pl.grid(grid) - sc.setylim() return def reset_ticks(ax, sim, y, commaticks, interval, as_dates): ''' Set the tick marks, using dates by default ''' + # Set the y axis limits and style + sc.setylim() if commaticks: if y.max() >= 1000: sc.commaticks() + # Set the x-axis intervals if interval: xmin,xmax = ax.get_xlim() ax.set_xticks(pl.arange(xmin, xmax+1, interval)) @@ -133,19 +139,23 @@ def date_formatter(x, pos): return -def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name): +def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name='covasim.png'): ''' Handle saving, figure showing, and what value to return ''' + + # Handle saving if do_save: if fig_path is None: # No figpath provided - see whether do_save is a figpath fig_path = default_name # Just give it a default name fig_path = sc.makefilepath(fig_path) # Ensure it's valid, including creating the folder pl.savefig(fig_path) + # Show or close the figure if do_show: pl.show() else: pl.close(fig) + # Return the figure or figures if sep_figs: return figs else: @@ -155,13 +165,13 @@ def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name): 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, - log_scale=False, do_show=True, sep_figs=False, verbose=None): + log_scale=False, do_show=True, sep_figs=False, fig=None): ''' Plot the results of a sim -- see Sim.plot() for documentation. ''' # Handle inputs args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) to_plot, n_rows = handle_to_plot('sim', to_plot, n_cols) - fig, figs, ax = create_figs(args, font_size, font_family, sep_figs) + fig, figs, ax = create_figs(args, font_size, font_family, sep_figs, fig) # Do the plotting for pnum,title,keylabels in to_plot.enumitems(): @@ -183,13 +193,13 @@ 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, - log_scale=False, do_show=True, sep_figs=False): + log_scale=False, do_show=True, sep_figs=False, fig=None): ''' 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) to_plot, n_rows = handle_to_plot('scens', to_plot, n_cols) - fig, figs, ax = create_figs(args, font_size, font_family, sep_figs) + fig, figs, ax = create_figs(args, font_size, font_family, sep_figs, fig) # Do the plotting for pnum,title,reskeys in to_plot.enumitems(): @@ -211,14 +221,14 @@ 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, font_size=18, font_family=None, grid=False, commaticks=True, as_dates=True, - dateformat=None, interval=None): + dateformat=None, interval=None, fig=None): ''' Plot a single result -- see Sim.plot_result() for documentation. ''' # Handle inputs fig_args = sc.mergedicts({'figsize':(16,8)}, fig_args) axis_args = sc.mergedicts({'top': 0.95}, axis_args) args = handle_args(fig_args, plot_args, scatter_args, axis_args) - fig, figs, ax = create_figs(args, font_size, font_family, sep_figs=False) + fig, figs, ax = create_figs(args, font_size, font_family, sep_figs=False, fig=fig) # Do the plotting ax = pl.subplot(111) diff --git a/covasim/run.py b/covasim/run.py index 2d0f809d5..bbfaac751 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -34,12 +34,14 @@ def make_metapars(): class MultiSim(sc.prettyobj): ''' - Class for running multiple copies of a simulation. + Class for running multiple copies of a simulation. The parameter n_runs + controls how many copies of the simulation there will be, if a list of sims + is not provided. Args: sims (Sim or list): a single sim or a list of sims base_sim (Sim): the sim used for shared properties; if not supplied, the first of the sims provided - make_metapars()['quantiles'] + quantiles (dict): the quantiles to use with reduce(), e.g. [0.1, 0.9] or {'low:'0.1, 'high':0.9} kwargs (dict): stored in run_args and passed to run() Returns: @@ -48,17 +50,15 @@ class MultiSim(sc.prettyobj): **Examples**:: sim = cv.Sim() # Create the sim - msim = cv.MultiSim(sim) # Create the multisim - msim.replicate(5) # Create 5 replicates + msim = cv.MultiSim(sim, n_runs=5) # Create the multisim msim.run() # Run them in parallel - msim.reduce() # Calculate statistics + msim.combine() # Calculate statistics msim.plot() # Plot results - sims = [cv.Sim(beta=0.015*[1+0.1*i]) for i in range(5)] # Create sims + sims = [cv.Sim(beta=0.015*(1+0.1*i)) for i in range(5)] # Create sims for sim in sims: sim.run() # Run sims in serial msim = cv.MultiSim(sims) # Convert to multisim - msim.reduce('add') # Add results together - msim.plot() # Plot as single sim + msim.plot() # Plot each sim separately ''' def __init__(self, sims=None, base_sim=None, quantiles=None, **kwargs): @@ -201,7 +201,12 @@ def reduce(self, quantiles=None, output=False): def plot(self, *args, **kwargs): ''' Convenience mthod for plotting ''' - fig = self.base_sim.plot(*args, **kwargs) + if self.which in ['combined', 'reduced']: + fig = self.base_sim.plot(*args, **kwargs) + else: + fig = None + for sim in self.sims: + fig = sim.plot(fig=fig, *args, **kwargs) return fig @@ -401,31 +406,32 @@ def print_heading(string): def plot(self, 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, - log_scale=False, do_show=True, sep_figs=False, verbose=None): + log_scale=False, do_show=True, sep_figs=False, fig=None): ''' Plot the results -- can supply arguments for both the figure and the plots. 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() + 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() - 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 - log_scale (bool or list): 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 - sep_figs (bool): Whether to show separate figures for different results instead of subplots + 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() + 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 + 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 + sep_figs (bool): Whether to show separate figures for different results instead of subplots + fig (fig): Existing figure to plot into Returns: fig: Figure handle @@ -433,7 +439,7 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar fig = cvplt.plot_scens(scens=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, fill_args=fill_args, legend_args=legend_args, as_dates=as_dates, dateformat=dateformat, interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, - log_scale=log_scale, do_show=do_show, sep_figs=sep_figs) + log_scale=log_scale, do_show=do_show, sep_figs=sep_figs, fig=fig) return fig diff --git a/covasim/sim.py b/covasim/sim.py index 5bbfa4b3a..a4fafa833 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -653,7 +653,7 @@ def summary_stats(self, verbose=None): def plot(self, 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, - log_scale=False, do_show=True, sep_figs=False, verbose=None): + log_scale=False, do_show=True, sep_figs=False, fig=None): ''' Plot the results -- can supply arguments for both the figure and the plots. @@ -676,6 +676,8 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar commaticks (bool): Plot y-axis with commas rather than scientific notation 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 + sep_figs (bool): Whether to show separate figures for different results instead of subplots + fig (fig): Handle of existing figure to plot into Returns: fig: Figure handle @@ -683,23 +685,25 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar fig = cvplt.plot_sim(sim=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, legend_args=legend_args, as_dates=as_dates, dateformat=dateformat, interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, - log_scale=log_scale, do_show=do_show) + log_scale=log_scale, do_show=do_show, sep_figs=sep_figs, fig=fig) return fig - def plot_result(self, key, fig_args=None, plot_args=None): + def plot_result(self, key, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, + font_size=18, font_family=None, grid=False, commaticks=True, as_dates=True, + dateformat=None, interval=None, fig=None): ''' Simple method to plot a single result. Useful for results that aren't - standard outputs. + standard outputs. See sim.plot() for explanation of other arguments. Args: key (str): the key of the result to plot - fig_args (dict): passed to pl.figure() - plot_args (dict): passed to pl.plot() **Examples**:: - sim.plot_result('doubling_time') + sim.plot_result('r_eff') ''' - fig = cvplt.plot_result(sim=self, key=key, fig_args=fig_args, plot_args=plot_args) + fig = cvplt.plot_result(sim=self, key=key, fig_args=fig_args, plot_args=plot_args, axis_args=axis_args, + scatter_args=scatter_args, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, + as_dates=as_dates, dateformat=dateformat, interval=interval, fig=fig) return fig From a4ae7faab2ad57e9f6e37101c4b9e9412490536e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 19:33:43 -0700 Subject: [PATCH 035/194] working on legend --- covasim/plotting.py | 32 +++++++++++++++++++++++--------- covasim/run.py | 5 ++++- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 16d5c45c6..83b3c61dc 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -64,17 +64,30 @@ def create_figs(args, font_size, font_family, sep_figs, fig=None): return fig, figs, None # Initialize axis to be None -def create_subplots(figs, ax, n_rows, n_cols, rk, fig_args, sep_figs, log_scale, title): +def create_subplots(figs, fig, shareax, n_rows, n_cols, pnum, fig_args, sep_figs, log_scale, title): ''' Create subplots and set logarithmic scale ''' + + # Try to find axes by label, if they've already been defined -- this is to avoid the deprecation warning of reusing axes + label = f'ax{pnum+1}' + ax = None + try: + for fig_ax in fig.axes: + if fig_ax.get_label() == label: + ax = fig_ax + break + except: + pass + + # Handle separate figs if sep_figs: figs.append(pl.figure(**fig_args)) - ax = pl.subplot(111) + if ax is None: + ax = pl.subplot(111, label=label) else: - if rk == 0: - ax = pl.subplot(n_rows, n_cols, rk+1) - else: - ax = pl.subplot(n_rows, n_cols, rk+1, sharex=ax) + if ax is None: + ax = pl.subplot(n_rows, n_cols, pnum+1, sharex=shareax, label=f'ax{pnum+1}') + # Handle log scale if log_scale: if isinstance(log_scale, list): if title in log_scale: @@ -104,6 +117,7 @@ def plot_interventions(sim, ax): def title_grid_legend(title, grid, legend_args, show_legend=True): ''' Plot styling -- set the plot title, add a legend, and optionally add gridlines''' + show_legend = legend_args.pop('show_legend', show_legend) # Allow show_legend to be specified in the legend args pl.title(title) if show_legend: # Only show the legend for some subplots pl.legend(**legend_args) @@ -175,7 +189,7 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot # Do the plotting for pnum,title,keylabels in to_plot.enumitems(): - ax = create_subplots(figs, ax, n_rows, n_cols, pnum, args.fig, sep_figs, log_scale, title) + ax = create_subplots(figs, fig, ax, n_rows, n_cols, pnum, args.fig, sep_figs, log_scale, title) for reskey in keylabels: res = sim.results[reskey] res_t = sim.results['t'] @@ -203,7 +217,7 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, # Do the plotting for pnum,title,reskeys in to_plot.enumitems(): - ax = create_subplots(figs, ax, n_rows, n_cols, pnum, args.fig, sep_figs, log_scale, title) + ax = create_subplots(figs, fig, ax, n_rows, n_cols, pnum, args.fig, sep_figs, log_scale, title) for reskey in reskeys: resdata = scens.results[reskey] for scenkey, scendata in resdata.items(): @@ -231,7 +245,7 @@ def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter fig, figs, ax = create_figs(args, font_size, font_family, sep_figs=False, fig=fig) # Do the plotting - ax = pl.subplot(111) + ax = pl.subplot(111, label='ax') res = sim.results[key] res_t = sim.results['t'] res_y = res.values diff --git a/covasim/run.py b/covasim/run.py index bbfaac751..d411baec7 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -205,7 +205,10 @@ def plot(self, *args, **kwargs): fig = self.base_sim.plot(*args, **kwargs) else: fig = None - for sim in self.sims: + kwargs['legend_args'] = sc.mergedicts({'show_legend':True}, kwargs.get('legend_args')) # Only plot the legend the first time + for s,sim in enumerate(self.sims): + if s != 0: + kwargs['legend_args']['show_legend'] = False fig = sim.plot(fig=fig, *args, **kwargs) return fig From 45d81c27b71b1f8c2e2304991e8a29c53efe8b41 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 20:11:04 -0700 Subject: [PATCH 036/194] working, but very complicated --- covasim/plotting.py | 68 ++++++++++++++++++++++++++++++--------------- covasim/run.py | 12 ++++++-- covasim/sim.py | 5 ++-- 3 files changed, 58 insertions(+), 27 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 83b3c61dc..92bd507e3 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -115,25 +115,44 @@ def plot_interventions(sim, ax): return -def title_grid_legend(title, grid, legend_args, show_legend=True): +def title_grid_legend(ax, title, grid, commaticks, setylim, legend_args, show_legend=True): ''' Plot styling -- set the plot title, add a legend, and optionally add gridlines''' - show_legend = legend_args.pop('show_legend', show_legend) # Allow show_legend to be specified in the legend args - pl.title(title) - if show_legend: # Only show the legend for some subplots - pl.legend(**legend_args) - pl.grid(grid) - return + # Handle show_legend being in the legend args, since in some cases this is the only way it can get passed + if 'show_legend' in legend_args: + show_legend = legend_args.pop('show_legend') + popped = True + else: + popped = False -def reset_ticks(ax, sim, y, commaticks, interval, as_dates): - ''' Set the tick marks, using dates by default ''' + # Show the legend + if show_legend: + ax.legend(**legend_args) - # Set the y axis limits and style - sc.setylim() + # If we removed it from the legend_args dict, put it back now + if popped: + legend_args['show_legend'] = show_legend + + # Set the title and gridlines + ax.set_title(title) + ax.grid(grid) + + # Set the y axis style + if setylim: + print(f'i am setylim {setylim} {title}') + ax.set_ylim(bottom=0) + # sc.setylim() if commaticks: - if y.max() >= 1000: + ylims = ax.get_ylim() + if ylims[1] >= 1000: sc.commaticks() + return + + +def reset_ticks(ax, sim, interval, as_dates): + ''' Set the tick marks, using dates by default ''' + # Set the x-axis intervals if interval: xmin,xmax = ax.get_xlim() @@ -178,10 +197,12 @@ def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name='covas 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, as_dates=True, dateformat=None, - interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, + interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, log_scale=False, do_show=True, sep_figs=False, fig=None): ''' Plot the results of a sim -- see Sim.plot() for documentation. ''' + print(f'BONJOUR i am {setylim}') + # Handle inputs args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) to_plot, n_rows = handle_to_plot('sim', to_plot, n_cols) @@ -197,16 +218,17 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot ax.fill_between(res_t, res.low, res.high, **args.fill) # Create the uncertainty bound ax.plot(res_t, res.values, label=res.name, **args.plot, c=res.color) plot_data(sim, reskey, args.scatter) # Plot the data - plot_interventions(sim, ax) # Plot the interventions - title_grid_legend(title, grid, args.legend) # Configure the title, grid, and legend - reset_ticks(ax, sim, res.values, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + plot_interventions(sim, ax) # Plot the interventions + print(f'hi, i am {pnum} {title}') + 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, default_name='covasim.png') 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, as_dates=True, dateformat=None, - interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, + interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, log_scale=False, do_show=True, sep_figs=False, fig=None): ''' Plot the results of a scenario -- see Scenarios.plot() for documentation. ''' @@ -227,15 +249,15 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, ax.plot(scens.tvec, res_y, label=scendata.name, **args.plot) # Plot the actual line plot_data(sim, reskey, args.scatter) # Plot the data plot_interventions(sim, ax) # Plot the interventions - title_grid_legend(title, grid, args.legend, pnum==0) # Configure the title, grid, and legend -- only show legend for first - reset_ticks(ax, sim, res_y, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + 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, default_name='covasim_scenarios.png') def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, - font_size=18, font_family=None, grid=False, commaticks=True, as_dates=True, - dateformat=None, interval=None, fig=None): + font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, + as_dates=True, dateformat=None, interval=None, fig=None): ''' Plot a single result -- see Sim.plot_result() for documentation. ''' # Handle inputs @@ -252,8 +274,8 @@ def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter ax.plot(res_t, res_y, c=res.color, **args.plot) plot_data(sim, key, args.scatter) # Plot the data plot_interventions(sim, ax) # Plot the interventions - title_grid_legend(res.name, grid, args.legend, show_legend=False) # Configure the title, grid, and legend - reset_ticks(ax, sim, res_y, commaticks, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + title_grid_legend(ax, res.name, grid, commaticks, setylim, args.legend, show_legend=False) # Configure the title, grid, and legend + reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) return fig diff --git a/covasim/run.py b/covasim/run.py index d411baec7..213e54896 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -205,10 +205,17 @@ def plot(self, *args, **kwargs): fig = self.base_sim.plot(*args, **kwargs) else: fig = None + orig_setylim = kwargs.get('setylim', True) kwargs['legend_args'] = sc.mergedicts({'show_legend':True}, kwargs.get('legend_args')) # Only plot the legend the first time for s,sim in enumerate(self.sims): if s != 0: kwargs['legend_args']['show_legend'] = False + if s == len(self.sims)-1: + kwargs['setylim'] = orig_setylim + else: + kwargs['setylim'] = False + print(f'ABOUT TO PLOT') + print(kwargs['setylim']) fig = sim.plot(fig=fig, *args, **kwargs) return fig @@ -408,7 +415,7 @@ def print_heading(string): def plot(self, 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, as_dates=True, dateformat=None, - interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, + interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, log_scale=False, do_show=True, sep_figs=False, fig=None): ''' Plot the results -- can supply arguments for both the figure and the plots. @@ -431,6 +438,7 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar 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 sep_figs (bool): Whether to show separate figures for different results instead of subplots @@ -441,7 +449,7 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar ''' fig = cvplt.plot_scens(scens=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, fill_args=fill_args, legend_args=legend_args, as_dates=as_dates, dateformat=dateformat, - interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, + interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, setylim=setylim, log_scale=log_scale, do_show=do_show, sep_figs=sep_figs, fig=fig) return fig diff --git a/covasim/sim.py b/covasim/sim.py index a4fafa833..9b2d072f4 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -652,7 +652,7 @@ def summary_stats(self, verbose=None): def plot(self, 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, as_dates=True, dateformat=None, - interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, + interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, log_scale=False, do_show=True, sep_figs=False, fig=None): ''' Plot the results -- can supply arguments for both the figure and the plots. @@ -674,6 +674,7 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar 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 sep_figs (bool): Whether to show separate figures for different results instead of subplots @@ -684,7 +685,7 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar ''' fig = cvplt.plot_sim(sim=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, legend_args=legend_args, as_dates=as_dates, dateformat=dateformat, - interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, + interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, setylim=setylim, log_scale=log_scale, do_show=do_show, sep_figs=sep_figs, fig=fig) return fig From 5632a1529012774e4bc7768420a920e16f083b83 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 20:19:16 -0700 Subject: [PATCH 037/194] improved plotting flexibility --- covasim/plotting.py | 5 ----- covasim/run.py | 15 +++++++++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 92bd507e3..8404c5135 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -139,9 +139,7 @@ def title_grid_legend(ax, title, grid, commaticks, setylim, legend_args, show_le # Set the y axis style if setylim: - print(f'i am setylim {setylim} {title}') ax.set_ylim(bottom=0) - # sc.setylim() if commaticks: ylims = ax.get_ylim() if ylims[1] >= 1000: @@ -201,8 +199,6 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot log_scale=False, do_show=True, sep_figs=False, fig=None): ''' Plot the results of a sim -- see Sim.plot() for documentation. ''' - print(f'BONJOUR i am {setylim}') - # Handle inputs args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) to_plot, n_rows = handle_to_plot('sim', to_plot, n_cols) @@ -220,7 +216,6 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot plot_data(sim, reskey, args.scatter) # Plot the data reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) plot_interventions(sim, ax) # Plot the interventions - print(f'hi, i am {pnum} {title}') 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, default_name='covasim.png') diff --git a/covasim/run.py b/covasim/run.py index 213e54896..1f1c0149e 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -52,13 +52,19 @@ class MultiSim(sc.prettyobj): sim = cv.Sim() # Create the sim msim = cv.MultiSim(sim, n_runs=5) # Create the multisim msim.run() # Run them in parallel - msim.combine() # Calculate statistics + msim.combine() # Combine into one sim msim.plot() # Plot results - sims = [cv.Sim(beta=0.015*(1+0.1*i)) for i in range(5)] # Create sims + sim = cv.Sim() # Create the sim + msim = cv.MultiSim(sim, n_runs=11, noise=0.1) # Set up a multisim with noise + msim.run() # Run + msim.reduce() # Compute statistics + msim.plot() # Plot + + sims = [cv.Sim(beta=0.015*(1+0.02*i)) for i in range(5)] # Create sims for sim in sims: sim.run() # Run sims in serial msim = cv.MultiSim(sims) # Convert to multisim - msim.plot() # Plot each sim separately + msim.plot() # Plot as single sim ''' def __init__(self, sims=None, base_sim=None, quantiles=None, **kwargs): @@ -122,7 +128,6 @@ def run(self, *args, **kwargs): sims = self.sims kwargs = sc.mergedicts(self.run_args, kwargs) - print(kwargs) self.sims = multi_run(sims, *args, **kwargs) return @@ -214,8 +219,6 @@ def plot(self, *args, **kwargs): kwargs['setylim'] = orig_setylim else: kwargs['setylim'] = False - print(f'ABOUT TO PLOT') - print(kwargs['setylim']) fig = sim.plot(fig=fig, *args, **kwargs) return fig From e2d6a4703ec10aed271631fd2a0c9d0db5ee69b8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 20:22:21 -0700 Subject: [PATCH 038/194] added dev test --- CHANGELOG.rst | 1 + tests/devtests/archive/multisim_tests.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 tests/devtests/archive/multisim_tests.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3e3c345e7..d6468ba3e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ strictly consecutive. Version 0.30.0 (2020-05-02) --------------------------- - Added new ``MultiSim`` class for plotting a single simulation with uncertainty. +- Added ``low`` and ``high`` attributes to the ``Result`` object. - Refactored plotting to increase consistency between ``sim.plot()``, ``sim.plot_result()``, ``scens.plot()``, and ``multisim.plot()``. - Doubling time calculation defaults have been updated to use a window of 3 days and a maximum of 30 days. diff --git a/tests/devtests/archive/multisim_tests.py b/tests/devtests/archive/multisim_tests.py new file mode 100644 index 000000000..4ea278df0 --- /dev/null +++ b/tests/devtests/archive/multisim_tests.py @@ -0,0 +1,18 @@ +import covasim as cv + +sim = cv.Sim() # Create the sim +msim = cv.MultiSim(sim, n_runs=5) # Create the multisim +msim.run() # Run them in parallel +msim.combine() # Combine into one sim +msim.plot() # Plot results + +sim = cv.Sim() # Create the sim +msim = cv.MultiSim(sim, n_runs=11, noise=0.1) # Set up a multisim with noise +msim.run() # Run +msim.reduce() # Compute statistics +msim.plot() # Plot + +sims = [cv.Sim(beta=0.015*(1+0.02*i)) for i in range(5)] # Create sims +for sim in sims: sim.run() # Run sims in serial +msim = cv.MultiSim(sims) # Convert to multisim +msim.plot() # Plot as single sim \ No newline at end of file From b3b434ee68139d3e1cfb1ea7c96a6ca2b12da6a6 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 20:24:21 -0700 Subject: [PATCH 039/194] minor test update --- tests/devtests/archive/multisim_tests.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/devtests/archive/multisim_tests.py b/tests/devtests/archive/multisim_tests.py index 4ea278df0..21353fb07 100644 --- a/tests/devtests/archive/multisim_tests.py +++ b/tests/devtests/archive/multisim_tests.py @@ -1,18 +1,18 @@ import covasim as cv sim = cv.Sim() # Create the sim -msim = cv.MultiSim(sim, n_runs=5) # Create the multisim -msim.run() # Run them in parallel -msim.combine() # Combine into one sim -msim.plot() # Plot results +msim1 = cv.MultiSim(sim, n_runs=5) # Create the multisim +msim1.run() # Run them in parallel +msim1.combine() # Combine into one sim +msim1.plot() # Plot results sim = cv.Sim() # Create the sim -msim = cv.MultiSim(sim, n_runs=11, noise=0.1) # Set up a multisim with noise -msim.run() # Run -msim.reduce() # Compute statistics -msim.plot() # Plot +msim2 = cv.MultiSim(sim, n_runs=11, noise=0.1) # Set up a multisim with noise +msim2.run() # Run +msim2.reduce() # Compute statistics +msim2.plot() # Plot sims = [cv.Sim(beta=0.015*(1+0.02*i)) for i in range(5)] # Create sims for sim in sims: sim.run() # Run sims in serial -msim = cv.MultiSim(sims) # Convert to multisim -msim.plot() # Plot as single sim \ No newline at end of file +msim3 = cv.MultiSim(sims) # Convert to multisim +msim3.plot() # Plot as single sim \ No newline at end of file From c9010b01a669c05e751654e0cf91abfceb2eda06 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 20:26:12 -0700 Subject: [PATCH 040/194] fix label --- covasim/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 8404c5135..0a734ddc3 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -85,7 +85,7 @@ def create_subplots(figs, fig, shareax, n_rows, n_cols, pnum, fig_args, sep_figs ax = pl.subplot(111, label=label) else: if ax is None: - ax = pl.subplot(n_rows, n_cols, pnum+1, sharex=shareax, label=f'ax{pnum+1}') + ax = pl.subplot(n_rows, n_cols, pnum+1, sharex=shareax, label=label) # Handle log scale if log_scale: From d86b38425c935823ff2b5b643ef1cda0915f9d15 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 20:27:38 -0700 Subject: [PATCH 041/194] tidy docstrings --- covasim/plotting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 0a734ddc3..7b99fd4e9 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -197,7 +197,7 @@ 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, log_scale=False, do_show=True, sep_figs=False, fig=None): - ''' Plot the results of a sim -- see Sim.plot() for documentation. ''' + ''' Plot the results of a sim -- see Sim.plot() for documentation ''' # Handle inputs args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) @@ -225,7 +225,7 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, log_scale=False, do_show=True, sep_figs=False, fig=None): - ''' Plot the results of a scenario -- see Scenarios.plot() for documentation. ''' + ''' 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) @@ -253,7 +253,7 @@ 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, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, as_dates=True, dateformat=None, interval=None, fig=None): - ''' Plot a single result -- see Sim.plot_result() for documentation. ''' + ''' Plot a single result -- see Sim.plot_result() for documentation ''' # Handle inputs fig_args = sc.mergedicts({'figsize':(16,8)}, fig_args) From b4ae116d8a2ae459dd61ee638986e1736f396d4e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 20:34:45 -0700 Subject: [PATCH 042/194] fixed color mismatch --- covasim/plotting.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 7b99fd4e9..ed9dcfe35 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -211,7 +211,7 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot res = sim.results[reskey] res_t = sim.results['t'] if res.low is not None and res.high is not None: - ax.fill_between(res_t, res.low, res.high, **args.fill) # Create the uncertainty bound + ax.fill_between(res_t, res.low, res.high, color=res.color, **args.fill) # Create the uncertainty bound ax.plot(res_t, res.values, label=res.name, **args.plot, c=res.color) plot_data(sim, reskey, args.scatter) # Plot the data reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) @@ -233,15 +233,16 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, fig, figs, ax = create_figs(args, font_size, font_family, sep_figs, fig) # Do the plotting + colors = sc.gridcolors(ncolors=len(scens.sims)) for pnum,title,reskeys in to_plot.enumitems(): ax = create_subplots(figs, fig, ax, n_rows, n_cols, pnum, args.fig, sep_figs, log_scale, title) for reskey in reskeys: resdata = scens.results[reskey] - for scenkey, scendata in resdata.items(): + 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 - ax.fill_between(scens.tvec, scendata.low, scendata.high, **args.fill) # Create the uncertainty bound - ax.plot(scens.tvec, res_y, label=scendata.name, **args.plot) # Plot the actual line + ax.fill_between(scens.tvec, scendata.low, scendata.high, color=colors[snum], **args.fill) # Create the uncertainty bound + ax.plot(scens.tvec, res_y, label=scendata.name, c=colors[snum], **args.plot) # Plot the actual line plot_data(sim, reskey, args.scatter) # Plot the data plot_interventions(sim, ax) # Plot the interventions reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) From 44d107cc99c520ed6d73af290051b006d251bf02 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 20:45:26 -0700 Subject: [PATCH 043/194] added custom colors --- covasim/plotting.py | 34 +++++++++++++++++++++++----------- covasim/run.py | 5 +++-- covasim/sim.py | 9 +++++---- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index ed9dcfe35..70b0a8ad1 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -196,7 +196,7 @@ def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name='covas 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, - log_scale=False, do_show=True, sep_figs=False, fig=None): + log_scale=False, colors=None, do_show=True, sep_figs=False, fig=None): ''' Plot the results of a sim -- see Sim.plot() for documentation ''' # Handle inputs @@ -210,9 +210,13 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot for reskey in keylabels: res = sim.results[reskey] res_t = sim.results['t'] + if colors is not None: + color = colors[reskey] + else: + color = res.color if res.low is not None and res.high is not None: - ax.fill_between(res_t, res.low, res.high, color=res.color, **args.fill) # Create the uncertainty bound - ax.plot(res_t, res.values, label=res.name, **args.plot, c=res.color) + ax.fill_between(res_t, res.low, res.high, color=color, **args.fill) # Create the uncertainty bound + ax.plot(res_t, res.values, label=res.name, **args.plot, c=color) plot_data(sim, reskey, args.scatter) # Plot the data reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) plot_interventions(sim, ax) # Plot the interventions @@ -224,7 +228,7 @@ 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, - log_scale=False, do_show=True, sep_figs=False, fig=None): + log_scale=False, colors=None, do_show=True, sep_figs=False, fig=None): ''' Plot the results of a scenario -- see Scenarios.plot() for documentation ''' # Handle inputs @@ -233,7 +237,7 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, fig, figs, ax = create_figs(args, font_size, font_family, sep_figs, fig) # Do the plotting - colors = sc.gridcolors(ncolors=len(scens.sims)) + default_colors = sc.gridcolors(ncolors=len(scens.sims)) for pnum,title,reskeys in to_plot.enumitems(): ax = create_subplots(figs, fig, ax, n_rows, n_cols, pnum, args.fig, sep_figs, log_scale, title) for reskey in reskeys: @@ -241,8 +245,12 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, 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 - ax.fill_between(scens.tvec, scendata.low, scendata.high, color=colors[snum], **args.fill) # Create the uncertainty bound - ax.plot(scens.tvec, res_y, label=scendata.name, c=colors[snum], **args.plot) # Plot the actual line + if colors is not None: + color = colors[scenkey] + else: + color = default_colors[snum] + ax.fill_between(scens.tvec, scendata.low, scendata.high, color=color, **args.fill) # Create the uncertainty bound + ax.plot(scens.tvec, res_y, label=scendata.name, c=color, **args.plot) # Plot the actual line plot_data(sim, reskey, args.scatter) # Plot the data plot_interventions(sim, ax) # Plot the interventions reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) @@ -253,7 +261,7 @@ 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, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, - as_dates=True, dateformat=None, interval=None, fig=None): + as_dates=True, dateformat=None, interval=None, color=None, fig=None): ''' Plot a single result -- see Sim.plot_result() for documentation ''' # Handle inputs @@ -262,12 +270,16 @@ def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter args = handle_args(fig_args, plot_args, scatter_args, axis_args) fig, figs, ax = create_figs(args, font_size, font_family, sep_figs=False, fig=fig) - # Do the plotting - ax = pl.subplot(111, label='ax') + # Gather results res = sim.results[key] res_t = sim.results['t'] res_y = res.values - ax.plot(res_t, res_y, c=res.color, **args.plot) + if color is None: + color = res.color + + # Do the plotting + ax = pl.subplot(111, label='ax') + ax.plot(res_t, res_y, c=color, **args.plot) plot_data(sim, key, args.scatter) # Plot the data plot_interventions(sim, ax) # Plot the interventions title_grid_legend(ax, res.name, grid, commaticks, setylim, args.legend, show_legend=False) # Configure the title, grid, and legend diff --git a/covasim/run.py b/covasim/run.py index 1f1c0149e..a3459c014 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -419,7 +419,7 @@ def print_heading(string): def plot(self, 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, - log_scale=False, do_show=True, sep_figs=False, fig=None): + log_scale=False, colors=None, do_show=True, sep_figs=False, fig=None): ''' Plot the results -- can supply arguments for both the figure and the plots. @@ -444,6 +444,7 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar 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 @@ -453,7 +454,7 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar fig = cvplt.plot_scens(scens=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, fill_args=fill_args, legend_args=legend_args, as_dates=as_dates, dateformat=dateformat, interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, setylim=setylim, - log_scale=log_scale, do_show=do_show, sep_figs=sep_figs, fig=fig) + log_scale=log_scale, colors=colors, do_show=do_show, sep_figs=sep_figs, fig=fig) return fig diff --git a/covasim/sim.py b/covasim/sim.py index 9b2d072f4..3a0eda381 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -653,7 +653,7 @@ def summary_stats(self, verbose=None): def plot(self, 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, - log_scale=False, do_show=True, sep_figs=False, fig=None): + log_scale=False, do_show=True, colors=None, sep_figs=False, fig=None): ''' Plot the results -- can supply arguments for both the figure and the plots. @@ -677,6 +677,7 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar 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 result, must be a dictionary with one entry per result key in to_plot sep_figs (bool): Whether to show separate figures for different results instead of subplots fig (fig): Handle of existing figure to plot into @@ -686,13 +687,13 @@ def plot(self, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_ar fig = cvplt.plot_sim(sim=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, legend_args=legend_args, as_dates=as_dates, dateformat=dateformat, interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, setylim=setylim, - log_scale=log_scale, do_show=do_show, sep_figs=sep_figs, fig=fig) + log_scale=log_scale, colors=colors, do_show=do_show, sep_figs=sep_figs, fig=fig) return fig def plot_result(self, key, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, font_size=18, font_family=None, grid=False, commaticks=True, as_dates=True, - dateformat=None, interval=None, fig=None): + dateformat=None, color=color, interval=None, fig=None): ''' Simple method to plot a single result. Useful for results that aren't standard outputs. See sim.plot() for explanation of other arguments. @@ -706,5 +707,5 @@ def plot_result(self, key, fig_args=None, plot_args=None, axis_args=None, scatte ''' fig = cvplt.plot_result(sim=self, key=key, fig_args=fig_args, plot_args=plot_args, axis_args=axis_args, scatter_args=scatter_args, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, - as_dates=as_dates, dateformat=dateformat, interval=interval, fig=fig) + as_dates=as_dates, dateformat=dateformat, interval=interval, color=color, fig=fig) return fig From 7f28b6b8bd9d19c979ad3a5ba4f8ca6e55f166fd Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 20:59:07 -0700 Subject: [PATCH 044/194] reorganize docstrings --- covasim/plotting.py | 77 +++++++++++++++++++++++++++++++++++++++++++-- covasim/run.py | 44 +++----------------------- covasim/sim.py | 50 ++++------------------------- 3 files changed, 85 insertions(+), 86 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 70b0a8ad1..be62de74a 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -197,7 +197,43 @@ 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, log_scale=False, colors=None, do_show=True, sep_figs=False, fig=None): - ''' Plot the results of a sim -- see Sim.plot() for documentation ''' + ''' + Plot the results of a single simulation. + + Args: + to_plot (dict): Dict of results to plot; see get_sim_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() + legend_args (dict): Dictionary of kwargs to be passed to pl.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 result, must be a dictionary with one entry per result key in to_plot + sep_figs (bool): Whether to show separate figures for different results instead of subplots + fig (fig): Handle of existing figure to plot into + + Returns: + fig: Figure handle + + + **Example**:: + + sim = cv.Sim() + sim.run() + sim.plot() + ''' # Handle inputs args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) @@ -229,7 +265,44 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, log_scale=False, colors=None, do_show=True, sep_figs=False, fig=None): - ''' Plot the results of a scenario -- see Scenarios.plot() for documentation ''' + ''' + 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() + 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 + + Returns: + fig: Figure handle + + + **Example**:: + + scens = cv.Scenarios() + scens.run() + scens.plot() + ''' # Handle inputs args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) diff --git a/covasim/run.py b/covasim/run.py index a3459c014..7be521648 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -416,46 +416,10 @@ def print_heading(string): return - def plot(self, 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, as_dates=True, dateformat=None, - interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, - log_scale=False, colors=None, do_show=True, sep_figs=False, fig=None): - ''' - Plot the results -- can supply arguments for both the figure and the plots. - - 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() - 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 - - Returns: - fig: Figure handle - ''' - fig = cvplt.plot_scens(scens=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, - scatter_args=scatter_args, axis_args=axis_args, fill_args=fill_args, legend_args=legend_args, as_dates=as_dates, dateformat=dateformat, - interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, setylim=setylim, - log_scale=log_scale, colors=colors, do_show=do_show, sep_figs=sep_figs, fig=fig) - + def plot(self, *args, **kwargs): + ''' Plot the results -- see documentation in the plotting module ''' + self.plot.__func__.__doc__ = cvplt.plot_scens.__doc__ # Use the docstring from the plotting module + fig = cvplt.plot_scens(scens=self, *args, **kwargs) return fig diff --git a/covasim/sim.py b/covasim/sim.py index 3a0eda381..79642679f 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -650,50 +650,14 @@ def summary_stats(self, verbose=None): return summary - def plot(self, 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, as_dates=True, dateformat=None, - interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, - log_scale=False, do_show=True, colors=None, sep_figs=False, fig=None): - ''' - Plot the results -- can supply arguments for both the figure and the plots. - - Args: - to_plot (dict): Dict of results to plot; see get_sim_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() - legend_args (dict): Dictionary of kwargs to be passed to pl.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 result, must be a dictionary with one entry per result key in to_plot - sep_figs (bool): Whether to show separate figures for different results instead of subplots - fig (fig): Handle of existing figure to plot into - - Returns: - fig: Figure handle - ''' - fig = cvplt.plot_sim(sim=self, to_plot=to_plot, do_save=do_save, fig_path=fig_path, fig_args=fig_args, plot_args=plot_args, - scatter_args=scatter_args, axis_args=axis_args, legend_args=legend_args, as_dates=as_dates, dateformat=dateformat, - interval=interval, n_cols=n_cols, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, setylim=setylim, - log_scale=log_scale, colors=colors, do_show=do_show, sep_figs=sep_figs, fig=fig) + def plot(self, *args, **kwargs): + ''' Plot the results -- see documentation in the plotting module ''' + self.plot.__func__.__doc__ = cvplt.plot_sim.__doc__ # Use the docstring from the plotting module + fig = cvplt.plot_sim(sim=self, *args, **kwargs) return fig - def plot_result(self, key, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, - font_size=18, font_family=None, grid=False, commaticks=True, as_dates=True, - dateformat=None, color=color, interval=None, fig=None): + def plot_result(self, key, *args, **kwargs): ''' Simple method to plot a single result. Useful for results that aren't standard outputs. See sim.plot() for explanation of other arguments. @@ -705,7 +669,5 @@ def plot_result(self, key, fig_args=None, plot_args=None, axis_args=None, scatte sim.plot_result('r_eff') ''' - fig = cvplt.plot_result(sim=self, key=key, fig_args=fig_args, plot_args=plot_args, axis_args=axis_args, - scatter_args=scatter_args, font_size=font_size, font_family=font_family, grid=grid, commaticks=commaticks, - as_dates=as_dates, dateformat=dateformat, interval=interval, color=color, fig=fig) + fig = cvplt.plot_result(sim=self, key=key, *args, **kwargs) return fig From 45bec238bf5a055fca64908eb00551cdbd65e653 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 21:10:59 -0700 Subject: [PATCH 045/194] moved docstrings back --- covasim/plotting.py | 77 ++------------------------------------------- covasim/run.py | 40 +++++++++++++++++++++-- covasim/sim.py | 41 ++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 80 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index be62de74a..5cb97988f 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -197,43 +197,7 @@ 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, log_scale=False, colors=None, do_show=True, sep_figs=False, fig=None): - ''' - Plot the results of a single simulation. - - Args: - to_plot (dict): Dict of results to plot; see get_sim_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() - legend_args (dict): Dictionary of kwargs to be passed to pl.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 result, must be a dictionary with one entry per result key in to_plot - sep_figs (bool): Whether to show separate figures for different results instead of subplots - fig (fig): Handle of existing figure to plot into - - Returns: - fig: Figure handle - - - **Example**:: - - sim = cv.Sim() - sim.run() - sim.plot() - ''' + ''' 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) @@ -265,44 +229,7 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, log_scale=False, colors=None, do_show=True, sep_figs=False, fig=None): - ''' - 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() - 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 - - Returns: - fig: Figure handle - - - **Example**:: - - scens = cv.Scenarios() - scens.run() - scens.plot() - ''' + ''' 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) diff --git a/covasim/run.py b/covasim/run.py index 7be521648..9c06c6c3e 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -417,8 +417,44 @@ def print_heading(string): def plot(self, *args, **kwargs): - ''' Plot the results -- see documentation in the plotting module ''' - self.plot.__func__.__doc__ = cvplt.plot_scens.__doc__ # Use the docstring from the plotting module + ''' + 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() + 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 + + Returns: + fig: Figure handle + + + **Example**:: + + scens = cv.Scenarios() + scens.run() + scens.plot() + ''' fig = cvplt.plot_scens(scens=self, *args, **kwargs) return fig diff --git a/covasim/sim.py b/covasim/sim.py index 79642679f..335af18bd 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -651,8 +651,43 @@ def summary_stats(self, verbose=None): def plot(self, *args, **kwargs): - ''' Plot the results -- see documentation in the plotting module ''' - self.plot.__func__.__doc__ = cvplt.plot_sim.__doc__ # Use the docstring from the plotting module + ''' + Plot the results of a single simulation. + + Args: + to_plot (dict): Dict of results to plot; see get_sim_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() + legend_args (dict): Dictionary of kwargs to be passed to pl.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 result, must be a dictionary with one entry per result key in to_plot + sep_figs (bool): Whether to show separate figures for different results instead of subplots + fig (fig): Handle of existing figure to plot into + + Returns: + fig: Figure handle + + + **Example**:: + + sim = cv.Sim() + sim.run() + sim.plot() + ''' fig = cvplt.plot_sim(sim=self, *args, **kwargs) return fig @@ -670,4 +705,4 @@ def plot_result(self, key, *args, **kwargs): sim.plot_result('r_eff') ''' fig = cvplt.plot_result(sim=self, key=key, *args, **kwargs) - return fig + return fig \ No newline at end of file From 190eeeb79ea8b6ba6dba8154ab32668e80647436 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 21:11:55 -0700 Subject: [PATCH 046/194] tidying --- covasim/plotting.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 5cb97988f..c7dc13780 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -7,8 +7,8 @@ ''' import numpy as np -import sciris as sc import pylab as pl +import sciris as sc import datetime as dt import matplotlib.ticker as ticker import plotly.graph_objects as go @@ -18,6 +18,8 @@ __all__ = ['plot_sim', 'plot_scens', 'plot_result', 'plotly_sim', 'plotly_people', 'plotly_animate'] +#%% Plotting helper functions + def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None): ''' Handle input arguments -- merge user input with defaults ''' args = sc.objdict() @@ -193,6 +195,8 @@ def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name='covas return fig +#%% 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, @@ -288,6 +292,8 @@ def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter return fig +#%% Plotly functions + def get_individual_states(sim): ''' Helper function to convert people into integers ''' From 49b0aaf15352a44f16037a3c529ab961a78ac5dd Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 21:18:41 -0700 Subject: [PATCH 047/194] working --- covasim/plotting.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index c7dc13780..ee3df1b13 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -281,12 +281,20 @@ def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter if color is None: color = res.color + # Reuse the figure, if available + try: + if fig.axes[0].get_label() == 'plot_result': + ax = fig.axes[0] + except: + pass + if ax is None: # Otherwise, make a new one + ax = pl.subplot(111, label='plot_result') + # Do the plotting - ax = pl.subplot(111, label='ax') - ax.plot(res_t, res_y, c=color, **args.plot) + ax.plot(res_t, res_y, c=color, label=res.name, **args.plot) plot_data(sim, key, args.scatter) # Plot the data plot_interventions(sim, ax) # Plot the interventions - title_grid_legend(ax, res.name, grid, commaticks, setylim, args.legend, show_legend=False) # Configure the title, grid, and legend + title_grid_legend(ax, res.name, grid, commaticks, setylim, args.legend) # Configure the title, grid, and legend reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) return fig From 27e5230f4e0998a927ea02fc67770cb94cb098fa Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 21:24:55 -0700 Subject: [PATCH 048/194] final tweaking --- covasim/plotting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index ee3df1b13..79fb68e37 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -473,9 +473,9 @@ def plotly_animate(sim, do_show=False): "yanchor": "top", "xanchor": "left", "currentvalue": { - "font": {"size": 20}, + "font": {"size": 16}, "prefix": "Day: ", - "visible": False, + "visible": True, "xanchor": "right" }, "transition": {"duration": 200}, From 28537299be482ead4dca8e9e766f6d34e42a0d1f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 21:45:58 -0700 Subject: [PATCH 049/194] updated colors --- covasim/plotting.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 79fb68e37..f827af756 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -310,22 +310,22 @@ def get_individual_states(sim): states = [ {'name': 'Healthy', 'quantity': None, - 'color': '#b9d58a', + 'color': '#a6cee3', 'value': 0 }, {'name': 'Exposed', 'quantity': 'date_exposed', - 'color': '#e37c30', + 'color': '#ff7f00', 'value': 2 }, {'name': 'Infectious', 'quantity': 'date_infectious', - 'color': '#c35d86', + 'color': '#e33d3e', 'value': 3 }, {'name': 'Recovered', 'quantity': 'date_recovered', - 'color': '#799956', + 'color': '#3e89bc', 'value': 4 }, {'name': 'Dead', From 905a7debe67e366fc9654077f173e069b417770d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 22:31:08 -0700 Subject: [PATCH 050/194] fix clip_edges end day --- CHANGELOG.rst | 1 + covasim/interventions.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d6468ba3e..62a002deb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,7 @@ Version 0.30.0 (2020-05-02) - Added ``low`` and ``high`` attributes to the ``Result`` object. - Refactored plotting to increase consistency between ``sim.plot()``, ``sim.plot_result()``, ``scens.plot()``, and ``multisim.plot()``. - Doubling time calculation defaults have been updated to use a window of 3 days and a maximum of 30 days. +- Fixed a bug whereby ``cv.clip_edges()`` with no end day specified resulted in large sim files when saved. Version 0.29.9 (2020-04-28) diff --git a/covasim/interventions.py b/covasim/interventions.py index 87a6db9a4..6ee35e8f8 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -389,6 +389,7 @@ def apply(self, sim): if verbose: print(f'Remaining contacts: {sim.people.contacts[lkey]}') + # At the end, move back if sim.t == self.end_day: if verbose: print(f'Before:\n{sim.people.contacts}') @@ -397,6 +398,10 @@ def apply(self, sim): print(f'After:\n{sim.people.contacts}') self.contacts = None # Reset to save memory + # If no end day is specified, ensure they get deleted + if sim.t == sim.tvec[-1]: + self.contacts = None # Reset to save memory + return From 42426b7376f44ff71daccc3a24a529245ed7da2f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 22:48:05 -0700 Subject: [PATCH 051/194] added until --- covasim/base.py | 14 +++++++++++++- covasim/sim.py | 5 ++++- tests/devtests/test_run_until.py | 15 +++++++++++++++ tests/test_sim.py | 6 ++++++ 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100755 tests/devtests/test_run_until.py diff --git a/covasim/base.py b/covasim/base.py index 7586aa2b4..7587869ba 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -452,7 +452,7 @@ def shrink(self, skip_attrs=None, in_place=True): return shrunken_sim - def save(self, filename=None, keep_people=False, skip_attrs=None, **kwargs): + def save(self, filename=None, keep_people=None, skip_attrs=None, **kwargs): ''' Save to disk as a gzipped pickle. @@ -467,15 +467,27 @@ def save(self, filename=None, keep_people=False, skip_attrs=None, **kwargs): sim.save() # Saves to a .sim file with the date and time of creation by default ''' + + # Set keep_people based on whether or not we're in the middle of a run + if keep_people is None: + if self.initialized and not self.results_ready: + keep_people = True + else: + keep_people = False + + # Handle the filename if filename is None: filename = self.simfile filename = sc.makefilepath(filename=filename, **kwargs) self.filename = filename # Store the actual saved filename + + # Handle the shrinkage and save if skip_attrs or not keep_people: obj = self.shrink(skip_attrs=skip_attrs, in_place=False) else: obj = self sc.saveobj(filename=filename, obj=obj) + return filename diff --git a/covasim/sim.py b/covasim/sim.py index 335af18bd..44e2133a6 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -400,12 +400,13 @@ def rescale(self): return - def run(self, do_plot=False, verbose=None, **kwargs): + def run(self, do_plot=False, until=None, verbose=None, **kwargs): ''' Run the simulation. Args: do_plot (bool): whether to plot + until (int): day to run until verbose (int): level of detail to print kwargs (dict): passed to self.plot() @@ -446,6 +447,8 @@ def run(self, do_plot=False, verbose=None, **kwargs): elif self['stopping_func'] and self['stopping_func'](self): sc.printv("Stopping function terminated the simulation", 1, verbose) break + if self.t == until: # If until is specified, just stop here + return # End of time loop; compute cumulative results outside of the time loop self.finalize(verbose=verbose) # Finalize the results diff --git a/tests/devtests/test_run_until.py b/tests/devtests/test_run_until.py new file mode 100755 index 000000000..212d8a775 --- /dev/null +++ b/tests/devtests/test_run_until.py @@ -0,0 +1,15 @@ +import covasim as cv + +fn = 'tmp.sim' + +s1 = cv.Sim() +s1.run() + +s2 = cv.Sim() +s2.run(until=30) +s2.save(fn) + +s3 = cv.load(fn) +s3.run() + +assert s3.summary == s1.summary diff --git a/tests/test_sim.py b/tests/test_sim.py index 95e526388..17010eac1 100644 --- a/tests/test_sim.py +++ b/tests/test_sim.py @@ -111,9 +111,15 @@ def test_start_stop(): # If being run via pytest, turn off sim2.step() sim2.finalize() + # Test that until works + sim3 = cv.Sim(pars) + sim3.run(until=20) + sim3.run() + # Compare results key = 'cum_infections' assert (sim1.results[key][:] == sim2.results[key][:]).all(), 'Next values do not match' + assert (sim1.results[key][:] == sim3.results[key][:]).all(), 'Until values do not match' return sim2 From a59955e2dcca701e27feb7c17c6b92c33e397dd1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 22:49:42 -0700 Subject: [PATCH 052/194] updated changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 62a002deb..a985e8757 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,7 @@ Version 0.30.0 (2020-05-02) - Added ``low`` and ``high`` attributes to the ``Result`` object. - Refactored plotting to increase consistency between ``sim.plot()``, ``sim.plot_result()``, ``scens.plot()``, and ``multisim.plot()``. - Doubling time calculation defaults have been updated to use a window of 3 days and a maximum of 30 days. +- Added a ``until`` keyword to ``sim.run()``, to make it easier to run a partially completed sim and then resume. See ``tests/devtests/test_run_until.py``. - Fixed a bug whereby ``cv.clip_edges()`` with no end day specified resulted in large sim files when saved. From 3f63eb2a1af9d32368089a3fc67cd8a330d8084b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 23:22:57 -0700 Subject: [PATCH 053/194] added par_args kwarg to multi_run --- covasim/interventions.py | 20 ++++++++++++++------ covasim/run.py | 15 ++++++++------- covasim/sim.py | 4 +++- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 6ee35e8f8..db7eb9930 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -51,6 +51,7 @@ def __init__(self, do_plot=None): do_plot = True self.do_plot = do_plot self.days = [] + self.initialized = False return @@ -84,6 +85,7 @@ def initialize(self, sim): Initialize intervention -- this is used to make modifications to the intervention that can't be done until after the sim is created. ''' + self.initialized = True return @@ -234,6 +236,7 @@ def __init__(self, days, interventions, do_plot=None): def initialize(self, sim): ''' Fix the dates ''' self.days = [sim.day(day) for day in self.days] + self.initialized = True return @@ -293,6 +296,7 @@ def initialize(self, sim): self.orig_betas['overall'] = sim['beta'] else: self.orig_betas[lkey] = sim['beta_layer'][lkey] + self.initialized = True return @@ -345,6 +349,7 @@ 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.initialized = True return @@ -450,9 +455,10 @@ def __init__(self, daily_tests, sympt_test=100.0, quar_test=1.0, sensitivity=1.0 def initialize(self, sim): ''' Fix the dates ''' - 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.start_day = sim.day(self.start_day) + self.end_day = sim.day(self.end_day) + self.days = [self.start_day, self.end_day] + self.initialized = True return @@ -537,9 +543,10 @@ def __init__(self, symp_prob=0, asymp_prob=0, symp_quar_prob=None, asymp_quar_pr def initialize(self, sim): ''' Fix the dates ''' - 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.start_day = sim.day(self.start_day) + self.end_day = sim.day(self.end_day) + self.days = [self.start_day, self.end_day] + self.initialized = True return @@ -606,6 +613,7 @@ 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 diff --git a/covasim/run.py b/covasim/run.py index 9c06c6c3e..4bbf7952e 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -664,7 +664,7 @@ def single_run(sim, ind=0, reseed=True, noise=0.0, noisepar=None, verbose=None, return new_sim -def multi_run(sim, n_runs=4, reseed=True, noise=0.0, noisepar=None, iterpars=None, verbose=None, combine=False, keep_people=None, run_args=None, sim_args=None, **kwargs): +def multi_run(sim, n_runs=4, reseed=True, noise=0.0, noisepar=None, iterpars=None, verbose=None, combine=False, keep_people=None, run_args=None, sim_args=None, par_args=None, **kwargs): ''' For running multiple runs in parallel. If the first argument is a list of sims, exactly these will be run and most other arguments will be ignored. @@ -681,6 +681,7 @@ def multi_run(sim, n_runs=4, reseed=True, noise=0.0, noisepar=None, iterpars=Non keep_people (bool): whether or not to keep the people in each sim run_args (dict): arguments passed to sim.run() sim_args (dict): extra parameters to pass to the sim + par_args (dict): arguments passed to sc.parallelize() kwargs (dict): also passed to the sim Returns: @@ -694,9 +695,9 @@ def multi_run(sim, n_runs=4, reseed=True, noise=0.0, noisepar=None, iterpars=Non sims = cv.multi_run(sim, n_runs=6, noise=0.2) ''' - # Create the sims - if sim_args is None: - sim_args = {} + # Handle inputs + sim_args = sc.mergedicts({}, sim_args) # Handle blank + par_args = sc.mergedicts({}, par_args) # Handle blank # Handle iterpars if iterpars is None: @@ -715,11 +716,11 @@ def multi_run(sim, n_runs=4, reseed=True, noise=0.0, noisepar=None, iterpars=Non iterkwargs = {'ind':np.arange(n_runs)} iterkwargs.update(iterpars) kwargs = dict(sim=sim, reseed=reseed, noise=noise, noisepar=noisepar, verbose=verbose, keep_people=keep_people, sim_args=sim_args, run_args=run_args) - sims = sc.parallelize(single_run, iterkwargs=iterkwargs, kwargs=kwargs) + sims = sc.parallelize(single_run, iterkwargs=iterkwargs, kwargs=kwargs, **par_args) elif isinstance(sim, list): # List of sims iterkwargs = {'sim':sim} - kwargs = {'verbose':verbose, 'keep_people':keep_people, 'sim_args':sim_args, 'run_args':run_args} - sims = sc.parallelize(single_run, iterkwargs=iterkwargs, kwargs=kwargs) + kwargs = dict(verbose=verbose, keep_people=keep_people, sim_args=sim_args, run_args=run_args) + sims = sc.parallelize(single_run, iterkwargs=iterkwargs, kwargs=kwargs, **par_args) else: errormsg = f'Must be Sim object or list, not {type(sim)}' raise TypeError(errormsg) diff --git a/covasim/sim.py b/covasim/sim.py index 44e2133a6..251052aaa 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -291,7 +291,8 @@ def init_interventions(self): ''' Initialize the interventions ''' # Initialize interventions for intervention in self['interventions']: - intervention.initialize(self) + if not intervention.initialized: + intervention.initialize(self) return @@ -420,6 +421,7 @@ def run(self, do_plot=False, until=None, verbose=None, **kwargs): self.initialize() else: self.validate_pars() # We always want to validate the parameters before running + self.init_interventions() # And interventions if verbose is None: verbose = self['verbose'] From 2de0cb1d513871569f2b3e6e756f1f8c05da8a10 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 23:36:50 -0700 Subject: [PATCH 054/194] add par_args to multi_run() --- covasim/run.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index 4bbf7952e..25e66266f 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -616,10 +616,11 @@ def single_run(sim, ind=0, reseed=True, noise=0.0, noisepar=None, verbose=None, new_sim = sc.dcp(sim) # Copy the sim to avoid overwriting it # Set sim and run arguments - if verbose is None: - verbose = new_sim['verbose'] sim_args = sc.mergedicts(sim_args, kwargs) run_args = sc.mergedicts({'verbose':verbose}, run_args) + if verbose is None: + verbose = new_sim['verbose'] + if not new_sim.label: new_sim.label = f'Sim {ind:d}' @@ -696,8 +697,8 @@ def multi_run(sim, n_runs=4, reseed=True, noise=0.0, noisepar=None, iterpars=Non ''' # Handle inputs - sim_args = sc.mergedicts({}, sim_args) # Handle blank - par_args = sc.mergedicts({}, par_args) # Handle blank + sim_args = sc.mergedicts(sim_args, kwargs) # Handle blank + par_args = sc.mergedicts(par_args) # Handle blank # Handle iterpars if iterpars is None: From bd00ab038185d4251d59d3bd29a63605e94aaa6c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 23:38:11 -0700 Subject: [PATCH 055/194] update changelog --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a985e8757..141e20651 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,7 +12,8 @@ Version 0.30.0 (2020-05-02) - Added ``low`` and ``high`` attributes to the ``Result`` object. - Refactored plotting to increase consistency between ``sim.plot()``, ``sim.plot_result()``, ``scens.plot()``, and ``multisim.plot()``. - Doubling time calculation defaults have been updated to use a window of 3 days and a maximum of 30 days. -- Added a ``until`` keyword to ``sim.run()``, to make it easier to run a partially completed sim and then resume. See ``tests/devtests/test_run_until.py``. +- Added an ``until`` argument to ``sim.run()``, to make it easier to run a partially completed sim and then resume. See ``tests/devtests/test_run_until.py``. +- Added a ``par_args`` arugument to ``multi_run()``, allowing arguments (e.g. ``ncpus``) to be passed to ``sc.parallelize()``. - Fixed a bug whereby ``cv.clip_edges()`` with no end day specified resulted in large sim files when saved. From 47915ccff118174687e8696754a60ce1338a793f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 23:42:12 -0700 Subject: [PATCH 056/194] indenting --- covasim/interventions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index db7eb9930..ad88f4da6 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -346,9 +346,9 @@ def __init__(self, start_day, change=None, end_day=None, verbose=False, do_plot= def initialize(self, sim): ''' Fix the dates ''' - 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.start_day = sim.day(self.start_day) + self.end_day = sim.day(self.end_day) + self.days = [self.start_day, self.end_day] self.initialized = True return From 65b1945e5f2c154936c22f03a91cfd024ac2036f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 23:44:53 -0700 Subject: [PATCH 057/194] update version and changelog --- CHANGELOG.rst | 6 +++++- covasim/version.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 141e20651..f5c157717 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,11 @@ All notable changes to the codebase are documented in this file. Note: in many c changes from multiple patch versions are grouped together, so numbering will not be strictly consecutive. +Version 0.30.1 (2020-05-02) +--------------------------- +- Added a ``par_args`` arugument to ``multi_run()``, allowing arguments (e.g. ``ncpus``) to be passed to ``sc.parallelize()``. +- Fixed bug whereby intervention were not getting initialized if they were added to a sim after it was initialized. + Version 0.30.0 (2020-05-02) --------------------------- @@ -13,7 +18,6 @@ Version 0.30.0 (2020-05-02) - Refactored plotting to increase consistency between ``sim.plot()``, ``sim.plot_result()``, ``scens.plot()``, and ``multisim.plot()``. - Doubling time calculation defaults have been updated to use a window of 3 days and a maximum of 30 days. - Added an ``until`` argument to ``sim.run()``, to make it easier to run a partially completed sim and then resume. See ``tests/devtests/test_run_until.py``. -- Added a ``par_args`` arugument to ``multi_run()``, allowing arguments (e.g. ``ncpus``) to be passed to ``sc.parallelize()``. - Fixed a bug whereby ``cv.clip_edges()`` with no end day specified resulted in large sim files when saved. diff --git a/covasim/version.py b/covasim/version.py index 97c5876c3..b5516b77a 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -2,6 +2,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '0.30.0' +__version__ = '0.30.1' __versiondate__ = '2020-05-02' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2020 by IDM' From 889e94a62ae6ba22ee366ba70fe6c10c36ed9c4b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 2 May 2020 23:55:05 -0700 Subject: [PATCH 058/194] added end day --- CHANGELOG.rst | 1 + covasim/README.rst | 3 ++- covasim/parameters.py | 3 ++- covasim/sim.py | 13 +++++++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f5c157717..b9a5176d1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,7 @@ strictly consecutive. Version 0.30.1 (2020-05-02) --------------------------- +- Added ``end_day`` as a parameter, allowing an end date to be specified instead of a number of days. - Added a ``par_args`` arugument to ``multi_run()``, allowing arguments (e.g. ``ncpus``) to be passed to ``sc.parallelize()``. - Fixed bug whereby intervention were not getting initialized if they were added to a sim after it was initialized. diff --git a/covasim/README.rst b/covasim/README.rst index 9eca8aaf7..cbe2755f4 100644 --- a/covasim/README.rst +++ b/covasim/README.rst @@ -14,7 +14,8 @@ Population parameters Simulation parameters --------------------- * ``start_day`` = Start day of the simulation -* ``n_days`` = Number of days of run, if end_day isn't used +* ``end_day`` = End day of the simulation, if ``n_days`` isn't used +* ``n_days`` = Number of days of run * ``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) diff --git a/covasim/parameters.py b/covasim/parameters.py index c4ee81b7a..8aa09731b 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -30,7 +30,8 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): # Simulation parameters pars['start_day'] = '2020-03-01' # Start day of the simulation - pars['n_days'] = 60 # Number of days of run, if end_day isn't used + pars['end_day'] = None # End day of the simulation, if n_days isn't specified + pars['n_days'] = 60 # Number of days of run pars['rand_seed'] = 1 # Random seed, if None, don't reset pars['verbose'] = 1 # Whether or not to display information during the run -- options are 0 (silent), 1 (default), 2 (everything) diff --git a/covasim/sim.py b/covasim/sim.py index 251052aaa..c80033930 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -152,6 +152,19 @@ def validate_pars(self): start_day = '2020-03-01' self['start_day'] = cvm.date(start_day) + # Handle end day and n_days + end_day = self['end_day'] + n_days = self['n_days'] + if n_days is not None: + self['end_day'] = self.date(n_days) # Convert from the number of days to the end day + else: + if not end_day: + errormsg = f'If n_days is not specified, you must specify a valid end day, not "{end_day}"' + raise ValueError(errormsg) + else: + self['end_day'] = cvm.date(end_day) + self['n_days'] = cvm.daydiff(self['end_day'], self['start_day']) + # Handle contacts contacts = self['contacts'] if sc.isnumber(contacts): # It's a scalar instead of a dict, assume it's all contacts From 5aac1c0eae50f3ba1638e8523a9310292f3f70db Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 00:10:24 -0700 Subject: [PATCH 059/194] updated run progress --- CHANGELOG.rst | 1 + covasim/sim.py | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b9a5176d1..d04c8b3cd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,7 @@ strictly consecutive. Version 0.30.1 (2020-05-02) --------------------------- - Added ``end_day`` as a parameter, allowing an end date to be specified instead of a number of days. +- ``Sim.run()`` now displays the date being simulated. - Added a ``par_args`` arugument to ``multi_run()``, allowing arguments (e.g. ``ncpus``) to be passed to ``sc.parallelize()``. - Fixed bug whereby intervention were not getting initialized if they were added to a sim after it was initialized. diff --git a/covasim/sim.py b/covasim/sim.py index c80033930..7809ee10b 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -155,15 +155,20 @@ def validate_pars(self): # Handle end day and n_days end_day = self['end_day'] n_days = self['n_days'] - if n_days is not None: - self['end_day'] = self.date(n_days) # Convert from the number of days to the end day - else: - if not end_day: - errormsg = f'If n_days is not specified, you must specify a valid end day, not "{end_day}"' + if end_day: + self['end_day'] = cvm.date(end_day) + n_days = cvm.daydiff(self['start_day'], self['end_day']) + if n_days <= 0: + errormsg = f"Number of days must be >0, but you supplied start={str(self['start_day'])} and end={str(self['end_day'])}, which gives n_days={n_days}" raise ValueError(errormsg) else: - self['end_day'] = cvm.date(end_day) - self['n_days'] = cvm.daydiff(self['end_day'], self['start_day']) + self['n_days'] = n_days + else: + if n_days: + self['end_day'] = self.date(n_days) # Convert from the number of days to the end day + else: + errormsg = f'You must supply one of n_days and end_day, not "{n_days}" and "{end_day}"' + raise ValueError(errormsg) # Handle contacts contacts = self['contacts'] @@ -445,11 +450,11 @@ def run(self, do_plot=False, until=None, verbose=None, **kwargs): if verbose >= 1: elapsed = sc.toc(output=True) simlabel = f'"{self.label}": ' if self.label else '' - string = f' Running {simlabel}day {t:2.0f}/{self.pars["n_days"]} ({elapsed:0.2f} s) ' + string = f' Running {simlabel}{self.datevec[t]} ({t:2.0f}/{self.pars["n_days"]}) ({elapsed:0.2f} s) ' if verbose >= 2: sc.heading(string) elif verbose == 1: - sc.progressbar(t+1, self.npts, label=string, newline=True) + sc.progressbar(t+1, self.npts, label=string, length=20, newline=True) # Do the heavy lifting -- actually run the model! self.step() From e92d1e68d8746f2d80f982fbb62433694cc6e8e8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 00:17:51 -0700 Subject: [PATCH 060/194] added test --- covasim/sim.py | 11 ++++++++--- tests/devtests/dev_test_date_setting.py | 13 +++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100755 tests/devtests/dev_test_date_setting.py diff --git a/covasim/sim.py b/covasim/sim.py index 7809ee10b..b5a7efa0f 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -143,8 +143,12 @@ def validate_pars(self): ''' Some parameters can take multiple types; this makes them consistent ''' # Handle types - for key in ['pop_size', 'pop_infected', 'pop_size', 'n_days']: - self[key] = int(self[key]) + for key in ['pop_size', 'pop_infected', 'pop_size']: + try: + self[key] = int(self[key]) + except Exception as E: + errormsg = f'Could not convert {key}={self[key]} of {type(self[key])} to integer' + raise TypeError(errormsg) from E # Handle start day start_day = self['start_day'] # Shorten @@ -162,9 +166,10 @@ def validate_pars(self): errormsg = f"Number of days must be >0, but you supplied start={str(self['start_day'])} and end={str(self['end_day'])}, which gives n_days={n_days}" raise ValueError(errormsg) else: - self['n_days'] = n_days + self['n_days'] = int(n_days) else: if n_days: + self['n_days'] = int(n_days) self['end_day'] = self.date(n_days) # Convert from the number of days to the end day else: errormsg = f'You must supply one of n_days and end_day, not "{n_days}" and "{end_day}"' diff --git a/tests/devtests/dev_test_date_setting.py b/tests/devtests/dev_test_date_setting.py new file mode 100755 index 000000000..59b247bed --- /dev/null +++ b/tests/devtests/dev_test_date_setting.py @@ -0,0 +1,13 @@ +import covasim as cv +import pytest + +with pytest.raises(ValueError): + s1 = cv.Sim(start_day='2020-06-01', end_day='2020-04-01') + s1.run() + +with pytest.raises(ValueError): + s2 = cv.Sim(end_day=None, n_days=None) + s2.run() + +sim = cv.Sim(pop_size=100e3, start_day='2020-01-01', end_day='2020-04-01') +sim.run() \ No newline at end of file From 65fbdf7446065c5174bbea6cdb66905ec39fcf8e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 00:19:16 -0700 Subject: [PATCH 061/194] flip description --- covasim/README.rst | 4 ++-- covasim/parameters.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/covasim/README.rst b/covasim/README.rst index cbe2755f4..2d9e7b4ea 100644 --- a/covasim/README.rst +++ b/covasim/README.rst @@ -14,8 +14,8 @@ Population parameters Simulation parameters --------------------- * ``start_day`` = Start day of the simulation -* ``end_day`` = End day of the simulation, if ``n_days`` isn't used -* ``n_days`` = Number of days of run +* ``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) diff --git a/covasim/parameters.py b/covasim/parameters.py index 8aa09731b..68d843eb1 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -30,8 +30,8 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): # Simulation parameters pars['start_day'] = '2020-03-01' # Start day of the simulation - pars['end_day'] = None # End day of the simulation, if n_days isn't specified - pars['n_days'] = 60 # Number of days of run + 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'] = 1 # Whether or not to display information during the run -- options are 0 (silent), 1 (default), 2 (everything) From 511e4d6d0c833f2ff7937fba6940c969f2795b2c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 00:20:32 -0700 Subject: [PATCH 062/194] comment on test --- tests/devtests/dev_test_date_setting.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/devtests/dev_test_date_setting.py b/tests/devtests/dev_test_date_setting.py index 59b247bed..95181861a 100755 --- a/tests/devtests/dev_test_date_setting.py +++ b/tests/devtests/dev_test_date_setting.py @@ -1,13 +1,20 @@ +''' +Test that different ways of specifying the date work as expected. +''' + import covasim as cv import pytest +# Can't have a start day after the end day with pytest.raises(ValueError): s1 = cv.Sim(start_day='2020-06-01', end_day='2020-04-01') s1.run() +# Must have one of end_day and n_days with pytest.raises(ValueError): s2 = cv.Sim(end_day=None, n_days=None) s2.run() +# But this should work sim = cv.Sim(pop_size=100e3, start_day='2020-01-01', end_day='2020-04-01') sim.run() \ No newline at end of file From dacf370fffee7a1c16ff363405f142337a613eb5 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 01:07:06 -0700 Subject: [PATCH 063/194] improved saving --- covasim/run.py | 73 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index 25e66266f..7805b1b25 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -204,6 +204,45 @@ def reduce(self, quantiles=None, output=False): return + def compare(self, t=-1, sim_inds=None, output=False): + ''' + Create a dataframe compare sims at a single point in time. + + Args: + t (int or str): the day (or date) to do the comparison; default, the end + sim_inds (list): list of integers of which sims to include (default: all) + output (bool): whether or not to return the comparison as a dataframe + + Returns: + df (dataframe): a dataframe comparison + ''' + + # Handle the indices + if sim_inds is None: + sim_inds = list(range(len(self.sims))) + + # Move the results to a dictionary + resdict = defaultdict(dict) + for i,s in enumerate(sim_inds): + sim = self.sims[s] + label = sim.label + if not label: + label = f'Sim {i}' + for reskey in sim.result_keys(): + val = sim.results[reskey].values[t] + if reskey not in ['r_eff', 'doubling_time']: + val = int(val) + resdict[label][reskey] = val + + # Convert to a dataframe + df = pd.DataFrame.from_dict(resdict).astype(object) # astype is necessary to prevent type coersion + if output: + return df + else: + print(df) + return None + + def plot(self, *args, **kwargs): ''' Convenience mthod for plotting ''' if self.which in ['combined', 'reduced']: @@ -223,29 +262,45 @@ def plot(self, *args, **kwargs): return fig - def save(self, filename=None, **kwargs): + def save(self, filename=None, keep_people=False, **kwargs): ''' Save to disk as a gzipped pickle. Load with cv.load(filename). Args: - filename (str or None): the name or path of the file to save to; if None, uses stored - kwargs: passed to makefilepath() + filename (str or None): 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) + keywords: passed to makefilepath() Returns: - filename (str): the validated absolute path to the saved file + scenfile (str): the validated absolute path to the saved file **Example**:: - msim.save() # Saves to a .sim file with the date and time of creation by default + msim.save() # Saves to an .msim file ''' if filename is None: - filename = self.simfile - filename = sc.makefilepath(filename=filename, **kwargs) + filename = 'covasim.msim' + scenfile = sc.makefilepath(filename=filename, **kwargs) self.filename = filename # Store the actual saved filename - sc.saveobj(filename=filename, obj=self) - return filename + # Store sims seperately + sims = self.sims + self.sims = None # Remove for now + + obj = sc.dcp(self) # This should be quick once we've removed the sims + if keep_people: + obj.sims = sims # Just restore the object in full + print('Note: saving people, which may produce a large file!') + else: + obj.sims = [] + for sim in sims: + obj.sims.append(sim.shrink(in_place=False)) + + sc.saveobj(filename=scenfile, obj=obj) # Actually save + + self.sims = sims # Restore + return scenfile class Scenarios(cvb.ParsObj): From 76a8deb66fba214b756688aff661d6941d98dde3 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 01:08:50 -0700 Subject: [PATCH 064/194] convert to_plot to odict --- covasim/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index f827af756..41e17a25f 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -43,7 +43,7 @@ def handle_to_plot(which, to_plot, n_cols): else: errormsg = f'"which" must be "sim" or "scens", not "{which}"' raise NotImplementedError(errormsg) - to_plot = sc.dcp(to_plot) # In case it's supplied as a dict + to_plot = sc.odict(sc.dcp(to_plot)) # In case it's supplied as a dict n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have From 25b902304ba2dae616bf2ef41f5fe45a13229086 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 01:09:20 -0700 Subject: [PATCH 065/194] added test for multisim compare --- tests/devtests/multisim_compare.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100755 tests/devtests/multisim_compare.py diff --git a/tests/devtests/multisim_compare.py b/tests/devtests/multisim_compare.py new file mode 100755 index 000000000..f2ee1f43b --- /dev/null +++ b/tests/devtests/multisim_compare.py @@ -0,0 +1,14 @@ +''' +Demonstrate the compare method of multisims +''' + +import covasim as cv + +s0 = cv.Sim(label='Normal beta') +s1 = cv.Sim(label='Low beta', beta=0.012) +s2 = cv.Sim(label='High beta', beta=0.018) + +msim = cv.MultiSim(sims=[s0, s1, s2]) +msim.run() +df = msim.compare() + From 8ec53d33bb4c171f00c1842fd9b30e0c3be4ff81 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 01:12:41 -0700 Subject: [PATCH 066/194] shrink base sim too --- covasim/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/run.py b/covasim/run.py index 7805b1b25..cbb4072ca 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -288,11 +288,11 @@ def save(self, filename=None, keep_people=False, **kwargs): self.sims = None # Remove for now obj = sc.dcp(self) # This should be quick once we've removed the sims - if keep_people: obj.sims = sims # Just restore the object in full print('Note: saving people, which may produce a large file!') else: + obj.base_sim.shrink(in_place=True) obj.sims = [] for sim in sims: obj.sims.append(sim.shrink(in_place=False)) From 1a1862fa74629355cc3927f856555768cbeea03b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 01:17:47 -0700 Subject: [PATCH 067/194] updated changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d04c8b3cd..080e2d260 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,7 @@ Version 0.30.1 (2020-05-02) - Added ``end_day`` as a parameter, allowing an end date to be specified instead of a number of days. - ``Sim.run()`` now displays the date being simulated. - Added a ``par_args`` arugument to ``multi_run()``, allowing arguments (e.g. ``ncpus``) to be passed to ``sc.parallelize()``. +- Added a ``compare()`` method to multisims and stopped people from being saved by default. - Fixed bug whereby intervention were not getting initialized if they were added to a sim after it was initialized. From 13b4c633213314c4cf99048fc80bb1b1d446e47d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 01:36:54 -0700 Subject: [PATCH 068/194] implemented r_eff calculations --- covasim/sim.py | 109 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 34 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index b5a7efa0f..198e0c11e 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -526,7 +526,7 @@ def compute_doubling(self, window=3, max_doubling_time=30): max_doubling_time (float): doubling time could be infinite, so this places a bound on it Returns: - None (modifies results in place) + doubling_time (Result): the doubling time result object ''' cum_infections = self.results['cum_infections'].values self.results['doubling_time'][:window] = np.nan @@ -538,16 +538,20 @@ def compute_doubling(self, window=3, max_doubling_time=30): doubling_time = window*np.log(2)/np.log(r) doubling_time = min(doubling_time, max_doubling_time) # Otherwise, it's unbounded self.results['doubling_time'][t] = doubling_time - return + return self.results['doubling_time'] - def compute_r_eff(self, window=7): + def compute_r_eff(self, method='daily', smoothing=3, window=7): ''' Effective reproductive number based on number of people each person infected. Args: - window (int): the size of the window used (larger values are more accurate but less precise) + method (str): 'instant' uses daily infections, 'infected' counts from the date infected, '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 'infected' and 'outcome' calculations (larger values are more accurate but less precise) + Returns: + r_eff (Result): the r_eff result object ''' # Initialize arrays to hold sources and targets infected each day @@ -555,44 +559,73 @@ def compute_r_eff(self, window=7): targets = np.zeros(self.npts) window = int(window) - for t in self.tvec: - - # Sources are easy -- count up the arrays - recov_inds = cvu.true(t == self.people.date_recovered) # Find people who recovered on this timestep - dead_inds = cvu.true(t == self.people.date_dead) # Find people who died on this timestep - outcome_inds = np.concatenate((recov_inds, dead_inds)) - sources[t] = len(outcome_inds) - - # Targets are hard -- loop over the transmission tree - for ind in outcome_inds: - targets[t] += len(self.people.transtree.targets[ind]) - - # Populate the array -- to avoid divide-by-zero, skip indices that are 0 - inds = sc.findinds(sources>0) - r_eff = np.zeros(self.npts)*np.nan - r_eff[inds] = targets[inds]/sources[inds] - - # use stored weights calculate the moving average over the window of timesteps, n - num = np.nancumsum(r_eff * sources) - num[window:] = num[window:] - num[:-window] - den = np.cumsum(sources) - den[window:] = den[window:] - den[:-window] - - # avoid dividing by zero - values = np.zeros(num.shape)*np.nan - ind = den > 0 - values[ind] = num[ind]/den[ind] + # Default method -- calculate the daily infections + if method == 'daily': + + # Find the dates that everyone became infectious and recovered, and hence calculate infectious duration + inds = self.people.defined('date_recovered') + date_rec = self.people.date_recovered[inds] + date_inf = self.people.date_infectious[inds] + diff = date_rec - date_inf + mean_inf = diff.mean() + + # 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 + r_eff = mean_inf*self.results['new_infections'].values/(self.results['n_infectious'].values+1e-6) + values = sc.smooth(r_eff, smoothing) + + # Alternate (traditional) method -- count from the date of infection or outcome + elif method in ['infection', 'outcome']: + + for t in self.tvec: + + # Sources are easy -- count up the arrays + if method == 'initial': + inds = cvu.true(t == self.people.date_infected) # Find people who were infected on this timestep + elif method == 'outcome': + recov_inds = cvu.true(t == self.people.date_recovered) # Find people who recovered on this timestep + dead_inds = cvu.true(t == self.people.date_dead) # Find people who died on this timestep + inds = np.concatenate((recov_inds, dead_inds)) + sources[t] = len(inds) + + # Targets are hard -- loop over the transmission tree + for ind in inds: + targets[t] += len(self.people.transtree.targets[ind]) + + # Populate the array -- to avoid divide-by-zero, skip indices that are 0 + inds = sc.findinds(sources>0) + r_eff = np.zeros(self.npts)*np.nan + r_eff[inds] = targets[inds]/sources[inds] + + # Use stored weights calculate the moving average over the window of timesteps, n + num = np.nancumsum(r_eff * sources) + num[window:] = num[window:] - num[:-window] + den = np.cumsum(sources) + den[window:] = den[window:] - den[:-window] + + # Avoid dividing by zero + values = np.zeros(num.shape)*np.nan + ind = den > 0 + values[ind] = num[ind]/den[ind] + + # Method not recognized + else: + errormsg = f'Method must be "daily", "infected", or "outcome", not "{method}"' + raise ValueError(errormsg) + # Set the values and return self.results['r_eff'].values[:] = values - return + return self.results['r_eff'] def compute_gen_time(self): ''' - Calculate the generation time (or serial interval) there are two + Calculate the generation time (or serial interval). There are two ways to do this calculation. The 'true' interval (exposure time to exposure time) or 'clinical' (symptom onset to symptom onset). + + Returns: + gen_time (dict): the generation time results ''' intervals1 = np.zeros(len(self.people)) @@ -617,13 +650,21 @@ def compute_gen_time(self): 'true_std': np.std(intervals1[:pos1]), 'clinical': np.mean(intervals2[:pos2]), 'clinical_std': np.std(intervals2[:pos2])} - return + return self.results['gen_time'] def likelihood(self, weights=None, verbose=None, eps=1e-16): ''' Compute the log-likelihood of the current simulation based on the number of new diagnoses. + + Args: + weights (dict): the relative wieght to place on each result + verbose (bool): detail to print + eps (float): to avoid divide-by-zero errors + + Returns: + loglike (float): the log-likelihood of the model given the data ''' if verbose is None: From a82fe72229465c3780cdd373990ef4b1ed5c579e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 02:03:50 -0700 Subject: [PATCH 069/194] implemented new method --- covasim/sim.py | 8 +++---- tests/devtests/dev_test_r_eff.py | 40 ++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 198e0c11e..030d425af 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -541,7 +541,7 @@ def compute_doubling(self, window=3, max_doubling_time=30): return self.results['doubling_time'] - def compute_r_eff(self, method='daily', smoothing=3, window=7): + def compute_r_eff(self, method='daily', smoothing=2, window=7): ''' Effective reproductive number based on number of people each person infected. @@ -574,13 +574,13 @@ def compute_r_eff(self, method='daily', smoothing=3, window=7): values = sc.smooth(r_eff, smoothing) # Alternate (traditional) method -- count from the date of infection or outcome - elif method in ['infection', 'outcome']: + elif method in ['infected', 'outcome']: for t in self.tvec: # Sources are easy -- count up the arrays - if method == 'initial': - inds = cvu.true(t == self.people.date_infected) # Find people who were infected on this timestep + if method == 'infected': + inds = cvu.true(t == self.people.date_infectious) # Find people who became infectious on this timestep elif method == 'outcome': recov_inds = cvu.true(t == self.people.date_recovered) # Find people who recovered on this timestep dead_inds = cvu.true(t == self.people.date_dead) # Find people who died on this timestep diff --git a/tests/devtests/dev_test_r_eff.py b/tests/devtests/dev_test_r_eff.py index 3f6804fc6..7e30465e3 100644 --- a/tests/devtests/dev_test_r_eff.py +++ b/tests/devtests/dev_test_r_eff.py @@ -3,23 +3,49 @@ Check that r_eff agrees for first evaluation regardless of window size """ import covasim as cv -import matplotlib.pyplot as plt +import pylab as pl import numpy as np +import sciris as sc + + +#%% Legacy tests sim = cv.Sim() sim.run(verbose=False); -fig, axes = plt.subplots(figsize=(5,4)) +fig, axes = pl.subplots(figsize=(5,4)) window= [7,1] reff_t0 = [] for iw, w in enumerate(window): - sim.compute_r_eff(window=w) - axes.plot(sim.tvec, sim.results['r_eff'].values, '-o', label=w) + r_eff = sim.compute_r_eff(method='infected', window=w) + axes.plot(sim.tvec, r_eff.values, '-o', label=w) reff_t0.append(sim.results['r_eff'].values[np.isfinite(sim.results['r_eff'].values)][0]) axes.legend() axes.set_xlabel('time (days)') axes.set_ylabel('r_eff'); -plt.tight_layout() -plt.show() +pl.tight_layout() + +assert len(np.unique(reff_t0)) == 1 + + +#%% New tests + +sim = cv.Sim(pop_size=100e3, pop_infected=100, n_days=90) +iday = 50 +cb = cv.change_beta(days=iday, changes=0) +sim.update_pars(interventions=cb) +sim.run(verbose=False) + +plot_args = dict(lw=3, alpha=0.7) +fig = sim.plot_result('r_eff') +r_eff_d = sc.dcp(sim.results['r_eff'].values) +r_eff_i = sc.dcp(sim.compute_r_eff(method='infected').values) +r_eff_o = sc.dcp(sim.compute_r_eff(method='outcome').values) +pl.plot(r_eff_i, label='Method from infected', c=[1.0,0.3,0], **plot_args) +pl.plot(r_eff_o, label='Method from outcome', c=[0,0.5,0.0], **plot_args) +pl.legend() + +print(np.nansum(r_eff_d)) +print(np.nansum(r_eff_i)) +print(np.nansum(r_eff_o)) -assert len(np.unique(reff_t0)) == 1 \ No newline at end of file From 722d7d7e966774e233badf472386d7620e86ebea Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 02:07:17 -0700 Subject: [PATCH 070/194] updated baseline and changelog --- CHANGELOG.rst | 7 +++++++ covasim/sim.py | 8 ++++---- covasim/version.py | 2 +- tests/baseline.json | 2 +- tests/benchmark.json | 4 ++-- tests/devtests/dev_test_r_eff.py | 6 +++--- 6 files changed, 18 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 080e2d260..0dc1946ec 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,13 @@ All notable changes to the codebase are documented in this file. Note: in many c changes from multiple patch versions are grouped together, so numbering will not be strictly consecutive. + +Version 0.30.2 (2020-05-02) +--------------------------- +- Updated ``r_eff`` to use a new method based on daily new infections. The previous version, where infections were counted from when someone recovered or died, is available as ``sim.compute_r_eff(method='outcome')``, while the traditional method, where infections are counted from the day someone becomes infectious, is available via ``sim.compute_r_eff(method='infectious')``. + + + Version 0.30.1 (2020-05-02) --------------------------- - Added ``end_day`` as a parameter, allowing an end date to be specified instead of a number of days. diff --git a/covasim/sim.py b/covasim/sim.py index 030d425af..e4339a1f1 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -546,9 +546,9 @@ def compute_r_eff(self, method='daily', smoothing=2, window=7): Effective reproductive number based on number of people each person infected. Args: - method (str): 'instant' uses daily infections, 'infected' counts from the date infected, 'outcome' counts from the date recovered/dead + method (str): 'instant' 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 'infected' and 'outcome' calculations (larger values are more accurate but less precise) + window (int): the size of the window used for 'infectious' and 'outcome' calculations (larger values are more accurate but less precise) Returns: r_eff (Result): the r_eff result object @@ -574,12 +574,12 @@ def compute_r_eff(self, method='daily', smoothing=2, window=7): values = sc.smooth(r_eff, smoothing) # Alternate (traditional) method -- count from the date of infection or outcome - elif method in ['infected', 'outcome']: + elif method in ['infectious', 'outcome']: for t in self.tvec: # Sources are easy -- count up the arrays - if method == 'infected': + if method == 'infectious': inds = cvu.true(t == self.people.date_infectious) # Find people who became infectious on this timestep elif method == 'outcome': recov_inds = cvu.true(t == self.people.date_recovered) # Find people who recovered on this timestep diff --git a/covasim/version.py b/covasim/version.py index b5516b77a..934ec946c 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -2,6 +2,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '0.30.1' +__version__ = '0.30.2' __versiondate__ = '2020-05-02' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2020 by IDM' diff --git a/tests/baseline.json b/tests/baseline.json index a5d1aa889..0814d8e55 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -29,7 +29,7 @@ "cum_deaths": 12.0, "new_quarantined": 0.0, "cum_quarantined": 0.0, - "r_eff": 1.7936786654960493, + "r_eff": 1.022952622052391, "doubling_time": 9.29296551635779 } } \ No newline at end of file diff --git a/tests/benchmark.json b/tests/benchmark.json index 681d7e45b..83b41bb99 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,7 +1,7 @@ { "time": { - "initialize": 0.228, - "run": 0.174 + "initialize": 0.213, + "run": 0.161 }, "parameters": { "pop_size": 20000, diff --git a/tests/devtests/dev_test_r_eff.py b/tests/devtests/dev_test_r_eff.py index 7e30465e3..68fe0bf7e 100644 --- a/tests/devtests/dev_test_r_eff.py +++ b/tests/devtests/dev_test_r_eff.py @@ -17,7 +17,7 @@ window= [7,1] reff_t0 = [] for iw, w in enumerate(window): - r_eff = sim.compute_r_eff(method='infected', window=w) + r_eff = sim.compute_r_eff(method='infectious', window=w) axes.plot(sim.tvec, r_eff.values, '-o', label=w) reff_t0.append(sim.results['r_eff'].values[np.isfinite(sim.results['r_eff'].values)][0]) axes.legend() @@ -39,9 +39,9 @@ plot_args = dict(lw=3, alpha=0.7) fig = sim.plot_result('r_eff') r_eff_d = sc.dcp(sim.results['r_eff'].values) -r_eff_i = sc.dcp(sim.compute_r_eff(method='infected').values) +r_eff_i = sc.dcp(sim.compute_r_eff(method='infectious').values) r_eff_o = sc.dcp(sim.compute_r_eff(method='outcome').values) -pl.plot(r_eff_i, label='Method from infected', c=[1.0,0.3,0], **plot_args) +pl.plot(r_eff_i, label='Method from infectious', c=[1.0,0.3,0], **plot_args) pl.plot(r_eff_o, label='Method from outcome', c=[0,0.5,0.0], **plot_args) pl.legend() From b088845dcb1f297d5ab2c6e53595247310b79eae Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 02:11:29 -0700 Subject: [PATCH 071/194] updated tests --- covasim/sim.py | 8 ++++---- tests/devtests/dev_test_r_eff.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index e4339a1f1..ea8c56ab2 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -526,7 +526,7 @@ def compute_doubling(self, window=3, max_doubling_time=30): max_doubling_time (float): doubling time could be infinite, so this places a bound on it Returns: - doubling_time (Result): the doubling time result object + doubling_time (array): the doubling time results array ''' cum_infections = self.results['cum_infections'].values self.results['doubling_time'][:window] = np.nan @@ -538,7 +538,7 @@ def compute_doubling(self, window=3, max_doubling_time=30): doubling_time = window*np.log(2)/np.log(r) doubling_time = min(doubling_time, max_doubling_time) # Otherwise, it's unbounded self.results['doubling_time'][t] = doubling_time - return self.results['doubling_time'] + return self.results['doubling_time'].values def compute_r_eff(self, method='daily', smoothing=2, window=7): @@ -551,7 +551,7 @@ def compute_r_eff(self, method='daily', smoothing=2, window=7): window (int): the size of the window used for 'infectious' and 'outcome' calculations (larger values are more accurate but less precise) Returns: - r_eff (Result): the r_eff result object + r_eff (array): the r_eff results array ''' # Initialize arrays to hold sources and targets infected each day @@ -615,7 +615,7 @@ def compute_r_eff(self, method='daily', smoothing=2, window=7): # Set the values and return self.results['r_eff'].values[:] = values - return self.results['r_eff'] + return self.results['r_eff'].values def compute_gen_time(self): diff --git a/tests/devtests/dev_test_r_eff.py b/tests/devtests/dev_test_r_eff.py index 68fe0bf7e..902a8baf8 100644 --- a/tests/devtests/dev_test_r_eff.py +++ b/tests/devtests/dev_test_r_eff.py @@ -18,8 +18,8 @@ reff_t0 = [] for iw, w in enumerate(window): r_eff = sim.compute_r_eff(method='infectious', window=w) - axes.plot(sim.tvec, r_eff.values, '-o', label=w) - reff_t0.append(sim.results['r_eff'].values[np.isfinite(sim.results['r_eff'].values)][0]) + axes.plot(sim.tvec, r_eff, '-o', label=w) + reff_t0.append(r_eff[np.isfinite(r_eff)][0]) axes.legend() axes.set_xlabel('time (days)') axes.set_ylabel('r_eff'); @@ -39,8 +39,8 @@ plot_args = dict(lw=3, alpha=0.7) fig = sim.plot_result('r_eff') r_eff_d = sc.dcp(sim.results['r_eff'].values) -r_eff_i = sc.dcp(sim.compute_r_eff(method='infectious').values) -r_eff_o = sc.dcp(sim.compute_r_eff(method='outcome').values) +r_eff_i = sc.dcp(sim.compute_r_eff(method='infectious')) +r_eff_o = sc.dcp(sim.compute_r_eff(method='outcome')) pl.plot(r_eff_i, label='Method from infectious', c=[1.0,0.3,0], **plot_args) pl.plot(r_eff_o, label='Method from outcome', c=[0,0.5,0.0], **plot_args) pl.legend() From d81ac76e45fc0bb73a8d91165c345765edda64f3 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 02:21:59 -0700 Subject: [PATCH 072/194] add check for length --- covasim/sim.py | 5 +++-- tests/unittests/test_simulation_parameter.py | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index ea8c56ab2..294222682 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -570,8 +570,9 @@ def compute_r_eff(self, method='daily', smoothing=2, window=7): mean_inf = diff.mean() # 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 - r_eff = mean_inf*self.results['new_infections'].values/(self.results['n_infectious'].values+1e-6) - values = sc.smooth(r_eff, smoothing) + values = mean_inf*self.results['new_infections'].values/(self.results['n_infectious'].values+1e-6) + if len(values) >= 3: # Can't smooth arrays shorter than this + values = sc.smooth(values, smoothing) # Alternate (traditional) method -- count from the date of infection or outcome elif method in ['infectious', 'outcome']: diff --git a/tests/unittests/test_simulation_parameter.py b/tests/unittests/test_simulation_parameter.py index 651a179ad..f5704f584 100644 --- a/tests/unittests/test_simulation_parameter.py +++ b/tests/unittests/test_simulation_parameter.py @@ -303,3 +303,7 @@ def remove_zeros(channel): print(f"Long sim length: {len(infections_channel_long)}") pass pass + + +if __name__ == '__main__': + unittest.main() From f7953293156ab98af6f0ffc2db967a9afedbb640 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 02:24:11 -0700 Subject: [PATCH 073/194] update spacing --- CHANGELOG.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0dc1946ec..6b64d8935 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,7 +11,6 @@ Version 0.30.2 (2020-05-02) - Updated ``r_eff`` to use a new method based on daily new infections. The previous version, where infections were counted from when someone recovered or died, is available as ``sim.compute_r_eff(method='outcome')``, while the traditional method, where infections are counted from the day someone becomes infectious, is available via ``sim.compute_r_eff(method='infectious')``. - Version 0.30.1 (2020-05-02) --------------------------- - Added ``end_day`` as a parameter, allowing an end date to be specified instead of a number of days. From 394971cf95aa1e7d3a942c14a9fdc94331d09ea8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 02:26:40 -0700 Subject: [PATCH 074/194] plotting fixes --- tests/devtests/dev_test_r_eff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/devtests/dev_test_r_eff.py b/tests/devtests/dev_test_r_eff.py index 902a8baf8..4e7ec1848 100644 --- a/tests/devtests/dev_test_r_eff.py +++ b/tests/devtests/dev_test_r_eff.py @@ -13,7 +13,7 @@ sim = cv.Sim() sim.run(verbose=False); -fig, axes = pl.subplots(figsize=(5,4)) +fig, axes = pl.subplots(figsize=(8,5), dpi=200) window= [7,1] reff_t0 = [] for iw, w in enumerate(window): @@ -41,7 +41,7 @@ r_eff_d = sc.dcp(sim.results['r_eff'].values) r_eff_i = sc.dcp(sim.compute_r_eff(method='infectious')) r_eff_o = sc.dcp(sim.compute_r_eff(method='outcome')) -pl.plot(r_eff_i, label='Method from infectious', c=[1.0,0.3,0], **plot_args) +pl.plot(r_eff_i, label='Method from infectious', c=[1.0,0.1,0], **plot_args) pl.plot(r_eff_o, label='Method from outcome', c=[0,0.5,0.0], **plot_args) pl.legend() From 8eaf1b551ee3cff774578fe43c1c9507772b3b6c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 02:37:13 -0700 Subject: [PATCH 075/194] include people who died in the time calculation --- covasim/sim.py | 11 +++++++---- tests/baseline.json | 2 +- tests/benchmark.json | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 294222682..99f9aa6ae 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -563,11 +563,14 @@ def compute_r_eff(self, method='daily', smoothing=2, window=7): if method == 'daily': # Find the dates that everyone became infectious and recovered, and hence calculate infectious duration - inds = self.people.defined('date_recovered') - date_rec = self.people.date_recovered[inds] + recov_inds = self.people.defined('date_recovered') + dead_inds = self.people.defined('date_dead') + date_recov = self.people.date_recovered[recov_inds] + date_dead = self.people.date_dead[dead_inds] + date_outcome = np.concatenate((date_recov, date_dead)) + inds = np.concatenate((recov_inds, dead_inds)) date_inf = self.people.date_infectious[inds] - diff = date_rec - date_inf - mean_inf = diff.mean() + mean_inf = date_outcome.mean() - date_inf.mean() # 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 values = mean_inf*self.results['new_infections'].values/(self.results['n_infectious'].values+1e-6) diff --git a/tests/baseline.json b/tests/baseline.json index 0814d8e55..318769f15 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -29,7 +29,7 @@ "cum_deaths": 12.0, "new_quarantined": 0.0, "cum_quarantined": 0.0, - "r_eff": 1.022952622052391, + "r_eff": 1.030526068157997, "doubling_time": 9.29296551635779 } } \ No newline at end of file diff --git a/tests/benchmark.json b/tests/benchmark.json index 83b41bb99..19c2d1a33 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,7 +1,7 @@ { "time": { - "initialize": 0.213, - "run": 0.161 + "initialize": 0.215, + "run": 0.164 }, "parameters": { "pop_size": 20000, From d45ba159e2c714bbbcc132669e7f3cbf7a9f6c95 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 03:01:16 -0700 Subject: [PATCH 076/194] something is wrong with make_susceptible --- covasim/sim.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/covasim/sim.py b/covasim/sim.py index 99f9aa6ae..55f51d10b 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -418,8 +418,9 @@ def rescale(self): max_ratio = pop_scale/current_scale # We don't want to exceed this scaling_ratio = min(self['rescale_factor'], max_ratio) self.rescale_vec[t+1:] *= scaling_ratio # Update the rescaling factor from here on - n = int(n_people*(1.0-1.0/scaling_ratio)) # For example, rescaling by 2 gives n = 0.5*n_people + n = int(n_people) #*(1.0-1.0/scaling_ratio)) # For example, rescaling by 2 gives n = 0.5*n_people new_sus_inds = cvu.choose(max_n=n_people, n=n) # Choose who to make susceptible again + print(len(new_sus_inds)) self.people.make_susceptible(new_sus_inds) return From 9e5c90af0fc445ed5349866e9db5346379c58e58 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 03:07:54 -0700 Subject: [PATCH 077/194] fixed major problem with rescaling --- covasim/people.py | 3 +++ covasim/sim.py | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index 980fdc906..96423a40b 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -258,6 +258,9 @@ def make_susceptible(self, inds): for key in self.meta.dates + self.meta.durs: self[key][inds] = np.nan + self.rel_sus[inds] = 1.0 # By default: is susceptible + self.rel_trans[inds] = 0.0 # By default: cannot transmit + return diff --git a/covasim/sim.py b/covasim/sim.py index 55f51d10b..4995f861b 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -417,10 +417,9 @@ def rescale(self): if n_not_sus / n_people > self['rescale_threshold']: # Check if we've reached point when we want to rescale max_ratio = pop_scale/current_scale # We don't want to exceed this scaling_ratio = min(self['rescale_factor'], max_ratio) - self.rescale_vec[t+1:] *= scaling_ratio # Update the rescaling factor from here on - n = int(n_people) #*(1.0-1.0/scaling_ratio)) # For example, rescaling by 2 gives n = 0.5*n_people + self.rescale_vec[t:] *= scaling_ratio # Update the rescaling factor from here on + n = int(n_people*(1.0-1.0/scaling_ratio)) # For example, rescaling by 2 gives n = 0.5*n_people new_sus_inds = cvu.choose(max_n=n_people, n=n) # Choose who to make susceptible again - print(len(new_sus_inds)) self.people.make_susceptible(new_sus_inds) return From ff1577a9c26652cae0a038d022e529d5353a4c10 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 11:45:14 -0700 Subject: [PATCH 078/194] added rescaling test --- covasim/run.py | 4 ++- tests/devtests/dev_test_rescaling.py | 50 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100755 tests/devtests/dev_test_rescaling.py diff --git a/covasim/run.py b/covasim/run.py index cbb4072ca..dbf723c0b 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -226,8 +226,10 @@ def compare(self, t=-1, sim_inds=None, output=False): for i,s in enumerate(sim_inds): sim = self.sims[s] label = sim.label - if not label: + if not label: # Give it a label if it doesn't have one label = f'Sim {i}' + if label in resdict: # Avoid duplicates + label += f' ({i})' for reskey in sim.result_keys(): val = sim.results[reskey].values[t] if reskey not in ['r_eff', 'doubling_time']: diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py new file mode 100755 index 000000000..017b45f70 --- /dev/null +++ b/tests/devtests/dev_test_rescaling.py @@ -0,0 +1,50 @@ +''' +Compare simulating the entire population vs. dynamic rescaling vs. static rescaling. +''' + +import sciris as sc +import covasim as cv + +p = sc.objdict() # Parameters +s = sc.objdict() # Sims +m = sc.objdict() # Multisims + +shared = sc.objdict( + n_days = 120, + beta = 0.012, + ) + + +p.entire = dict( + pop_size = 200e3, + pop_infected = 100, + pop_scale = 1, + rescale = False, +) + +p.rescale = dict( + pop_size = 20e3, + pop_infected = 100, + pop_scale = 10, + rescale = True, +) + +p.static = dict( + pop_size = 20e3, + pop_infected = 10, + pop_scale = 10, + rescale = False, +) + +keys = p.keys() + +for key in keys: + p[key].update(shared) + +for key in keys: + s[key] = cv.Sim(pars=p[key], label=key) + +msim = cv.MultiSim(sims=s.values(), reseed=False) +msim.run() +msim.compare() +msim.plot() \ No newline at end of file From 416c446abc46ff167d024892029365ac34206f6f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 12:02:46 -0700 Subject: [PATCH 079/194] basic results make sense --- tests/devtests/dev_test_rescaling.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py index 017b45f70..f580e0685 100755 --- a/tests/devtests/dev_test_rescaling.py +++ b/tests/devtests/dev_test_rescaling.py @@ -43,8 +43,14 @@ for key in keys: s[key] = cv.Sim(pars=p[key], label=key) + m[key] = cv.MultiSim(base_sim=s[key], n_runs=10) -msim = cv.MultiSim(sims=s.values(), reseed=False) -msim.run() -msim.compare() -msim.plot() \ No newline at end of file +for key in keys: + m[key].run() + m[key].reduce() + m[key].plot() + +# msim = cv.MultiSim(sims=s.values(), reseed=False) +# msim.run() +# msim.compare() +# msim.plot() \ No newline at end of file From f8063182d2a7b47543f05115a6b3f819de4a59d0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 12:11:23 -0700 Subject: [PATCH 080/194] not bad, but results not as similar as would be nice --- tests/devtests/dev_test_rescaling.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py index f580e0685..fd3105fb0 100755 --- a/tests/devtests/dev_test_rescaling.py +++ b/tests/devtests/dev_test_rescaling.py @@ -9,31 +9,34 @@ s = sc.objdict() # Sims m = sc.objdict() # Multisims +# Properties that are shared across sims shared = sc.objdict( n_days = 120, beta = 0.012, - ) - +) +# Simulate the entire population p.entire = dict( - pop_size = 200e3, + pop_size = 500e3, pop_infected = 100, - pop_scale = 1, - rescale = False, + pop_scale = 1, + rescale = False, ) +# Simulate a small population with dynamic scaling p.rescale = dict( - pop_size = 20e3, + pop_size = 25e3, pop_infected = 100, - pop_scale = 10, - rescale = True, + pop_scale = 20, + rescale = True, ) +# Simulate a small population with static scaling p.static = dict( - pop_size = 20e3, - pop_infected = 10, - pop_scale = 10, - rescale = False, + pop_size = 25e3, + pop_infected = 5, + pop_scale = 20, + rescale = False, ) keys = p.keys() From 0ac69d080d058debb6f27d65bd6c84e719f08a59 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 12:32:16 -0700 Subject: [PATCH 081/194] works with change beta --- CHANGELOG.rst | 9 ++++++--- covasim/parameters.py | 8 ++++---- covasim/run.py | 10 +++++----- tests/devtests/dev_test_rescaling.py | 27 +++++++++++++++++++-------- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6b64d8935..a135abd39 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,9 +1,12 @@ What's new ========== -All notable changes to the codebase are documented in this file. Note: in many cases, -changes from multiple patch versions are grouped together, so numbering will not be -strictly consecutive. +All notable changes to the codebase are documented in this file. Note: in many cases, changes from multiple patch versions are grouped together, so numbering will not be strictly consecutive. + + +Version 0.30.3 (2020-05-03) +--------------------------- +- Fixed bugs in dynamic scaling; see ``tests/devtests/dev_test_rescaling.py``. When using ``pop_scale>1``, the recommendation is now to use ``rescale=True``. Version 0.30.2 (2020-05-02) diff --git a/covasim/parameters.py b/covasim/parameters.py index 68d843eb1..abf8a4640 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -36,10 +36,10 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): pars['verbose'] = 1 # Whether or not to display information during the run -- options are 0 (silent), 1 (default), 2 (everything) # Rescaling parameters - pars['pop_scale'] = 1 # Factor by which to scale the population -- e.g. 1000 with pop_size = 10e3 means a population of 10m - pars['rescale'] = 0 # Enable dynamic rescaling of the population - pars['rescale_threshold'] = 0.1 # Fraction susceptible population that will trigger rescaling if rescaling - pars['rescale_factor'] = 2 # Factor by which we rescale the population + pars['pop_scale'] = 1 # Factor by which to scale the population -- e.g. 1000 with pop_size = 10e3 means a population of 10m + pars['rescale'] = 0 # Enable dynamic rescaling of the population + pars['rescale_threshold'] = 0.05 # Fraction susceptible population that will trigger rescaling if rescaling + pars['rescale_factor'] = 2 # Factor by which we rescale the population # Basic disease transmission pars['beta'] = 0.015 # Beta per symptomatic contact; absolute value, calibrated diff --git a/covasim/run.py b/covasim/run.py index dbf723c0b..d13978466 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -188,7 +188,7 @@ def reduce(self, quantiles=None, output=False): raw[reskey][:,s] = sim.results[reskey].values for reskey in reskeys: - reduced_sim.results[reskey].values[:] = np.median(raw[reskey], axis=1) # Changed from median to mean for smoother plots + reduced_sim.results[reskey].values[:] = np.quantile(raw[reskey], q=0.5, axis=1) # Changed from median to mean for smoother plots 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.likelihood() # Recompute the likelihood for the average sim @@ -432,7 +432,7 @@ def print_heading(string): scenraw = {} for reskey in reskeys: - scenraw[reskey] = pl.zeros((self.npts, len(scen_sims))) + scenraw[reskey] = np.zeros((self.npts, len(scen_sims))) for s,sim in enumerate(scen_sims): scenraw[reskey][:,s] = sim.results[reskey].values @@ -441,9 +441,9 @@ def print_heading(string): scenres.low = {} scenres.high = {} for reskey in reskeys: - scenres.best[reskey] = pl.median(scenraw[reskey], axis=1) # Changed from median to mean for smoother plots - scenres.low[reskey] = pl.quantile(scenraw[reskey], q=self['quantiles']['low'], axis=1) - scenres.high[reskey] = pl.quantile(scenraw[reskey], q=self['quantiles']['high'], axis=1) + 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) for reskey in reskeys: self.results[reskey][scenkey]['name'] = scenname diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py index fd3105fb0..de71c62d2 100755 --- a/tests/devtests/dev_test_rescaling.py +++ b/tests/devtests/dev_test_rescaling.py @@ -9,33 +9,40 @@ s = sc.objdict() # Sims m = sc.objdict() # Multisims +# Interventions +cb = cv.change_beta(days=60, changes=0.5) # Change beta +# tn = cv.test_num() # Test a number of people +# tp = cv.test_prob() # Test a number of people + + # Properties that are shared across sims shared = sc.objdict( n_days = 120, beta = 0.012, + interventions = cb, ) # Simulate the entire population p.entire = dict( - pop_size = 500e3, - pop_infected = 100, + pop_size = 200e3, + pop_infected = 20, pop_scale = 1, rescale = False, ) # Simulate a small population with dynamic scaling p.rescale = dict( - pop_size = 25e3, - pop_infected = 100, - pop_scale = 20, + pop_size = 20e3, + pop_infected = 20, + pop_scale = 10, rescale = True, ) # Simulate a small population with static scaling p.static = dict( - pop_size = 25e3, - pop_infected = 5, - pop_scale = 20, + pop_size = 20e3, + pop_infected = 2, + pop_scale = 10, rescale = False, ) @@ -53,6 +60,10 @@ m[key].reduce() m[key].plot() +bsims = [msim.base_sim for msim in m.values()] +mm = cv.MultiSim(sims=bsims) +mm.compare() + # msim = cv.MultiSim(sims=s.values(), reseed=False) # msim.run() # msim.compare() From 8145dbd980650efbc5ac0ce232c272e9b2448700 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 12:44:13 -0700 Subject: [PATCH 082/194] confirm that test_num is wrong --- CHANGELOG.rst | 1 + covasim/interventions.py | 12 ++++++---- covasim/parameters.py | 4 ++-- tests/devtests/dev_test_rescaling.py | 36 +++++++++++++++------------- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a135abd39..c5a26adaf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,7 @@ All notable changes to the codebase are documented in this file. Note: in many c Version 0.30.3 (2020-05-03) --------------------------- - Fixed bugs in dynamic scaling; see ``tests/devtests/dev_test_rescaling.py``. When using ``pop_scale>1``, the recommendation is now to use ``rescale=True``. +- In ``cv.test_num()``, renamed argument from ``sympt_test`` to ``symp_test`` for consistency. Version 0.30.2 (2020-05-02) diff --git a/covasim/interventions.py b/covasim/interventions.py index ad88f4da6..ad3957aac 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -421,7 +421,7 @@ class test_num(Intervention): Test a fixed number of people per day. Args: - daily_tests (int or arr): number of tests per day + daily_tests (int or arr): number of tests per day; if integer, use that number every day symp_test (float): odds ratio of a symptomatic person testing quar_test (float): probability of a person in quarantine testing sensitivity (float): test sensitivity @@ -439,10 +439,10 @@ class test_num(Intervention): Intervention ''' - def __init__(self, daily_tests, sympt_test=100.0, quar_test=1.0, sensitivity=1.0, loss_prob=0, test_delay=0, start_day=0, end_day=None, do_plot=None): + def __init__(self, daily_tests, symp_test=100.0, quar_test=1.0, sensitivity=1.0, loss_prob=0, test_delay=0, start_day=0, end_day=None, do_plot=None): super().__init__(do_plot=do_plot) self.daily_tests = daily_tests #: Should be a list of length matching time - self.sympt_test = sympt_test + self.symp_test = symp_test self.quar_test = quar_test self.sensitivity = sensitivity self.loss_prob = loss_prob @@ -454,7 +454,9 @@ def __init__(self, daily_tests, sympt_test=100.0, quar_test=1.0, sensitivity=1.0 def initialize(self, sim): - ''' Fix the dates ''' + ''' Fix the dates and number of tests ''' + if sc.isnumber(self.daily_tests): # If a number, convert to an array + self.daily_tests = np.array([int(self.daily_tests)]*sim.npts) self.start_day = sim.day(self.start_day) self.end_day = sim.day(self.end_day) self.days = [self.start_day, self.end_day] @@ -492,7 +494,7 @@ def apply(self, sim): symp_inds = cvu.true(sim.people.symptomatic) quar_inds = cvu.true(sim.people.quarantined) diag_inds = cvu.true(sim.people.diagnosed) - test_probs[symp_inds] *= self.sympt_test + test_probs[symp_inds] *= self.symp_test test_probs[quar_inds] *= self.quar_test test_probs[diag_inds] = 0. diff --git a/covasim/parameters.py b/covasim/parameters.py index abf8a4640..04e1c2c5a 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -36,8 +36,8 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): pars['verbose'] = 1 # Whether or not to display information during the run -- options are 0 (silent), 1 (default), 2 (everything) # Rescaling parameters - pars['pop_scale'] = 1 # Factor by which to scale the population -- e.g. 1000 with pop_size = 10e3 means a population of 10m - pars['rescale'] = 0 # Enable dynamic rescaling of the population + 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['rescale'] = 0 # 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'] = 2 # Factor by which we rescale the population diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py index de71c62d2..58d620edb 100755 --- a/tests/devtests/dev_test_rescaling.py +++ b/tests/devtests/dev_test_rescaling.py @@ -5,44 +5,51 @@ import sciris as sc import covasim as cv +T = sc.tic() + p = sc.objdict() # Parameters s = sc.objdict() # Sims m = sc.objdict() # Multisims # Interventions -cb = cv.change_beta(days=60, changes=0.5) # Change beta -# tn = cv.test_num() # Test a number of people +iday = 60 +cb = cv.change_beta(days=iday, changes=0.5) # Change beta +tn = cv.test_num(start_day=iday, daily_tests=1000, symp_test=10) # Test a number of people # tp = cv.test_prob() # Test a number of people # Properties that are shared across sims +basepop = 10e3 +popscale = 10 +popinfected = 20 + shared = sc.objdict( n_days = 120, - beta = 0.012, - interventions = cb, + beta = 0.015, + interventions = tn, ) # Simulate the entire population p.entire = dict( - pop_size = 200e3, - pop_infected = 20, + pop_size = basepop*popscale, + pop_infected = popinfected, pop_scale = 1, rescale = False, ) # Simulate a small population with dynamic scaling p.rescale = dict( - pop_size = 20e3, - pop_infected = 20, - pop_scale = 10, + pop_size = basepop, + pop_infected = popinfected, + pop_scale = popscale, rescale = True, ) # Simulate a small population with static scaling p.static = dict( - pop_size = 20e3, - pop_infected = 2, - pop_scale = 10, + pop_size = basepop, + pop_infected = popinfected//popscale, + pop_scale = popscale, rescale = False, ) @@ -64,7 +71,4 @@ mm = cv.MultiSim(sims=bsims) mm.compare() -# msim = cv.MultiSim(sims=s.values(), reseed=False) -# msim.run() -# msim.compare() -# msim.plot() \ No newline at end of file +sc.toc(T) \ No newline at end of file From 7a3045f988a99a117ab997ab8db203672797c0dc Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 12:53:49 -0700 Subject: [PATCH 083/194] test_num working --- covasim/interventions.py | 19 +++++++++++-------- covasim/sim.py | 38 +++++++++++++++++++------------------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index ad3957aac..180388b26 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -455,12 +455,22 @@ def __init__(self, daily_tests, symp_test=100.0, quar_test=1.0, sensitivity=1.0, def initialize(self, sim): ''' Fix the dates and number of tests ''' + + # Process daily tests -- has to be here rather than init so have access to the sim object if sc.isnumber(self.daily_tests): # If a number, convert to an array self.daily_tests = np.array([int(self.daily_tests)]*sim.npts) + elif isinstance(self.daily_tests, (pd.Series, pd.DataFrame)): + start_date = sim['start_day'] + dt.timedelta(days=self.start_day) + end_date = self.daily_tests.index[-1] + dateindex = pd.date_range(start_date, end_date) + self.daily_tests = self.daily_tests.reindex(dateindex, fill_value=0).to_numpy() + + # Handle days 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.initialized = True + return @@ -472,17 +482,10 @@ def apply(self, sim): elif self.end_day is not None and t > self.end_day: return - # Process daily tests -- has to be here rather than init so have access to the sim object - if isinstance(self.daily_tests, (pd.Series, pd.DataFrame)): - start_date = sim['start_day'] + dt.timedelta(days=self.start_day) - end_date = self.daily_tests.index[-1] - dateindex = pd.date_range(start_date, end_date) - self.daily_tests = self.daily_tests.reindex(dateindex, fill_value=0).to_numpy() - # Check that there are still tests rel_t = t - self.start_day if rel_t < len(self.daily_tests): - n_tests = self.daily_tests[rel_t] # Number of tests for this day + n_tests = int(self.daily_tests[rel_t]/sim.rescale_vec[t]) # Number of tests for this day -- rescaled if not (n_tests and pl.isfinite(n_tests)): # If there are no tests today, abort early return else: diff --git a/covasim/sim.py b/covasim/sim.py index 4995f861b..64a0aba42 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -319,6 +319,25 @@ def init_interventions(self): return + def rescale(self): + ''' Dynamically rescale the population ''' + if self['rescale']: + t = self.t + pop_scale = self['pop_scale'] + current_scale = self.rescale_vec[t] + if current_scale < pop_scale: # We have room to rescale + n_not_sus = self.people.count_not('susceptible') + n_people = len(self.people) + if n_not_sus / n_people > self['rescale_threshold']: # Check if we've reached point when we want to rescale + max_ratio = pop_scale/current_scale # We don't want to exceed this + scaling_ratio = min(self['rescale_factor'], max_ratio) + self.rescale_vec[t:] *= scaling_ratio # Update the rescaling factor from here on + n = int(n_people*(1.0-1.0/scaling_ratio)) # For example, rescaling by 2 gives n = 0.5*n_people + new_sus_inds = cvu.choose(max_n=n_people, n=n) # Choose who to make susceptible again + self.people.make_susceptible(new_sus_inds) + return + + def step(self): ''' Step the simulation forward in time @@ -405,25 +424,6 @@ def step(self): return - def rescale(self): - ''' Dynamically rescale the population ''' - if self['rescale']: - t = self.t - pop_scale = self['pop_scale'] - current_scale = self.rescale_vec[t] - if current_scale < pop_scale: # We have room to rescale - n_not_sus = self.people.count_not('susceptible') - n_people = len(self.people) - if n_not_sus / n_people > self['rescale_threshold']: # Check if we've reached point when we want to rescale - max_ratio = pop_scale/current_scale # We don't want to exceed this - scaling_ratio = min(self['rescale_factor'], max_ratio) - self.rescale_vec[t:] *= scaling_ratio # Update the rescaling factor from here on - n = int(n_people*(1.0-1.0/scaling_ratio)) # For example, rescaling by 2 gives n = 0.5*n_people - new_sus_inds = cvu.choose(max_n=n_people, n=n) # Choose who to make susceptible again - self.people.make_susceptible(new_sus_inds) - return - - def run(self, do_plot=False, until=None, verbose=None, **kwargs): ''' Run the simulation. From 17194d8bfa0dfe2e82378f72aa8cb8d181aa6e49 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 12:56:50 -0700 Subject: [PATCH 084/194] test prob working too --- tests/devtests/dev_test_rescaling.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py index 58d620edb..99f15ff50 100755 --- a/tests/devtests/dev_test_rescaling.py +++ b/tests/devtests/dev_test_rescaling.py @@ -15,18 +15,18 @@ iday = 60 cb = cv.change_beta(days=iday, changes=0.5) # Change beta tn = cv.test_num(start_day=iday, daily_tests=1000, symp_test=10) # Test a number of people -# tp = cv.test_prob() # Test a number of people - +tp = cv.test_prob(start_day=iday, symp_prob=0.1, asymp_prob=0.01) # Test a number of people # Properties that are shared across sims -basepop = 10e3 -popscale = 10 -popinfected = 20 +basepop = 10e3 +popscale = 10 +popinfected = 20 +which_interv = 2 # Which intervention to test shared = sc.objdict( n_days = 120, beta = 0.015, - interventions = tn, + interventions = [cb, tn, tp][which_interv], ) # Simulate the entire population From d668758a36a7768d7280c4287e0f4f308709d688 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 13:18:24 -0700 Subject: [PATCH 085/194] looking into numbers of tests as well --- tests/devtests/dev_test_rescaling.py | 48 ++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py index 99f15ff50..0e710fb75 100755 --- a/tests/devtests/dev_test_rescaling.py +++ b/tests/devtests/dev_test_rescaling.py @@ -19,8 +19,9 @@ # Properties that are shared across sims basepop = 10e3 -popscale = 10 popinfected = 20 +popscale1 = 10 +popscale2 = 20 # Try a different population scale which_interv = 2 # Which intervention to test shared = sc.objdict( @@ -31,7 +32,7 @@ # Simulate the entire population p.entire = dict( - pop_size = basepop*popscale, + pop_size = basepop*popscale1, pop_infected = popinfected, pop_scale = 1, rescale = False, @@ -41,18 +42,44 @@ p.rescale = dict( pop_size = basepop, pop_infected = popinfected, - pop_scale = popscale, + pop_scale = popscale1, rescale = True, ) # Simulate a small population with static scaling p.static = dict( pop_size = basepop, - pop_infected = popinfected//popscale, - pop_scale = popscale, + pop_infected = popinfected//popscale1, + pop_scale = popscale1, rescale = False, ) +# Simulate an extra large population +p.entire2 = dict( + pop_size = basepop*popscale2, + pop_infected = popinfected, + pop_scale = 1, + rescale = False, +) + +# Simulate a small population with dynamic scaling +p.rescale2 = dict( + pop_size = basepop, + pop_infected = popinfected, + pop_scale = popscale2, + rescale = True, +) + +# Simulate a small population with static scaling +p.static2 = dict( + pop_size = basepop, + pop_infected = popinfected//popscale2, + pop_scale = popscale2, + rescale = False, +) + + +# Create and run the sims keys = p.keys() for key in keys: @@ -65,7 +92,16 @@ for key in keys: m[key].run() m[key].reduce() - m[key].plot() + + +# Plot +to_plot = { + 'Totals': ['cum_infections', 'cum_tests', 'cum_diagnoses'], + 'New': ['new_infections', 'new_tests', 'new_diagnoses'], + } + +for key in keys: + m[key].plot(to_plot=to_plot) bsims = [msim.base_sim for msim in m.values()] mm = cv.MultiSim(sims=bsims) From ac1c29e9635dba03cd084cc50ece322ceddf72f1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 13:20:46 -0700 Subject: [PATCH 086/194] numbers of tests now seems to not be working --- tests/devtests/dev_test_rescaling.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py index 0e710fb75..0c0d321d4 100755 --- a/tests/devtests/dev_test_rescaling.py +++ b/tests/devtests/dev_test_rescaling.py @@ -12,7 +12,7 @@ m = sc.objdict() # Multisims # Interventions -iday = 60 +iday = 30 cb = cv.change_beta(days=iday, changes=0.5) # Change beta tn = cv.test_num(start_day=iday, daily_tests=1000, symp_test=10) # Test a number of people tp = cv.test_prob(start_day=iday, symp_prob=0.1, asymp_prob=0.01) # Test a number of people @@ -25,7 +25,7 @@ which_interv = 2 # Which intervention to test shared = sc.objdict( - n_days = 120, + n_days = 60, beta = 0.015, interventions = [cb, tn, tp][which_interv], ) From 4ba58550761f30ec31f5c8806f89cfbd17ad8800 Mon Sep 17 00:00:00 2001 From: krosenfeld-IDM Date: Sun, 3 May 2020 13:28:45 -0700 Subject: [PATCH 087/194] pull out target index from dict --- covasim/sim.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) mode change 100644 => 100755 covasim/sim.py diff --git a/covasim/sim.py b/covasim/sim.py old mode 100644 new mode 100755 index 99f9aa6ae..c5f1b5ec3 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -641,7 +641,8 @@ def compute_gen_time(self): date_symptomatic = self.people.date_symptomatic for p in range(len(self.people)): if len(targets[p])>0: - for target_ind in targets[p]: + for target in targets[p]: + target_ind = target['target'] intervals1[pos1] = date_exposed[target_ind] - date_exposed[p] pos1 += 1 if not np.isnan(date_symptomatic[p]): From 8da19e28da7ec55d88426d82e40832c9fb3062d0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 13:32:38 -0700 Subject: [PATCH 088/194] fix compute_gen_time --- covasim/sim.py | 3 ++- tests/devtests/dev_test_rescaling.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index 64a0aba42..a4f9c023c 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -641,7 +641,8 @@ def compute_gen_time(self): date_symptomatic = self.people.date_symptomatic for p in range(len(self.people)): if len(targets[p])>0: - for target_ind in targets[p]: + for target in targets[p]: + target_ind = target['target'] intervals1[pos1] = date_exposed[target_ind] - date_exposed[p] pos1 += 1 if not np.isnan(date_symptomatic[p]): diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py index 0c0d321d4..0e710fb75 100755 --- a/tests/devtests/dev_test_rescaling.py +++ b/tests/devtests/dev_test_rescaling.py @@ -12,7 +12,7 @@ m = sc.objdict() # Multisims # Interventions -iday = 30 +iday = 60 cb = cv.change_beta(days=iday, changes=0.5) # Change beta tn = cv.test_num(start_day=iday, daily_tests=1000, symp_test=10) # Test a number of people tp = cv.test_prob(start_day=iday, symp_prob=0.1, asymp_prob=0.01) # Test a number of people @@ -25,7 +25,7 @@ which_interv = 2 # Which intervention to test shared = sc.objdict( - n_days = 60, + n_days = 120, beta = 0.015, interventions = [cb, tn, tp][which_interv], ) From 83c49c1dc7c58f44ad74ed4213539e0e8088ee95 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 13:47:31 -0700 Subject: [PATCH 089/194] fixed test_prob --- covasim/interventions.py | 2 +- tests/devtests/dev_test_rescaling.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 180388b26..219566859 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -579,7 +579,7 @@ def apply(self, sim): test_inds = cvu.binomial_arr(test_probs).nonzero()[0] sim.people.test(test_inds, test_sensitivity=self.test_sensitivity, loss_prob=self.loss_prob, test_delay=self.test_delay) - sim.results['new_tests'][t] += len(test_inds) + sim.results['new_tests'][t] += int(len(test_inds)*sim['pop_scale']/sim.rescale_vec[t]) # If we're using dynamic scaling, we have to scale by pop_scale, not rescale_vec return diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py index 0e710fb75..ec884a0c7 100755 --- a/tests/devtests/dev_test_rescaling.py +++ b/tests/devtests/dev_test_rescaling.py @@ -12,7 +12,7 @@ m = sc.objdict() # Multisims # Interventions -iday = 60 +iday = 30 cb = cv.change_beta(days=iday, changes=0.5) # Change beta tn = cv.test_num(start_day=iday, daily_tests=1000, symp_test=10) # Test a number of people tp = cv.test_prob(start_day=iday, symp_prob=0.1, asymp_prob=0.01) # Test a number of people From 5fc3ec9ea5e6c25329f8e144629013574a623a1a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 14:01:16 -0700 Subject: [PATCH 090/194] working for test_num --- tests/devtests/dev_test_rescaling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py index ec884a0c7..c72cf1193 100755 --- a/tests/devtests/dev_test_rescaling.py +++ b/tests/devtests/dev_test_rescaling.py @@ -22,7 +22,7 @@ popinfected = 20 popscale1 = 10 popscale2 = 20 # Try a different population scale -which_interv = 2 # Which intervention to test +which_interv = 1 # Which intervention to test shared = sc.objdict( n_days = 120, From fb3b526c82e2cb396e24235f4fe87fc8cf81ae19 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 14:08:58 -0700 Subject: [PATCH 091/194] all seems to work --- tests/devtests/dev_test_rescaling.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py index c72cf1193..32d7d5b0d 100755 --- a/tests/devtests/dev_test_rescaling.py +++ b/tests/devtests/dev_test_rescaling.py @@ -12,22 +12,22 @@ m = sc.objdict() # Multisims # Interventions -iday = 30 -cb = cv.change_beta(days=iday, changes=0.5) # Change beta -tn = cv.test_num(start_day=iday, daily_tests=1000, symp_test=10) # Test a number of people -tp = cv.test_prob(start_day=iday, symp_prob=0.1, asymp_prob=0.01) # Test a number of people +tn = cv.test_num(start_day=40, daily_tests=1000, symp_test=10) # Test a number of people +tp = cv.test_prob(start_day=30, symp_prob=0.1, asymp_prob=0.01) # Test a number of people +cb = cv.change_beta(days=50, changes=0.5) # Change beta # Properties that are shared across sims basepop = 10e3 -popinfected = 20 +popinfected = 100 popscale1 = 10 popscale2 = 20 # Try a different population scale -which_interv = 1 # Which intervention to test +which_interv = 2 # Which intervention to test shared = sc.objdict( n_days = 120, beta = 0.015, - interventions = [cb, tn, tp][which_interv], + rand_seed = 239487, + interventions = [cb, tn, tp], ) # Simulate the entire population @@ -96,12 +96,16 @@ # Plot to_plot = { - 'Totals': ['cum_infections', 'cum_tests', 'cum_diagnoses'], - 'New': ['new_infections', 'new_tests', 'new_diagnoses'], + 'Totals': ['cum_infections', 'cum_diagnoses'], + 'New': ['new_infections', 'new_diagnoses'], + 'Total tests': ['cum_tests'], + 'New tests': ['new_tests'], } +log_scale = ['Total tests'] + for key in keys: - m[key].plot(to_plot=to_plot) + m[key].plot(to_plot=to_plot, log_scale=log_scale) bsims = [msim.base_sim for msim in m.values()] mm = cv.MultiSim(sims=bsims) From 77906222d977e3f459ca347e0e756a3ad7ca1fce Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 15:59:45 -0700 Subject: [PATCH 092/194] plot_compare working --- covasim/plotting.py | 68 +++++++++++++++++++++++++++--- covasim/run.py | 46 +++++++++++++++++++- tests/devtests/multisim_compare.py | 22 +++++++--- 3 files changed, 122 insertions(+), 14 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 41e17a25f..540ceea6a 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -15,7 +15,7 @@ from . import defaults as cvd -__all__ = ['plot_sim', 'plot_scens', 'plot_result', 'plotly_sim', 'plotly_people', 'plotly_animate'] +__all__ = ['plot_sim', 'plot_scens', 'plot_result', 'plot_compare', 'plotly_sim', 'plotly_people', 'plotly_animate'] #%% Plotting helper functions @@ -200,7 +200,7 @@ def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, default_name='covas 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, - log_scale=False, colors=None, do_show=True, sep_figs=False, fig=None): + log_scale=False, colors=None, labels=None, do_show=True, sep_figs=False, fig=None): ''' Plot the results of a single simulation -- see Sim.plot() for documentation ''' # Handle inputs @@ -218,9 +218,13 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot color = colors[reskey] else: color = res.color + if labels is not None: + label = labels[reskey] + else: + label = res.name 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=res.name, **args.plot, c=color) + ax.plot(res_t, res.values, label=label, **args.plot, c=color) plot_data(sim, reskey, args.scatter) # Plot the data reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) plot_interventions(sim, ax) # Plot the interventions @@ -232,7 +236,7 @@ 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, as_dates=True, dateformat=None, interval=None, n_cols=1, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, - log_scale=False, colors=None, do_show=True, sep_figs=False, fig=None): + log_scale=False, colors=None, labels=None, do_show=True, sep_figs=False, fig=None): ''' Plot the results of a scenario -- see Scenarios.plot() for documentation ''' # Handle inputs @@ -253,8 +257,12 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, color = colors[scenkey] else: color = default_colors[snum] + if labels is not None: + label = labels[scenkey] + else: + label = scendata.name ax.fill_between(scens.tvec, scendata.low, scendata.high, color=color, **args.fill) # Create the uncertainty bound - ax.plot(scens.tvec, res_y, label=scendata.name, c=color, **args.plot) # Plot the actual line + ax.plot(scens.tvec, res_y, label=label, c=color, **args.plot) # Plot the actual line plot_data(sim, reskey, args.scatter) # Plot the data plot_interventions(sim, ax) # Plot the interventions reset_ticks(ax, sim, interval, as_dates) # Optionally reset tick marks (useful for e.g. plotting weeks/months) @@ -265,7 +273,7 @@ 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, font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, - as_dates=True, dateformat=None, interval=None, color=None, fig=None): + as_dates=True, dateformat=None, interval=None, color=None, label=None, fig=None): ''' Plot a single result -- see Sim.plot_result() for documentation ''' # Handle inputs @@ -291,7 +299,9 @@ def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter ax = pl.subplot(111, label='plot_result') # Do the plotting - ax.plot(res_t, res_y, c=color, label=res.name, **args.plot) + if label is None: + label = res.name + ax.plot(res_t, res_y, c=color, label=label, **args.plot) plot_data(sim, key, args.scatter) # 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 @@ -300,6 +310,50 @@ def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter return fig +def plot_compare(df, log_scale=True, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, + font_size=18, font_family=None, grid=False, commaticks=True, setylim=True, + as_dates=True, dateformat=None, interval=None, color=None, label=None, fig=None): + ''' Plot a MultiSim comparison -- see MultiSim.plot_compare() for documentation ''' + + # Handle inputs + fig_args = sc.mergedicts({'figsize':(16,16)}, 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) + fig, figs, ax = create_figs(args, font_size, font_family, sep_figs=False, fig=fig) + + # Map from results into different categories + mapping = { + 'cum': 'Cumulative counts', + 'new': 'New counts', + 'n': 'Number in state', + 'r': 'R_eff', + } + category = [] + for v in df.index.values: + v_type = v.split('_')[0] + if v_type in mapping: + category.append(v_type) + else: + category.append('other') + df['category'] = category + + # Plot + for i,m in enumerate(mapping): + not_r_eff = m != 'r' + if not_r_eff: + ax = fig.add_subplot(2, 2, i+1) + else: + ax = fig.add_subplot(8, 2, 10) + dfm = df[df['category'] == m] + logx = not_r_eff and log_scale + dfm.plot(ax=ax, kind='barh', logx=logx, legend=False) + if not(not_r_eff): + ax.legend(loc='upper left', bbox_to_anchor=(0,-0.3)) + ax.grid(True) + + return fig + + #%% Plotly functions def get_individual_states(sim): diff --git a/covasim/run.py b/covasim/run.py index d13978466..c48b9e4b9 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -204,7 +204,7 @@ def reduce(self, quantiles=None, output=False): return - def compare(self, t=-1, sim_inds=None, output=False): + def compare(self, t=-1, sim_inds=None, output=False, do_plot=False, **kwargs): ''' Create a dataframe compare sims at a single point in time. @@ -212,6 +212,8 @@ def compare(self, t=-1, sim_inds=None, output=False): t (int or str): the day (or date) to do the comparison; default, the end sim_inds (list): list of integers of which sims to include (default: all) output (bool): whether or not to return the comparison as a dataframe + do_plot (bool): whether or not to plot the comparison (see also plot_compare()) + kwargs (dict): passed to plot_compare() Returns: df (dataframe): a dataframe comparison @@ -238,6 +240,10 @@ def compare(self, t=-1, sim_inds=None, output=False): # Convert to a dataframe df = pd.DataFrame.from_dict(resdict).astype(object) # astype is necessary to prevent type coersion + + if do_plot: + self.plot_compare(df, *args, **kwargs) + if output: return df else: @@ -246,7 +252,7 @@ def compare(self, t=-1, sim_inds=None, output=False): def plot(self, *args, **kwargs): - ''' Convenience mthod for plotting ''' + ''' Convenience method for plotting -- arguments passed to Sim.plot() ''' if self.which in ['combined', 'reduced']: fig = self.base_sim.plot(*args, **kwargs) else: @@ -264,6 +270,42 @@ def plot(self, *args, **kwargs): return fig + def plot_result(self, key, colors=None, labels=None, *args, **kwargs): + ''' 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: + fig = None + if colors is None: + colors = sc.gridcolors(len(self)) + if labels is None: + labels = [sim.label for sim in self.sims] + orig_setylim = kwargs.get('setylim', True) + for s,sim in enumerate(self.sims): + if s == len(self.sims)-1: + kwargs['setylim'] = orig_setylim + else: + kwargs['setylim'] = False + fig = sim.plot_result(key=key, fig=fig, color=colors[s], label=labels[s], *args, **kwargs) + return fig + + + def plot_compare(self, t=-1, sim_inds=None, log_scale=True, **kwargs): + ''' + Plot a comparison between sims. + + Args: + t (int): index of results, passed to compare() + sim_inds (list): which sims to include, passed to compare() + log_scale (bool): whether to plot with a logarithmic x-axis + kwargs (dict): standard plotting arguments, see Sim.plot() for explanation + + Returns: + fig (figure): the figure handle + ''' + df = self.compare(t=t, sim_inds=sim_inds, output=True) + cvplt.plot_compare(df, log_scale=log_scale, **kwargs) + def save(self, filename=None, keep_people=False, **kwargs): ''' Save to disk as a gzipped pickle. Load with cv.load(filename). diff --git a/tests/devtests/multisim_compare.py b/tests/devtests/multisim_compare.py index f2ee1f43b..ccafab438 100755 --- a/tests/devtests/multisim_compare.py +++ b/tests/devtests/multisim_compare.py @@ -4,11 +4,23 @@ import covasim as cv -s0 = cv.Sim(label='Normal beta') -s1 = cv.Sim(label='Low beta', beta=0.012) +option = 2 + +s0 = cv.Sim(label='Low beta', beta=0.012) +s1 = cv.Sim(label='Normal beta') s2 = cv.Sim(label='High beta', beta=0.018) -msim = cv.MultiSim(sims=[s0, s1, s2]) -msim.run() -df = msim.compare() +if option == 1: + msim = cv.MultiSim(sims=[s0, s1, s2]) + msim.run() +elif option == 2: + msim = cv.MultiSim(sims=s1) + msim.run(n_runs=10) + +df = msim.compare(output=True) +print(df) + +msim.plot_compare() + +msim.plot_result('r_eff') From 9e0413750c48c126daeafacf9ad700460c380a53 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 16:04:25 -0700 Subject: [PATCH 093/194] updated changelog --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c5a26adaf..f0fe4135b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,8 @@ Version 0.30.3 (2020-05-03) --------------------------- - Fixed bugs in dynamic scaling; see ``tests/devtests/dev_test_rescaling.py``. When using ``pop_scale>1``, the recommendation is now to use ``rescale=True``. - In ``cv.test_num()``, renamed argument from ``sympt_test`` to ``symp_test`` for consistency. +- Added ``plot_compare()`` method to ``MultiSim``. +- Added ``labels`` arguments to plotting methods, to allow custom labels to be used. Version 0.30.2 (2020-05-02) From 648bc90f94b30320437cf7c651cc7aced0a0953a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 3 May 2020 16:04:59 -0700 Subject: [PATCH 094/194] updated benchmark and version --- covasim/version.py | 4 ++-- tests/benchmark.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/covasim/version.py b/covasim/version.py index 934ec946c..950d0efef 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -2,6 +2,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '0.30.2' -__versiondate__ = '2020-05-02' +__version__ = '0.30.3' +__versiondate__ = '2020-05-03' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2020 by IDM' diff --git a/tests/benchmark.json b/tests/benchmark.json index 19c2d1a33..f5440dd48 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,7 +1,7 @@ { "time": { - "initialize": 0.215, - "run": 0.164 + "initialize": 0.221, + "run": 0.168 }, "parameters": { "pop_size": 20000, From ca23bb7d73f6dc10d19209d0400faa24d8478d38 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 4 May 2020 15:40:14 -0700 Subject: [PATCH 095/194] updated transtree --- covasim/base.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 7587869ba..9afe0d748 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1046,23 +1046,30 @@ def make_detailed(self, people, reset=False): if transdict is not None: # Pull out key quantities - ddict = sc.dcp(transdict) # For "detailed dictionary" - source = ddict['source'] - target = ddict['target'] - date = ddict['date'] + ddict = sc.objdict(sc.dcp(transdict)) # For "detailed dictionary" + source = ddict.source + target = ddict.target + ddict.s = sc.objdict() # Source + ddict.t = sc.objdict() # Target + + if source is not None: + stdict = {'s':source, 't':target} + else: + stdict = {'t':target} + + attrs = ['age', 'date_symptomatic', 'date_diagnosed', 'date_quarantined', 'date_severe', 'date_critical', 'date_quarantined'] + 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_'): + tf_attr = attr.replace('date_', 'tf_') + ddict.s[tf_attr] = ddict.s[attr] <= ddict['date']# These don't make sense for people just infected (targets), only sources - # Only need to check against the date, since will return False if condition is false (NaN) if source is not None: # This information is only available for people infected by other people, not e.g. importations - ddict['s_age'] = people.age[source] - ddict['t_age'] = people.age[target] - ddict['s_symp'] = people.date_symptomatic[source] <= date - ddict['s_diag'] = people.date_diagnosed[source] <= date - ddict['s_quar'] = people.date_quarantined[source] <= date - ddict['s_sev'] = people.date_severe[source] <= date - ddict['s_crit'] = people.date_critical[source] <= date - ddict['t_quar'] = people.date_quarantined[target] <= date - ddict['s_asymp'] = np.isnan(people.date_symptomatic[source]) - ddict['s_presymp'] = ~ddict['s_asymp'] and ~ddict['s_symp'] # Not asymptomatic and not currently symptomatic + ddict.s.is_asymp = np.isnan(people.date_symptomatic[source]) + ddict.s.is_presymp = ~ddict.s.is_asymp and ~ddict.s.tf_symptomatic # Not asymptomatic and not currently symptomatic self.detailed[target] = ddict From f9f8f56e06fcacd312c344a7fe4868fa10b4c40a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 4 May 2020 15:52:16 -0700 Subject: [PATCH 096/194] include date_known_contact --- covasim/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/base.py b/covasim/base.py index 9afe0d748..0610fcac0 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1057,7 +1057,7 @@ def make_detailed(self, people, reset=False): else: stdict = {'t':target} - attrs = ['age', 'date_symptomatic', 'date_diagnosed', 'date_quarantined', 'date_severe', 'date_critical', 'date_quarantined'] + attrs = ['age', 'date_symptomatic', 'date_diagnosed', 'date_quarantined', 'date_severe', 'date_critical', 'date_known_contact', 'date_quarantined'] for st,stind in stdict.items(): for attr in attrs: ddict[st][attr] = people[attr][stind] From 4e883dbcb6fabd9460f7afe396466acb8e3d2b54 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 4 May 2020 19:35:09 -0700 Subject: [PATCH 097/194] additional detail --- covasim/base.py | 2 +- covasim/people.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/covasim/base.py b/covasim/base.py index 0610fcac0..a36f9102c 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1057,7 +1057,7 @@ def make_detailed(self, people, reset=False): else: stdict = {'t':target} - attrs = ['age', 'date_symptomatic', 'date_diagnosed', 'date_quarantined', 'date_severe', 'date_critical', 'date_known_contact', 'date_quarantined'] + attrs = ['age', 'date_symptomatic', 'date_tested', 'date_diagnosed', 'date_quarantined', 'date_severe', 'date_critical', 'date_known_contact', 'date_quarantined'] for st,stind in stdict.items(): for attr in attrs: ddict[st][attr] = people[attr][stind] diff --git a/covasim/people.py b/covasim/people.py index 96423a40b..7769e3ff9 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -419,6 +419,9 @@ def trace(self, inds, trace_probs, trace_time): p1inds = np.isin(self.contacts[lkey]['p1'], inds).nonzero()[0] # Get all the indices of the pairs that each person is in nzp1inds = np.intersect1d(nzinds, p1inds) p2inds = np.unique(self.contacts[lkey]['p2'][nzp1inds]) # Find their pairing partner + print('hiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii') + print(inds) + print(p2inds) # Check not diagnosed! contact_inds = cvu.binomial_filter(this_trace_prob, p2inds) # Filter the indices according to the probability of being able to trace this layer self.known_contact[contact_inds] = True From de4423889c185fc6a601e64f4e63bd73b36ba4fc Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 4 May 2020 19:49:09 -0700 Subject: [PATCH 098/194] remove debugging --- covasim/people.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/covasim/people.py b/covasim/people.py index 7769e3ff9..913718574 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -419,9 +419,7 @@ def trace(self, inds, trace_probs, trace_time): p1inds = np.isin(self.contacts[lkey]['p1'], inds).nonzero()[0] # Get all the indices of the pairs that each person is in nzp1inds = np.intersect1d(nzinds, p1inds) p2inds = np.unique(self.contacts[lkey]['p2'][nzp1inds]) # Find their pairing partner - print('hiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii') - print(inds) - print(p2inds) + # Check not diagnosed! contact_inds = cvu.binomial_filter(this_trace_prob, p2inds) # Filter the indices according to the probability of being able to trace this layer self.known_contact[contact_inds] = True From d57814e5f3fe0422f24d6cd3e72abdf55fa369aa Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 4 May 2020 20:04:31 -0700 Subject: [PATCH 099/194] working on fixing original plotting --- covasim/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/covasim/base.py b/covasim/base.py index a36f9102c..a6e4a883e 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1082,6 +1082,9 @@ def plot(self): errormsg = 'Please run sim.people.make_detailed_transtree() before calling plotting' raise ValueError(errormsg) + tdict = {} + + detailed = filter(None, self.detailed) df = pd.DataFrame(detailed).rename(columns={'date': 'Day'}) From 248e327d7c68e8575cbfdd90c573221551694ac8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 4 May 2020 20:24:32 -0700 Subject: [PATCH 100/194] fixed previous plotting --- covasim/base.py | 35 ++++-- tests/devtests/transtree_plotting.py | 181 +++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 11 deletions(-) create mode 100755 tests/devtests/transtree_plotting.py diff --git a/covasim/base.py b/covasim/base.py index a6e4a883e..b722366df 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -1052,24 +1052,26 @@ def make_detailed(self, people, reset=False): ddict.s = sc.objdict() # Source ddict.t = sc.objdict() # Target + # 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} - attrs = ['age', 'date_symptomatic', 'date_tested', 'date_diagnosed', 'date_quarantined', 'date_severe', 'date_critical', 'date_known_contact', 'date_quarantined'] + # Pull out each of the attributes relevant to transmission + attrs = ['age', 'date_symptomatic', 'date_tested', 'date_diagnosed', 'date_quarantined', '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_'): - tf_attr = attr.replace('date_', 'tf_') - ddict.s[tf_attr] = ddict.s[attr] <= ddict['date']# These don't make sense for people just infected (targets), only sources + is_attr = attr.replace('date_', 'is_') # Convert date to a boolean, e.g. date_diagnosed -> is_diagnosed + ddict.s[is_attr] = ddict.s[attr] <= ddict['date'] # These don't make sense for people just infected (targets), only sources - if source is not None: # This information is only available for people infected by other people, not e.g. importations ddict.s.is_asymp = np.isnan(people.date_symptomatic[source]) - ddict.s.is_presymp = ~ddict.s.is_asymp and ~ddict.s.tf_symptomatic # Not asymptomatic and not currently symptomatic + 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'] # This is the only target date that it makes sense to define since it can happen before infection self.detailed[target] = ddict @@ -1082,12 +1084,23 @@ def plot(self): errormsg = 'Please run sim.people.make_detailed_transtree() before calling plotting' raise ValueError(errormsg) - tdict = {} - - - detailed = filter(None, self.detailed) - - df = pd.DataFrame(detailed).rename(columns={'date': 'Day'}) + ttlist = [] + for entry in self.detailed: + if entry and entry.source: + tdict = { + 'date': entry.date, + 'layer': entry.layer, + 's_asymp': entry.s.is_asymp, + 's_presymp': entry.s.is_presymp, + 's_sev': entry.s.is_severe, + 's_crit': entry.s.is_critical, + 's_diag': entry.s.is_diagnosed, + 's_quar': entry.s.is_quarantined, + 't_quar': entry.t.is_quarantined, + } + ttlist.append(tdict) + + df = pd.DataFrame(ttlist).rename(columns={'date': 'Day'}) df = df.loc[df['layer'] != 'seed_infection'] df['Stage'] = 'Symptomatic' diff --git a/tests/devtests/transtree_plotting.py b/tests/devtests/transtree_plotting.py new file mode 100755 index 000000000..31535d41e --- /dev/null +++ b/tests/devtests/transtree_plotting.py @@ -0,0 +1,181 @@ +''' +Demonstrate different transtree plotting options +''' + +import covasim as cv +import pylab as pl +import sciris as sc +import numpy as np + + +tstart = sc.tic() + +plot_sim = 0 +verbose = 0 +simple = 1 +animated = 0 +animate = 1 +animate_all = 0 + +iday = 15 +pop_type = 'random' +lkeys = dict( + random='a', + hybrid='hswc' + ) +tp = cv.test_prob(start_day=iday, symp_prob=0.01, asymp_prob=0.0, symp_quar_prob=1.0, asymp_quar_prob=1.0, test_delay=0) +ct = cv.contact_tracing(trace_probs={k:1.0 for k in lkeys[pop_type]}, + trace_time={k:0 for k in lkeys[pop_type]}, + start_day=iday) + +pars = dict( + pop_size = 100, + pop_type = pop_type, + n_days = 60, + contacts = 3, + beta = 0.2, + ) + +labels = ['No interventions', 'Testing only', 'Test + trace'] +sims = sc.objdict() +sims.base = cv.Sim(pars) # Baseline +sims.test = cv.Sim(pars, interventions=tp) # Testing only +sims.trace = cv.Sim(pars, interventions=[tp, ct]) # Testing + contact tracing + +tts = sc.objdict() +for key,sim in sims.items(): + sim.run() + sim.people.make_detailed_transtree() + tts[key] = sim.people.transtree.detailed + if plot_sim: + to_plot = cv.get_sim_plots() + to_plot['Total counts'] = ['cum_infections', 'cum_diagnoses', 'cum_quarantined', 'n_quarantined'] + sim.plot(to_plot=to_plot) + + +#%% Plotting + +if simple: + for sim in sims.values(): + sim.people.transtree.plot() + +if animated: + + colors = sc.vectocolor(sim.n, cmap='parula') + + msize = 10 + suscol = [0.5,0.5,0.5] + plargs = dict(lw=2, alpha=0.5) + idelay = 0.05 + daydelay = 0.2 + pl.rcParams['font.size'] = 18 + + F = sc.objdict() + T = sc.objdict() + D = sc.objdict() + Q = sc.objdict() + + for key in sims.keys(): + + tt = tts[key] + + frames = [list() for i in range(sim.npts)] + tests = [list() for i in range(sim.npts)] + diags = [list() for i in range(sim.npts)] + quars = [list() for i in range(sim.npts)] + + for i,entry in enumerate(tt): + frame = sc.objdict() + dq = sc.objdict() + if entry: + source = entry['source'] + target = entry['target'] + target_date = entry['date'] + if source: + source_date = tt[source]['date'] + else: + source = 0 + source_date = 0 + + frame.x = [source_date, target_date] + frame.y = [source, target] + frame.c = colors[source] # colors[target_date] + frame.e = True + frames[target_date].append(frame) + + dq.t = target + dq.d = target_date + dq.c = colors[target] + date_t = entry.t.date_tested + date_d = entry.t.date_diagnosed + date_q = entry.t.date_known_contact + if ~np.isnan(date_t) and date_t Date: Mon, 4 May 2020 21:26:16 -0700 Subject: [PATCH 101/194] some progress, but nowhere near ready --- covasim/__init__.py | 3 +- covasim/base.py | 9 ++ covasim/plotting.py | 180 ++++++++++++++++++++++++++- tests/devtests/transtree_plotting.py | 131 ++----------------- 4 files changed, 198 insertions(+), 125 deletions(-) diff --git a/covasim/__init__.py b/covasim/__init__.py index b4c8f59c4..876dc2fb9 100644 --- a/covasim/__init__.py +++ b/covasim/__init__.py @@ -10,6 +10,7 @@ from .utils import * from .misc import * from .defaults import * +from .plotting import * from .base import * from .parameters import * from .people import * @@ -17,4 +18,4 @@ from .interventions import * from .sim import * from .run import * -from .plotting import * + diff --git a/covasim/base.py b/covasim/base.py index b722366df..dc5efffc4 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -10,6 +10,7 @@ from . import utils as cvu from . import misc as cvm from . import defaults as cvd +from . import plotting as cvplt # Specify all externally visible classes this file defines __all__ = ['ParsObj', 'Result', 'BaseSim', 'BasePeople', 'Person', 'FlexDict', 'Contacts', 'Layer', 'TransTree'] @@ -1080,6 +1081,10 @@ def make_detailed(self, people, reset=False): def plot(self): ''' Plot the transmission tree ''' + + return cvplt.plot_transtree(self, *args, **kwargs) + + if self.detailed is None: errormsg = 'Please run sim.people.make_detailed_transtree() before calling plotting' raise ValueError(errormsg) @@ -1133,3 +1138,7 @@ def plot(key, title, i): plot(key, title, i+1) return fig + + + def animate(self, *args, **kwargs): + return cvplt.animate_transtree(self, *args, **kwargs) \ No newline at end of file diff --git a/covasim/plotting.py b/covasim/plotting.py index 540ceea6a..6e9901cc5 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -15,7 +15,7 @@ from . import defaults as cvd -__all__ = ['plot_sim', 'plot_scens', 'plot_result', 'plot_compare', 'plotly_sim', 'plotly_people', 'plotly_animate'] +__all__ = ['plot_sim', 'plot_scens', 'plot_result', 'plot_compare', 'plot_transtree', 'animate_transtree', 'plotly_sim', 'plotly_people', 'plotly_animate'] #%% Plotting helper functions @@ -354,6 +354,184 @@ def plot_compare(df, log_scale=True, fig_args=None, plot_args=None, axis_args=No return fig +#%% Transtree functions +def plot_transtree(self, *args, **kwargs): + ''' Plot the transmission tree ''' + + if self.detailed is None: + errormsg = 'Please run sim.people.make_detailed_transtree() before calling plotting' + raise ValueError(errormsg) + + ttlist = [] + for entry in self.detailed: + if entry and entry.source: + tdict = { + 'date': entry.date, + 'layer': entry.layer, + 's_asymp': entry.s.is_asymp, + 's_presymp': entry.s.is_presymp, + 's_sev': entry.s.is_severe, + 's_crit': entry.s.is_critical, + 's_diag': entry.s.is_diagnosed, + 's_quar': entry.s.is_quarantined, + 't_quar': entry.t.is_quarantined, + } + 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' + + df['Severity'] = 'Mild' + df.loc[df['s_sev'], 'Severity'] = 'Severe' + df.loc[df['s_crit'], 'Severity'] = 'Critical' + + fig = pl.figure(figsize=(16,10)) + i=1; r=2; c=3 + + def plot(key, title, i): + dat = df.groupby(['Day', key]).size().unstack(key) + ax = pl.subplot(r,c,i); + dat.plot(ax=ax, legend=None) + pl.legend(title=None) + ax.set_title(title) + + to_plot = { + 'layer':'Layer', + 'Stage':'Source stage', + 's_diag':'Source diagnosed', + 's_quar':'Source quarantined', + 't_quar':'Target quarantined', + 'Severity':'Symptomatic source severity' + } + for i, (key, title) in enumerate(to_plot.items()): + plot(key, title, i+1) + + return fig + +def animate_transtree(transtree): + colors = sc.vectocolor(sim.n, cmap='parula') + + msize = 10 + suscol = [0.5,0.5,0.5] + plargs = dict(lw=2, alpha=0.5) + idelay = 0.05 + daydelay = 0.2 + pl.rcParams['font.size'] = 18 + + F = sc.objdict() + T = sc.objdict() + D = sc.objdict() + Q = sc.objdict() + + for key in sims.keys(): + + tt = tts[key] + + frames = [list() for i in range(sim.npts)] + tests = [list() for i in range(sim.npts)] + diags = [list() for i in range(sim.npts)] + quars = [list() for i in range(sim.npts)] + + for i,entry in enumerate(tt): + frame = sc.objdict() + dq = sc.objdict() + if entry: + source = entry['source'] + target = entry['target'] + target_date = entry['date'] + if source: + source_date = tt[source]['date'] + else: + source = 0 + source_date = 0 + + frame.x = [source_date, target_date] + frame.y = [source, target] + frame.c = colors[source] # colors[target_date] + frame.e = True + frames[target_date].append(frame) + + dq.t = target + dq.d = target_date + dq.c = colors[target] + date_t = entry.t.date_tested + date_d = entry.t.date_diagnosed + date_q = entry.t.date_known_contact + if ~np.isnan(date_t) and date_t Date: Mon, 4 May 2020 22:18:37 -0700 Subject: [PATCH 102/194] working --- covasim/base.py | 84 ++++------- covasim/plotting.py | 213 +++++++++++++-------------- tests/devtests/transtree_plotting.py | 9 +- 3 files changed, 131 insertions(+), 175 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index dc5efffc4..488aaf353 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -534,6 +534,9 @@ def __init__(self, pars=None, pop_size=None, *args, **kwargs): pop_size = 0 pop_size = int(pop_size) self.pop_size = pop_size + if pars is not None: + n_days = pars['n_days'] + self.n_days = n_days # Other initialization self.t = 0 # Keep current simulation time @@ -541,7 +544,7 @@ def __init__(self, pars=None, pop_size=None, *args, **kwargs): self.meta = cvd.PeopleMeta() # Store list of keys and dtypes self.contacts = None self.init_contacts() # Initialize the contacts - self.transtree = TransTree(pop_size=pop_size) # Initialize the transmission tree + self.transtree = TransTree(pop_size=pop_size, n_days=n_days) # Initialize the transmission tree return @@ -1001,10 +1004,12 @@ class TransTree(sc.prettyobj): pop_size (int): the number of people in the population ''' - def __init__(self, pop_size): + def __init__(self, pop_size, n_days): self.linelist = [None]*pop_size self.targets = [[] for p in range(len(self))] # Make a list of empty lists self.detailed = None + self.pop_size = pop_size + self.n_days = n_days return @@ -1079,66 +1084,29 @@ def make_detailed(self, people, reset=False): return - def plot(self): + def plot(self, *args, **kwargs): ''' Plot the transmission tree ''' - return cvplt.plot_transtree(self, *args, **kwargs) - if self.detailed is None: - errormsg = 'Please run sim.people.make_detailed_transtree() before calling plotting' - raise ValueError(errormsg) - - ttlist = [] - for entry in self.detailed: - if entry and entry.source: - tdict = { - 'date': entry.date, - 'layer': entry.layer, - 's_asymp': entry.s.is_asymp, - 's_presymp': entry.s.is_presymp, - 's_sev': entry.s.is_severe, - 's_crit': entry.s.is_critical, - 's_diag': entry.s.is_diagnosed, - 's_quar': entry.s.is_quarantined, - 't_quar': entry.t.is_quarantined, - } - 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' - - df['Severity'] = 'Mild' - df.loc[df['s_sev'], 'Severity'] = 'Severe' - df.loc[df['s_crit'], 'Severity'] = 'Critical' - - fig = pl.figure(figsize=(16,10)) - i=1; r=2; c=3 - - def plot(key, title, i): - dat = df.groupby(['Day', key]).size().unstack(key) - ax = pl.subplot(r,c,i); - dat.plot(ax=ax, legend=None) - pl.legend(title=None) - ax.set_title(title) - - to_plot = { - 'layer':'Layer', - 'Stage':'Source stage', - 's_diag':'Source diagnosed', - 's_quar':'Source quarantined', - 't_quar':'Target quarantined', - 'Severity':'Symptomatic source severity' - } - for i, (key, title) in enumerate(to_plot.items()): - plot(key, title, i+1) - - return fig + def animate(self, *args, **kwargs): + ''' + Animate the transmission tree. + Args: + animate (bool): whether to animate the plot (otherwise, show when finished) + verbose (bool): print out progress of each frame + markersize (int): size of the markers + sus_color (list): color for susceptibles + fig_args (dict): arguments passed to pl.figure() + axis_args (dict): arguments passed to pl.subplots_adjust() + plot_args (dict): arguments passed to pl.plot() + delay (float): delay between frames in seconds + font_size (int): size of the font + colors (list): color of each person + cmap (str): colormap for each person (if colors is not supplied) - def animate(self, *args, **kwargs): + Returns: + fig: the figure object + ''' return cvplt.animate_transtree(self, *args, **kwargs) \ No newline at end of file diff --git a/covasim/plotting.py b/covasim/plotting.py index 6e9901cc5..c52919e52 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -7,6 +7,7 @@ ''' import numpy as np +import pandas as pd import pylab as pl import sciris as sc import datetime as dt @@ -355,15 +356,15 @@ def plot_compare(df, log_scale=True, fig_args=None, plot_args=None, axis_args=No #%% Transtree functions -def plot_transtree(self, *args, **kwargs): +def plot_transtree(tt, *args, **kwargs): ''' Plot the transmission tree ''' - if self.detailed is None: + if tt.detailed is None: errormsg = 'Please run sim.people.make_detailed_transtree() before calling plotting' raise ValueError(errormsg) ttlist = [] - for entry in self.detailed: + for entry in tt.detailed: if entry and entry.source: tdict = { 'date': entry.date, @@ -412,123 +413,113 @@ def plot(key, title, i): return fig -def animate_transtree(transtree): - colors = sc.vectocolor(sim.n, cmap='parula') - - msize = 10 - suscol = [0.5,0.5,0.5] - plargs = dict(lw=2, alpha=0.5) - idelay = 0.05 - daydelay = 0.2 - pl.rcParams['font.size'] = 18 - - F = sc.objdict() - T = sc.objdict() - D = sc.objdict() - Q = sc.objdict() - - for key in sims.keys(): - - tt = tts[key] - - frames = [list() for i in range(sim.npts)] - tests = [list() for i in range(sim.npts)] - diags = [list() for i in range(sim.npts)] - quars = [list() for i in range(sim.npts)] - - for i,entry in enumerate(tt): - frame = sc.objdict() - dq = sc.objdict() - if entry: - source = entry['source'] - target = entry['target'] - target_date = entry['date'] - if source: - source_date = tt[source]['date'] - else: - source = 0 - source_date = 0 - - frame.x = [source_date, target_date] - frame.y = [source, target] - frame.c = colors[source] # colors[target_date] - frame.e = True - frames[target_date].append(frame) - - dq.t = target - dq.d = target_date - dq.c = colors[target] - date_t = entry.t.date_tested - date_d = entry.t.date_diagnosed - date_q = entry.t.date_known_contact - if ~np.isnan(date_t) and date_t Date: Mon, 4 May 2020 22:21:38 -0700 Subject: [PATCH 103/194] generate synthpops populations of arbitrary size --- covasim/population.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/population.py b/covasim/population.py index 1f061499a..9d76db28c 100644 --- a/covasim/population.py +++ b/covasim/population.py @@ -275,7 +275,7 @@ def make_synthpop(sim): ''' Make a population using synthpops, including contacts ''' import synthpops as sp # Optional import pop_size = sim['pop_size'] - population = sp.make_population(n=pop_size) + population = sp.make_population(n=pop_size, generate=True) uids, ages, sexes, contacts = [], [], [], [] for uid,person in population.items(): uids.append(uid) From bce0f3d0f8a3d8524a151714291cf9da9181f389 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 4 May 2020 22:56:05 -0700 Subject: [PATCH 104/194] working --- CHANGELOG.rst | 7 +++ covasim/plotting.py | 75 ++++++++++++++++------------ covasim/version.py | 2 +- tests/devtests/transtree_plotting.py | 3 +- 4 files changed, 51 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f0fe4135b..8461de3c9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,13 @@ What's new All notable changes to the codebase are documented in this file. Note: in many cases, changes from multiple patch versions are grouped together, so numbering will not be strictly consecutive. +Version 0.30.4 (2020-05-04) +--------------------------- +- Changed the detailed transmission tree (``sim.people.transtree.detailed``) to include much more information. +- Added animation method to transmission tree: ``sim.people.transtree.animate()``. +- Added support to generate populations on the fly in SynthPops. + + Version 0.30.3 (2020-05-03) --------------------------- - Fixed bugs in dynamic scaling; see ``tests/devtests/dev_test_rescaling.py``. When using ``pop_scale>1``, the recommendation is now to use ``rescale=True``. diff --git a/covasim/plotting.py b/covasim/plotting.py index c52919e52..958696152 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -357,7 +357,9 @@ def plot_compare(df, log_scale=True, fig_args=None, plot_args=None, axis_args=No #%% Transtree functions def plot_transtree(tt, *args, **kwargs): - ''' Plot the transmission tree ''' + ''' Plot the transmission tree; see TransTree.plot() for documentation ''' + + fig_args = kwargs.get('fig_args', dict(figsize=(16,10))) if tt.detailed is None: errormsg = 'Please run sim.people.make_detailed_transtree() before calling plotting' @@ -390,7 +392,7 @@ def plot_transtree(tt, *args, **kwargs): df.loc[df['s_sev'], 'Severity'] = 'Severe' df.loc[df['s_crit'], 'Severity'] = 'Critical' - fig = pl.figure(figsize=(16,10)) + fig = pl.figure(**fig_args) i=1; r=2; c=3 def plot(key, title, i): @@ -415,12 +417,12 @@ def plot(key, title, i): def animate_transtree(tt, **kwargs): - ''' Plot an animation of the transmission tree ''' + ''' Plot an animation of the transmission tree; see TransTree.animate() for documentation ''' # Settings animate = kwargs.get('animate', True) verbose = kwargs.get('verbose', False) - markersize = kwargs.get('markersize', 10) + msize = kwargs.get('markersize', 10) sus_color = kwargs.get('sus_color', [0.5, 0.5, 0.5]) fig_args = kwargs.get('fig_args', dict(figsize=(24,16))) axis_args = kwargs.get('axis_args', dict(left=0.10, bottom=0.05, right=0.85, top=0.97, wspace=0.25, hspace=0.25)) @@ -441,42 +443,48 @@ def animate_transtree(tt, **kwargs): quars = [list() for i in range(n)] # Construct each frame of the animation - for i,entry in enumerate(tt.detailed): + for i,entry in enumerate(tt.detailed): # Loop over every person frame = sc.objdict() - dq = sc.objdict() + tdq = sc.objdict() # Short for "tested, diagnosed, or quarantined" + + # This person became infected if entry: source = entry['source'] target = entry['target'] target_date = entry['date'] - if source: + if source: # Seed infections and importations won't have a source source_date = tt.detailed[source]['date'] else: source = 0 source_date = 0 + # Construct this frame frame.x = [source_date, target_date] frame.y = [source, target] - frame.c = colors[source] # colors[target_date] - frame.e = True + frame.c = colors[source] + frame.i = True # If this person is infected frames[target_date].append(frame) - dq.t = target - dq.d = target_date - dq.c = colors[target] + # Handle testing, diagnosis, and quarantine + tdq.t = target + tdq.d = target_date + tdq.c = colors[target] date_t = entry.t.date_tested date_d = entry.t.date_diagnosed date_q = entry.t.date_known_contact - if ~np.isnan(date_t) and date_t Date: Mon, 4 May 2020 23:44:55 -0700 Subject: [PATCH 105/194] fix intervention order --- covasim/interventions.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 219566859..ea2529e55 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -456,6 +456,11 @@ def __init__(self, daily_tests, symp_test=100.0, quar_test=1.0, sensitivity=1.0, def initialize(self, sim): ''' Fix the dates and number of tests ''' + # Handle days + self.start_day = sim.day(self.start_day) + self.end_day = sim.day(self.end_day) + self.days = [self.start_day, self.end_day] + # Process daily tests -- has to be here rather than init so have access to the sim object if sc.isnumber(self.daily_tests): # If a number, convert to an array self.daily_tests = np.array([int(self.daily_tests)]*sim.npts) @@ -465,10 +470,6 @@ def initialize(self, sim): dateindex = pd.date_range(start_date, end_date) self.daily_tests = self.daily_tests.reindex(dateindex, fill_value=0).to_numpy() - # Handle days - 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.initialized = True return @@ -525,13 +526,13 @@ class test_prob(Intervention): **Example**:: - interv = cv.test_prob(symptomatic_prob=0.1, asymptomatic_prob=0.01) # Test 10% of symptomatics and 1% of asymptomatics + interv = cv.test_prob(symp_prob=0.1, asymp_prob=0.01) # Test 10% of symptomatics and 1% of asymptomatics interv = cv.test_prob(symp_quar_prob=0.4) # Test 40% of those in quarantine with symptoms Returns: Intervention ''' - def __init__(self, symp_prob=0, asymp_prob=0, symp_quar_prob=None, asymp_quar_prob=None, test_sensitivity=1.0, loss_prob=0.0, test_delay=1, start_day=0, end_day=None, do_plot=None): + def __init__(self, symp_prob, asymp_prob=0.0, symp_quar_prob=None, asymp_quar_prob=None, test_sensitivity=1.0, loss_prob=0.0, test_delay=1, start_day=0, end_day=None, do_plot=None): super().__init__(do_plot=do_plot) self.symp_prob = symp_prob self.asymp_prob = asymp_prob @@ -596,8 +597,12 @@ class contact_tracing(Intervention): contact_red (float): not implemented currently, but could potentially scale contact in this intervention do_plot (bool): whether or not to plot ''' - def __init__(self, trace_probs, trace_time, start_day=0, end_day=None, contact_red=None, do_plot=None): + def __init__(self, trace_probs=None, trace_time=None, start_day=0, end_day=None, contact_red=None, do_plot=None): super().__init__(do_plot=do_plot) + if trace_probs is None: + trace_probs = 1.0 + if trace_time is None: + trace_time = 0.0 self.trace_probs = trace_probs self.trace_time = trace_time self.contact_red = contact_red From 5be0669fb17e3f1d4bc41aa80de15e622c5ae62e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 00:45:10 -0700 Subject: [PATCH 106/194] added showcase --- covasim/interventions.py | 15 ++-- tests/devtests/intervention_showcase.py | 95 +++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 7 deletions(-) create mode 100755 tests/devtests/intervention_showcase.py diff --git a/covasim/interventions.py b/covasim/interventions.py index ea2529e55..952a69642 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -164,8 +164,7 @@ class dynamic_pars(Intervention): **Examples**:: - interv = cv.dynamic_pars({'diag_factor':{'days':30, 'vals':0.5}, 'cont_factor':{'days':30, 'vals':0.5}}) # Starting day 30, make diagnosed people and people with contacts half as likely to transmit - interv = cv.dynamic_pars({'beta':{'days':[14, 28], 'vals':[0.005, 0.015]}}) # On day 14, change beta to 0.005, and on day 28 change it back to 0.015 + interv = cv.dynamic_pars({'beta':{'days':[14, 28], 'vals':[0.005, 0.015]}, 'diag_factor':{'days':30, 'vals':0.0}}) # Change beta, and make diagnosed people stop transmitting ''' def __init__(self, pars, do_plot=None): @@ -193,11 +192,11 @@ def apply(self, sim): for parkey,parval in self.pars.items(): inds = sc.findinds(parval['days'], t) # Look for matches if len(inds): - if len(inds)>1: raise ValueError(f'Duplicate days are not allowed for Dynamic interventions (day={t}, indices={inds})') else: match = inds[0] + self.days.append(t) val = parval['vals'][match] if isinstance(val, dict): sim[parkey].update(val) # Set the parameter if a nested dict @@ -211,7 +210,7 @@ class sequence(Intervention): This is an example of a meta-intervention which switches between a sequence of interventions. Args: - days (list): the days on which to apply each intervention + days (list): the days on which to start applying each intervention interventions (list): the interventions to apply on those days do_plot (bool): whether or not to plot the intervention @@ -228,7 +227,6 @@ def __init__(self, days, interventions, do_plot=None): assert len(days) == len(interventions) self.days = days self.interventions = interventions - self._cum_days = np.cumsum(days) self._store_args() return @@ -236,13 +234,16 @@ def __init__(self, days, interventions, do_plot=None): def initialize(self, sim): ''' Fix the dates ''' self.days = [sim.day(day) for day in self.days] + self.days_arr = np.array(self.days + [sim.npts]) self.initialized = True return def apply(self, sim): - idx = np.argmax(self._cum_days > sim.t) # Index of the intervention to apply on this day - self.interventions[idx].apply(sim) + inds = sc.findinds(self.days_arr <= sim.t) + if len(inds): + idx = max(inds) # Index of the intervention to apply on this day + self.interventions[idx].apply(sim) return diff --git a/tests/devtests/intervention_showcase.py b/tests/devtests/intervention_showcase.py new file mode 100755 index 000000000..35a6c5f52 --- /dev/null +++ b/tests/devtests/intervention_showcase.py @@ -0,0 +1,95 @@ +''' +Demonstrate all interventions, taken from intervention docstrings +''' + +#%% Housekeeping + +import sciris as sc +import pylab as pl +import covasim as cv + +do_plot = 1 +verbose = 0 + +pars = sc.objdict( + pop_size = 10e3, + pop_infected = 100, + pop_type = 'hybrid', + n_days = 90, + ) + + +#%% Define the interventions + +# 1. Dynamic pars +i00 = cv.test_prob(start_day=5, symp_prob=0.3) +i01 = cv.dynamic_pars({'beta':{'days':[40, 50], 'vals':[0.005, 0.015]}, 'diag_factor':{'days':30, 'vals':0.0}}) # Starting day 30, make diagnosed people stop transmitting + + +# 2. Sequence +i02 = cv.sequence(days=[20, 40, 60], interventions=[ + cv.test_num(daily_tests=[20]*pars.n_days), + cv.test_prob(symp_prob=0.0), + cv.test_prob(symp_prob=0.2), + ]) + + +# 3. Change beta +i03 = cv.change_beta([30, 50], [0.0, 1], layers='h') +i04 = cv.change_beta([30, 40, 60], [0.0, 1.0, 0.5]) + + +# 4. Clip edges -- should match the change_beta scenarios +i05 = cv.clip_edges(start_day=30, end_day=50, change={'h':0.0}) +i06 = cv.clip_edges(start_day=30, end_day=40, change=0.0) +i07 = cv.clip_edges(start_day=60, end_day=None, change=0.5) + + +# 5. Test number +i08 = cv.test_num(daily_tests=[100, 100, 100, 0, 0, 0]*(pars.n_days//6)) + + +# 6. Test probability +i09 = cv.test_prob(symp_prob=0.1) + + +# 7. Contact tracing +i10 = cv.test_prob(start_day=20, symp_prob=0.01, asymp_prob=0.0, symp_quar_prob=1.0, asymp_quar_prob=1.0, test_delay=0) +i11 = 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 +i12 = cv.clip_edges(start_day=18, change={'s':0.0}) # Close schools +i13 = cv.clip_edges(start_day=20, end_day=32, change={'w':0.7, 'c':0.7}) # Reduce work and community +i14 = cv.clip_edges(start_day=32, end_day=45, change={'w':0.3, 'c':0.3}) # Reduce work and community more +i15 = cv.clip_edges(start_day=45, end_day=None, change={'w':0.9, 'c':0.9}) # Reopen work and community more +i16 = 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 +i17 = 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 + + +#%% Create and run the simulations +sims = sc.objdict() +sims.dynamic = cv.Sim(pars=pars, interventions=[i00, i01]) +sims.sequence = cv.Sim(pars=pars, interventions=i02) +sims.change_beta1 = cv.Sim(pars=pars, interventions=i03) +sims.clip_edges1 = cv.Sim(pars=pars, interventions=i05) # Roughly equivalent to change_beta1 +sims.change_beta2 = cv.Sim(pars=pars, interventions=i04) +sims.clip_edges2 = cv.Sim(pars=pars, interventions=[i06, i07]) # Roughly euivalent to change_beta2 +sims.test_num = cv.Sim(pars=pars, interventions=i08) +sims.test_prob = cv.Sim(pars=pars, interventions=i09) +sims.tracing = cv.Sim(pars=pars, interventions=[i10, i11]) +sims.combo = cv.Sim(pars=pars, interventions=[i12, i13, i14, i15, i16, i17]) + +for key,sim in sims.items(): + sim.label = key + sim.run(verbose=verbose) + + +#%% Plotting +if do_plot: + for sim in sims.values(): + print(f'Running {sim.label}...') + sim.plot() + fig = pl.gcf() + fig.axes[0].set_title(f'Simulation: {sim.label}') + From 0e3124baabbe117b2e7c066190d23e3ec0c07a8f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 00:49:45 -0700 Subject: [PATCH 107/194] update changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8461de3c9..08ac37538 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ Version 0.30.4 (2020-05-04) - Changed the detailed transmission tree (``sim.people.transtree.detailed``) to include much more information. - Added animation method to transmission tree: ``sim.people.transtree.animate()``. - Added support to generate populations on the fly in SynthPops. +- Adjusted the default arguments for ``test_prob`` and fixed a bug with ``test_num`` not accepting date input. Version 0.30.3 (2020-05-03) From 2a82ec1ff2b3597a0ebacfe50ae381f0f25132e5 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 00:50:10 -0700 Subject: [PATCH 108/194] chmod --- tests/devtests/dev_test_date_setting.py | 0 tests/devtests/dev_test_rescaling.py | 0 tests/devtests/intervention_showcase.py | 0 tests/devtests/multisim_compare.py | 0 tests/devtests/plotly_tests.py | 0 tests/devtests/test_run_until.py | 0 tests/devtests/transtree_plotting.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 tests/devtests/dev_test_date_setting.py mode change 100755 => 100644 tests/devtests/dev_test_rescaling.py mode change 100755 => 100644 tests/devtests/intervention_showcase.py mode change 100755 => 100644 tests/devtests/multisim_compare.py mode change 100755 => 100644 tests/devtests/plotly_tests.py mode change 100755 => 100644 tests/devtests/test_run_until.py mode change 100755 => 100644 tests/devtests/transtree_plotting.py diff --git a/tests/devtests/dev_test_date_setting.py b/tests/devtests/dev_test_date_setting.py old mode 100755 new mode 100644 diff --git a/tests/devtests/dev_test_rescaling.py b/tests/devtests/dev_test_rescaling.py old mode 100755 new mode 100644 diff --git a/tests/devtests/intervention_showcase.py b/tests/devtests/intervention_showcase.py old mode 100755 new mode 100644 diff --git a/tests/devtests/multisim_compare.py b/tests/devtests/multisim_compare.py old mode 100755 new mode 100644 diff --git a/tests/devtests/plotly_tests.py b/tests/devtests/plotly_tests.py old mode 100755 new mode 100644 diff --git a/tests/devtests/test_run_until.py b/tests/devtests/test_run_until.py old mode 100755 new mode 100644 diff --git a/tests/devtests/transtree_plotting.py b/tests/devtests/transtree_plotting.py old mode 100755 new mode 100644 From 4345e853c4a756450b0e93c16d79d4edba6ec0b4 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 00:50:47 -0700 Subject: [PATCH 109/194] updated changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 08ac37538..297875e91 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,7 @@ Version 0.30.4 (2020-05-04) - Added animation method to transmission tree: ``sim.people.transtree.animate()``. - Added support to generate populations on the fly in SynthPops. - Adjusted the default arguments for ``test_prob`` and fixed a bug with ``test_num`` not accepting date input. +- Added ``tests/devtests/intervention_showcase.py``, using and comparing all available interventions. Version 0.30.3 (2020-05-03) From 61ddc358c79ed99e3b1f13a79797cad8c33f0455 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 10:53:52 -0700 Subject: [PATCH 110/194] moved load_data to misc --- covasim/misc.py | 65 ++++++++++++++++++++++++++++++++++++++++--- covasim/parameters.py | 64 ++---------------------------------------- covasim/sim.py | 2 +- 3 files changed, 65 insertions(+), 66 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index d1b8b22b2..ac22ee51b 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -4,13 +4,70 @@ import datetime as dt import numpy as np -import pylab as pl # Used by fixaxis() -import sciris as sc # Used by fixaxis() -import scipy.stats as sps # Used by poisson_test() +import pandas as pd +import pylab as pl +import sciris as sc +import scipy.stats as sps from . import version as cvver -__all__ = ['date', 'daydiff', 'load', 'save', 'check_version', 'git_info', 'fixaxis', 'get_doubling_time', 'poisson_test'] +__all__ = ['load_data', 'date', 'daydiff', 'load', 'save', 'check_version', 'git_info', 'fixaxis', 'get_doubling_time', 'poisson_test'] + + +def load_data(filename, columns=None, calculate=True, verbose=True, **kwargs): + ''' + Load data for comparing to the model output. + + Args: + filename (str): the name of the file to load (either Excel or CSV) + columns (list): list of column names (otherwise, load all) + calculate (bool): whether or not to calculate cumulative values from daily counts + kwargs (dict): passed to pd.read_excel() + + Returns: + data (dataframe): pandas dataframe of the loaded data + ''' + + # Load data + if filename.lower().endswith('csv'): + raw_data = pd.read_csv(filename, **kwargs) + elif filename.lower().endswith('xlsx'): + raw_data = pd.read_excel(filename, **kwargs) + else: + errormsg = f'Currently loading is only supported from .csv and .xlsx files, not {filename}' + raise NotImplementedError(errormsg) + + # Confirm data integrity and simplify + if columns is not None: + for col in columns: + if col not in raw_data.columns: + errormsg = f'Column "{col}" is missing from the loaded data' + raise ValueError(errormsg) + data = raw_data[columns] + else: + data = raw_data + + # Calculate any cumulative columns that are missing + if calculate: + columns = data.columns + for col in columns: + if col.startswith('new'): + cum_col = col.replace('new_', 'cum_') + if cum_col not in columns: + data[cum_col] = np.cumsum(data[col]) + if verbose: + print(f' Automatically adding cumulative column {cum_col} from {col}') + + # Ensure required columns are present + if 'date' not in data.columns: + errormsg = f'Required column "date" not found; columns are {data.columns}' + raise ValueError(errormsg) + else: + data['date'] = pd.to_datetime(data['date']).dt.date + + data.set_index('date', inplace=True) + + return data def date(obj, *args, **kwargs): diff --git a/covasim/parameters.py b/covasim/parameters.py index 87ab54188..6bb55c744 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -3,10 +3,9 @@ ''' import numpy as np -import pandas as pd -__all__ = ['make_pars', 'reset_layer_pars', 'get_prognoses', 'load_data'] +__all__ = ['make_pars', 'reset_layer_pars', 'get_prognoses'] def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): @@ -158,8 +157,8 @@ def get_prognoses(by_age=True): ) else: prognoses = dict( - age_cutoffs = np.array([10, 20, 30, 40, 50, 60, 70, 80, max_age]), # Age cutoffs - sus_ORs = np.array([1., 1., 1., 1., 1., 1., 2., 2., 2.]), # Odds ratios for relative susceptibility - from https://www.medrxiv.org/content/10.1101/2020.03.03.20028423v3.full.pdf, no significant differences by age + age_cutoffs = np.array([10, 20, 30, 40, 50, 60, 70, 80, max_age]), # Age cutoffs (upper limits) + sus_ORs = np.array([0.34, 0.67, 1.0, 1.0, 1.0, 1.0, 1.24, 1.47, 1.47]), # Odds ratios for relative susceptibility - from https://science.sciencemag.org/content/early/2020/05/04/science.abb8001; 10-20 and 60-70 bins are the average across the ORs symp_probs = np.array([0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90]), # Overall probability of developing symptoms (based on https://www.medrxiv.org/content/10.1101/2020.03.24.20043018v1.full.pdf, scaled for overall symptomaticity) severe_probs = np.array([0.00050, 0.00165, 0.00720, 0.02080, 0.03430, 0.07650, 0.13280, 0.20655, 0.24570]), # Overall probability of developing severe symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) crit_probs = np.array([0.00003, 0.00008, 0.00036, 0.00104, 0.00216, 0.00933, 0.03639, 0.08923, 0.17420]), # Overall probability of developing critical symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) @@ -171,60 +170,3 @@ def get_prognoses(by_age=True): prognoses['severe_probs'] /= prognoses['symp_probs'] # Conditional probability of symptoms becoming severe, given symptomatic return prognoses - - -def load_data(filename, columns=None, calculate=True, verbose=True, **kwargs): - ''' - Load data for comparing to the model output. - - Args: - filename (str): the name of the file to load (either Excel or CSV) - columns (list): list of column names (otherwise, load all) - calculate (bool): whether or not to calculate cumulative values from daily counts - kwargs (dict): passed to pd.read_excel() - - Returns: - data (dataframe): pandas dataframe of the loaded data - ''' - - # Load data - if filename.lower().endswith('csv'): - raw_data = pd.read_csv(filename, **kwargs) - elif filename.lower().endswith('xlsx'): - raw_data = pd.read_excel(filename, **kwargs) - else: - errormsg = f'Currently loading is only supported from .csv and .xlsx files, not {filename}' - raise NotImplementedError(errormsg) - - # Confirm data integrity and simplify - if columns is not None: - for col in columns: - if col not in raw_data.columns: - errormsg = f'Column "{col}" is missing from the loaded data' - raise ValueError(errormsg) - data = raw_data[columns] - else: - data = raw_data - - # Calculate any cumulative columns that are missing - if calculate: - columns = data.columns - for col in columns: - if col.startswith('new'): - cum_col = col.replace('new_', 'cum_') - if cum_col not in columns: - data[cum_col] = np.cumsum(data[col]) - if verbose: - print(f' Automatically adding cumulative column {cum_col} from {col}') - - # Ensure required columns are present - if 'date' not in data.columns: - errormsg = f'Required column "date" not found; columns are {data.columns}' - raise ValueError(errormsg) - else: - data['date'] = pd.to_datetime(data['date']).dt.date - - data.set_index('date', inplace=True) - - return data - diff --git a/covasim/sim.py b/covasim/sim.py index a4f9c023c..d73d31d0c 100755 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -99,7 +99,7 @@ def load_data(self, datafile=None, datacols=None, **kwargs): ''' Load the data to calibrate against, if provided ''' self.datafile = datafile # Store this if datafile is not None: # If a data file is provided, load it - self.data = cvpar.load_data(filename=datafile, columns=datacols, **kwargs) + self.data = cvm.load_data(filename=datafile, columns=datacols, **kwargs) return From 682db595bbab6654c9354f73453fcb9f44606807 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 11:14:04 -0700 Subject: [PATCH 111/194] convert test_baselines to pandas --- tests/test_baselines.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_baselines.py b/tests/test_baselines.py index 7f0559226..28c54efcb 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -3,6 +3,7 @@ """ import sciris as sc +import pandas as pd import covasim as cv do_save = False @@ -61,11 +62,8 @@ def test_baseline(): errormsg = '\nThe following values have changed between old and new!\n' errormsg += 'Please rerun "tests/update_baseline" if this is intentional.\n' errormsg += 'Mismatches:\n' - space = ' '*17 - for mkey,mval in mismatches.items(): - errormsg += f' {mkey}:\n' - errormsg += f'{space}old = {mval["old"]}\n' - errormsg += f'{space}new = {mval["new"]}\n' + df = pd.DataFrame.from_dict(mismatches).transpose() + errormsg += str(df) # Raise an error if mismatches were found if errormsg: From 897588628caf5f121434e6f680ae408a2c6a7005 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 11:35:10 -0700 Subject: [PATCH 112/194] updated mismatch message --- tests/test_baselines.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/test_baselines.py b/tests/test_baselines.py index 28c54efcb..fcabd39f2 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -3,6 +3,7 @@ """ import sciris as sc +import numpy as np import pandas as pd import covasim as cv @@ -63,6 +64,46 @@ def test_baseline(): errormsg += 'Please rerun "tests/update_baseline" if this is intentional.\n' errormsg += 'Mismatches:\n' df = pd.DataFrame.from_dict(mismatches).transpose() + ratio = [] + change = [] + small_change = 1e-3 # Define a small change, e.g. a rounding error + for mdict in mismatches.values(): + old = mdict['old'] + new = mdict['new'] + if sc.isnumber(new) and sc.isnumber(old) and old>0: + this_ratio = new/old + abs_ratio = max(this_ratio, 1.0/this_ratio) + + # Set the character to use + if abs_ratio old: + change_char = '↑' + elif new < old: + change_char = '↓' + else: + errormsg = f'Could not determine relationship between old={old} and new={new}' + raise ValueError(errormsg) + + # Set how many repeats it should have + repeats = 1 + if abs_ratio >= 1.1: + repeats = 2 + if abs_ratio >= 2: + repeats = 3 + if abs_ratio >= 10: + repeats = 4 + + this_change = change_char*repeats + else: + this_ratio = np.nan + this_change = 'N/A' + + ratio.append(this_ratio) + change.append(this_change) + + df['ratio'] = ratio + df['change'] = change errormsg += str(df) # Raise an error if mismatches were found From aaa3f1f1d220ac2504f01cb3a57ea2fa89f8d2d4 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 11:55:52 -0700 Subject: [PATCH 113/194] added transmission ORs --- covasim/parameters.py | 4 ++-- covasim/people.py | 8 +------- covasim/sim.py | 17 ++++++++++------- covasim/utils.py | 8 ++++---- tests/test_baselines.py | 14 ++++++++------ 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 6bb55c744..bf011fa14 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -142,7 +142,6 @@ def get_prognoses(by_age=True): Returns: prog_pars (dict): the dictionary of prognosis probabilities - ''' max_age = 120 # For the sake of having a finite age cutoff @@ -158,7 +157,8 @@ def get_prognoses(by_age=True): else: prognoses = dict( age_cutoffs = np.array([10, 20, 30, 40, 50, 60, 70, 80, max_age]), # Age cutoffs (upper limits) - sus_ORs = np.array([0.34, 0.67, 1.0, 1.0, 1.0, 1.0, 1.24, 1.47, 1.47]), # Odds ratios for relative susceptibility - from https://science.sciencemag.org/content/early/2020/05/04/science.abb8001; 10-20 and 60-70 bins are the average across the ORs + sus_ORs = np.array([0.34, 0.67, 1.00, 1.00, 1.00, 1.00, 1.24, 1.47, 1.47]), # Odds ratios for relative susceptibility -- from https://science.sciencemag.org/content/early/2020/05/04/science.abb8001; 10-20 and 60-70 bins are the average across the ORs + trans_ORs = np.array([1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00]), # Odds ratios for relative transmissibility -- no evidence of differences symp_probs = np.array([0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90]), # Overall probability of developing symptoms (based on https://www.medrxiv.org/content/10.1101/2020.03.24.20043018v1.full.pdf, scaled for overall symptomaticity) severe_probs = np.array([0.00050, 0.00165, 0.00720, 0.02080, 0.03430, 0.07650, 0.13280, 0.20655, 0.24570]), # Overall probability of developing severe symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) crit_probs = np.array([0.00003, 0.00008, 0.00036, 0.00104, 0.00216, 0.00933, 0.03639, 0.08923, 0.17420]), # Overall probability of developing critical symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) diff --git a/covasim/people.py b/covasim/people.py index 52bffddf9..c8274dd86 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -75,7 +75,7 @@ def find_cutoff(age_cutoffs, age): self.crit_prob[:] = prognoses['crit_probs'][inds] self.death_prob[:] = prognoses['death_probs'][inds] self.rel_sus[:] = prognoses['sus_ORs'][inds] # Default susceptibilities - self.rel_trans[:] = 0.0 # By default: cannot transmit + self.rel_trans[:] = prognoses['trans_ORs'][inds] # Default transmissibilities return @@ -193,7 +193,6 @@ def check_recovery(self): self.severe[inds] = False self.critical[inds] = False self.recovered[inds] = True - self.rel_trans[inds] = 0.0 return len(inds) @@ -207,7 +206,6 @@ def check_death(self): self.critical[inds] = False self.recovered[inds] = False self.dead[inds] = True - self.rel_trans[inds] = 0.0 return len(inds) @@ -258,9 +256,6 @@ def make_susceptible(self, inds): for key in self.meta.dates + self.meta.durs: self[key][inds] = np.nan - self.rel_sus[inds] = 1.0 # By default: is susceptible - self.rel_trans[inds] = 0.0 # By default: cannot transmit - return @@ -289,7 +284,6 @@ def infect(self, inds, bed_max=None, verbose=True): # Set states self.susceptible[inds] = False self.exposed[inds] = True - self.rel_sus[inds] = 0.0 # Not susceptible after becoming infected self.date_exposed[inds] = self.t # Calculate how long before this person can infect other people diff --git a/covasim/sim.py b/covasim/sim.py index d73d31d0c..e49f6209a 100755 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -383,20 +383,23 @@ 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 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 + for lkey,layer in contacts.items(): sources = layer['p1'] targets = layer['p2'] betas = layer['beta'] - # Compute relative transmission and susceptibility - rel_trans = people.rel_trans - rel_sus = people.rel_sus - symp = people.symptomatic - diag = people.diagnosed - quar = people.quarantined quar_eff = cvd.default_float(self['quar_eff'][lkey]) beta_layer = cvd.default_float(self['beta_layer'][lkey]) - rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, diag_factor, quar_eff) + rel_trans, rel_sus = cvu.compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, diag_factor, quar_eff) # Calculate actual transmission target_inds, edge_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! diff --git a/covasim/utils.py b/covasim/utils.py index b7875281d..b7e58effe 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -68,14 +68,14 @@ def compute_viral_load(t, time_start, time_recovered, time_dead, frac_time, return load -@nb.njit( (nbfloat[:], nbfloat[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat), cache=True) -def compute_trans_sus(rel_trans, rel_sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, diag_factor, quar_trans): +@nb.njit( (nbfloat[:], nbfloat[:], nbbool[:], nbbool[:], nbfloat, nbfloat[:], nbbool[:], nbbool[:], nbbool[:], nbfloat, nbfloat, nbfloat), cache=True) +def compute_trans_sus(rel_trans, rel_sus, inf, sus, beta_layer, viral_load, symp, diag, quar, asymp_factor, diag_factor, quar_trans): ''' 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_diag = ~diag + diag * diag_factor # Diagnosis factor, changes e.g. [0,1] with a factor of 0.8 to [1,0.8] f_quar_eff = ~quar + quar * quar_trans # Quarantine - rel_trans = rel_trans * f_quar_eff * f_asymp * f_diag * beta_layer * viral_load # Recalulate transmisibility - rel_sus = rel_sus * f_quar_eff # Recalulate susceptibility + rel_trans = rel_trans * inf * f_quar_eff * f_asymp * f_diag * beta_layer * viral_load # Recalulate transmisibility + rel_sus = rel_sus * sus * f_quar_eff # Recalulate susceptibility return rel_trans, rel_sus diff --git a/tests/test_baselines.py b/tests/test_baselines.py index fcabd39f2..0c56d8232 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -46,18 +46,20 @@ def test_baseline(): if old_keys != new_keys: errormsg = f"Keys don't match!\n" missing = old_keys - new_keys - extra = new_keys - old_keys + extra = new_keys - old_keys if missing: errormsg += f' Missing old keys: {missing}\n' if extra: errormsg += f' Extra new keys: {extra}\n' mismatches = {} - for key in old_keys.union(new_keys): - old_val = old[key] if key in old else 'not present' - new_val = new[key] if key in new else 'not present' - if old_val != new_val: - mismatches[key] = {'old': old_val, 'new': new_val} + union = old_keys.union(new_keys) + for key in new.keys(): # To ensure order + if key in union: + old_val = old[key] if key in old else 'not present' + new_val = new[key] if key in new else 'not present' + if old_val != new_val: + mismatches[key] = {'old': old_val, 'new': new_val} if len(mismatches): errormsg = '\nThe following values have changed between old and new!\n' From 0f462eb144c1bd8ffc107726d0efa9d3f5f9c9c9 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 12:00:51 -0700 Subject: [PATCH 114/194] updated test --- tests/test_baselines.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/test_baselines.py b/tests/test_baselines.py index 0c56d8232..c8f081b05 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -62,19 +62,21 @@ def test_baseline(): mismatches[key] = {'old': old_val, 'new': new_val} if len(mismatches): - errormsg = '\nThe following values have changed between old and new!\n' - errormsg += 'Please rerun "tests/update_baseline" if this is intentional.\n' + errormsg = '\nThe following values have changed from the previous baseline!\n' + errormsg += 'If this is intentional, please rerun "tests/update_baseline" and commit.\n' errormsg += 'Mismatches:\n' df = pd.DataFrame.from_dict(mismatches).transpose() - ratio = [] + diff = [] + ratio = [] change = [] small_change = 1e-3 # Define a small change, e.g. a rounding error for mdict in mismatches.values(): old = mdict['old'] new = mdict['new'] if sc.isnumber(new) and sc.isnumber(old) and old>0: + this_diff = new - old this_ratio = new/old - abs_ratio = max(this_ratio, 1.0/this_ratio) + abs_ratio = max(this_ratio, 1.0/this_ratio) # Set the character to use if abs_ratio Date: Tue, 5 May 2020 12:02:37 -0700 Subject: [PATCH 115/194] update duration from infectious to symptomatic --- covasim/parameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index bf011fa14..e9a4d1811 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -58,7 +58,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): # Duration parameters: time for disease progression pars['dur'] = {} pars['dur']['exp2inf'] = {'dist':'lognormal_int', 'par1':4.6, 'par2':4.8} # Duration from exposed to infectious; see Linton et al., https://doi.org/10.3390/jcm9020538 - pars['dur']['inf2sym'] = {'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']['inf2sym'] = {'dist':'lognormal_int', 'par1':1.5, 'par2':1.0} # Duration from infectious to symptomatic; see Linton et al., https://doi.org/10.3390/jcm9020538 pars['dur']['sym2sev'] = {'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'] = {'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 From 386f3a3635f30d879bcc54d5d5c428ff0c418a52 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 12:04:20 -0700 Subject: [PATCH 116/194] undo parameter change, too different --- covasim/parameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index e9a4d1811..bf011fa14 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -58,7 +58,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): # Duration parameters: time for disease progression pars['dur'] = {} pars['dur']['exp2inf'] = {'dist':'lognormal_int', 'par1':4.6, 'par2':4.8} # Duration from exposed to infectious; see Linton et al., https://doi.org/10.3390/jcm9020538 - pars['dur']['inf2sym'] = {'dist':'lognormal_int', 'par1':1.5, 'par2':1.0} # Duration from infectious to symptomatic; see Linton et al., https://doi.org/10.3390/jcm9020538 + pars['dur']['inf2sym'] = {'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'] = {'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'] = {'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 From afef29be4a8695abf2721c58943c393ccccfcd16 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 12:09:01 -0700 Subject: [PATCH 117/194] round output --- tests/test_baselines.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_baselines.py b/tests/test_baselines.py index c8f081b05..fd149fe09 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -110,6 +110,8 @@ def test_baseline(): df['diff'] = diff df['ratio'] = ratio + for col in ['old', 'new', 'diff', 'ratio']: + df[col] = df[col].round(decimals=3) df['change'] = change errormsg += str(df) @@ -119,7 +121,7 @@ def test_baseline(): else: print('Baseline matches') - return new + return df def test_benchmark(do_save=do_save): @@ -172,7 +174,7 @@ def test_benchmark(do_save=do_save): if __name__ == '__main__': - new = test_baseline() + df = test_baseline() json = test_benchmark(do_save=do_save) print('Done.') \ No newline at end of file From f8d0f9c189f1b5b7fdea1146265bdf2795525108 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 12:14:05 -0700 Subject: [PATCH 118/194] do not require return value for intervention function --- covasim/sim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/sim.py b/covasim/sim.py index e49f6209a..d0e8b7372 100755 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -367,7 +367,7 @@ def step(self): for intervention in self['interventions']: intervention.apply(self) if self['interv_func'] is not None: # Apply custom intervention function - self =self['interv_func'](self) + self['interv_func'](self) flows = people.update_states_post(flows) # Check for state changes after interventions From 65db60691ca7ebfaaefb8aae2ea53de2af97ac1a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 12:19:58 -0700 Subject: [PATCH 119/194] very different results from before --- covasim/parameters.py | 3 ++- covasim/people.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index bf011fa14..7b1051500 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -157,7 +157,8 @@ def get_prognoses(by_age=True): else: prognoses = dict( age_cutoffs = np.array([10, 20, 30, 40, 50, 60, 70, 80, max_age]), # Age cutoffs (upper limits) - sus_ORs = np.array([0.34, 0.67, 1.00, 1.00, 1.00, 1.00, 1.24, 1.47, 1.47]), # Odds ratios for relative susceptibility -- from https://science.sciencemag.org/content/early/2020/05/04/science.abb8001; 10-20 and 60-70 bins are the average across the ORs + # sus_ORs = np.array([0.34, 0.67, 1.00, 1.00, 1.00, 1.00, 1.24, 1.47, 1.47]), # Odds ratios for relative susceptibility -- from https://science.sciencemag.org/content/early/2020/05/04/science.abb8001; 10-20 and 60-70 bins are the average across the ORs + sus_ORs = np.array([1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00]), # Odds ratios for relative transmissibility -- no evidence of differences trans_ORs = np.array([1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00]), # Odds ratios for relative transmissibility -- no evidence of differences symp_probs = np.array([0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90]), # Overall probability of developing symptoms (based on https://www.medrxiv.org/content/10.1101/2020.03.24.20043018v1.full.pdf, scaled for overall symptomaticity) severe_probs = np.array([0.00050, 0.00165, 0.00720, 0.02080, 0.03430, 0.07650, 0.13280, 0.20655, 0.24570]), # Overall probability of developing severe symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) diff --git a/covasim/people.py b/covasim/people.py index c8274dd86..cadcca56f 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -159,7 +159,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.rel_trans[inds] = cvu.sample(**self.pars['beta_dist'], size=len(inds)) + self.rel_trans[inds] = self.rel_trans[inds]*cvu.sample(**self.pars['beta_dist'], size=len(inds)) return len(inds) From fa1e3242a24cece2e4d8d6ae5c231f7b994c8ba8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 12:33:49 -0700 Subject: [PATCH 120/194] working much better --- covasim/sim.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index d0e8b7372..b4b0dcba7 100755 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -383,20 +383,19 @@ 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 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 - for lkey,layer in contacts.items(): sources = layer['p1'] targets = 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 quar_eff = cvd.default_float(self['quar_eff'][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, diag_factor, quar_eff) From 7b474db4c0165ce9d1b9a0d10a83a0a4f1e29e39 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 12:37:51 -0700 Subject: [PATCH 121/194] exactly the same results! --- covasim/parameters.py | 6 ++++-- tests/test_baselines.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index 7b1051500..bface9036 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -161,8 +161,10 @@ def get_prognoses(by_age=True): sus_ORs = np.array([1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00]), # Odds ratios for relative transmissibility -- no evidence of differences trans_ORs = np.array([1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00]), # Odds ratios for relative transmissibility -- no evidence of differences symp_probs = np.array([0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90]), # Overall probability of developing symptoms (based on https://www.medrxiv.org/content/10.1101/2020.03.24.20043018v1.full.pdf, scaled for overall symptomaticity) - severe_probs = np.array([0.00050, 0.00165, 0.00720, 0.02080, 0.03430, 0.07650, 0.13280, 0.20655, 0.24570]), # Overall probability of developing severe symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) - crit_probs = np.array([0.00003, 0.00008, 0.00036, 0.00104, 0.00216, 0.00933, 0.03639, 0.08923, 0.17420]), # Overall probability of developing critical symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) + # severe_probs = np.array([0.00050, 0.00165, 0.00720, 0.02080, 0.03430, 0.07650, 0.13280, 0.20655, 0.24570]), # Overall probability of developing severe symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) + severe_probs = np.array([0.00100, 0.00100, 0.01100, 0.03400, 0.04300, 0.08200, 0.11800, 0.16600, 0.18400]), # Overall probability of developing severe symptoms (https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf) + # crit_probs = np.array([0.00003, 0.00008, 0.00036, 0.00104, 0.00216, 0.00933, 0.03639, 0.08923, 0.17420]), # Overall probability of developing critical symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) + crit_probs = np.array([0.00004, 0.00011, 0.00050, 0.00123, 0.00214, 0.00800, 0.02750, 0.06000, 0.10333]), # Overall probability of developing critical symptoms (derived from https://www.cdc.gov/mmwr/volumes/69/wr/mm6912e2.htm) death_probs = np.array([0.00002, 0.00006, 0.00030, 0.00080, 0.00150, 0.00600, 0.02200, 0.05100, 0.09300]), # Overall probability of dying (https://www.imperial.ac.uk/media/imperial-college/medicine/sph/ide/gida-fellowships/Imperial-College-COVID19-NPI-modelling-16-03-2020.pdf) ) diff --git a/tests/test_baselines.py b/tests/test_baselines.py index fd149fe09..3779b2b3b 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -121,7 +121,7 @@ def test_baseline(): else: print('Baseline matches') - return df + return new def test_benchmark(do_save=do_save): From cd8883c2b9d6791ce4f94310c4189054dd085ccd Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 17:34:16 -0700 Subject: [PATCH 122/194] updated version --- CHANGELOG.rst | 5 +++++ covasim/people.py | 7 +++---- covasim/version.py | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 297875e91..a9ecd007b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,11 @@ What's new All notable changes to the codebase are documented in this file. Note: in many cases, changes from multiple patch versions are grouped together, so numbering will not be strictly consecutive. +Version 0.31.0 (2020-05-05) +--------------------------- +- Added age-susceptible odds ratios. To restore previous behavior, set ``pars['prognoses']['sus_ORs'][:] = 1.0``. + + Version 0.30.4 (2020-05-04) --------------------------- - Changed the detailed transmission tree (``sim.people.transtree.detailed``) to include much more information. diff --git a/covasim/people.py b/covasim/people.py index cadcca56f..01d2bc073 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -59,7 +59,7 @@ def initialize(self, pars=None): def set_prognoses(self, pars=None): - ''' Set the prognoses for each person based on age ''' + ''' Set the prognoses for each person based on age during initialization ''' if pars is None: pars = self.pars @@ -75,13 +75,13 @@ def find_cutoff(age_cutoffs, age): self.crit_prob[:] = prognoses['crit_probs'][inds] self.death_prob[:] = prognoses['death_probs'][inds] self.rel_sus[:] = prognoses['sus_ORs'][inds] # Default susceptibilities - self.rel_trans[:] = prognoses['trans_ORs'][inds] # Default transmissibilities + self.rel_trans[:] = prognoses['trans_ORs'][inds]*cvu.sample(**self.pars['beta_dist'], size=len(inds)) # Default transmissibilities, drawn from a distribution return def update_states_pre(self, t): - ''' Perform all state updates ''' + ''' Perform all state updates at the current timestep ''' # Initialize self.t = t @@ -159,7 +159,6 @@ 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.rel_trans[inds] = self.rel_trans[inds]*cvu.sample(**self.pars['beta_dist'], size=len(inds)) return len(inds) diff --git a/covasim/version.py b/covasim/version.py index 4b2625863..58c184639 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -2,6 +2,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '0.30.4' -__versiondate__ = '2020-05-03' +__version__ = '0.31.0' +__versiondate__ = '2020-05-05' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2020 by IDM' From 186c7bc9f9930123bc15ea11a2e879b58e055992 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 17:51:08 -0700 Subject: [PATCH 123/194] updating changelog --- CHANGELOG.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a9ecd007b..733de76b1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,7 +6,13 @@ All notable changes to the codebase are documented in this file. Note: in many c Version 0.31.0 (2020-05-05) --------------------------- -- Added age-susceptible odds ratios. To restore previous behavior, set ``pars['prognoses']['sus_ORs'][:] = 1.0``. +- Added age-susceptible odds ratios, and modified severe and critical progression probabilities. To restore previous behavior (which was based on the `Imperial paper `__), set the following values in ``sim.pars['prognoses']``:: + + sus_ORs[:] = 1.0 + severe_probs = np.array([0.00100, 0.00100, 0.01100, 0.03400, 0.04300, 0.08200, 0.11800, 0.16600, 0.18400]) + crit_probs = np.array([0.00004, 0.00011, 0.00050, 0.00123, 0.00214, 0.00800, 0.02750, 0.06000, 0.10333]) + +- Relative susceptibility and transmissibility (i.e., ``sim.people.rel_sus``) are now set when the population is initialized (before, they were modified dynamically when a person became infected or recovered). This means that modifying them before a simulation starts, or during a simulation, should be more robust. Version 0.30.4 (2020-05-04) From b017b0cf4917f55696c4a63bec22c4d475267b4e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 18:14:54 -0700 Subject: [PATCH 124/194] remove old values --- CHANGELOG.rst | 1 + covasim/parameters.py | 9 +++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 733de76b1..83261cafb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,7 @@ Version 0.31.0 (2020-05-05) crit_probs = np.array([0.00004, 0.00011, 0.00050, 0.00123, 0.00214, 0.00800, 0.02750, 0.06000, 0.10333]) - Relative susceptibility and transmissibility (i.e., ``sim.people.rel_sus``) are now set when the population is initialized (before, they were modified dynamically when a person became infected or recovered). This means that modifying them before a simulation starts, or during a simulation, should be more robust. +- GitHub info: PR `480 `__, previous head ``c7171f8`` Version 0.30.4 (2020-05-04) diff --git a/covasim/parameters.py b/covasim/parameters.py index bface9036..bf011fa14 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -157,14 +157,11 @@ def get_prognoses(by_age=True): else: prognoses = dict( age_cutoffs = np.array([10, 20, 30, 40, 50, 60, 70, 80, max_age]), # Age cutoffs (upper limits) - # sus_ORs = np.array([0.34, 0.67, 1.00, 1.00, 1.00, 1.00, 1.24, 1.47, 1.47]), # Odds ratios for relative susceptibility -- from https://science.sciencemag.org/content/early/2020/05/04/science.abb8001; 10-20 and 60-70 bins are the average across the ORs - sus_ORs = np.array([1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00]), # Odds ratios for relative transmissibility -- no evidence of differences + sus_ORs = np.array([0.34, 0.67, 1.00, 1.00, 1.00, 1.00, 1.24, 1.47, 1.47]), # Odds ratios for relative susceptibility -- from https://science.sciencemag.org/content/early/2020/05/04/science.abb8001; 10-20 and 60-70 bins are the average across the ORs trans_ORs = np.array([1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00]), # Odds ratios for relative transmissibility -- no evidence of differences symp_probs = np.array([0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90]), # Overall probability of developing symptoms (based on https://www.medrxiv.org/content/10.1101/2020.03.24.20043018v1.full.pdf, scaled for overall symptomaticity) - # severe_probs = np.array([0.00050, 0.00165, 0.00720, 0.02080, 0.03430, 0.07650, 0.13280, 0.20655, 0.24570]), # Overall probability of developing severe symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) - severe_probs = np.array([0.00100, 0.00100, 0.01100, 0.03400, 0.04300, 0.08200, 0.11800, 0.16600, 0.18400]), # Overall probability of developing severe symptoms (https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf) - # crit_probs = np.array([0.00003, 0.00008, 0.00036, 0.00104, 0.00216, 0.00933, 0.03639, 0.08923, 0.17420]), # Overall probability of developing critical symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) - crit_probs = np.array([0.00004, 0.00011, 0.00050, 0.00123, 0.00214, 0.00800, 0.02750, 0.06000, 0.10333]), # Overall probability of developing critical symptoms (derived from https://www.cdc.gov/mmwr/volumes/69/wr/mm6912e2.htm) + severe_probs = np.array([0.00050, 0.00165, 0.00720, 0.02080, 0.03430, 0.07650, 0.13280, 0.20655, 0.24570]), # Overall probability of developing severe symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) + crit_probs = np.array([0.00003, 0.00008, 0.00036, 0.00104, 0.00216, 0.00933, 0.03639, 0.08923, 0.17420]), # Overall probability of developing critical symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) death_probs = np.array([0.00002, 0.00006, 0.00030, 0.00080, 0.00150, 0.00600, 0.02200, 0.05100, 0.09300]), # Overall probability of dying (https://www.imperial.ac.uk/media/imperial-college/medicine/sph/ide/gida-fellowships/Imperial-College-COVID19-NPI-modelling-16-03-2020.pdf) ) From cba1d57e4279865a7f1ff9a5bfb2d8427f2e86b4 Mon Sep 17 00:00:00 2001 From: Lauren George Date: Tue, 5 May 2020 18:24:09 -0700 Subject: [PATCH 125/194] #233: update container id to idmod/covasim --- .platform/covasim-deployment.yaml | 2 +- docker/docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.platform/covasim-deployment.yaml b/.platform/covasim-deployment.yaml index b3d58015c..0d2f5df79 100644 --- a/.platform/covasim-deployment.yaml +++ b/.platform/covasim-deployment.yaml @@ -33,7 +33,7 @@ spec: service: covasim spec: containers: - - image: "registry.hub.docker.com/jules2689/covasim:latest" + - image: "registry.hub.docker.com/idmod/covasim:latest-webapp" name: covasim ports: - containerPort: 80 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 61b315f77..2b3053e59 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -4,6 +4,6 @@ services: build: context: ../ dockerfile: docker/Dockerfile - image: jules2689/covasim:latest + image: idmod/covasim:latest ports: - 8000:80 From 2a612893b77d8029328f650b4319a097096d5012 Mon Sep 17 00:00:00 2001 From: Lauren George Date: Tue, 5 May 2020 18:24:56 -0700 Subject: [PATCH 126/194] #233: remove unnecessary docker build workflow --- .github/workflows/build.yaml | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 .github/workflows/build.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index eaf3bb6b9..000000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: Covasim CI workflow -on: - push: - branches: - - master - -jobs: - build_and_push_docker: - runs-on: ubuntu-latest - if: github.repository == 'InstituteforDiseaseModeling/covasim' - name: Build and Publish Docker Image - steps: - - name: Checkout sources - uses: actions/checkout@v2 - - - name: Set lowercase repo env variable - shell: python - run: print("::set-env name=LOWER_REPO::{}".format('${{github.repository}}'.lower())) - - name: Build Docker - run: | - docker build -f docker/Dockerfile -t docker.pkg.github.com/${LOWER_REPO}/covasim:latest . - - name: Push the image to GPR - run: | - docker login docker.pkg.github.com -u publisher -p "${GITHUB_PACKAGE_REGISTRY_TOKEN}" - docker push docker.pkg.github.com/${LOWER_REPO}/covasim:latest - env: - GITHUB_PACKAGE_REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 5fae73b98557e316d3c0434621d2a2c3ac1f824f Mon Sep 17 00:00:00 2001 From: Lauren George Date: Tue, 5 May 2020 18:25:30 -0700 Subject: [PATCH 127/194] #233: update tests to run missing examples --- .github/workflows/{ubuntu.yaml => test.yaml} | 8 ++--- tests/test_examples.py | 31 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) rename .github/workflows/{ubuntu.yaml => test.yaml} (91%) create mode 100644 tests/test_examples.py diff --git a/.github/workflows/ubuntu.yaml b/.github/workflows/test.yaml similarity index 91% rename from .github/workflows/ubuntu.yaml rename to .github/workflows/test.yaml index 1a390e321..04911df02 100644 --- a/.github/workflows/ubuntu.yaml +++ b/.github/workflows/test.yaml @@ -36,12 +36,12 @@ jobs: run: | python setup.py develop --install-dir ~/.cache/site-packages pip install pytest - - name: Run tests + - name: Run integration tests and examples working-directory: ./tests run: pytest test*.py --durations=0 # Run actual tests - name: Run unit tests working-directory: ./tests/unittests run: pytest test*.py --durations=0 # Run actual tests - - name: Run other examples - working-directory: ./ - run: pytest ./examples/*.py ./covasim/cruise_ship/test*.py + - name: Run cruise ship tests + working-directory: ./covasim/cruise_ship + run: pytest test*.py diff --git a/tests/test_examples.py b/tests/test_examples.py new file mode 100644 index 000000000..4adef2d81 --- /dev/null +++ b/tests/test_examples.py @@ -0,0 +1,31 @@ +import importlib.util as iu +import os +from pathlib import Path + +cwd = Path(os.path.dirname(os.path.abspath(__file__))) +examples_dir = cwd.joinpath('../examples') + +def run_example(name): + spec = iu.spec_from_file_location("__main__", examples_dir.joinpath(f"{name}.py")) + module = iu.module_from_spec(spec) + spec.loader.exec_module(module) + + +def test_run_scenarios(): + run_example("run_scenarios") + +def test_run_sim(): + run_example("run_sim") + +def test_simple(): + run_example("simple") + + +#%% Run as a script +if __name__ == '__main__': + sc.tic() + test_run_scenarios() + test_run_sim() + test_simple() + sc.toc() + print('Done.') From 61280e29ad8d0dabda36d62ef63c62cd13ff0d65 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 18:26:39 -0700 Subject: [PATCH 128/194] reordered results object --- covasim/parameters.py | 2 +- covasim/sim.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index bf011fa14..84cb54da1 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -41,7 +41,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): pars['rescale_factor'] = 2 # Factor by which we rescale the population # Basic disease transmission - pars['beta'] = 0.015 # 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 below pars['dynam_layer'] = None # Which layers are dynamic; set below pars['beta_layer'] = None # Transmissibility per layer; set below diff --git a/covasim/sim.py b/covasim/sim.py index b4b0dcba7..031a4af1f 100755 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -227,17 +227,19 @@ def init_res(*args, **kwargs): dcols = cvd.get_colors() # Get default 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]) # 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" + # Stock variables for key,label in cvd.result_stocks.items(): self.results[f'n_{key}'] = init_res(label, color=dcols[key]) self.results['n_susceptible'].scale = 'static' self.results['bed_capacity'] = init_res('Bed demand relative to capacity', scale=False) - # Flows and cumulative flows - for key,label in cvd.result_flows.items(): - 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'cum_{key}'] = init_res(f'Cumulative {label}', color=dcols[key]) # Cumulative variables -- e.g. "Cumulative infections" - # Other variables self.results['r_eff'] = init_res('Effective reproductive number', scale=False) self.results['doubling_time'] = init_res('Doubling time', scale=False) From 2a951962c995e2a03a81b9e27f37c83c7335ae2c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 18:29:23 -0700 Subject: [PATCH 129/194] updated changelog --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 83261cafb..782fd6797 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,13 +6,14 @@ All notable changes to the codebase are documented in this file. Note: in many c Version 0.31.0 (2020-05-05) --------------------------- -- Added age-susceptible odds ratios, and modified severe and critical progression probabilities. To restore previous behavior (which was based on the `Imperial paper `__), set the following values in ``sim.pars['prognoses']``:: +- Added age-susceptible odds ratios, and modified severe and critical progression probabilities. To compensate, default ``beta`` has been increased from 0.015 to 0.016. To restore previous behavior (which was based on the `Imperial paper `__), set ``beta=0.015`` and set the following values in ``sim.pars['prognoses']``:: sus_ORs[:] = 1.0 severe_probs = np.array([0.00100, 0.00100, 0.01100, 0.03400, 0.04300, 0.08200, 0.11800, 0.16600, 0.18400]) crit_probs = np.array([0.00004, 0.00011, 0.00050, 0.00123, 0.00214, 0.00800, 0.02750, 0.06000, 0.10333]) - Relative susceptibility and transmissibility (i.e., ``sim.people.rel_sus``) are now set when the population is initialized (before, they were modified dynamically when a person became infected or recovered). This means that modifying them before a simulation starts, or during a simulation, should be more robust. +- Reordered results dictionary to start with cumulative counts. - GitHub info: PR `480 `__, previous head ``c7171f8`` From 013b1a0d3b5537581b7bc492e7f9b6cefd7ad390 Mon Sep 17 00:00:00 2001 From: Lauren George Date: Tue, 5 May 2020 18:33:09 -0700 Subject: [PATCH 130/194] #233: actually still run examples separately --- .github/workflows/test.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 04911df02..fd58eee86 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -9,7 +9,7 @@ jobs: max-parallel: 8 matrix: python-version: ['3.7'] - name: Install and test + name: Run tests steps: - name: Set repo owner env variable shell: python @@ -36,7 +36,7 @@ jobs: run: | python setup.py develop --install-dir ~/.cache/site-packages pip install pytest - - name: Run integration tests and examples + - name: Run integration tests working-directory: ./tests run: pytest test*.py --durations=0 # Run actual tests - name: Run unit tests @@ -45,3 +45,5 @@ jobs: - name: Run cruise ship tests working-directory: ./covasim/cruise_ship run: pytest test*.py + - name: Run examples + run: pytest --capture=tee-sys ./tests/test_examples.py \ No newline at end of file From 2b0086496375cbf85e4928573ee7a3e31a65186b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 18:36:31 -0700 Subject: [PATCH 131/194] move households to devtests --- tests/{ => devtests}/test_country_households.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{ => devtests}/test_country_households.py (100%) diff --git a/tests/test_country_households.py b/tests/devtests/test_country_households.py similarity index 100% rename from tests/test_country_households.py rename to tests/devtests/test_country_households.py From 3ce948312a6be53a0700b6bf2db071d8d17b0ef3 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 18:38:38 -0700 Subject: [PATCH 132/194] updated baseline values --- tests/baseline.json | 52 ++++++++++++++++++++--------------------- tests/benchmark.json | 4 ++-- tests/test_baselines.py | 2 +- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/tests/baseline.json b/tests/baseline.json index 318769f15..7227051ea 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -1,35 +1,35 @@ { "summary": { - "n_susceptible": 12758.0, - "n_exposed": 4960.0, - "n_infectious": 2896.0, - "n_symptomatic": 1783.0, - "n_severe": 114.0, - "n_critical": 19.0, - "n_diagnosed": 0.0, - "n_quarantined": 0.0, - "bed_capacity": 0.0, - "new_infections": 528.0, - "cum_infections": 7242.0, - "new_infectious": 397.0, - "cum_infectious": 5178.0, - "new_tests": 0.0, + "cum_infections": 6102.0, + "cum_infectious": 4298.0, "cum_tests": 0.0, - "new_diagnoses": 0.0, "cum_diagnoses": 0.0, - "new_recoveries": 229.0, - "cum_recoveries": 2270.0, - "new_symptomatic": 271.0, - "cum_symptomatic": 3222.0, - "new_severe": 23.0, - "cum_severe": 148.0, - "new_critical": 3.0, + "cum_recoveries": 1922.0, + "cum_symptomatic": 2755.0, + "cum_severe": 155.0, "cum_critical": 32.0, - "new_deaths": 2.0, "cum_deaths": 12.0, - "new_quarantined": 0.0, "cum_quarantined": 0.0, - "r_eff": 1.030526068157997, - "doubling_time": 9.29296551635779 + "new_infections": 416.0, + "new_infectious": 321.0, + "new_tests": 0.0, + "new_diagnoses": 0.0, + "new_recoveries": 176.0, + "new_symptomatic": 213.0, + "new_severe": 8.0, + "new_critical": 6.0, + "new_deaths": 3.0, + "new_quarantined": 0.0, + "n_susceptible": 13898.0, + "n_exposed": 4168.0, + "n_infectious": 2364.0, + "n_symptomatic": 1485.0, + "n_severe": 112.0, + "n_critical": 19.0, + "n_diagnosed": 0.0, + "n_quarantined": 0.0, + "bed_capacity": 0.0, + "r_eff": 1.0759853505262773, + "doubling_time": 9.522931495254712 } } \ No newline at end of file diff --git a/tests/benchmark.json b/tests/benchmark.json index f5440dd48..83b41bb99 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,7 +1,7 @@ { "time": { - "initialize": 0.221, - "run": 0.168 + "initialize": 0.213, + "run": 0.161 }, "parameters": { "pop_size": 20000, diff --git a/tests/test_baselines.py b/tests/test_baselines.py index 3779b2b3b..5a21db054 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -174,7 +174,7 @@ def test_benchmark(do_save=do_save): if __name__ == '__main__': - df = test_baseline() + new = test_baseline() json = test_benchmark(do_save=do_save) print('Done.') \ No newline at end of file From 8a065800d6405847854ae0a346c964d00b80e34f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 18:48:37 -0700 Subject: [PATCH 133/194] added parameters export to file option --- covasim/base.py | 40 +++++++++++++++++++++++++++------------- covasim/parameters.py | 2 +- covasim/sim.py | 2 +- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 488aaf353..bed88d0dc 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -280,18 +280,27 @@ def layer_keys(self): return keys - def export_results(self, for_json=True): + def copy(self): + ''' Returns a deep copy of the sim ''' + return sc.dcp(self) + + + def export_results(self, for_json=True, filename=None, indent=2, *args, **kwargs): ''' - Convert results to dict + Convert results to dict -- see also to_json(). The results written to Excel must have a regular table shape, whereas for the JSON output, arbitrary data shapes are supported. Args: - for_json: If False, only data associated with Result objects will be included in the converted output + for_json (bool): if False, only data associated with Result objects will be included in the converted output + filename (str): filename to save to; if None, do not save + indent (int): indent (int): if writing to file, how many indents to use per nested level + args (list): passed to savejson() + kwargs (dict): passed to savejson() Returns: - resdict (dict): Dictionary representation of the results + resdict (dict): dictionary representation of the results ''' resdict = {} @@ -307,18 +316,26 @@ def export_results(self, for_json=True): resdict[key] = [str(d) for d in res] # Convert dates to strings else: resdict[key] = res + if filename is not None: + sc.savejson(filename=filename, obj=resdict, indent=indent, *args, **kwargs) return resdict - def export_pars(self): + def export_pars(self, filename=None, indent=2, *args, **kwargs): ''' - Return parameters for JSON export + Return parameters for JSON export -- see also to_json(). This method is required so that interventions can specify - their JSON-friendly representation + their JSON-friendly representation. - Returns: + Args: + filename (str): filename to save to; if None, do not save + indent (int): indent (int): if writing to file, how many indents to use per nested level + args (list): passed to savejson() + kwargs (dict): passed to savejson() + Returns: + pardict (dict): a dictionary containing all the parameter values ''' pardict = {} for key in self.pars.keys(): @@ -328,14 +345,11 @@ def export_pars(self): pardict[key] = str(self.pars[key]) else: pardict[key] = self.pars[key] + if filename is not None: + sc.savejson(filename=filename, obj=pardict, indent=indent, *args, **kwargs) return pardict - def copy(self): - ''' Returns a deep copy of the sim ''' - return sc.dcp(self) - - def to_json(self, filename=None, keys=None, tostring=False, indent=2, verbose=False, *args, **kwargs): ''' Export results as JSON. diff --git a/covasim/parameters.py b/covasim/parameters.py index 84cb54da1..ed90c4686 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -85,7 +85,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): pars['stopping_func'] = None # A function to call to stop the sim partway through # Health system parameters - pars['n_beds'] = np.inf # Baseline assumption is that there's no upper limit on the number of beds i.e. there's enough for everyone + pars['n_beds'] = None # Baseline assumption is that there's no upper limit on the number of beds i.e. there's enough for everyone # Update with any supplied parameter values and generate things that need to be generated pars.update(kwargs) diff --git a/covasim/sim.py b/covasim/sim.py index 031a4af1f..8ee02eb90 100755 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -355,7 +355,7 @@ def step(self): people = self.people # Shorten this for later use flows = people.update_states_pre(t=t) # Update the state of everyone and count the flows contacts = people.update_contacts() # Compute new contacts - bed_max = people.count('severe') > self['n_beds'] # Check for a bed constraint + bed_max = people.count('severe') > self['n_beds'] if self['n_beds'] else False # Check for a bed constraint # Randomly infect some people (imported infections) n_imports = cvu.poisson(self['n_imports']) # Imported cases From bf06f4c0efd4d9d9a325f7901ad720e2c906e978 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 18:51:52 -0700 Subject: [PATCH 134/194] more changelog updates --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 782fd6797..2fb94a6a4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,8 @@ Version 0.31.0 (2020-05-05) - Relative susceptibility and transmissibility (i.e., ``sim.people.rel_sus``) are now set when the population is initialized (before, they were modified dynamically when a person became infected or recovered). This means that modifying them before a simulation starts, or during a simulation, should be more robust. - Reordered results dictionary to start with cumulative counts. +- ``sim.export_pars()`` now accepts a filename to save to. +- Added a ``tests/regression`` folder with previous versions of default parameter values. - GitHub info: PR `480 `__, previous head ``c7171f8`` From 6f25e90fe30637481ef3992694a3274b67c84adb Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 18:58:07 -0700 Subject: [PATCH 135/194] added regression parameters jsons --- covasim/sim.py | 2 +- tests/baseline.json | 2 +- tests/benchmark.json | 2 +- tests/regression/parameters_v0.30.4.json | 157 ++++++++++++++++++++ tests/regression/parameters_v0.31.0.json | 179 +++++++++++++++++++++++ tests/test_baselines.py | 2 + 6 files changed, 341 insertions(+), 3 deletions(-) mode change 100755 => 100644 covasim/sim.py create mode 100644 tests/regression/parameters_v0.30.4.json create mode 100644 tests/regression/parameters_v0.31.0.json diff --git a/covasim/sim.py b/covasim/sim.py old mode 100755 new mode 100644 index 8ee02eb90..c63808352 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -417,7 +417,7 @@ 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) - self.results['bed_capacity'][t] = self.results['n_severe'][t]/self['n_beds'] if self['n_beds']>0 else np.nan + self.results['bed_capacity'][t] = self.results['n_severe'][t]/self['n_beds'] if self['n_beds'] else np.nan # Update counts for this time step: flows for key,count in flows.items(): diff --git a/tests/baseline.json b/tests/baseline.json index 7227051ea..d1d9e1844 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -28,7 +28,7 @@ "n_critical": 19.0, "n_diagnosed": 0.0, "n_quarantined": 0.0, - "bed_capacity": 0.0, + "bed_capacity": null, "r_eff": 1.0759853505262773, "doubling_time": 9.522931495254712 } diff --git a/tests/benchmark.json b/tests/benchmark.json index 83b41bb99..e9e121bc4 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,6 +1,6 @@ { "time": { - "initialize": 0.213, + "initialize": 0.216, "run": 0.161 }, "parameters": { diff --git a/tests/regression/parameters_v0.30.4.json b/tests/regression/parameters_v0.30.4.json new file mode 100644 index 000000000..9567dd167 --- /dev/null +++ b/tests/regression/parameters_v0.30.4.json @@ -0,0 +1,157 @@ +{ + "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": 0, + "rescale_threshold": 0.05, + "rescale_factor": 2, + "beta": 0.015, + "contacts": { + "a": 20 + }, + "dynam_layer": { + "a": 0 + }, + "beta_layer": { + "a": 1.0 + }, + "n_imports": 0, + "beta_dist": { + "dist": "lognormal", + "par1": 0.84, + "par2": 0.3 + }, + "viral_dist": { + "frac_time": 0.3, + "load_ratio": 2, + "high_cap": 4 + }, + "asymp_factor": 1.0, + "diag_factor": 0.2, + "quar_eff": { + "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 + } + }, + "OR_no_treat": 2.0, + "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": [ + 10, + 20, + 30, + 40, + 50, + 60, + 70, + 80, + 120 + ], + "symp_probs": [ + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9 + ], + "severe_probs": [ + 0.002, + 0.0018181818181818182, + 0.018333333333333333, + 0.05230769230769231, + 0.06142857142857143, + 0.10933333333333334, + 0.1475, + 0.19529411764705884, + 0.20444444444444443 + ], + "crit_probs": [ + 0.04, + 0.11, + 0.045454545454545456, + 0.03617647058823529, + 0.04976744186046512, + 0.0975609756097561, + 0.23305084745762714, + 0.36144578313253006, + 0.5615760869565217 + ], + "death_probs": [ + 0.5, + 0.5454545454545454, + 0.6, + 0.6504065040650407, + 0.7009345794392524, + 0.75, + 0.7999999999999999, + 0.85, + 0.9000290331946191 + ] + }, + "interventions": [], + "interv_func": null, + "timelimit": 3600, + "stopping_func": null, + "n_beds": Infinity +} \ No newline at end of file diff --git a/tests/regression/parameters_v0.31.0.json b/tests/regression/parameters_v0.31.0.json new file mode 100644 index 000000000..5efa93412 --- /dev/null +++ b/tests/regression/parameters_v0.31.0.json @@ -0,0 +1,179 @@ +{ + "pop_size": 20000, + "pop_infected": 10, + "pop_type": "random", + "location": null, + "start_day": "2020-03-01", + "end_day": "2020-04-30", + "n_days": 60, + "rand_seed": 1, + "verbose": 0, + "pop_scale": 1, + "rescale": 0, + "rescale_threshold": 0.05, + "rescale_factor": 2, + "beta": 0.016, + "contacts": { + "a": 20 + }, + "dynam_layer": { + "a": 0 + }, + "beta_layer": { + "a": 1.0 + }, + "n_imports": 0, + "beta_dist": { + "dist": "lognormal", + "par1": 0.84, + "par2": 0.3 + }, + "viral_dist": { + "frac_time": 0.3, + "load_ratio": 2, + "high_cap": 4 + }, + "asymp_factor": 1.0, + "diag_factor": 0.2, + "quar_eff": { + "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 + } + }, + "OR_no_treat": 2.0, + "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": [ + 10, + 20, + 30, + 40, + 50, + 60, + 70, + 80, + 120 + ], + "sus_ORs": [ + 0.34, + 0.67, + 1.0, + 1.0, + 1.0, + 1.0, + 1.24, + 1.47, + 1.47 + ], + "trans_ORs": [ + 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 + ], + "severe_probs": [ + 0.001, + 0.0029999999999999996, + 0.012, + 0.032, + 0.049, + 0.102, + 0.16599999999999998, + 0.24300000000000002, + 0.273 + ], + "crit_probs": [ + 0.06, + 0.04848484848484849, + 0.05, + 0.049999999999999996, + 0.06297376093294461, + 0.12196078431372549, + 0.2740210843373494, + 0.43200193657709995, + 0.708994708994709 + ], + "death_probs": [ + 0.6666666666666667, + 0.75, + 0.8333333333333333, + 0.7692307692307694, + 0.6944444444444444, + 0.6430868167202572, + 0.6045616927727397, + 0.5715566513504426, + 0.5338691159586683 + ] + }, + "interventions": [], + "interv_func": null, + "timelimit": 3600, + "stopping_func": null, + "n_beds": null +} \ No newline at end of file diff --git a/tests/test_baselines.py b/tests/test_baselines.py index 5a21db054..396291592 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -10,6 +10,7 @@ do_save = False baseline_filename = sc.thisdir(__file__, 'baseline.json') benchmark_filename = sc.thisdir(__file__, 'benchmark.json') +parameters_filename = sc.thisdir(__file__, 'regression', f'parameters_v{cv.__version__}.json') baseline_key = 'summary' @@ -21,6 +22,7 @@ def save_baseline(do_save=do_save): sim.run() if do_save: sim.to_json(filename=baseline_filename, keys=baseline_key) + sim.export_pars(filename=parameters_filename) print('Done.') From d7e36fa8848b07f66fd60af06da0497407c0549e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 19:03:17 -0700 Subject: [PATCH 136/194] added readmes --- tests/devtests/{README.md => README.rst} | 3 ++- tests/devtests/archive/README.rst | 4 ++++ tests/regression/README.rst | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) rename tests/devtests/{README.md => README.rst} (86%) create mode 100644 tests/devtests/archive/README.rst create mode 100644 tests/regression/README.rst diff --git a/tests/devtests/README.md b/tests/devtests/README.rst similarity index 86% rename from tests/devtests/README.md rename to tests/devtests/README.rst index 06662c5df..0a71d6df6 100644 --- a/tests/devtests/README.md +++ b/tests/devtests/README.rst @@ -1,3 +1,4 @@ -# Devtests +Devtests +======== This folder contains tests that are useful checks during development, but are not critical so are not part of CI. \ No newline at end of file diff --git a/tests/devtests/archive/README.rst b/tests/devtests/archive/README.rst new file mode 100644 index 000000000..5c604cdc2 --- /dev/null +++ b/tests/devtests/archive/README.rst @@ -0,0 +1,4 @@ +Archive +======== + +This folder contains tests that were used during the development process but are no longer required. They are preserved for historical interest. \ No newline at end of file diff --git a/tests/regression/README.rst b/tests/regression/README.rst new file mode 100644 index 000000000..ccc7ae97e --- /dev/null +++ b/tests/regression/README.rst @@ -0,0 +1,4 @@ +Regression +========== + +This folder contains previous versions of the default parameters for Covasim. Although older versions of the parameters are not guaranteed to be compatible across versions, they may help explain differences between versions. \ No newline at end of file From b39ca3722ce3d16250c84b52c0885411dd2f3db1 Mon Sep 17 00:00:00 2001 From: Lauren George Date: Tue, 5 May 2020 19:03:44 -0700 Subject: [PATCH 137/194] #233: added documentation to test_examples.py --- tests/test_examples.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_examples.py b/tests/test_examples.py index 4adef2d81..2b1ca2822 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,3 +1,7 @@ +''' +Run examples/*.py using pytest +''' + import importlib.util as iu import os from pathlib import Path @@ -6,6 +10,10 @@ examples_dir = cwd.joinpath('../examples') def run_example(name): + """ + Execute an example py script as __main__ + :param name: the filename without the .py extension + """ spec = iu.spec_from_file_location("__main__", examples_dir.joinpath(f"{name}.py")) module = iu.module_from_spec(spec) spec.loader.exec_module(module) From 5408fad9079b49dafe9bbe57fcab2abcd1dca899 Mon Sep 17 00:00:00 2001 From: Lauren George Date: Tue, 5 May 2020 19:10:19 -0700 Subject: [PATCH 138/194] #233: removed duplicate run of examples --- .github/workflows/test.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fd58eee86..69ed8af85 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -45,5 +45,3 @@ jobs: - name: Run cruise ship tests working-directory: ./covasim/cruise_ship run: pytest test*.py - - name: Run examples - run: pytest --capture=tee-sys ./tests/test_examples.py \ No newline at end of file From b9cc311df4529b85fcb0d27c08e7d6feabd037a3 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 19:18:13 -0700 Subject: [PATCH 139/194] updated baseline with correct beds value --- CHANGELOG.rst | 1 + covasim/sim.py | 2 +- tests/baseline.json | 2 +- tests/benchmark.json | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2fb94a6a4..389fb2253 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,7 @@ Version 0.31.0 (2020-05-05) - Reordered results dictionary to start with cumulative counts. - ``sim.export_pars()`` now accepts a filename to save to. - Added a ``tests/regression`` folder with previous versions of default parameter values. +- Changed ``pars['n_beds']`` to interpret 0 or ``None`` as no bed constraint. - GitHub info: PR `480 `__, previous head ``c7171f8`` diff --git a/covasim/sim.py b/covasim/sim.py index c63808352..d9571bb78 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -417,7 +417,7 @@ 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) - self.results['bed_capacity'][t] = self.results['n_severe'][t]/self['n_beds'] if self['n_beds'] else np.nan + self.results['bed_capacity'][t] = self.results['n_severe'][t]/self['n_beds'] if self['n_beds'] else 0 # Update counts for this time step: flows for key,count in flows.items(): diff --git a/tests/baseline.json b/tests/baseline.json index d1d9e1844..7227051ea 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -28,7 +28,7 @@ "n_critical": 19.0, "n_diagnosed": 0.0, "n_quarantined": 0.0, - "bed_capacity": null, + "bed_capacity": 0.0, "r_eff": 1.0759853505262773, "doubling_time": 9.522931495254712 } diff --git a/tests/benchmark.json b/tests/benchmark.json index e9e121bc4..ec81e5b69 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,7 +1,7 @@ { "time": { - "initialize": 0.216, - "run": 0.161 + "initialize": 0.212, + "run": 0.162 }, "parameters": { "pop_size": 20000, From c0a2bd81cd654d9b3bab6b5ccadc1bf8f75de563 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 20:33:10 -0700 Subject: [PATCH 140/194] add comorobidities --- CHANGELOG.rst | 6 ++++++ README.rst | 28 +++++++++++++++++++++++----- covasim/parameters.py | 15 ++++++++------- covasim/people.py | 2 +- covasim/version.py | 2 +- 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 389fb2253..05983dc6c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,12 @@ What's new All notable changes to the codebase are documented in this file. Note: in many cases, changes from multiple patch versions are grouped together, so numbering will not be strictly consecutive. +Version 0.32.0 (2020-05-05) +--------------------------- +- Changed the edges of the contact network from being directed to undirected, halving the amount of memory required and making contact tracing and edge clipping more realistic. +- Added a comorbidities to the prognoses parameters. +- GitHub info: PR `481 `__, previous head ``029585f`` + Version 0.31.0 (2020-05-05) --------------------------- diff --git a/README.rst b/README.rst index 6b39ee109..641ef5dce 100644 --- a/README.rst +++ b/README.rst @@ -8,13 +8,13 @@ indicators such as numbers of infections and peak hospital demand. Covasim can also be used to explore the potential impact of different interventions, including social distancing, school closures, testing, contact tracing, and quarantine. -The Covasim webapp is available at https://covasim.idmod.org. +The Covasim webapp is available at https://app.covasim.org. Questions or comments can be directed to covasim@idmod.org, or on this project's GitHub_ page. Full information about Covasim is provided in the documentation_. .. _GitHub: https://github.com/institutefordiseasemodeling/covasim -.. _documentation: https://institutefordiseasemodeling.github.io/covasim-docs +.. _documentation: https://docs.covasim.org .. contents:: Contents :local: @@ -32,6 +32,7 @@ more information, see documentation for venv_ or 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 + Quick start guide ================== @@ -93,7 +94,7 @@ Module structure ================ All core model code is located in the ``covasim`` subfolder; standard usage is -``import covasim as cv``. The other subfolders, ``cruise_ship`` and ``webapp``, are +``import covasim as cv``. The other subfolders, ``cruise_ship``, ``data``, and ``webapp``, are also described below. The model consists of two core classes: the ``Person`` class (which contains @@ -116,25 +117,36 @@ The structure of the ``covasim`` folder is as follows: * ``utils.py``: Functions for choosing random numbers, many based on Numba, plus other helper functions. * ``version.py``: Version, date, and license information. -cruise_ship + +Cruise ship ----------- A version of the Covasim model specifically adapted for modeling the Diamond Princess cruise ship. It uses its own parameters file (``parameters.py``) and has slight variations to the model (``model.py``). -webapp + +Data +---- + +This folder contains loading scripts for the epidemiological data in the root ``data`` folder, as well as data on age distributions for different countries and household sizes. + + + +Webapp ------ For running the interactive web application. See the `webapp README`_ for more information. .. _webapp README: https://github.com/InstituteforDiseaseModeling/covasim/tree/master/covasim/webapp + Other folders ============= Please see the readme in each subfolder for more information. + bin --- @@ -145,33 +157,39 @@ This folder contains a command-line interface (CLI) version of Covasim; example Note: the CLI is currently not compatible with Windows. You will need to add this folder to your path to run from other folders. + data ---- Scripts to automatically scrape data (including demographics and COVID epidemiology data), and the data files themselves (which are not part of the repository). + docker ------ This folder contains the ``Dockerfile`` and other files that allow Covasim to be run as a webapp via Docker. + examples -------- This folder contains demonstrations of simple Covasim usage. + licenses -------- Licensing information and legal notices. + tests ----- Integration, development, and unit tests. + sweep ----- diff --git a/covasim/parameters.py b/covasim/parameters.py index ed90c4686..a1e47364d 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -156,13 +156,14 @@ def get_prognoses(by_age=True): ) else: prognoses = dict( - age_cutoffs = np.array([10, 20, 30, 40, 50, 60, 70, 80, max_age]), # Age cutoffs (upper limits) - sus_ORs = np.array([0.34, 0.67, 1.00, 1.00, 1.00, 1.00, 1.24, 1.47, 1.47]), # Odds ratios for relative susceptibility -- from https://science.sciencemag.org/content/early/2020/05/04/science.abb8001; 10-20 and 60-70 bins are the average across the ORs - trans_ORs = np.array([1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00]), # Odds ratios for relative transmissibility -- no evidence of differences - symp_probs = np.array([0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90]), # Overall probability of developing symptoms (based on https://www.medrxiv.org/content/10.1101/2020.03.24.20043018v1.full.pdf, scaled for overall symptomaticity) - severe_probs = np.array([0.00050, 0.00165, 0.00720, 0.02080, 0.03430, 0.07650, 0.13280, 0.20655, 0.24570]), # Overall probability of developing severe symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) - crit_probs = np.array([0.00003, 0.00008, 0.00036, 0.00104, 0.00216, 0.00933, 0.03639, 0.08923, 0.17420]), # Overall probability of developing critical symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) - death_probs = np.array([0.00002, 0.00006, 0.00030, 0.00080, 0.00150, 0.00600, 0.02200, 0.05100, 0.09300]), # Overall probability of dying (https://www.imperial.ac.uk/media/imperial-college/medicine/sph/ide/gida-fellowships/Imperial-College-COVID19-NPI-modelling-16-03-2020.pdf) + age_cutoffs = np.array([10, 20, 30, 40, 50, 60, 70, 80, max_age]), # Age cutoffs (upper limits) + sus_ORs = np.array([0.34, 0.67, 1.00, 1.00, 1.00, 1.00, 1.24, 1.47, 1.47]), # Odds ratios for relative susceptibility -- from https://science.sciencemag.org/content/early/2020/05/04/science.abb8001; 10-20 and 60-70 bins are the average across the ORs + trans_ORs = np.array([1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00]), # Odds ratios for relative transmissibility -- no evidence of differences + comorbidities = np.array([1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00]), # Comorbidities by age -- set to 1 by default since already included in disease progression rates + symp_probs = np.array([0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90]), # Overall probability of developing symptoms (based on https://www.medrxiv.org/content/10.1101/2020.03.24.20043018v1.full.pdf, scaled for overall symptomaticity) + severe_probs = np.array([0.00050, 0.00165, 0.00720, 0.02080, 0.03430, 0.07650, 0.13280, 0.20655, 0.24570]), # Overall probability of developing severe symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) + crit_probs = np.array([0.00003, 0.00008, 0.00036, 0.00104, 0.00216, 0.00933, 0.03639, 0.08923, 0.17420]), # Overall probability of developing critical symptoms (derived from Table 1 of https://www.imperial.ac.uk/media/imperial-college/medicine/mrc-gida/2020-03-16-COVID19-Report-9.pdf) + death_probs = np.array([0.00002, 0.00006, 0.00030, 0.00080, 0.00150, 0.00600, 0.02200, 0.05100, 0.09300]), # Overall probability of dying (https://www.imperial.ac.uk/media/imperial-college/medicine/sph/ide/gida-fellowships/Imperial-College-COVID19-NPI-modelling-16-03-2020.pdf) ) prognoses['death_probs'] /= prognoses['crit_probs'] # Conditional probability of dying, given critical symptoms diff --git a/covasim/people.py b/covasim/people.py index 01d2bc073..ca4e86543 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -71,7 +71,7 @@ def find_cutoff(age_cutoffs, age): age_cutoffs = prognoses['age_cutoffs'] inds = np.fromiter((find_cutoff(age_cutoffs, this_age) for this_age in self.age), dtype=cvd.default_int, count=len(self)) self.symp_prob[:] = prognoses['symp_probs'][inds] - self.severe_prob[:] = prognoses['severe_probs'][inds] + self.severe_prob[:] = prognoses['severe_probs'][inds]*pars['prognoses']['comorbidities'][inds] self.crit_prob[:] = prognoses['crit_probs'][inds] self.death_prob[:] = prognoses['death_probs'][inds] self.rel_sus[:] = prognoses['sus_ORs'][inds] # Default susceptibilities diff --git a/covasim/version.py b/covasim/version.py index 58c184639..53f50b90d 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -2,6 +2,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '0.31.0' +__version__ = '0.32.0' __versiondate__ = '2020-05-05' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2020 by IDM' From 2511e3b582fae98060838ab6ed7c5e8d5e2ff332 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 20:35:45 -0700 Subject: [PATCH 141/194] update changelog --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 05983dc6c..4ea7cc1be 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,8 +6,8 @@ All notable changes to the codebase are documented in this file. Note: in many c Version 0.32.0 (2020-05-05) --------------------------- - Changed the edges of the contact network from being directed to undirected, halving the amount of memory required and making contact tracing and edge clipping more realistic. -- Added a comorbidities to the prognoses parameters. -- GitHub info: PR `481 `__, previous head ``029585f`` +- Added comorbidities to the prognoses parameters. +- GitHub info: PR `482 `__, previous head ``029585f`` Version 0.31.0 (2020-05-05) From a7d54199e1871d04447c65d71fe61a0bf164065e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 21:32:56 -0700 Subject: [PATCH 142/194] working for hybrid --- covasim/population.py | 4 ++-- tests/devtests/draw_networks.py | 30 ++++++++++++++++++------------ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/covasim/population.py b/covasim/population.py index 9d76db28c..db7e493b5 100644 --- a/covasim/population.py +++ b/covasim/population.py @@ -165,12 +165,12 @@ def make_random_contacts(pop_size, contacts, overshoot=1.2): contacts_list = [] # Precalculate contacts - n_across_layers = np.prod(list(contacts.values())) + n_across_layers = np.sum(list(contacts.values())) n_all_contacts = int(pop_size*n_across_layers*overshoot) all_contacts = cvu.choose_r(max_n=pop_size, n=n_all_contacts) # Choose people at random p_counts = {} for lkey in layer_keys: - p_counts[lkey] = cvu.n_poisson(contacts[lkey], pop_size) # Draw the number of Poisson contacts for this person + p_counts[lkey] = cvu.n_poisson(contacts[lkey], pop_size) #round(/2) # Draw the number of Poisson contacts for this person # Make contacts count = 0 diff --git a/tests/devtests/draw_networks.py b/tests/devtests/draw_networks.py index 80b5a0f5a..496c4d55a 100644 --- a/tests/devtests/draw_networks.py +++ b/tests/devtests/draw_networks.py @@ -2,18 +2,19 @@ Show contacts within different layers """ -import networkx as nx import covasim as cv -import matplotlib.pyplot as plt +import networkx as nx +import pylab as pl import seaborn as sns +import sciris as sc sns.set(font_scale=2) pars = { - 'pop_size': 1e2, # start with a small pool + 'pop_size': 200, # start with a small pool 'pop_type': 'hybrid', # synthpops, hybrid 'pop_infected': 0, # Infect none for starters 'n_days': 100, # 40d is long enough for everything to play out - 'contacts': {'h': 4.0, 's': 10, 'w': 10, 'c': 10}, + 'contacts': {'h': 2.0, 's': 4, 'w': 6, 'c': 10}, 'beta_layer': {'h': 1, 's': 1, 'w': 1, 'c': 1}, 'quar_eff': {'h': 1, 's': 1, 'w': 1, 'c': 1}, } @@ -22,20 +23,25 @@ sim = cv.Sim(pars=pars) sim.initialize() -fig = plt.figure(figsize=(16,16)) +fig = pl.figure(figsize=(16,16), dpi=120) mapping = dict(h='Households', s='Schools', w='Work', c='Community') +colors = sc.vectocolor(sim.people.age, cmap='turbo') + for i, layer in enumerate(['h', 's', 'w', 'c']): - ax = plt.subplot(2,2,i+1) + ax = pl.subplot(2,2,i+1) hdf = sim.people.contacts[layer].to_df() - G = nx.Graph() - G.add_nodes_from(set(list(hdf['p1'].unique()) + list(hdf['p2'].unique()))) + inds = list(set(list(hdf['p1'].unique()) + list(hdf['p2'].unique()))) + color = colors[inds] + + G = nx.DiGraph() + G.add_nodes_from(inds) f = hdf['p1'] t = hdf['p2'] G.add_edges_from(zip(f,t)) - print('Nodes:', G.number_of_nodes()) - print('Edges:', G.number_of_edges()) + print(f'Layer: {layer}, nodes: {G.number_of_nodes()}, edges: {G.number_of_edges()}') - nx.draw(G, ax=ax, node_size=10, width=0.1) + pos = nx.spring_layout(G, k=0.2) + nx.draw(G, pos=pos, ax=ax, node_size=40, width=0.1, arrows=True, node_color=color) ax.set_title(mapping[layer]) -plt.show() +pl.show() From 63f59ade37c914eecfa39be9382157d4f6b769a6 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 21:42:24 -0700 Subject: [PATCH 143/194] with synthpops --- tests/devtests/draw_networks.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/tests/devtests/draw_networks.py b/tests/devtests/draw_networks.py index 496c4d55a..555da850c 100644 --- a/tests/devtests/draw_networks.py +++ b/tests/devtests/draw_networks.py @@ -5,18 +5,24 @@ import covasim as cv import networkx as nx import pylab as pl +import numpy as np import seaborn as sns import sciris as sc sns.set(font_scale=2) +pop_size = 200 +pop_type = 'synthpops' + +contacts = dict( + random = {'a':20}, + hybrid = {'h': 2.0, 's': 4, 'w': 6, 'c': 10}, + synthpops = {'h': 2.0, 's': 4, 'w': 6, 'c': 10}, + ) + pars = { - 'pop_size': 200, # start with a small pool - 'pop_type': 'hybrid', # synthpops, hybrid - 'pop_infected': 0, # Infect none for starters - 'n_days': 100, # 40d is long enough for everything to play out - 'contacts': {'h': 2.0, 's': 4, 'w': 6, 'c': 10}, - 'beta_layer': {'h': 1, 's': 1, 'w': 1, 'c': 1}, - 'quar_eff': {'h': 1, 's': 1, 'w': 1, 'c': 1}, + 'pop_size': pop_size, # start with a small pool + 'pop_type': pop_type, # synthpops, hybrid + 'contacts': contacts[pop_type], } # Create sim @@ -24,11 +30,14 @@ sim.initialize() fig = pl.figure(figsize=(16,16), dpi=120) -mapping = dict(h='Households', s='Schools', w='Work', c='Community') +mapping = dict(a='All', h='Households', s='Schools', w='Work', c='Community') colors = sc.vectocolor(sim.people.age, cmap='turbo') -for i, layer in enumerate(['h', 's', 'w', 'c']): - ax = pl.subplot(2,2,i+1) +keys = list(contacts[pop_type].keys()) +nrowcol = np.ceil(np.sqrt(len(keys))) + +for i, layer in enumerate(keys): + ax = pl.subplot(nrowcol,nrowcol,i+1) hdf = sim.people.contacts[layer].to_df() inds = list(set(list(hdf['p1'].unique()) + list(hdf['p2'].unique()))) From 7fdab4997cf4bc83890626179bf44b5db44dbfd7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 21:59:46 -0700 Subject: [PATCH 144/194] matches for random and hybrid --- covasim/population.py | 12 ++++++++---- tests/devtests/draw_networks.py | 15 ++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/covasim/population.py b/covasim/population.py index db7e493b5..8ad0f9fd2 100644 --- a/covasim/population.py +++ b/covasim/population.py @@ -155,7 +155,7 @@ def make_randpop(sim, use_age_data=True, use_household_data=True, sex_ratio=0.5, return popdict, layer_keys -def make_random_contacts(pop_size, contacts, overshoot=1.2): +def make_random_contacts(pop_size, contacts, overshoot=1.2, undirected=True): ''' Make random static contacts ''' # Preprocessing @@ -170,7 +170,10 @@ def make_random_contacts(pop_size, contacts, overshoot=1.2): all_contacts = cvu.choose_r(max_n=pop_size, n=n_all_contacts) # Choose people at random p_counts = {} for lkey in layer_keys: - p_counts[lkey] = cvu.n_poisson(contacts[lkey], pop_size) #round(/2) # Draw the number of Poisson contacts for this person + if undirected: + p_counts[lkey] = np.array((cvu.n_poisson(contacts[lkey], pop_size)/2.0).round(), dtype=cvd.default_int) # Draw the number of Poisson contacts for this person + else: + p_counts[lkey] = cvu.n_poisson(contacts[lkey], pop_size) # Make contacts count = 0 @@ -185,7 +188,7 @@ def make_random_contacts(pop_size, contacts, overshoot=1.2): return contacts_list, layer_keys -def make_microstructured_contacts(pop_size, contacts): +def make_microstructured_contacts(pop_size, contacts, undirected=True): ''' Create microstructured contacts -- i.e. households, schools, etc. ''' # Preprocessing -- same as above @@ -217,7 +220,8 @@ def make_microstructured_contacts(pop_size, contacts): pass else: contacts_dict[i].add(j) - contacts_dict[j].add(i) + if not undirected: + contacts_dict[j].add(i) n_remaining -= this_cluster diff --git a/tests/devtests/draw_networks.py b/tests/devtests/draw_networks.py index 555da850c..15de31ca6 100644 --- a/tests/devtests/draw_networks.py +++ b/tests/devtests/draw_networks.py @@ -6,12 +6,11 @@ import networkx as nx import pylab as pl import numpy as np -import seaborn as sns import sciris as sc -sns.set(font_scale=2) -pop_size = 200 -pop_type = 'synthpops' +pop_size = 100 +pop_type = 'hybrid' +undirected = True contacts = dict( random = {'a':20}, @@ -45,9 +44,11 @@ G = nx.DiGraph() G.add_nodes_from(inds) - f = hdf['p1'] - t = hdf['p2'] - G.add_edges_from(zip(f,t)) + p1 = hdf['p1'] + p2 = hdf['p2'] + G.add_edges_from(zip(p1,p2)) + if undirected: + G.add_edges_from(zip(p2,p1)) print(f'Layer: {layer}, nodes: {G.number_of_nodes()}, edges: {G.number_of_edges()}') pos = nx.spring_layout(G, k=0.2) From 9801a420f270227558a478e8224e1494ce15eae7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 22:22:14 -0700 Subject: [PATCH 145/194] working for synthpops --- covasim/population.py | 32 ++++++++++++++++++-------------- tests/devtests/draw_networks.py | 7 ++++--- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/covasim/population.py b/covasim/population.py index 8ad0f9fd2..0b0baf594 100644 --- a/covasim/population.py +++ b/covasim/population.py @@ -155,7 +155,7 @@ def make_randpop(sim, use_age_data=True, use_household_data=True, sex_ratio=0.5, return popdict, layer_keys -def make_random_contacts(pop_size, contacts, overshoot=1.2, undirected=True): +def make_random_contacts(pop_size, contacts, overshoot=1.2, directed=False): ''' Make random static contacts ''' # Preprocessing @@ -170,10 +170,11 @@ def make_random_contacts(pop_size, contacts, overshoot=1.2, undirected=True): all_contacts = cvu.choose_r(max_n=pop_size, n=n_all_contacts) # Choose people at random p_counts = {} for lkey in layer_keys: - if undirected: - p_counts[lkey] = np.array((cvu.n_poisson(contacts[lkey], pop_size)/2.0).round(), dtype=cvd.default_int) # Draw the number of Poisson contacts for this person - else: + if directed: p_counts[lkey] = cvu.n_poisson(contacts[lkey], pop_size) + else: + p_counts[lkey] = np.array((cvu.n_poisson(contacts[lkey], pop_size)/2.0).round(), dtype=cvd.default_int) # Draw the number of Poisson contacts for this person + # Make contacts count = 0 @@ -188,7 +189,7 @@ def make_random_contacts(pop_size, contacts, overshoot=1.2, undirected=True): return contacts_list, layer_keys -def make_microstructured_contacts(pop_size, contacts, undirected=True): +def make_microstructured_contacts(pop_size, contacts, directed=False): ''' Create microstructured contacts -- i.e. households, schools, etc. ''' # Preprocessing -- same as above @@ -220,7 +221,7 @@ def make_microstructured_contacts(pop_size, contacts, undirected=True): pass else: contacts_dict[i].add(j) - if not undirected: + if directed: contacts_dict[j].add(i) n_remaining -= this_cluster @@ -275,7 +276,7 @@ def make_hybrid_contacts(pop_size, ages, contacts, school_ages=None, work_ages=N -def make_synthpop(sim): +def make_synthpop(sim, directed=False): ''' Make a population using synthpops, including contacts ''' import synthpops as sp # Optional import pop_size = sim['pop_size'] @@ -286,19 +287,22 @@ def make_synthpop(sim): ages.append(person['age']) sexes.append(person['sex']) - # Replace contact UIDs with ints... + # Replace contact UIDs with ints uid_mapping = {uid:u for u,uid in enumerate(uids)} key_mapping = {'H':'h', 'S':'s', 'W':'w', 'C':'c'} # Remap keys from old names to new names for uid in uids: + iid = uid_mapping[uid] # Integer UID person = population.pop(uid) uid_contacts = sc.dcp(person['contacts']) int_contacts = {} - for key in uid_contacts.keys(): - new_key = key_mapping[key] - int_contacts[new_key] = [] - for uid in uid_contacts[key]: - int_contacts[new_key].append(uid_mapping[uid]) - int_contacts[new_key] = np.array(int_contacts[new_key], dtype=cvd.default_int) + for spkey in uid_contacts.keys(): + lkey = key_mapping[spkey] # Map the SynthPops key into a Covasim layer key + int_contacts[lkey] = [] + for cid in uid_contacts[spkey]: # Contact ID + icid = uid_mapping[cid] # Integer contact ID + if icid>iid or directed: # Don't add duplicate contacts + int_contacts[lkey].append(icid) + int_contacts[lkey] = np.array(int_contacts[lkey], dtype=cvd.default_int) contacts.append(int_contacts) # Add community contacts diff --git a/tests/devtests/draw_networks.py b/tests/devtests/draw_networks.py index 15de31ca6..60719501b 100644 --- a/tests/devtests/draw_networks.py +++ b/tests/devtests/draw_networks.py @@ -1,5 +1,6 @@ """ -Show contacts within different layers +Show contacts within different layers. Used with PR #482 to check that network +properties are preserved. """ import covasim as cv @@ -8,8 +9,8 @@ import numpy as np import sciris as sc -pop_size = 100 -pop_type = 'hybrid' +pop_size = 200 +pop_type = 'synthpops' undirected = True contacts = dict( From 404256dcb48105686df8666b03c38282fe0a61a9 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 22:32:05 -0700 Subject: [PATCH 146/194] implemented in sim.py --- covasim/sim.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/covasim/sim.py b/covasim/sim.py index d9571bb78..33b970572 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -386,8 +386,8 @@ def step(self): 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(): - sources = layer['p1'] - targets = layer['p2'] + p1 = layer['p1'] + p2 = layer['p2'] betas = layer['beta'] # Compute relative transmission and susceptibility @@ -403,16 +403,17 @@ 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, diag_factor, quar_eff) # Calculate actual transmission - target_inds, edge_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! - flows['new_infections'] += people.infect(inds=target_inds, bed_max=bed_max) # Actually infect people - - # Store the transmission tree - for ind in edge_inds: - source = sources[ind] - target = targets[ind] - transdict = dict(source=source, target=target, date=self.t, layer=lkey) - self.people.transtree.linelist[target] = transdict - self.people.transtree.targets[source].append(transdict) + for sources,targets in [[p1,p2], [p2,p1]]: # Loop over the contact network from p1->p2 and p2->p1 + target_inds, edge_inds = cvu.compute_infections(beta, sources, targets, betas, rel_trans, rel_sus) # Calculate transmission! + flows['new_infections'] += people.infect(inds=target_inds, bed_max=bed_max) # Actually infect people + + # Store the transmission tree + for ind in edge_inds: + source = sources[ind] + target = targets[ind] + transdict = dict(source=source, target=target, date=self.t, layer=lkey) + self.people.transtree.linelist[target] = transdict + self.people.transtree.targets[source].append(transdict) # Update counts for this time step: stocks for key in cvd.result_stocks.keys(): From 322b4d928556867a8ccca8728cee5e33955bfdbf Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 22:39:20 -0700 Subject: [PATCH 147/194] added multisim test --- tests/devtests/default_multisim.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100755 tests/devtests/default_multisim.py diff --git a/tests/devtests/default_multisim.py b/tests/devtests/default_multisim.py new file mode 100755 index 000000000..e694c3a05 --- /dev/null +++ b/tests/devtests/default_multisim.py @@ -0,0 +1,11 @@ +import covasim as cv + +n_runs = 11 +repeats = 3 + +for i in range(repeats): + sim = cv.Sim(rand_seed=59448+i*92348) + msim = cv.MultiSim(sim, n_runs=n_runs) + msim.run() + msim.reduce() + msim.plot() From e5f2a454291e5642ea6a30779099ac1f078ceb6f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 23:00:28 -0700 Subject: [PATCH 148/194] fix tracing --- covasim/base.py | 2 +- covasim/people.py | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index bed88d0dc..9324cb41a 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -861,7 +861,7 @@ def make_edgelist(self, contacts): @staticmethod def remove_duplicates(df): - ''' Sort the dataframe and remove duplicates ''' + ''' Sort the dataframe and remove duplicates -- note, not extensively tested ''' p1 = df[['p1', 'p2']].values.min(1) # Reassign p1 to be the lower-valued of the two contacts p2 = df[['p1', 'p2']].values.max(1) # Reassign p2 to be the higher-valued of the two contacts df['p1'] = p1 diff --git a/covasim/people.py b/covasim/people.py index ca4e86543..e9385e946 100644 --- a/covasim/people.py +++ b/covasim/people.py @@ -109,7 +109,7 @@ def update_states_post(self, flows): return flows - def update_contacts(self): + def update_contacts(self, directed=False): ''' Refresh dynamic contacts, e.g. community ''' # Figure out if anything needs to be done -- e.g. {'h':False, 'c':True} @@ -120,10 +120,14 @@ def update_contacts(self): # Remove existing contacts self.contacts.pop(lkey) - # Create new contacts + # Choose how many contacts to make pop_size = len(self) n_contacts = self.pars['contacts'][lkey] n_new = n_contacts*pop_size + if not directed: + n_new = int(n_new/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) @@ -406,15 +410,18 @@ def trace(self, inds, trace_probs, trace_time): traceable_layers = {k:v for k,v in trace_probs.items() if v != 0.} # Only trace if there's a non-zero tracing probability for lkey,this_trace_prob in traceable_layers.items(): if self.pars['beta_layer'][lkey]: # Skip if beta is 0 for this layer - this_trace_time = trace_time[lkey] + # Find all the contacts of these people nzinds = self.contacts[lkey]['beta'].nonzero()[0] # Find nonzero beta edges - p1inds = np.isin(self.contacts[lkey]['p1'], inds).nonzero()[0] # Get all the indices of the pairs that each person is in - nzp1inds = np.intersect1d(nzinds, p1inds) - p2inds = np.unique(self.contacts[lkey]['p2'][nzp1inds]) # Find their pairing partner + inds_list = [] + for k1,k2 in [['p1','p2'],['p2','p1']]: # Loop over the contact network in both directions + in_k1 = np.isin(self.contacts[lkey][k1], inds).nonzero()[0] # Get all the indices of the pairs that each person is in + nz_k1 = np.intersect1d(nzinds, in_k1) # Find the ones that are nonzero + inds_list.append(self.contacts[lkey][k2][nz_k1]) # Find their pairing partner + edge_inds = np.unique(np.concatenate(inds_list)) # Find all edges # Check not diagnosed! - contact_inds = cvu.binomial_filter(this_trace_prob, p2inds) # Filter the indices according to the probability of being able to trace this layer + contact_inds = cvu.binomial_filter(this_trace_prob, edge_inds) # Filter the indices according to the probability of being able to trace this layer self.known_contact[contact_inds] = True # Set the date of contact, careful not to override what might be an earlier date. TODO: this could surely be one operation? @@ -422,10 +429,9 @@ def trace(self, inds, trace_probs, trace_time): contacted_before_inds = np.setdiff1d(contact_inds, first_time_contacted_inds) # indices of people who've been contacted before if len(first_time_contacted_inds): - self.date_known_contact[first_time_contacted_inds] = self.t + this_trace_time # Store when they were contacted + self.date_known_contact[first_time_contacted_inds] = self.t + trace_time[lkey] # Store when they were contacted if len(contacted_before_inds): - self.date_known_contact[contacted_before_inds] = np.minimum(self.date_known_contact[contacted_before_inds], self.t + this_trace_time) - + self.date_known_contact[contacted_before_inds] = np.minimum(self.date_known_contact[contacted_before_inds], self.t + trace_time[lkey]) return From 5e9057325a3b15220c442f8b9b7697ed17ede404 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 5 May 2020 23:06:37 -0700 Subject: [PATCH 149/194] updated baseline --- tests/baseline.json | 44 +++--- tests/benchmark.json | 4 +- tests/regression/parameters_v0.32.0.json | 190 +++++++++++++++++++++++ 3 files changed, 214 insertions(+), 24 deletions(-) create mode 100644 tests/regression/parameters_v0.32.0.json diff --git a/tests/baseline.json b/tests/baseline.json index 7227051ea..1e847e5e9 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -1,35 +1,35 @@ { "summary": { - "cum_infections": 6102.0, - "cum_infectious": 4298.0, + "cum_infections": 7552.0, + "cum_infectious": 5595.0, "cum_tests": 0.0, "cum_diagnoses": 0.0, - "cum_recoveries": 1922.0, - "cum_symptomatic": 2755.0, - "cum_severe": 155.0, - "cum_critical": 32.0, - "cum_deaths": 12.0, + "cum_recoveries": 2543.0, + "cum_symptomatic": 3596.0, + "cum_severe": 191.0, + "cum_critical": 54.0, + "cum_deaths": 18.0, "cum_quarantined": 0.0, - "new_infections": 416.0, - "new_infectious": 321.0, + "new_infections": 436.0, + "new_infectious": 409.0, "new_tests": 0.0, "new_diagnoses": 0.0, - "new_recoveries": 176.0, - "new_symptomatic": 213.0, - "new_severe": 8.0, - "new_critical": 6.0, - "new_deaths": 3.0, + "new_recoveries": 227.0, + "new_symptomatic": 268.0, + "new_severe": 17.0, + "new_critical": 4.0, + "new_deaths": 4.0, "new_quarantined": 0.0, - "n_susceptible": 13898.0, - "n_exposed": 4168.0, - "n_infectious": 2364.0, - "n_symptomatic": 1485.0, - "n_severe": 112.0, - "n_critical": 19.0, + "n_susceptible": 12448.0, + "n_exposed": 4991.0, + "n_infectious": 3034.0, + "n_symptomatic": 1908.0, + "n_severe": 137.0, + "n_critical": 30.0, "n_diagnosed": 0.0, "n_quarantined": 0.0, "bed_capacity": 0.0, - "r_eff": 1.0759853505262773, - "doubling_time": 9.522931495254712 + "r_eff": 0.8970813187827296, + "doubling_time": 10.602107136721475 } } \ No newline at end of file diff --git a/tests/benchmark.json b/tests/benchmark.json index ec81e5b69..7e2068abc 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,7 +1,7 @@ { "time": { - "initialize": 0.212, - "run": 0.162 + "initialize": 0.164, + "run": 0.166 }, "parameters": { "pop_size": 20000, diff --git a/tests/regression/parameters_v0.32.0.json b/tests/regression/parameters_v0.32.0.json new file mode 100644 index 000000000..b780c89cb --- /dev/null +++ b/tests/regression/parameters_v0.32.0.json @@ -0,0 +1,190 @@ +{ + "pop_size": 20000, + "pop_infected": 10, + "pop_type": "random", + "location": null, + "start_day": "2020-03-01", + "end_day": "2020-04-30", + "n_days": 60, + "rand_seed": 1, + "verbose": 0, + "pop_scale": 1, + "rescale": 0, + "rescale_threshold": 0.05, + "rescale_factor": 2, + "beta": 0.016, + "contacts": { + "a": 20 + }, + "dynam_layer": { + "a": 0 + }, + "beta_layer": { + "a": 1.0 + }, + "n_imports": 0, + "beta_dist": { + "dist": "lognormal", + "par1": 0.84, + "par2": 0.3 + }, + "viral_dist": { + "frac_time": 0.3, + "load_ratio": 2, + "high_cap": 4 + }, + "asymp_factor": 1.0, + "diag_factor": 0.2, + "quar_eff": { + "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 + } + }, + "OR_no_treat": 2.0, + "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": [ + 10, + 20, + 30, + 40, + 50, + 60, + 70, + 80, + 120 + ], + "sus_ORs": [ + 0.34, + 0.67, + 1.0, + 1.0, + 1.0, + 1.0, + 1.24, + 1.47, + 1.47 + ], + "trans_ORs": [ + 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 + ], + "symp_probs": [ + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9 + ], + "severe_probs": [ + 0.001, + 0.0029999999999999996, + 0.012, + 0.032, + 0.049, + 0.102, + 0.16599999999999998, + 0.24300000000000002, + 0.273 + ], + "crit_probs": [ + 0.06, + 0.04848484848484849, + 0.05, + 0.049999999999999996, + 0.06297376093294461, + 0.12196078431372549, + 0.2740210843373494, + 0.43200193657709995, + 0.708994708994709 + ], + "death_probs": [ + 0.6666666666666667, + 0.75, + 0.8333333333333333, + 0.7692307692307694, + 0.6944444444444444, + 0.6430868167202572, + 0.6045616927727397, + 0.5715566513504426, + 0.5338691159586683 + ] + }, + "interventions": [], + "interv_func": null, + "timelimit": 3600, + "stopping_func": null, + "n_beds": null +} \ No newline at end of file From d5d8b1d83cf0239590abb334e848b3128a3a8b23 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 6 May 2020 00:06:40 -0700 Subject: [PATCH 150/194] added animation --- tests/devtests/transtree_animation.py | 189 ++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 tests/devtests/transtree_animation.py diff --git a/tests/devtests/transtree_animation.py b/tests/devtests/transtree_animation.py new file mode 100644 index 000000000..fd78a8045 --- /dev/null +++ b/tests/devtests/transtree_animation.py @@ -0,0 +1,189 @@ +''' +Animation of transmission tree plotting for no interventions, testing only, +and TTQ. +''' + +import covasim as cv +import pylab as pl +import sciris as sc +import numpy as np + + +tstart = sc.tic() + +plot_sim = 0 +verbose = 0 +animate = 1 +animate_all = 0 + +isday = 15 +ieday = None +pop_type = 'hybrid' +lkeys = dict( + random='a', + hybrid='hswc' + ) +tp = cv.test_prob(start_day=isday, end_day=ieday, symp_prob=0.01, asymp_prob=0.0, symp_quar_prob=1.0, asymp_quar_prob=1.0, test_delay=0) +ct = cv.contact_tracing(trace_probs={k:1.0 for k in lkeys[pop_type]}, + trace_time={k:0 for k in lkeys[pop_type]}, + start_day=isday) + +contacts = dict( + random=10, + hybrid=dict(h=2, s=2, w=2, c=2), + ) +beta = dict( + random=0.6/contacts['random'], + hybrid=0.1, + ) +pop_infected = dict( + random=1, + hybrid=10, + ) +pars = dict( + pop_size = 800, + pop_infected = pop_infected[pop_type], + pop_type = pop_type, + n_days = 90, + contacts = contacts[pop_type], + beta = beta[pop_type], + rand_seed = 3248, + ) + +labels = ['No interventions', 'Testing only', 'Test + trace'] +sims = sc.objdict() +sims.base = cv.Sim(pars) # Baseline +sims.test = cv.Sim(pars, interventions=tp) # Testing only +sims.trace = cv.Sim(pars, interventions=[tp, ct]) # Testing + contact tracing + +tts = sc.objdict() +for key,sim in sims.items(): + sim.run() + sim.people.make_detailed_transtree() + tts[key] = sim.people.transtree.detailed + if plot_sim: + to_plot = cv.get_sim_plots() + to_plot['Total counts'] = ['cum_infections', 'cum_diagnoses', 'cum_quarantined', 'n_quarantined'] + sim.plot(to_plot=to_plot) + + +#%% Plotting + +colors = sc.vectocolor(sim.n, cmap='parula') + +msize = 10 +suscol = [0.5,0.5,0.5] +plargs = dict(lw=2, alpha=0.5) +idelay = 0.05 +daydelay = 0.3 +pl.rcParams['font.size'] = 18 + +F = sc.objdict() +T = sc.objdict() +D = sc.objdict() +Q = sc.objdict() + +for key in sims.keys(): + + tt = tts[key] + + frames = [list() for i in range(sim.npts)] + tests = [list() for i in range(sim.npts)] + diags = [list() for i in range(sim.npts)] + quars = [list() for i in range(sim.npts)] + + for i,entry in enumerate(tt): + frame = sc.objdict() + dq = sc.objdict() + if entry: + source = entry['source'] + target = entry['target'] + target_date = entry['date'] + if source: + source_date = tt[source]['date'] + else: + source = 0 + source_date = 0 + + frame.x = [source_date, target_date] + frame.y = [source, target] + frame.c = colors[source] # colors[target_date] + frame.e = True + frames[target_date].append(frame) + + dq.t = target + dq.d = target_date + dq.c = colors[target] + date_t = entry.t.date_tested + date_d = entry.t.date_diagnosed + date_q = entry.t.date_known_contact + if ~np.isnan(date_t) and date_t Date: Wed, 6 May 2020 19:19:44 +0000 Subject: [PATCH 151/194] fixed the run until parameter --- covasim/sim.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/covasim/sim.py b/covasim/sim.py index 33b970572..585960d78 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -452,6 +452,8 @@ def run(self, do_plot=False, until=None, verbose=None, **kwargs): self.init_interventions() # And interventions if verbose is None: verbose = self['verbose'] + if ~sc.isnumber(until) and until!=None: + until = self.day(until) # Main simulation loop for t in self.tvec: @@ -784,4 +786,4 @@ def plot_result(self, key, *args, **kwargs): sim.plot_result('r_eff') ''' fig = cvplt.plot_result(sim=self, key=key, *args, **kwargs) - return fig \ No newline at end of file + return fig From 9f5db46a6ba164c699eefc69dc8954aa5f8244ca Mon Sep 17 00:00:00 2001 From: Gregory Hart Date: Wed, 6 May 2020 19:43:35 +0000 Subject: [PATCH 152/194] made changes more condensed and hopefully removed the new line --- covasim/sim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/sim.py b/covasim/sim.py index 585960d78..a4fa28e4c 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -452,7 +452,7 @@ def run(self, do_plot=False, until=None, verbose=None, **kwargs): self.init_interventions() # And interventions if verbose is None: verbose = self['verbose'] - if ~sc.isnumber(until) and until!=None: + if until: until = self.day(until) # Main simulation loop From 3424fd352671f74eafa56af8c0a8c439ec654a69 Mon Sep 17 00:00:00 2001 From: Gregory Hart Date: Wed, 6 May 2020 19:48:13 +0000 Subject: [PATCH 153/194] removing the end of line charater from the end of the file? --- covasim/sim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/sim.py b/covasim/sim.py index a4fa28e4c..4efe6a8f6 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -786,4 +786,4 @@ def plot_result(self, key, *args, **kwargs): sim.plot_result('r_eff') ''' fig = cvplt.plot_result(sim=self, key=key, *args, **kwargs) - return fig + return fig \ No newline at end of file From 026d84b1f1cb82724582156aa9ca63c0997cb1bb Mon Sep 17 00:00:00 2001 From: Mary Fisher Date: Wed, 6 May 2020 15:56:41 -0700 Subject: [PATCH 154/194] add ipywidgets req got plotly graph_obj error --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5b146d7be..55101a645 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ sciris>=0.17.0 scirisweb>=0.17.0 gunicorn plotly_express -fire \ No newline at end of file +fire +ipywidgets \ No newline at end of file From 079bd2bfddcec1349154247f3713b921024a48a3 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 6 May 2020 16:16:54 -0700 Subject: [PATCH 155/194] updated version and changelog --- CHANGELOG.rst | 6 ++++++ covasim/version.py | 4 ++-- requirements.txt | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4ea7cc1be..9e350f730 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,12 @@ What's new All notable changes to the codebase are documented in this file. Note: in many cases, changes from multiple patch versions are grouped together, so numbering will not be strictly consecutive. + +Version 0.32.1 (2020-05-06) +--------------------------- +- Added `ipywidgets` dependency since otherwise the webapp breaks due to a `bug `__ with the latest Plotly version (4.7). + + Version 0.32.0 (2020-05-05) --------------------------- - Changed the edges of the contact network from being directed to undirected, halving the amount of memory required and making contact tracing and edge clipping more realistic. diff --git a/covasim/version.py b/covasim/version.py index 53f50b90d..021df1bb8 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -2,6 +2,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '0.32.0' -__versiondate__ = '2020-05-05' +__version__ = '0.32.1' +__versiondate__ = '2020-05-06' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2020 by IDM' diff --git a/requirements.txt b/requirements.txt index 55101a645..5ab2a06fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,5 +7,5 @@ sciris>=0.17.0 scirisweb>=0.17.0 gunicorn plotly_express -fire -ipywidgets \ No newline at end of file +ipywidgets +fire \ No newline at end of file From 915e94859111f86b42ebd892ca75733ab63b79ec Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 6 May 2020 16:18:24 -0700 Subject: [PATCH 156/194] update changelog --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9e350f730..529deef04 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,7 +6,8 @@ All notable changes to the codebase are documented in this file. Note: in many c Version 0.32.1 (2020-05-06) --------------------------- -- Added `ipywidgets` dependency since otherwise the webapp breaks due to a `bug `__ with the latest Plotly version (4.7). +- Allow ``until`` to be a date, e.g. ``sim.run(until='2020-05-06')``. +- Added ``ipywidgets`` dependency since otherwise the webapp breaks due to a `bug `__ with the latest Plotly version (4.7). Version 0.32.0 (2020-05-05) From 17108261e0cb7af81226176a77905888a691b292 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 14:19:23 -0700 Subject: [PATCH 157/194] updated makefile warnings --- docs/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index 5b2b34ada..d2b12cb98 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,7 @@ # # You can set these variables from the command line. -SPHINXOPTS = +SPHINXOPTS = -W INTERNALOPTS = -t internal SPHINXBUILD = sphinx-build INDEXBUILD = python $(BUILDDIR)/../scripts/gti.py From ba190c7d012cd5bea8dfea117c009fecca6fb80d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 14:22:30 -0700 Subject: [PATCH 158/194] updated version! --- covasim/parameters.py | 2 +- covasim/version.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index a1e47364d..be454a32c 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -57,7 +57,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): # Duration parameters: time for disease progression pars['dur'] = {} - pars['dur']['exp2inf'] = {'dist':'lognormal_int', 'par1':4.6, 'par2':4.8} # Duration from exposed to infectious; see Linton et al., https://doi.org/10.3390/jcm9020538 + pars['dur']['exp2inf'] = {'dist':'lognormal_int', 'par1':4.6, 'par2':1.0} # Duration from exposed to infectious; see Linton et al., https://doi.org/10.3390/jcm9020538 pars['dur']['inf2sym'] = {'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'] = {'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'] = {'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/version.py b/covasim/version.py index 021df1bb8..bb8d8a5d0 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -2,6 +2,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '0.32.1' -__versiondate__ = '2020-05-06' +__version__ = '1.0.0' +__versiondate__ = '2020-05-08' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2020 by IDM' From de8144c7c63df81502aecd541093940191098a6a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 14:24:15 -0700 Subject: [PATCH 159/194] updated changelog --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 529deef04..9d8299c62 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,11 @@ What's new All notable changes to the codebase are documented in this file. Note: in many cases, changes from multiple patch versions are grouped together, so numbering will not be strictly consecutive. +Version 1.0.0 (2020-05-08) +-------------------------- +- Updated standard deviation of ``exp2inf`` to be 1 day. +- GitHub info: PR `482 `__, previous head ``c8ca32d`` + Version 0.32.1 (2020-05-06) --------------------------- From 977601f5d5533b9f796664a896b71b35adcee50c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 14:35:48 -0700 Subject: [PATCH 160/194] reverting for now pending literature review --- covasim/parameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index be454a32c..a1e47364d 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -57,7 +57,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): # Duration parameters: time for disease progression pars['dur'] = {} - pars['dur']['exp2inf'] = {'dist':'lognormal_int', 'par1':4.6, 'par2':1.0} # Duration from exposed to infectious; see Linton et al., https://doi.org/10.3390/jcm9020538 + pars['dur']['exp2inf'] = {'dist':'lognormal_int', 'par1':4.6, 'par2':4.8} # Duration from exposed to infectious; see Linton et al., https://doi.org/10.3390/jcm9020538 pars['dur']['inf2sym'] = {'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'] = {'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'] = {'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 From 9b545e7e949aa7601886b21becd7f4478a7ffe81 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 14:54:17 -0700 Subject: [PATCH 161/194] updated run_coverage and stopping plots from appearing in tests --- CHANGELOG.rst | 2 +- tests/run_coverage | 3 +++ tests/test_examples.py | 5 ++++- tests/unittests/run_coverage | 3 --- 4 files changed, 8 insertions(+), 5 deletions(-) create mode 100755 tests/run_coverage delete mode 100755 tests/unittests/run_coverage diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9d8299c62..b73af73f3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,7 +6,7 @@ All notable changes to the codebase are documented in this file. Note: in many c Version 1.0.0 (2020-05-08) -------------------------- - Updated standard deviation of ``exp2inf`` to be 1 day. -- GitHub info: PR `482 `__, previous head ``c8ca32d`` +- GitHub info: PR `487 `__, previous head ``c8ca32d`` Version 0.32.1 (2020-05-06) diff --git a/tests/run_coverage b/tests/run_coverage new file mode 100755 index 000000000..2fc8377d5 --- /dev/null +++ b/tests/run_coverage @@ -0,0 +1,3 @@ +#!/bin/bash +coverage run -m pytest test_* unittests/test_* +coverage report diff --git a/tests/test_examples.py b/tests/test_examples.py index 2b1ca2822..7302523ba 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -2,10 +2,13 @@ Run examples/*.py using pytest ''' -import importlib.util as iu import os from pathlib import Path +import importlib.util as iu +import pylab as pl +import sciris as sc +pl.switch_backend('agg') # To avoid graphs from appearing cwd = Path(os.path.dirname(os.path.abspath(__file__))) examples_dir = cwd.joinpath('../examples') diff --git a/tests/unittests/run_coverage b/tests/unittests/run_coverage deleted file mode 100755 index 560919765..000000000 --- a/tests/unittests/run_coverage +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -coverage run -m unittest unittest_* test_* -coverage html From 244b16e257d0e88db7aefffd3f240aca324fcda0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 17:02:48 -0700 Subject: [PATCH 162/194] updated setup.py --- CHANGELOG.rst | 1 + setup.py | 8 ++++---- tests/run_coverage | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b73af73f3..7892fa1d3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,7 @@ All notable changes to the codebase are documented in this file. Note: in many c Version 1.0.0 (2020-05-08) -------------------------- +- Official release of Covasim. - Updated standard deviation of ``exp2inf`` to be 1 day. - GitHub info: PR `487 `__, previous head ``c8ca32d`` diff --git a/setup.py b/setup.py index 3614f1d3d..9c2b6bab4 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules", - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3.7", ] @@ -58,11 +58,11 @@ name="covasim", version=version, author="Cliff Kerr, Robyn Stuart, Romesh Abeysuriya, Dina Mistry, Lauren George, and Daniel Klein, on behalf of the IDM COVID-19 Response Team", - author_email="covid@idmod.org", - description="COVID-19 agent-based simulator", + author_email="covasim@idmod.org", + description="COVID-19 Agent-based Simulator", long_description=long_description, long_description_content_type="text/x-rst", - url='http://github.com/institutefordiseasemodeling/covasim', + url='http://covasim.org', keywords=["Covid-19", "coronavirus", "SARS-CoV-2", "stochastic", "agent-based model"], platforms=["OS Independent"], classifiers=CLASSIFIERS, diff --git a/tests/run_coverage b/tests/run_coverage index 2fc8377d5..4b5018797 100755 --- a/tests/run_coverage +++ b/tests/run_coverage @@ -1,3 +1,4 @@ #!/bin/bash -coverage run -m pytest test_* unittests/test_* +coverage run --source=../covasim --omit=*cruise_ship*,*webapp* -m pytest test_* +echo 'Running report...' coverage report From a6f6e789b6ab32f4fff2b517abfa78582a52276a Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 20:24:05 -0700 Subject: [PATCH 163/194] added comment to parameter --- covasim/parameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covasim/parameters.py b/covasim/parameters.py index a1e47364d..2d27cfb3b 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -57,7 +57,7 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): # Duration parameters: time for disease progression pars['dur'] = {} - pars['dur']['exp2inf'] = {'dist':'lognormal_int', 'par1':4.6, 'par2':4.8} # Duration from exposed to infectious; see Linton et al., https://doi.org/10.3390/jcm9020538 + pars['dur']['exp2inf'] = {'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'] = {'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'] = {'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'] = {'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 From f7058ab7f54850efc2cc8c4c162ef50a76394c7c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 20:31:29 -0700 Subject: [PATCH 164/194] fixed custom scenario plotting --- covasim/plotting.py | 1 + tests/test_interventions_other.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 958696152..73f6117fc 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -249,6 +249,7 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, default_colors = sc.gridcolors(ncolors=len(scens.sims)) for pnum,title,reskeys in to_plot.enumitems(): ax = create_subplots(figs, fig, ax, n_rows, n_cols, pnum, args.fig, sep_figs, log_scale, title) + reskeys = sc.promotetolist(reskeys) # In case it's a string for reskey in reskeys: resdata = scens.results[reskey] for snum,scenkey,scendata in resdata.enumitems(): diff --git a/tests/test_interventions_other.py b/tests/test_interventions_other.py index a417d6dd2..abc735f87 100644 --- a/tests/test_interventions_other.py +++ b/tests/test_interventions_other.py @@ -87,10 +87,10 @@ def test_beds(do_plot=False, do_show=True, do_save=False, fig_path=None): if do_plot: to_plot = sc.odict({ - 'cum_deaths': 'Cumulative deaths', - 'bed_capacity': 'People needing beds / beds', - 'n_severe': 'Number of cases requiring hospitalization', - 'n_critical': 'Number of cases requiring ICU', + 'Cumulative deaths': 'cum_deaths', + 'People needing beds / beds': 'bed_capacity', + 'Number of cases requiring hospitalization': 'n_severe', + 'Number of cases requiring ICU': 'n_critical', }) scens.plot(to_plot=to_plot, do_save=do_save, do_show=do_show, fig_path=fig_path) From 8424bd0320ae19206512f5612217d6331daa84d0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 20:56:51 -0700 Subject: [PATCH 165/194] make scenario plotting more flexible --- covasim/plotting.py | 14 ++++++++++++-- covasim/run.py | 1 - covasim/sim.py | 2 +- tests/test_interventions_testing.py | 8 ++++---- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 73f6117fc..f7c717d7c 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -33,7 +33,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): +def handle_to_plot(which, to_plot, n_cols, sim=None): ''' Handle which quantities to plot ''' if to_plot is None: @@ -44,6 +44,16 @@ def handle_to_plot(which, to_plot, n_cols): else: errormsg = f'"which" must be "sim" or "scens", not "{which}"' raise NotImplementedError(errormsg) + elif isinstance(to_plot, list): # If a list of keys has been supplied + if sim is None: + errormsg = f'Cannot plot {to_plot} as a list without a sim supplied; supply as a title:resultkey dict instead' + raise ValueError(errormsg) + else: + 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 + to_plot = sc.odict(sc.dcp(to_plot)) # In case it's supplied as a dict n_rows = np.ceil(len(to_plot)/n_cols) # Number of subplot rows to have @@ -242,7 +252,7 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, # Handle inputs args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) - to_plot, n_rows = handle_to_plot('scens', to_plot, n_cols) + to_plot, n_rows = handle_to_plot('scens', to_plot, n_cols, sim=scens.base_sim) fig, figs, ax = create_figs(args, font_size, font_family, sep_figs, fig) # Do the plotting diff --git a/covasim/run.py b/covasim/run.py index c48b9e4b9..a995888f9 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -4,7 +4,6 @@ #%% Imports import numpy as np -import pylab as pl import pandas as pd import sciris as sc from collections import defaultdict diff --git a/covasim/sim.py b/covasim/sim.py index 4efe6a8f6..c52dc6dee 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -216,7 +216,7 @@ def init_results(self): We differentiate between flows, stocks, and cumulative results The prefix "new" is used for flow variables, i.e. counting new events (infections/deaths/recoveries) on each timestep The prefix "n" is used for stock variables, i.e. counting the total number in any given state (sus/inf/rec/etc) on any particular timestep - The prefix "cum\_" is used for cumulative variables, i.e. counting the total number that have ever been in a given state at some point in the sim + The prefix "cum" is used for cumulative variables, i.e. counting the total number that have ever been in a given state at some point in the sim Note that, by definition, n_dead is the same as cum_deaths and n_recovered is the same as cum_recoveries, so we only define the cumulative versions ''' diff --git a/tests/test_interventions_testing.py b/tests/test_interventions_testing.py index a98f5e7be..323492fd5 100644 --- a/tests/test_interventions_testing.py +++ b/tests/test_interventions_testing.py @@ -166,7 +166,7 @@ def test_tracedelay(do_plot=False, do_show=True, do_save=False, fig_path=None): # Define the scenarios scenarios = { 'lowtrace': { - 'name': 'Poor contact tracing; 7d quarantine; 50% acquision reduction', + 'name': 'Poor contact tracing', 'pars': { 'quar_eff': {'h': 1, 's': 0.5, 'w': 0.5, 'c': 0.25}, 'quar_period': 7, @@ -176,7 +176,7 @@ def test_tracedelay(do_plot=False, do_show=True, do_save=False, fig_path=None): } }, 'modtrace': { - 'name': 'Moderate contact tracing; 10d quarantine; 75% acquision reduction', + 'name': 'Moderate contact tracing', 'pars': { 'quar_eff': {'h': 0.75, 's': 0.25, 'w': 0.25, 'c': 0.1}, 'quar_period': 10, @@ -186,7 +186,7 @@ def test_tracedelay(do_plot=False, do_show=True, do_save=False, fig_path=None): } }, 'hightrace': { - 'name': 'Fast contact tracing; 14d quarantine; 90% acquision reduction', + 'name': 'Fast contact tracing', 'pars': { 'quar_eff': {'h': 0.5, 's': 0.1, 'w': 0.1, 'c': 0.1}, 'quar_period': 14, @@ -196,7 +196,7 @@ def test_tracedelay(do_plot=False, do_show=True, do_save=False, fig_path=None): } }, 'alltrace': { - 'name': 'Same-day contact tracing; 21d quarantine; 100% acquision reduction', + 'name': 'Same-day contact tracing', 'pars': { 'quar_eff': {'h': 0.0, 's': 0.0, 'w': 0.0, 'c': 0.0}, 'quar_period': 21, From 0e14966e25f8b54895f23f9bb423cd9775720d5e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 20:58:44 -0700 Subject: [PATCH 166/194] update changelog --- CHANGELOG.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7892fa1d3..0097095b6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,10 +3,12 @@ What's new All notable changes to the codebase are documented in this file. Note: in many cases, changes from multiple patch versions are grouped together, so numbering will not be strictly consecutive. + Version 1.0.0 (2020-05-08) -------------------------- - Official release of Covasim. -- Updated standard deviation of ``exp2inf`` to be 1 day. +- Made scenario and simulation plotting more flexible: ``to_plot`` can now simply be a list of results keys, e.g. ``cum_deaths``. +- Added additional tests. - GitHub info: PR `487 `__, previous head ``c8ca32d`` From 1280055befd894a66a1705d1ea1afe7645f70db9 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 21:20:04 -0700 Subject: [PATCH 167/194] sped up unit tests --- tests/unittests/test_interventions.py | 34 ++++++------ tests/unittests/test_population_types.py | 2 +- tests/unittests/test_simulation_parameter.py | 58 ++------------------ 3 files changed, 25 insertions(+), 69 deletions(-) diff --git a/tests/unittests/test_interventions.py b/tests/unittests/test_interventions.py index cd7787fd9..fc396631e 100644 --- a/tests/unittests/test_interventions.py +++ b/tests/unittests/test_interventions.py @@ -6,6 +6,8 @@ import unittest +AGENT_COUNT = 1000 + ResultsKeys = TestProperties.ResultsDataKeys SimKeys = TestProperties.ParameterKeys.SimulationKeys @@ -21,7 +23,7 @@ def tearDown(self): # region change beta def test_brutal_change_beta_intervention(self): params = { - SimKeys.number_agents: 5000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } self.set_simulation_parameters(params_dict=params) @@ -52,7 +54,7 @@ def test_brutal_change_beta_intervention(self): def test_change_beta_days(self): params = { - SimKeys.number_agents: 10000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } self.set_simulation_parameters(params_dict=params) @@ -100,7 +102,7 @@ def test_change_beta_days(self): def test_change_beta_multipliers(self): params = { - SimKeys.number_agents: 5000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 40 } self.set_simulation_parameters(params_dict=params) @@ -154,7 +156,7 @@ def test_change_beta_layers_clustered(self): for seed in seed_list: params = { SimKeys.random_seed: seed, - SimKeys.number_agents: 5000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60, SimKeys.initial_infected_count: initial_infected } @@ -225,7 +227,7 @@ def test_change_beta_layers_random(self): for seed in seed_list: params = { SimKeys.random_seed: seed, - SimKeys.number_agents: 5000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60, SimKeys.initial_infected_count: initial_infected } @@ -283,7 +285,7 @@ def test_change_beta_layers_hybrid(self): for seed in seed_list: params = { SimKeys.random_seed: seed, - SimKeys.number_agents: 5000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60, SimKeys.initial_infected_count: initial_infected } @@ -348,7 +350,7 @@ def test_change_beta_layers_synthpops(self): self.is_debugging = False initial_infected = 10 params = { - SimKeys.number_agents: 5000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60, SimKeys.initial_infected_count: initial_infected } @@ -461,9 +463,9 @@ def test_test_prob_perfect_asymptomatic(self): ''' self.is_debugging = False - agent_count = 5000 + agent_count = AGENT_COUNT params = { - SimKeys.number_agents: 5000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } self.set_simulation_parameters(params_dict=params) @@ -500,7 +502,7 @@ def test_test_prob_perfect_asymptomatic(self): def test_test_prob_perfect_symptomatic(self): self.is_debugging = False params = { - SimKeys.number_agents: 5000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } self.set_simulation_parameters(params_dict=params) @@ -532,9 +534,9 @@ def test_test_prob_perfect_symptomatic(self): def test_test_prob_perfect_not_quarantined(self): self.is_debugging = False - agent_count = 5000 + agent_count = AGENT_COUNT params = { - SimKeys.number_agents: 5000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } self.set_simulation_parameters(params_dict=params) @@ -571,7 +573,7 @@ def test_test_prob_sensitivity(self, subtract_today_recoveries=False): for seed in seed_list: params = { SimKeys.random_seed: seed, - SimKeys.number_agents: 5000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 31 } self.set_simulation_parameters(params_dict=params) @@ -653,7 +655,7 @@ def test_test_prob_sensitivity(self, subtract_today_recoveries=False): def test_test_prob_symptomatic_prob_of_test(self): params = { - SimKeys.number_agents: 5000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 31 } self.set_simulation_parameters(params_dict=params) @@ -704,7 +706,7 @@ def test_test_prob_symptomatic_prob_of_test(self): # region contact tracing def test_brutal_contact_tracing(self): params = { - SimKeys.number_agents: 5000, + SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 55 } self.set_simulation_parameters(params_dict=params) @@ -765,7 +767,7 @@ def test_contact_tracing_perfect_school_layer(self): self.is_debugging = False initial_infected = 10 params = { - SimKeys.number_agents: 5000, + 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}, 'quar_period': 10, diff --git a/tests/unittests/test_population_types.py b/tests/unittests/test_population_types.py index 1b0d0a51b..4a02d3747 100644 --- a/tests/unittests/test_population_types.py +++ b/tests/unittests/test_population_types.py @@ -16,7 +16,7 @@ def test_different_pop_types(self): pop_types = ['random', 'hybrid', 'clustered'] #, 'synthpops'] results = {} short_sample = { - TPKeys.number_agents: 10000, + TPKeys.number_agents: 1000, TPKeys.number_simulated_days: 10, TPKeys.initial_infected_count: 50 } diff --git a/tests/unittests/test_simulation_parameter.py b/tests/unittests/test_simulation_parameter.py index f5704f584..f7eebb9c5 100644 --- a/tests/unittests/test_simulation_parameter.py +++ b/tests/unittests/test_simulation_parameter.py @@ -52,36 +52,6 @@ def test_population_size(self): TPKeys.number_agents: 1234, TPKeys.initial_infected_count: 0 } - pop_10k_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 1e4, - TPKeys.initial_infected_count: 0 - } - pop_100k_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 1e5, - TPKeys.initial_infected_count: 0 - } - pop_200k_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 2e5, - TPKeys.initial_infected_count: 0 - } - pop_400k_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 4e5, - TPKeys.initial_infected_count: 0 - } - pop_1M_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 1e6, - TPKeys.initial_infected_count: 0 - } self.run_sim(pop_2_one_day) pop_2_pop = self.get_day_zero_channel_value() self.run_sim(pop_10_one_day) @@ -91,29 +61,11 @@ def test_population_size(self): self.run_sim(pop_1234_one_day) pop_1234_pop = self.get_day_zero_channel_value() - self.run_sim(pop_10k_one_day) - pop_10k_pop = self.get_day_zero_channel_value() - self.run_sim(pop_100k_one_day) - pop_100k_pop = self.get_day_zero_channel_value() - # self.run_sim(pop_200k_one_day) - # pop_200k_pop = self.get_day_zero_channel_value() - - # self.run_sim(pop_400k_one_day) - # pop_400k_pop = self.get_day_zero_channel_value() - # self.run_sim(pop_1M_one_day) - # pop_1M_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_10k_pop, pop_10k_one_day[TPKeys.number_agents]) - self.assertEqual(pop_100k_pop, pop_100k_one_day[TPKeys.number_agents]) - # self.assertEqual(pop_200k_pop, pop_200k_one_day[TPKeys.number_agents]) - - # self.assertEqual(pop_400k_pop, pop_400k_one_day[TPKeys.number_agents]) - # self.assertEqual(pop_1M_pop, pop_1M_one_day[TPKeys.number_agents]) pass def test_population_size_ranges(self): @@ -141,10 +93,6 @@ def test_population_size_ranges(self): self.assertEqual(self.simulation_result['results'][ResKeys.susceptible_at_timestep][-1], 0) self.assertEqual(self.simulation_result['results'][ResKeys.susceptible_at_timestep][0], 0) - # with self.assertRaises(Exception) as context: - # self.run_sim(pop_zero_one_day) - # error_message = str(context.exception) - # self.assertIn("population", error_message) pass def test_population_scaling(self): @@ -155,14 +103,17 @@ 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 } scale_2_one_day = { + TPKeys.number_agents: 100, TPKeys.population_scaling_factor: 2, TPKeys.number_simulated_days: 1 } scale_10_one_day = { + TPKeys.number_agents: 100, TPKeys.population_scaling_factor: 10, TPKeys.number_simulated_days: 1 } @@ -182,16 +133,19 @@ def test_initial_infected_count(self): Run sim for one day and verify correct count """ infected_0_one_day = { + TPKeys.number_agents: 100, TPKeys.number_simulated_days: 1, TPKeys.population_scaling_factor: 1, TPKeys.initial_infected_count: 0 } infected_1_one_day = { + TPKeys.number_agents: 100, TPKeys.population_scaling_factor: 1, TPKeys.number_simulated_days: 1, TPKeys.initial_infected_count: 1 } infected_321_one_day = { + TPKeys.number_agents: 1000, TPKeys.population_scaling_factor: 1, TPKeys.number_simulated_days: 1, TPKeys.initial_infected_count: 321 From 8e6f87eb3d76b8d19d716b75064417339fb08c0e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 21:58:03 -0700 Subject: [PATCH 168/194] improved coverage of base.py --- covasim/base.py | 11 +----- tests/run_coverage | 2 + tests/test_other.py | 94 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 9 deletions(-) create mode 100755 tests/test_other.py diff --git a/covasim/base.py b/covasim/base.py index 9324cb41a..79e55b5f6 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -35,7 +35,6 @@ def __getitem__(self, key): 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 __setitem__(self, key, value): ''' Ditto ''' @@ -274,12 +273,6 @@ def result_keys(self): return keys - def layer_keys(self): - ''' Get the available contact keys -- set by beta_layer rather than contacts since only the former is required ''' - keys = list(self['beta_layer'].keys()) - return keys - - def copy(self): ''' Returns a deep copy of the sim ''' return sc.dcp(self) @@ -379,7 +372,7 @@ def to_json(self, filename=None, keys=None, tostring=False, indent=2, verbose=Fa keys = ['results', 'pars', 'summary'] keys = sc.promotetolist(keys) - # Convert to JSON-compatibleformat + # Convert to JSON-compatible format d = {} for key in keys: if key == 'results': @@ -903,7 +896,7 @@ def __getitem__(self, key): dictkey = self.keys()[key] return self[dictkey] except: - raise KE # This is the original errors + raise sc.KeyNotFoundError(KE) # Raise the original error def keys(self): return list(super().keys()) diff --git a/tests/run_coverage b/tests/run_coverage index 4b5018797..aa29a8922 100755 --- a/tests/run_coverage +++ b/tests/run_coverage @@ -1,4 +1,6 @@ #!/bin/bash coverage run --source=../covasim --omit=*cruise_ship*,*webapp* -m pytest test_* +echo 'Creating HTML report...' +coverage html echo 'Running report...' coverage report diff --git a/tests/test_other.py b/tests/test_other.py new file mode 100755 index 000000000..b562d6f25 --- /dev/null +++ b/tests/test_other.py @@ -0,0 +1,94 @@ +''' +Tests for things that do not belong in other files. +''' + +#%% Imports and settings +import os +import pytest +import sciris as sc +import covasim as cv + + +#%% Define the tests + +def test_base(): + sc.heading('Testing base.py...') + + json_path = 'base_tests.json' + sim_path = 'base_tests.sim' + + # Create a small sim for later use + sim = cv.Sim(pop_size=100, verbose=0) + sim.run() + + # Check setting invalid key + with pytest.raises(sc.KeyNotFoundError): + po = cv.ParsObj(pars={'a':2, 'b':3}) + po.update_pars({'c':4}) + + # Printing result + r = cv.Result() + print(r) + print(r.npts) + + # Day conversion + daystr = '2020-04-04' + sim.day(daystr) + sim.day(sc.readdate(daystr)) + with pytest.raises(ValueError): + sim.day('not a date') + + # BaseSim methods + sim.copy() + sim.export_results(filename=json_path) + sim.export_pars(filename=json_path) + sim.shrink(in_place=False) + for keep_people in [True, False]: + sim.save(filename=sim_path, keep_people=keep_people) + cv.Sim.load(sim_path) + + # BasePeople methods + ppl = sim.people + ppl.get(['susceptible', 'infectious']) + ppl.keys(which='all_states') + ppl.index() + ppl.resize(pop_size=200) + ppl.to_df() + ppl.to_arr() + ppl.person(50) + people = ppl.to_people() + ppl.from_people(people) + with pytest.raises(sc.KeyNotFoundError): + ppl.make_edgelist([{'invalid_key':[0,1,2]}]) + + # Contacts methods + contacts = ppl.contacts + df = contacts['a'].to_df() + ppl.remove_duplicates(df) + with pytest.raises(sc.KeyNotFoundError): + contacts['invalid_key'] + contacts.values() + len(contacts) + + # Transmission tree methods + ppl.transtree.make_targets() + ppl.make_detailed_transtree() + ppl.transtree.plot() + ppl.transtree.animate(animate=False) + + # Tidy up + for path in [json_path, sim_path]: + print(f'Removing {path}') + os.remove(path) + + return + + +#%% Run as a script +if __name__ == '__main__': + sc.tic() + + test_base() + + print('\n'*2) + sc.toc() \ No newline at end of file From 884bf60f2c237a59e5500333c4987926fa11fcca Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 22:12:27 -0700 Subject: [PATCH 169/194] made webapp test more efficient --- tests/test_webapp.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_webapp.py b/tests/test_webapp.py index 117581b5e..5f3d31742 100644 --- a/tests/test_webapp.py +++ b/tests/test_webapp.py @@ -21,11 +21,9 @@ def test_webapp(): sc.heading('Testing webapp') pars = cw.get_defaults(die=True) + pars['sim_pars']['pop_size']['best'] = 200 + pars['sim_pars']['n_days']['best'] = 30 output = cw.run_sim(sim_pars=pars['sim_pars'], epi_pars=pars['epi_pars'], die=True) - if output['errs']: - errormsg = 'Webapp encountered an error:\n' - errormsg += sc.pp(str(output['errs']), doprint=False) - raise Exception(errormsg) output2 = cw.run_sim(sim_pars='invalid', epi_pars='invalid') if not output2['errs']: From d4e062b61943c2b8d07acb3b660a058b91d95be1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 22:14:29 -0700 Subject: [PATCH 170/194] rename testing interventions --- tests/{test_interventions_testing.py => test_interventions.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_interventions_testing.py => test_interventions.py} (100%) diff --git a/tests/test_interventions_testing.py b/tests/test_interventions.py similarity index 100% rename from tests/test_interventions_testing.py rename to tests/test_interventions.py From af24d8abae8c0daedef0c1a599123035ea08101f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 22:19:31 -0700 Subject: [PATCH 171/194] merged interventions tests --- tests/test_interventions.py | 138 +++++++++++++++++++++++++- tests/test_interventions_other.py | 157 ------------------------------ tests/test_other.py | 0 3 files changed, 136 insertions(+), 159 deletions(-) delete mode 100644 tests/test_interventions_other.py mode change 100755 => 100644 tests/test_other.py diff --git a/tests/test_interventions.py b/tests/test_interventions.py index 323492fd5..8b2845063 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -3,6 +3,7 @@ ''' #%% Imports and settings +import os import sciris as sc import covasim as cv @@ -12,6 +13,7 @@ debug = 1 keep_sims = 0 fig_paths = [f'results/testing_scen_{i}.png' for i in range(3)] +fig_paths += ['results/testing_other.png'] def test_interventions(do_plot=False, do_show=True, do_save=False, fig_path=None): @@ -101,7 +103,7 @@ def test_turnaround(do_plot=False, do_show=True, do_save=False, fig_path=None): n_runs = 3 verbose = 1 base_pars = { - 'pop_size': 5000, + 'pop_size': 1000, 'pop_type': 'hybrid', } @@ -147,7 +149,7 @@ def test_tracedelay(do_plot=False, do_show=True, do_save=False, fig_path=None): n_runs = 3 verbose = 1 base_pars = { - 'pop_size': 2000, + 'pop_size': 1000, 'pop_type': 'hybrid', } @@ -227,6 +229,130 @@ def test_tracedelay(do_plot=False, do_show=True, do_save=False, fig_path=None): +def test_beta_edges(do_plot=False, do_show=True, do_save=False, fig_path=None): + + pars = dict( + pop_size=1000, + pop_infected=20, + pop_type='hybrid', + ) + + start_day = 25 # Day to start the intervention + end_day = 40 # Day to end the intervention + change = 0.3 # Amount of change + + sims = sc.objdict() + sims.b = cv.Sim(pars) # Beta intervention + sims.e = cv.Sim(pars) # Edges intervention + + beta_interv = cv.change_beta(days=[start_day, end_day], changes=[change, 1.0]) + edge_interv = cv.clip_edges(start_day=start_day, end_day=end_day, change=change, verbose=True) + sims.b.update_pars(interventions=beta_interv) + sims.e.update_pars(interventions=edge_interv) + + for sim in sims.values(): + sim.run() + if do_plot: + sim.plot(do_save=do_save, do_show=do_show, fig_path=fig_path) + sim.plot_result('r_eff') + + return sims + + +def test_beds(do_plot=False, do_show=True, do_save=False, fig_path=None): + sc.heading('Test of bed capacity estimation') + + sc.heading('Setting up...') + + sc.tic() + + n_runs = 2 + verbose = 1 + + basepars = {'pop_size': 1000} + metapars = {'n_runs': n_runs} + + sim = cv.Sim() + + # Define the scenarios + scenarios = { + 'baseline': { + 'name': 'No bed constraints', + 'pars': { + 'pop_infected': 100 + } + }, + 'bedconstraint': { + 'name': 'Only 50 beds available', + 'pars': { + 'pop_infected': 100, + 'n_beds': 50, + } + }, + 'bedconstraint2': { + 'name': 'Only 10 beds available', + 'pars': { + 'pop_infected': 100, + 'n_beds': 10, + } + }, + } + + scens = cv.Scenarios(sim=sim, basepars=basepars, metapars=metapars, scenarios=scenarios) + scens.run(verbose=verbose, debug=debug) + + if do_plot: + to_plot = sc.odict({ + 'Cumulative deaths': 'cum_deaths', + 'People needing beds / beds': 'bed_capacity', + 'Number of cases requiring hospitalization': 'n_severe', + 'Number of cases requiring ICU': 'n_critical', + }) + scens.plot(to_plot=to_plot, do_save=do_save, do_show=do_show, fig_path=fig_path) + + return scens + + +def test_borderclosure(do_plot=False, do_show=True, do_save=False, fig_path=None): + sc.heading('Test effect of border closures') + + sc.heading('Setting up...') + + sc.tic() + + n_runs = 2 + verbose = 1 + + basepars = {'pop_size': 1000} + basepars = {'n_imports': 5} + metapars = {'n_runs': n_runs} + + sim = cv.Sim() + + # Define the scenarios + scenarios = { + 'baseline': { + 'name': 'No border closures', + 'pars': { + } + }, + 'borderclosures_day10': { + 'name': 'Close borders on day 10', + 'pars': { + 'interventions': [cv.dynamic_pars({'n_imports': {'days': 10, 'vals': 0}})] + } + }, + } + + scens = cv.Scenarios(sim=sim, basepars=basepars, metapars=metapars, scenarios=scenarios) + scens.run(verbose=verbose, debug=debug) + + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path=fig_path) + + return scens + + #%% Run as a script if __name__ == '__main__': sc.tic() @@ -234,6 +360,14 @@ def test_tracedelay(do_plot=False, do_show=True, do_save=False, fig_path=None): scens1 = test_interventions(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[0]) scens2 = test_turnaround(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[1]) scens3 = test_tracedelay(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[2]) + sims = test_beta_edges(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[3]) + bed_scens = test_beds(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[3]) + border_scens = test_borderclosure(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[3]) + + for path in fig_paths: + if os.path.exists(path): + print(f'Removing {path}') + os.remove(path) sc.toc() diff --git a/tests/test_interventions_other.py b/tests/test_interventions_other.py deleted file mode 100644 index abc735f87..000000000 --- a/tests/test_interventions_other.py +++ /dev/null @@ -1,157 +0,0 @@ -''' -Testing the effect of testing interventions in Covasim -''' - -#%% Imports and settings -import sciris as sc -import covasim as cv - -do_plot = 1 -do_show = 1 -do_save = 0 -debug = 1 -fig_path = 'results/testing_other.png' - - -def test_beta_edges(do_plot=False, do_show=True, do_save=False, fig_path=None): - - pars = dict( - pop_size=2000, - pop_infected=20, - pop_type='hybrid', - ) - - start_day = 25 # Day to start the intervention - end_day = 40 # Day to end the intervention - change = 0.3 # Amount of change - - sims = sc.objdict() - sims.b = cv.Sim(pars) # Beta intervention - sims.e = cv.Sim(pars) # Edges intervention - - beta_interv = cv.change_beta(days=[start_day, end_day], changes=[change, 1.0]) - edge_interv = cv.clip_edges(start_day=start_day, end_day=end_day, change=change, verbose=True) - sims.b.update_pars(interventions=beta_interv) - sims.e.update_pars(interventions=edge_interv) - - for sim in sims.values(): - sim.run() - if do_plot: - sim.plot(do_save=do_save, do_show=do_show, fig_path=fig_path) - sim.plot_result('r_eff') - - return sims - - -def test_beds(do_plot=False, do_show=True, do_save=False, fig_path=None): - sc.heading('Test of bed capacity estimation') - - sc.heading('Setting up...') - - sc.tic() - - n_runs = 3 - verbose = 1 - - basepars = {'pop_size': 1000} - metapars = {'n_runs': n_runs} - - sim = cv.Sim() - - # Define the scenarios - scenarios = { - 'baseline': { - 'name': 'No bed constraints', - 'pars': { - 'pop_infected': 100 - } - }, - 'bedconstraint': { - 'name': 'Only 50 beds available', - 'pars': { - 'pop_infected': 100, - 'n_beds': 50, - } - }, - 'bedconstraint2': { - 'name': 'Only 10 beds available', - 'pars': { - 'pop_infected': 100, - 'n_beds': 10, - } - }, - } - - scens = cv.Scenarios(sim=sim, basepars=basepars, metapars=metapars, scenarios=scenarios) - scens.run(verbose=verbose, debug=debug) - - if do_plot: - to_plot = sc.odict({ - 'Cumulative deaths': 'cum_deaths', - 'People needing beds / beds': 'bed_capacity', - 'Number of cases requiring hospitalization': 'n_severe', - 'Number of cases requiring ICU': 'n_critical', - }) - scens.plot(to_plot=to_plot, do_save=do_save, do_show=do_show, fig_path=fig_path) - - return scens - - -def test_borderclosure(do_plot=False, do_show=True, do_save=False, fig_path=None): - sc.heading('Test effect of border closures') - - sc.heading('Setting up...') - - sc.tic() - - n_runs = 3 - verbose = 1 - - basepars = {'pop_size': 1000} - basepars = {'n_imports': 5} - metapars = {'n_runs': n_runs} - - sim = cv.Sim() - - # Define the scenarios - scenarios = { - 'baseline': { - 'name': 'No border closures', - 'pars': { - } - }, - 'borderclosures_day1': { - 'name':'Close borders on day 1', - 'pars': { - 'interventions': [cv.dynamic_pars({'n_imports': {'days': 1, 'vals': 0}})] - } - }, - 'borderclosures_day10': { - 'name': 'Close borders on day 10', - 'pars': { - 'interventions': [cv.dynamic_pars({'n_imports': {'days': 10, 'vals': 0}})] - } - }, - } - - scens = cv.Scenarios(sim=sim, basepars=basepars, metapars=metapars, scenarios=scenarios) - scens.run(verbose=verbose, debug=debug) - - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=fig_path) - - return scens - - -#%% Run as a script -if __name__ == '__main__': - sc.tic() - - sims = test_beta_edges(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_path) - bed_scens = test_beds(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_path) - border_scens = test_borderclosure(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_path) - - sc.toc() - - -print('Done.') diff --git a/tests/test_other.py b/tests/test_other.py old mode 100755 new mode 100644 From eeced9d335a596a2f8c289d3d23afeceaf9f0473 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Fri, 8 May 2020 22:32:09 -0700 Subject: [PATCH 172/194] working on requirements --- tests/test_other.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_other.py b/tests/test_other.py index b562d6f25..cb89526b8 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -7,6 +7,9 @@ import pytest import sciris as sc import covasim as cv +import pylab as pl + +do_plot = False #%% Define the tests @@ -84,11 +87,22 @@ def test_base(): return +def test_requirements(): + sc.heading('Testing requirements') + + return + + #%% Run as a script if __name__ == '__main__': + + if not do_plot: + pl.switch_backend('agg') + sc.tic() test_base() + test_requirements() print('\n'*2) sc.toc() \ No newline at end of file From 755fa2d0ad24614bf9d11fb1633e184dd813b4d7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 00:10:27 -0700 Subject: [PATCH 173/194] added test coverage to misc, sim, and population --- covasim/misc.py | 43 +++------ covasim/population.py | 2 +- covasim/sim.py | 4 +- tests/example_data.xlsx | Bin 0 -> 5687 bytes tests/test_other.py | 203 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 217 insertions(+), 35 deletions(-) create mode 100644 tests/example_data.xlsx diff --git a/covasim/misc.py b/covasim/misc.py index ac22ee51b..031952891 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -5,13 +5,12 @@ import datetime as dt import numpy as np import pandas as pd -import pylab as pl import sciris as sc import scipy.stats as sps from . import version as cvver -__all__ = ['load_data', 'date', 'daydiff', 'load', 'save', 'check_version', 'git_info', 'fixaxis', 'get_doubling_time', 'poisson_test'] +__all__ = ['load_data', 'date', 'daydiff', 'load', 'save', 'check_version', 'git_info', 'get_doubling_time', 'poisson_test'] def load_data(filename, columns=None, calculate=True, verbose=True, **kwargs): @@ -87,8 +86,7 @@ def date(obj, *args, **kwargs): cv.date('2020-04-05') # Returns datetime.date(2020, 4, 5) ''' # Convert to list - if sc.isstring(obj) or sc.isnumber(obj) or isinstance(obj, (dt.date, dt.datetime)): - obj = sc.promotetolist(obj) # Ensure it's iterable + obj = sc.promotetolist(obj) # Ensure it's iterable obj.extend(args) dates = [] @@ -101,7 +99,7 @@ def date(obj, *args, **kwargs): elif isinstance(d, dt.datetime): d = d.date() else: - errormsg = f'Could not interpret "{d}" of type {type(d)} as a date' + errormsg = f'Cannot interpret {type(d)} as a date, must be date, datetime, or string' raise TypeError(errormsg) dates.append(d) except Exception as E: @@ -163,18 +161,16 @@ def load(*args, **kwargs): def save(*args, **kwargs): ''' - Convenience method for sc.loadobj() and equivalent to cv.Sim.load() or - cv.Scenarios.load(). + Convenience method for sc.saveobj() and equivalent to cv.Sim.save() or + cv.Scenarios.save(). **Examples**:: - sim = cv.load('calib.sim') - scens = cv.load(filename='school-closures.scens', folder='schools') + cv.save('calib.sim', sim) + cv.save(filename='school-closures.scens', folder='schools', obj=scens) ''' - obj = sc.loadobj(*args, **kwargs) - if hasattr(obj, 'version'): - check_version(obj.version) - return obj + filepath = sc.saveobj(*args, **kwargs) + return filepath def check_version(expected, die=False, verbose=True, **kwargs): @@ -218,6 +214,7 @@ def git_info(filename=None, check=False, old_info=None, die=False, verbose=True, output = sc.savejson(filename, info, **kwargs) else: output = info + return output else: if filename is not None: old_info = sc.loadjson(filename, **kwargs) @@ -228,18 +225,7 @@ def git_info(filename=None, check=False, old_info=None, die=False, verbose=True, raise ValueError(string) elif verbose: print(string) - return output - - -def fixaxis(sim, useSI=True, boxoff=False): - ''' Make the plotting more consistent -- add a legend and ensure the axes start at 0 ''' - delta = 0.5 - pl.legend() # Add legend - sc.setylim() # Rescale y to start at 0 - pl.xlim((0, sim['n_days']+delta)) - if boxoff: - sc.boxoff() # Turn off top and right lines - return + return def get_doubling_time(sim, series=None, interval=None, start_day=None, end_day=None, moving_window=None, exp_approx=False, max_doubling_time=100, eps=1e-3, verbose=None): @@ -341,7 +327,7 @@ def get_doubling_time(sim, series=None, interval=None, start_day=None, end_day=N def poisson_test(count1, count2, exposure1=1, exposure2=1, ratio_null=1, - method='score', alternative='2-sided'): + method='score', alternative='two-sided'): '''Test for ratio of two sample Poisson intensities If the two Poisson rates are g1 and g2, then the Null hypothesis is @@ -422,7 +408,7 @@ def zstat_generic2(value, std_diff, alternative): elif alternative in ['smaller', 's']: pvalue = sps.norm.cdf(zstat) else: - raise ValueError('invalid alternative') + raise ValueError(f'invalid alternative "{alternative}"') return pvalue# zstat # shortcut names @@ -450,8 +436,9 @@ def zstat_generic2(value, std_diff, alternative): if method in ['cond-midp']: # not inplace in case we still want binom pvalue pvalue = pvalue - 0.5 * sps.binom.pmf(y1, y_total, bp) - dist = 'binomial' + else: + raise ValueError(f'invalid method "{method}"') if dist == 'normal': return zstat_generic2(stat, 1, alternative) diff --git a/covasim/population.py b/covasim/population.py index 0b0baf594..313f63593 100644 --- a/covasim/population.py +++ b/covasim/population.py @@ -70,7 +70,7 @@ def make_people(sim, save_pop=False, popfile=None, verbose=None, die=True, reset popdict, layer_keys = make_synthpop(sim) else: errormsg = f'Population type "{pop_type}" not found; choices are random, clustered, hybrid, or synthpops' - raise NotImplementedError(errormsg) + raise ValueError(errormsg) # Ensure prognoses are set if sim['prognoses'] is None: diff --git a/covasim/sim.py b/covasim/sim.py index c52dc6dee..a1852e99d 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -148,7 +148,7 @@ def validate_pars(self): self[key] = int(self[key]) except Exception as E: errormsg = f'Could not convert {key}={self[key]} of {type(self[key])} to integer' - raise TypeError(errormsg) from E + raise ValueError(errormsg) from E # Handle start day start_day = self['start_day'] # Shorten @@ -199,7 +199,7 @@ def validate_pars(self): if choice not in popdata_choices: choicestr = ', '.join(popdata_choices) errormsg = f'Population type "{choice}" not available; choices are: {choicestr}' - raise sc.KeyNotFoundError(errormsg) + raise ValueError(errormsg) # Handle interventions self['interventions'] = sc.promotetolist(self['interventions'], keepnone=False) diff --git a/tests/example_data.xlsx b/tests/example_data.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f35e0c3b6008d1c760f3fee22bd87b04dfeb27cb GIT binary patch literal 5687 zcmaJ_1zb~Y+XqLC8l!XMh6)Iy8>AcQ6r=^DLpnwXBGM%w0us_OLXd6{7)VKnpnxJJ zE%1%!4SoII_qon~XJ_}>b=~LwpZ|T8rYZo790LdhVxYK+H8Fk@TJ*J#BliP$8%Gar zZx{DRmM$)jxO|)(xvodb6VxAd!3m%2Tu>++_bB69#R#{=NLOB*VD@UQj&=(sPSsrn zanYIJ+k6{M8l669RXW__f4&vfElY?~l7|)1(eoJCg1Gp;y+WfuoVU1a2aBs>BrRMgPRxkhtui0#JUs<1l-V6#RcBmJ@xy5UN`Ip28QPUM27qy$avUUxZ7Ckc)Hs= zKlHc;CS|fiBODIy-FtU$?8itw1UyYtg-iD8E~*egqLd?FSax=lhfBt7+h(;Hys?rd z<8_&S(`!Yqm)c<)q|X6z`qn!-eRjIZBjx}bh#a}c<616d^rNw{ST1o^F>Jz5$zFtp zb-NvzCVOA_n^>jW%fQ*RM=sVzGv2)LB41u>Cwp5dobuVX5xQTSCmaYq&Gizu5of6M z7M_W$L7%xCBaa(^l~hyeU&svD1X508FYy3vZJw`pWXV%YZ<4AuwSNCtw9=HtIWzS6 zg_og*VYZboV!gr8%<>eH%2@rAVen;-yV|5h2}s8)y`)PQQn&@zi!2sZOM&Zk>f-6Z z{~ME-H>3r}bmi!msTpyp1d&MY ziw20DJDoADHTZQk;@2W-T7g0;dX?uFeswF@rT%%V1(Hb08gmuK!Sf(fUvt%PHH za-wtW_k&f{yT;j8vSrgV(cC=_CcD}K3Q*LG4=dA|WhGrR6MiSqFt1b&E~zNp$QE1Q z;lsx0g#)^ZR^6lKCW==-+AlqW1-BchP=)q|64d%P2BxW2#P0y7W(++T2Md{Yb#K->rsVz^x#{=U2!4b zCmn<4S5rrk%GBa`TJvRw6-F#tp6)-#%s`4nuMECvIo`RmZKT8LEfc^e+v{v?J78;Z zQl5UhOfVX5?&sMTH%1?qsTy|XFUH!3$M@u3+fLEv52>-)UsLgh&;6YZClO=WT-!H? zkAw1U4Ec4<_8=&M#U^pcxSAl%k5`T9>HqvX1IrTCxppG^C z>jmTGhelP5qAC|JQjU=Nakiv{FQ3zp9wz3lEKfJ(m#c2M(;TzyReIAja9KsZS9ak) zJ7h?jvRYM1{L$aoTyOPd*hUVwd=4?U9WtBfj6a}1op@U?Q)LgVWPTrm#@6Yevv&S1~n zfkSchQ>i~lxA*g5vdXn%f#EW_VOwVA-QY6xgq~|sM_E_Y>%>Lgl9!thFe@pBk);wY zCK(D#4UTEP|Cvm@Ed(Pby`3V28G;qx-+&bj$6m%1bm8HqKmd3Vdduye)yt+p(jgf_ zBc>BuC1xVf5KxksK8K4TfI}t}#*D~_7nWlVU>Ak&fw}N;-#o)z8jLr$2igp0nVUxX zwx>fGq!~siqq-YppoT1@Z%CfX-jGRHZXd-9P;0si5kJw7#!{8PQOD9D8kfiTjG9cR zmTQDErn|usYRF6ahGqaovC@fPhDsBBa$Hdg;7|-LU`5O(m@iPh#>MH4$HKFtDhUHF zktUiGDUY#5%d+H&!Fs{8>P#dLIh0pTR7lHUEBmj=$^g0gX2S4r5D9u*Wy?fMyX@9V zYJZmHZDEYk;FWeTFCvqGOBa`GVmmumfY1dtH&km|L?>hj-X-Q>M69e3@XTCLso`DCaRlJFEF-q{ChGl&eefHTASP zadKVJlMet(RB+#X#aS`Y(=KDT7(*4y^5jYQ^szxdfVnhriCgtxJtd^kt0Fzdu7m>N z09oMtsFA3qDM435Nxq?8C*4Js<(>4LP*+D48J-%?kS59}F6ifc6O;QU-w?m^WoiUK zHV@O!(^A(!`h+rt{F%zzvo90XvUtHRBJd9(o(LEcaZs`fK5362ZDT*%u+)&_{8NDs&$r zY8pP2Emv&Vx?HfjpRa7wS1_o5BG+b*IB#DIEQEZiujAZ$-uqg5&gHSVZe9%p0T31a zxrx_ArhwrY$^dH31yWJdEl&Eh_+G}LM_`gm2@>Js7Skx9`ElH8A6M&2?! zJ%1R~HvGa-!6XXHSyagRZQH>+1_I#>Ed;a@{SHcY&^ySxM-j{CqJPlg9Y2Y zxo)G)Th7Qn`e-x^I?^fRVw;^R^u^du5w34Xfpn#Y6YEwG_-kcGu1u54OCzs2hVSt5KfFd&h&x*^4m)8vjS^Gflf{!i)$3Da_7E4&Xcw<3-n zezFl?V$4XcvC?<9N6?o4*wi^9a?g)_n>@rJEF( z@}7;_F=kGio431d_2jc_xq_V5+EujUoV3LL(9y^NQZqubCr01X*GsbT6Y^G79=3)e z(rxD*pt=-pi*#k+A@)5Q1+m*Eg;b<;8hXmp;}>bf-aRyaM!WyOYwht=u*5nJ{zEe% z($)_lKC0;~$JmqJ4$>;Z_#C{-&BHR9?ekO#UE3XRHnZxPz=Z2J&vK%aKJxNu2907% zm*H*G1{;k{E{4cy=tfF|>#JOQk0}?9OKui#{!A;(?_pbYpPUjH~bU~p?O z_Qldy%R_=|>STy4$6umzEl<8pkiOP6x~b4oyn~W>0*rL)=cqR~M#$;jS9G0~Z5Mc;{o0A) z?(#;3oI`#5pnU5#01rBC~-O}+|K~#5fyL0_;;-YB1+brY>-|a;}xip_p z=hgv=hzdOBI~JDcjti@mnUiCW+HbfSvXob^5%6I+1skQY70v60zqbDtWc5;2_g*0q zAv>P__E@H{@W#7-vX9bCuv=90WAAuFY4Y&97|x*CKDfQiJud{i6W1b~>TXG>=GB=O zj-P%UjcwdX-^DBZF_n7I;Mn4~6kk!3w%(VC5P|9Y&v~hBW2C^!sX7zwj8-V$I;ts* zV#7op`gH9Y<3A@`$m+$3m7Smol~LMve6x-7+39eut5n&Nxxoy`Z?;L7n_$J0kqu=} z!}*q^WnIOJgOJ)>5@fO6{qF0}AJNRYMK!vY^mou9K zkeF{WrDKkSPa?`!erV+3SEkSZJZ!0WI`$+lba0Ydw8_4uWNbB&pxWOy9-xiI>U;(2Y24E-{!fumAz@*P40-| z|KKHRDR$~n|A>zdy77&No_=qgQ)?CU+1@w)P)(h7D;ZCLx09V!FhgRdABe(adzXtHulu7k+l-tXLkKMGBLYJ(#K--eghGq1!#s0R01*89A?P8_r?&9jfZQ<&A zU0c13zjqH^()a8HM6=JbxDX{Od+kHBQ`0hN|qA2E7sVpF35Ooh#6I^}LRsd$sMck(dML|TE zdIbuMAngP5RV-+D9yCSRHIG!gxO?*B+(b`sO@sRRkuaWs{8~NVxv%4n zU@2061}`9fsy3fpB@wZruQc9ZeSVoJmB7iw7kc|q4vY$DJ-Hw-KRNH`b1;Qa#AALY z%{DP#XC1fUpaNJ5v(1ePe>(i~FvB9bwJ3IvdkWCP{a&#I^M!f;26g(oWThY$?0X5{ zHzvuMgt4({32}@mC@ySBnnBNS3rC2GVP^E4dDFIeC}}oV>L->y>7yr?(LrSogvJd# z?0dD`m`quHh9%&&t&cX!qv}H1TRf>1C55i2wK#0gtg`OmvMo8YL5)7GQ4qY16`NHC zqteo=m*wAgDaZP64?b0LZ)TyFB5rqUm3nIUS`Hf-c;{-n=^W=869kL|Cm{=s2D_ zZD%B2N_!`O$50*{oeOi~UuDEVV z7Q&nDrTbL0*Oe8vNh>NJLA>nqaI;z`fzb^X_TjlPqM1IfGXIDx^qH?3gQc+~98`{Q ztbi|;g4mhQ^(9$rp-bX|+>1ikVw9WD`<)xk$a6{A!>1f5L@Mr!9bjh%ha|bD=g#6Z zEn_kVW9gKGhpgnjt3CCn{lJ~3AGUW^XQeESDNR#?$s%;J76?@=k=#%MyCcau2fn@A9_ls(73BGdsBD_i;$TZL!T!AOSL6k0D1v9UWH2FR= z%bEpR@>1X0>DIa($=LG)a*et}C3%bh8g&D#Yi?uSAjkOC+zd!B# z-psk~i2V{;^vq~G|F1vxr-k3M+v}#!FY!gQ1#RKKwt)V$@_V9loeBSv61;y~`Bz^2 zr}FP%#HC3_E1qcQP9{P|%zn0a%wEhnsNt|8) literal 0 HcmV?d00001 diff --git a/tests/test_other.py b/tests/test_other.py index cb89526b8..a5a0563b2 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -1,5 +1,6 @@ ''' -Tests for things that do not belong in other files. +Tests for things that are not tested in other files, typically because they are +corner cases or otherwise not part of major workflows. ''' #%% Imports and settings @@ -11,6 +12,14 @@ do_plot = False +def remove_files(*args): + ''' Remove files that were created ''' + for path in args: + if os.path.exists(path): + print(f'Removing {path}') + os.remove(path) + return + #%% Define the tests @@ -80,16 +89,198 @@ def test_base(): ppl.transtree.animate(animate=False) # Tidy up - for path in [json_path, sim_path]: - print(f'Removing {path}') - os.remove(path) + remove_files(json_path, sim_path) return +def test_misc(): + sc.heading('Testing miscellaneous functions') + + sim_path = 'test_misc.sim' + json_path = 'test_misc.json' + + # Data loading + cv.load_data('example_data.csv') + cv.load_data('example_data.xlsx') + + with pytest.raises(NotImplementedError): + cv.load_data('example_data.unsupported_extension') + + with pytest.raises(ValueError): + cv.load_data('example_data.xlsx', columns=['missing_column']) + + # Dates + d1 = cv.date('2020-04-04') + d2 = cv.date(sc.readdate('2020-04-04')) + ds = cv.date('2020-04-04', d2) + assert d1 == d2 + assert d2 == ds[0] + + with pytest.raises(ValueError): + cv.date([(2020,4,4)]) # Raises a TypeError which raises a ValueError + + with pytest.raises(ValueError): + cv.date('Not a date') + + cv.daydiff('2020-04-04') + + # Saving and loading + sim = cv.Sim() + cv.save(filename=sim_path, obj=sim) + cv.load(filename=sim_path) + + # Version checks + cv.check_version('0.0.0') # Nonsense version + print('↑ Should complain about version') + with pytest.raises(ValueError): + cv.check_version('0.0.0', die=True) + + # Git checks + cv.git_info(json_path) + cv.git_info(json_path, check=True) + + # Poisson tests + c1 = 5 + c2 = 8 + for alternative in ['two-sided', 'larger', 'smaller']: + cv.poisson_test(c1, c2, alternative=alternative) + for method in ['score', 'wald', 'sqrt', 'exact-cond']: + cv.poisson_test(c1, c2, method=method) + + with pytest.raises(ValueError): + cv.poisson_test(c1, c2, method='not a method') + + # Tidy up + remove_files(sim_path, json_path) + + return + + +def test_people(): + sc.heading('Testing people (dynamic layers)') + + sim = cv.Sim(pop_size=100, n_days=10, verbose=0, dynam_layer={'a':1}) + sim.run() + + return + + + +def test_population(): + sc.heading('Testing the population') + + pop_path = 'pop_test.pop' + + # Test locations, including ones that don't work + cv.Sim(pop_size=100, pop_type='hybrid', location='nigeria').initialize() + cv.Sim(pop_size=100, pop_type='hybrid', location='not_a_location').initialize() + print('↑ Should complain about location not found') + cv.Sim(pop_size=100, pop_type='random', location='lithuania').initialize() + print('↑ Should complain about missing h layer') + + # Test synthpops + try: + sim = cv.Sim(pop_size=5000, pop_type='synthpops') + sim.initialize() + except Exception as E: + errormsg = f'Synthpops test did not pass:\n{str(E)}\nNote: synthpops is optional so this exception is OK.' + print(errormsg) + + # Not working + with pytest.raises(ValueError): + sim = cv.Sim(pop_type='not_an_option') + sim.initialize() + + # Save/load + sim = cv.Sim(pop_size=100, popfile=pop_path) + sim.initialize(save_pop=True) + cv.Sim(pop_size=100, popfile=pop_path, load_pop=True) + with pytest.raises(ValueError): + cv.Sim(pop_size=101, popfile=pop_path, load_pop=True) + + remove_files(pop_path) + + return + + + def test_requirements(): sc.heading('Testing requirements') + with pytest.raises(ImportError): + cv.requirements.min_versions['sciris'] = '99.99.99' + cv.requirements.check_sciris() + + with pytest.raises(ImportError): + cv.requirements.min_versions['scirisweb'] = '99.99.99' + cv.requirements.check_scirisweb(die=True) + + cv.requirements.check_synthpops() + + return + + +def test_sim(): + sc.heading('Testing sim') + + # Test resetting layer parameters + sim = cv.Sim(pop_size=100, label='test_label') + sim.reset_layer_pars() + sim.initialize() + sim.reset_layer_pars() + + # Test validation + sim['pop_size'] = 'invalid' + with pytest.raises(ValueError): + sim.validate_pars() + sim['pop_size'] = 100 # Restore + + # Handle missing start day + sim['start_day'] = None + sim.validate_pars() + + # Can't have an end day before the start day + sim['end_day'] = '2019-01-01' + with pytest.raises(ValueError): + sim.validate_pars() + + # Can't have both end_days and n_days None + sim['end_day'] = None + sim['n_days'] = None + with pytest.raises(ValueError): + sim.validate_pars() + sim['n_days'] = 30 # Restore + + # Check layer pars are internally consistent + sim['quar_eff'] = {'invalid':30} + with pytest.raises(sc.KeyNotFoundError): + sim.validate_pars() + sim.reset_layer_pars() # Restore + + # Check mismatch with population + for key in ['beta_layer', 'contacts', 'quar_eff']: + sim[key] = {'invalid':1} + with pytest.raises(sc.KeyNotFoundError): + sim.validate_pars() + sim.reset_layer_pars() # Restore + + # Convert interventions dict to intervention + sim['interventions'] = {'which': 'change_beta', 'pars': {'days': 10, 'changes': 0.5}} + sim.validate_pars() + + # Test intervention functions and results analyses + sim = cv.Sim(pop_size=100) + sim['interv_func'] = lambda sim: (sim.t==20 and (sim.__setitem__('beta', 0) or print(f'Applying lambda intervention to set beta=0 on day {sim.t}'))) # The world's most ridiculous way of defining an intervention + sim['verbose'] = 0 + sim.run() + sim.compute_r_eff(method='infectious') + sim.compute_r_eff(method='outcome') + sim.compute_gen_time() + + # Plot results + sim.plot_result('r_eff') + return @@ -102,7 +293,11 @@ def test_requirements(): sc.tic() test_base() + test_misc() + test_people() + # test_population() test_requirements() + test_sim() print('\n'*2) sc.toc() \ No newline at end of file From dfa03e93a92ad423e245e911feb451559cbc9ae2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 00:34:51 -0700 Subject: [PATCH 174/194] refactored run.py --- covasim/run.py | 20 ++++++++++++----- tests/test_other.py | 55 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index a995888f9..aa8a1aeb7 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -131,6 +131,16 @@ def run(self, *args, **kwargs): return + def reset(self): + ''' Undo a combine() or reduce() by resetting the base sim, which, and results ''' + if hasattr(self, 'orig_base_sim'): + self.base_sim = self.orig_base_sim + delattr(self, 'orig_base_sim') + self.which = None + self.results = None + return + + def combine(self, output=False): ''' Combine multiple sims into a single sim with scaled results ''' @@ -151,6 +161,7 @@ def combine(self, output=False): if not combined_sim.results[key].scale: combined_sim.results[key].values /= n_runs + self.orig_base_sim = self.base_sim self.base_sim = combined_sim self.results = combined_sim.results self.which = 'combined' @@ -168,7 +179,7 @@ def reduce(self, quantiles=None, output=False): quantiles = self.quantiles if not isinstance(quantiles, dict): try: - quantiles = {'low':quantiles[0], 'high':quantiles[1]} + quantiles = {'low':float(quantiles[0]), 'high':float(quantiles[1])} except Exception as E: errormsg = f'Could not figure out how to convert {quantiles} into a quantiles object: must be a dict with keys low, high or a 2-element array ({str(E)})' raise ValueError(errormsg) @@ -193,6 +204,7 @@ def reduce(self, quantiles=None, output=False): reduced_sim.likelihood() # Recompute the likelihood for the average sim reduced_sim.summary_stats(verbose=False) # Recalculate the summary stats + self.orig_base_sim = self.base_sim self.base_sim = reduced_sim self.results = reduced_sim.results self.which = 'reduced' @@ -237,13 +249,11 @@ def compare(self, t=-1, sim_inds=None, output=False, do_plot=False, **kwargs): val = int(val) resdict[label][reskey] = val - # Convert to a dataframe - df = pd.DataFrame.from_dict(resdict).astype(object) # astype is necessary to prevent type coersion - if do_plot: - self.plot_compare(df, *args, **kwargs) + self.plot_compare(**kwargs) if output: + df = pd.DataFrame.from_dict(resdict).astype(object) # astype is necessary to prevent type coersion return df else: print(df) diff --git a/tests/test_other.py b/tests/test_other.py index a5a0563b2..be56ef4c6 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -221,6 +221,58 @@ def test_requirements(): return +def test_run(): + sc.heading('Testing run') + + msim_path = 'run_test.msim' + scens_path = 'run_test.scens' + + # Test creation + s1 = cv.Sim(pop_size=100) + s2 = s1.copy() + msim = cv.MultiSim(sims=[s1, s2]) + with pytest.raises(TypeError): + cv.MultiSim(sims='not a sim') + + # Test other properties + len(msim) + msim.result_keys() + msim.base_sim = None + with pytest.raises(ValueError): + msim.result_keys() + msim.base_sim = msim.sims[0] # Restore + + # Run + msim.run(verbose=0) + msim.reduce(quantiles=[0.1, 0.9], output=True) + with pytest.raises(ValueError): + msim.reduce(quantiles='invalid') + msim.compare(output=True, do_plot=True, log_scale=False) + + # Plot + for i in range(2): + if i == 1: + msim.reset() # Reset as if reduce() was not called + msim.plot() + msim.plot_result('r_eff') + + # Save + for keep_people in [True, False]: + msim.save(filename=msim_path, keep_people=keep_people) + + # Scenarios + scens = cv.Scenarios(sim=s1, metapars={'n_runs':1}) + scens.run(keep_people=True, verbose=0) + for keep_people in [True, False]: + scens.save(scens_path, keep_people=keep_people) + cv.Scenarios.load(scens_path) + + # Tidy up + remove_files(msim_path, scens_path) + + return + + def test_sim(): sc.heading('Testing sim') @@ -295,9 +347,10 @@ def test_sim(): test_base() test_misc() test_people() - # test_population() + test_population() test_requirements() test_sim() + test_run() print('\n'*2) sc.toc() \ No newline at end of file From eed039062e1dd7820088b201424aac6d68b502d2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 00:40:12 -0700 Subject: [PATCH 175/194] add extra requirements check --- tests/test_other.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_other.py b/tests/test_other.py index be56ef4c6..c020968b8 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -208,12 +208,14 @@ def test_population(): def test_requirements(): sc.heading('Testing requirements') + cv.requirements.min_versions['sciris'] = '99.99.99' with pytest.raises(ImportError): - cv.requirements.min_versions['sciris'] = '99.99.99' + cv.requirements.check_sciris() + cv.requirements.min_versions['scirisweb'] = '99.99.99' + cv.requirements.check_scirisweb() with pytest.raises(ImportError): - cv.requirements.min_versions['scirisweb'] = '99.99.99' cv.requirements.check_scirisweb(die=True) cv.requirements.check_synthpops() From 3f358a50dbf6f9b08363fab427409e54dc45d08e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 00:49:45 -0700 Subject: [PATCH 176/194] refactor lambda function --- tests/test_other.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/tests/test_other.py b/tests/test_other.py index c020968b8..22bf0203c 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -94,6 +94,16 @@ def test_base(): return +def test_interventions(): + sc.heading('Testing interventions') + + sim = cv.Sim(pop_size=300, n_days=60) + sim.run() + + return + + + def test_misc(): sc.heading('Testing miscellaneous functions') @@ -158,8 +168,9 @@ def test_misc(): def test_people(): - sc.heading('Testing people (dynamic layers)') + sc.heading('Testing people') + # Test dynamic layers sim = cv.Sim(pop_size=100, n_days=10, verbose=0, dynam_layer={'a':1}) sim.run() @@ -257,6 +268,7 @@ def test_run(): msim.reset() # Reset as if reduce() was not called msim.plot() msim.plot_result('r_eff') + print('↑ May print some plotting warnings') # Save for keep_people in [True, False]: @@ -324,9 +336,10 @@ def test_sim(): sim.validate_pars() # Test intervention functions and results analyses - sim = cv.Sim(pop_size=100) - sim['interv_func'] = lambda sim: (sim.t==20 and (sim.__setitem__('beta', 0) or print(f'Applying lambda intervention to set beta=0 on day {sim.t}'))) # The world's most ridiculous way of defining an intervention - sim['verbose'] = 0 + cv.Sim(pop_size=100, verbose=0, interv_func=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. + + # Test other outputs + sim = cv.Sim(pop_size=100, verbose=0, n_days=30) sim.run() sim.compute_r_eff(method='infectious') sim.compute_r_eff(method='outcome') @@ -346,13 +359,14 @@ def test_sim(): sc.tic() - test_base() - test_misc() - test_people() - test_population() - test_requirements() - test_sim() - test_run() + # test_base() + test_interventions() + # test_misc() + # test_people() + # test_population() + # test_requirements() + # test_run() + # test_sim() print('\n'*2) sc.toc() \ No newline at end of file From ee2944c57ea119af602422bcb8fb178ccc99065d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 01:02:19 -0700 Subject: [PATCH 177/194] moved scenarios test to test_run --- tests/test_interventions.py | 6 -- tests/test_run.py | 115 +++++++++++++++++++++++++++++++----- 2 files changed, 101 insertions(+), 20 deletions(-) diff --git a/tests/test_interventions.py b/tests/test_interventions.py index 8b2845063..45f07c641 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -61,12 +61,6 @@ def test_interventions(do_plot=False, do_show=True, do_save=False, fig_path=None 'interventions': cv.test_prob(symp_prob=max_optimistic_testing, asymp_prob=0.0) } }, - # 'historical': { - # 'name': 'Test a known number of positive cases', - # 'pars': { - # 'interventions': cv.test_historical(n_tests=[100]*npts, n_positive = [1]*npts) - # } - # }, 'sequence': { 'name': 'Historical switching to probability', 'pars': { diff --git a/tests/test_run.py b/tests/test_run.py index bff5c9f6b..0e8a4cf0a 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -10,6 +10,8 @@ do_plot = 1 do_save = 0 do_show = 1 +debug = 0 +verbose = 0 #%% Define the tests @@ -20,7 +22,7 @@ def test_singlerun(): iterpars = {'beta': 0.035, } - sim = cv.Sim() + sim = cv.Sim(verbose=verbose) sim['n_days'] = 20 sim['pop_size'] = 1000 sim = cv.single_run(sim=sim, **iterpars) @@ -39,14 +41,14 @@ def test_multirun(do_plot=False): # If being run via pytest, turn off 'diag_factor': [0.1, 0.5, 0.9], } sim = cv.Sim(n_days=n_days, pop_size=pop_size) - sims = cv.multi_run(sim=sim, iterpars=iterpars) + sims = cv.multi_run(sim=sim, iterpars=iterpars, verbose=verbose) # Method 2 -- run a list of sims simlist = [] for i in range(len(iterpars['beta'])): sim = cv.Sim(n_days=n_days, pop_size=pop_size, beta=iterpars['beta'][i], diag_factor=iterpars['diag_factor'][i]) simlist.append(sim) - sims2 = cv.multi_run(sim=simlist) + sims2 = cv.multi_run(sim=simlist, verbose=verbose) if do_plot: for sim in sims + sims2: @@ -65,7 +67,7 @@ def test_multisim_reduce(do_plot=False): # If being run via pytest, turn off print('Running first sim...') sim = cv.Sim(pop_size=pop_size, pop_infected=pop_infected) msim = cv.MultiSim(sim, n_runs=n_runs, noise=0.1) - msim.run() + msim.run(verbose=verbose) msim.reduce() if do_plot: msim.plot() @@ -81,7 +83,7 @@ def test_multisim_combine(do_plot=False): # If being run via pytest, turn off pop_infected = 10 print('Running first sim...') - sim = cv.Sim(pop_size=pop_size, pop_infected=pop_infected) + sim = cv.Sim(pop_size=pop_size, pop_infected=pop_infected, verbose=verbose) msim = cv.MultiSim(sim) msim.run(n_runs=n_runs, keep_people=True) sim1 = msim.combine(output=True) @@ -89,7 +91,7 @@ def test_multisim_combine(do_plot=False): # If being run via pytest, turn off print('Running second sim, results should be similar but not identical (stochastic differences)...') sim2 = cv.Sim(pop_size=pop_size*n_runs, pop_infected=pop_infected*n_runs) - sim2.run() + sim2.run(verbose=verbose) if do_plot: msim.plot() @@ -98,15 +100,15 @@ def test_multisim_combine(do_plot=False): # If being run via pytest, turn off return msim -def test_scenarios(do_plot=False): - sc.heading('Scenarios test') +def test_simple_scenarios(do_plot=False): + sc.heading('Simple scenarios test') basepars = {'pop_size':1000} json_path = 'scen_test.json' xlsx_path = 'scen_test.xlsx' scens = cv.Scenarios(basepars=basepars) - scens.run() + scens.run(verbose=verbose) if do_plot: scens.plot() scens.to_json(json_path) @@ -118,15 +120,100 @@ def test_scenarios(do_plot=False): return scens +def test_complex_scenarios(do_plot=False, do_show=True, do_save=False, fig_path=None): + sc.heading('Test impact of reducing delay time for finding contacts of positives') + + n_runs = 3 + base_pars = { + 'pop_size': 1000, + 'pop_type': 'hybrid', + } + + base_sim = cv.Sim(base_pars) # create sim object + base_sim['n_days'] = 50 + base_sim['beta'] = 0.03 # Increase beta + + n_people = base_sim['pop_size'] + npts = base_sim.npts + + # Define overall testing assumptions + testing_prop = 0.1 # Assumes we could test 10% of the population daily (way too optimistic!!) + daily_tests = [testing_prop*n_people]*npts # Number of daily tests + + # Define the scenarios + scenarios = { + 'lowtrace': { + 'name': 'Poor contact tracing', + 'pars': { + 'quar_eff': {'h': 1, 's': 0.5, 'w': 0.5, 'c': 0.25}, + 'quar_period': 7, + 'interventions': [cv.test_num(daily_tests=daily_tests), + cv.contact_tracing(trace_probs = {'h': 0, 's': 0, 'w': 0, 'c': 0}, + trace_time = {'h': 1, 's': 7, 'w': 7, 'c': 7})] + } + }, + 'modtrace': { + 'name': 'Moderate contact tracing', + 'pars': { + 'quar_eff': {'h': 0.75, 's': 0.25, 'w': 0.25, 'c': 0.1}, + 'quar_period': 10, + 'interventions': [cv.test_num(daily_tests=daily_tests), + cv.contact_tracing(trace_probs = {'h': 1, 's': 0.8, 'w': 0.5, 'c': 0.1}, + trace_time = {'h': 0, 's': 3, 'w': 3, 'c': 8})] + } + }, + 'hightrace': { + 'name': 'Fast contact tracing', + 'pars': { + 'quar_eff': {'h': 0.5, 's': 0.1, 'w': 0.1, 'c': 0.1}, + 'quar_period': 14, + 'interventions': [cv.test_num(daily_tests=daily_tests), + cv.contact_tracing(trace_probs = {'h': 1, 's': 0.8, 'w': 0.8, 'c': 0.2}, + trace_time = {'h': 0, 's': 1, 'w': 1, 'c': 5})] + } + }, + 'alltrace': { + 'name': 'Same-day contact tracing', + 'pars': { + 'quar_eff': {'h': 0.0, 's': 0.0, 'w': 0.0, 'c': 0.0}, + 'quar_period': 21, + 'interventions': [cv.test_num(daily_tests=daily_tests), + cv.contact_tracing(trace_probs = {'h': 1, 's': 1, 'w': 1, 'c': 1}, + trace_time = {'h': 0, 's': 1, 'w': 1, 'c': 2})] + } + }, + } + + metapars = {'n_runs': n_runs} + + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run(verbose=verbose, debug=debug) + + if do_plot: + to_plot = [ + 'cum_infections', + 'cum_recoveries', + 'new_infections', + 'cum_severe', + 'n_quarantined', + 'new_quarantined' + ] + fig_args = dict(figsize=(24,16)) + scens.plot(do_save=do_save, do_show=do_show, to_plot=to_plot, fig_path=fig_path, n_cols=2, fig_args=fig_args) + + return scens + + #%% Run as a script if __name__ == '__main__': T = sc.tic() - sim1 = test_singlerun() - sims2 = test_multirun(do_plot=do_plot) - msim1 = test_multisim_reduce(do_plot=do_plot) - msim2 = test_multisim_combine(do_plot=do_plot) - scens = test_scenarios(do_plot=do_plot) + sim1 = test_singlerun() + sims2 = test_multirun(do_plot=do_plot) + msim1 = test_multisim_reduce(do_plot=do_plot) + msim2 = test_multisim_combine(do_plot=do_plot) + scens1 = test_simple_scenarios(do_plot=do_plot) + scens2 = test_complex_scenarios(do_plot=do_plot) sc.toc(T) From b3c2b3d7965fd2c9ae358b89ace7c554d9062bd0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 01:29:31 -0700 Subject: [PATCH 178/194] swapped intervention tests --- tests/devtests/intervention_showcase.py | 95 ------ tests/devtests/test_interventions.py | 369 ++++++++++++++++++++++ tests/test_interventions.py | 400 ++++-------------------- 3 files changed, 432 insertions(+), 432 deletions(-) delete mode 100644 tests/devtests/intervention_showcase.py create mode 100644 tests/devtests/test_interventions.py diff --git a/tests/devtests/intervention_showcase.py b/tests/devtests/intervention_showcase.py deleted file mode 100644 index 35a6c5f52..000000000 --- a/tests/devtests/intervention_showcase.py +++ /dev/null @@ -1,95 +0,0 @@ -''' -Demonstrate all interventions, taken from intervention docstrings -''' - -#%% Housekeeping - -import sciris as sc -import pylab as pl -import covasim as cv - -do_plot = 1 -verbose = 0 - -pars = sc.objdict( - pop_size = 10e3, - pop_infected = 100, - pop_type = 'hybrid', - n_days = 90, - ) - - -#%% Define the interventions - -# 1. Dynamic pars -i00 = cv.test_prob(start_day=5, symp_prob=0.3) -i01 = cv.dynamic_pars({'beta':{'days':[40, 50], 'vals':[0.005, 0.015]}, 'diag_factor':{'days':30, 'vals':0.0}}) # Starting day 30, make diagnosed people stop transmitting - - -# 2. Sequence -i02 = cv.sequence(days=[20, 40, 60], interventions=[ - cv.test_num(daily_tests=[20]*pars.n_days), - cv.test_prob(symp_prob=0.0), - cv.test_prob(symp_prob=0.2), - ]) - - -# 3. Change beta -i03 = cv.change_beta([30, 50], [0.0, 1], layers='h') -i04 = cv.change_beta([30, 40, 60], [0.0, 1.0, 0.5]) - - -# 4. Clip edges -- should match the change_beta scenarios -i05 = cv.clip_edges(start_day=30, end_day=50, change={'h':0.0}) -i06 = cv.clip_edges(start_day=30, end_day=40, change=0.0) -i07 = cv.clip_edges(start_day=60, end_day=None, change=0.5) - - -# 5. Test number -i08 = cv.test_num(daily_tests=[100, 100, 100, 0, 0, 0]*(pars.n_days//6)) - - -# 6. Test probability -i09 = cv.test_prob(symp_prob=0.1) - - -# 7. Contact tracing -i10 = cv.test_prob(start_day=20, symp_prob=0.01, asymp_prob=0.0, symp_quar_prob=1.0, asymp_quar_prob=1.0, test_delay=0) -i11 = 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 -i12 = cv.clip_edges(start_day=18, change={'s':0.0}) # Close schools -i13 = cv.clip_edges(start_day=20, end_day=32, change={'w':0.7, 'c':0.7}) # Reduce work and community -i14 = cv.clip_edges(start_day=32, end_day=45, change={'w':0.3, 'c':0.3}) # Reduce work and community more -i15 = cv.clip_edges(start_day=45, end_day=None, change={'w':0.9, 'c':0.9}) # Reopen work and community more -i16 = 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 -i17 = 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 - - -#%% Create and run the simulations -sims = sc.objdict() -sims.dynamic = cv.Sim(pars=pars, interventions=[i00, i01]) -sims.sequence = cv.Sim(pars=pars, interventions=i02) -sims.change_beta1 = cv.Sim(pars=pars, interventions=i03) -sims.clip_edges1 = cv.Sim(pars=pars, interventions=i05) # Roughly equivalent to change_beta1 -sims.change_beta2 = cv.Sim(pars=pars, interventions=i04) -sims.clip_edges2 = cv.Sim(pars=pars, interventions=[i06, i07]) # Roughly euivalent to change_beta2 -sims.test_num = cv.Sim(pars=pars, interventions=i08) -sims.test_prob = cv.Sim(pars=pars, interventions=i09) -sims.tracing = cv.Sim(pars=pars, interventions=[i10, i11]) -sims.combo = cv.Sim(pars=pars, interventions=[i12, i13, i14, i15, i16, i17]) - -for key,sim in sims.items(): - sim.label = key - sim.run(verbose=verbose) - - -#%% Plotting -if do_plot: - for sim in sims.values(): - print(f'Running {sim.label}...') - sim.plot() - fig = pl.gcf() - fig.axes[0].set_title(f'Simulation: {sim.label}') - diff --git a/tests/devtests/test_interventions.py b/tests/devtests/test_interventions.py new file mode 100644 index 000000000..45f07c641 --- /dev/null +++ b/tests/devtests/test_interventions.py @@ -0,0 +1,369 @@ +''' +Testing the effect of testing interventions in Covasim +''' + +#%% Imports and settings +import os +import sciris as sc +import covasim as cv + +do_plot = 1 +do_show = 1 +do_save = 0 +debug = 1 +keep_sims = 0 +fig_paths = [f'results/testing_scen_{i}.png' for i in range(3)] +fig_paths += ['results/testing_other.png'] + + +def test_interventions(do_plot=False, do_show=True, do_save=False, fig_path=None): + sc.heading('Test of testing interventions') + + + sc.heading('Setting up...') + + sc.tic() + + n_runs = 3 + verbose = 1 + base_pars = { + 'pop_size': 1000, + 'pop_type': 'hybrid', + } + + base_sim = cv.Sim(base_pars) # create sim object + n_people = base_sim['pop_size'] + npts = base_sim.npts + + # Define overall testing assumptions + # Remember that this is the daily % of the population that gets tested. S Korea (one of the highest-testing places) tested + # an average of 10000 people/day over March, or 270,000 in total. This is ~200 people per million every day (0.02%).... + max_optimistic_testing = 0.1 # ... which means that this is an artificially high number, for testing purposes only!! + optimistic_daily_tests = [max_optimistic_testing*n_people]*npts # Very best-case scenario for asymptomatic testing + + # Define the scenarios + scenarios = { + 'baseline': { + 'name':'Status quo, no testing', + 'pars': { + 'interventions': None, + } + }, + 'test_skorea': { + 'name':'Assuming South Korea testing levels of 0.02% daily (untargeted); isolate positives', + 'pars': { + 'interventions': cv.test_num(daily_tests=optimistic_daily_tests) + } + }, + 'floating': { + 'name': 'Test with constant probability based on symptoms', + 'pars': { + 'interventions': cv.test_prob(symp_prob=max_optimistic_testing, asymp_prob=0.0) + } + }, + 'sequence': { + 'name': 'Historical switching to probability', + 'pars': { + 'interventions': cv.sequence(days=[10, 51], interventions=[ + cv.test_num(daily_tests=optimistic_daily_tests), + cv.test_prob(symp_prob=0.2, asymp_prob=0.002), + ]) + } + }, + + } + + metapars = {'n_runs': n_runs} + + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run(verbose=verbose, debug=debug) + + to_plot = ['cum_infections', 'n_infectious', 'new_tests', 'new_diagnoses'] + fig_args = dict(figsize=(20, 24)) + + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path=fig_path, interval=7, fig_args=fig_args, to_plot=to_plot) + + return scens + + +def test_turnaround(do_plot=False, do_show=True, do_save=False, fig_path=None): + sc.heading('Test impact of reducing delay time for getting test results') + + sc.heading('Setting up...') + + sc.tic() + + n_runs = 3 + verbose = 1 + base_pars = { + 'pop_size': 1000, + 'pop_type': 'hybrid', + } + + base_sim = cv.Sim(base_pars) # create sim object + n_people = base_sim['pop_size'] + npts = base_sim.npts + + # Define overall testing assumptions + testing_prop = 0.1 # Assumes we could test 10% of the population daily (!!) + daily_tests = [testing_prop*n_people]*npts # Number of daily tests + + # Define the scenarios + scenarios = { + f'{d}dayturnaround': { + 'name':f'Symptomatic testing with {d} days to get results', + 'pars': { + 'interventions': cv.test_num(daily_tests=daily_tests, test_delay=d) + } + } for d in range(1, 3+1, 2) + } + + metapars = {'n_runs': n_runs} + + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run(verbose=verbose, debug=debug) + + to_plot = ['cum_infections', 'n_infectious', 'new_tests', 'new_diagnoses'] + fig_args = dict(figsize=(20, 24)) + + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path=fig_path, interval=7, fig_args=fig_args, to_plot=to_plot) + + return scens + + +def test_tracedelay(do_plot=False, do_show=True, do_save=False, fig_path=None): + sc.heading('Test impact of reducing delay time for finding contacts of positives') + + sc.heading('Setting up...') + + sc.tic() + + n_runs = 3 + verbose = 1 + base_pars = { + 'pop_size': 1000, + 'pop_type': 'hybrid', + } + + base_sim = cv.Sim(base_pars) # create sim object + base_sim['n_days'] = 50 + base_sim['beta'] = 0.03 # Increase beta + + n_people = base_sim['pop_size'] + npts = base_sim.npts + + + # Define overall testing assumptions + testing_prop = 0.1 # Assumes we could test 10% of the population daily (way too optimistic!!) + daily_tests = [testing_prop*n_people]*npts # Number of daily tests + + # Define the scenarios + scenarios = { + 'lowtrace': { + 'name': 'Poor contact tracing', + 'pars': { + 'quar_eff': {'h': 1, 's': 0.5, 'w': 0.5, 'c': 0.25}, + 'quar_period': 7, + 'interventions': [cv.test_num(daily_tests=daily_tests), + cv.contact_tracing(trace_probs = {'h': 0, 's': 0, 'w': 0, 'c': 0}, + trace_time = {'h': 1, 's': 7, 'w': 7, 'c': 7})] + } + }, + 'modtrace': { + 'name': 'Moderate contact tracing', + 'pars': { + 'quar_eff': {'h': 0.75, 's': 0.25, 'w': 0.25, 'c': 0.1}, + 'quar_period': 10, + 'interventions': [cv.test_num(daily_tests=daily_tests), + cv.contact_tracing(trace_probs = {'h': 1, 's': 0.8, 'w': 0.5, 'c': 0.1}, + trace_time = {'h': 0, 's': 3, 'w': 3, 'c': 8})] + } + }, + 'hightrace': { + 'name': 'Fast contact tracing', + 'pars': { + 'quar_eff': {'h': 0.5, 's': 0.1, 'w': 0.1, 'c': 0.1}, + 'quar_period': 14, + 'interventions': [cv.test_num(daily_tests=daily_tests), + cv.contact_tracing(trace_probs = {'h': 1, 's': 0.8, 'w': 0.8, 'c': 0.2}, + trace_time = {'h': 0, 's': 1, 'w': 1, 'c': 5})] + } + }, + 'alltrace': { + 'name': 'Same-day contact tracing', + 'pars': { + 'quar_eff': {'h': 0.0, 's': 0.0, 'w': 0.0, 'c': 0.0}, + 'quar_period': 21, + 'interventions': [cv.test_num(daily_tests=daily_tests), + cv.contact_tracing(trace_probs = {'h': 1, 's': 1, 'w': 1, 'c': 1}, + trace_time = {'h': 0, 's': 1, 'w': 1, 'c': 2})] + } + }, + } + + metapars = {'n_runs': n_runs} + + scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) + scens.run(verbose=verbose, debug=debug) + + if do_plot: + to_plot = [ + 'cum_infections', + 'cum_recoveries', + 'new_infections', + 'n_quarantined', + 'new_quarantined' + ] + fig_args = dict(figsize=(24,16)) + scens.plot(do_save=do_save, do_show=do_show, to_plot=to_plot, fig_path=fig_path, n_cols=2, fig_args=fig_args) + + return scens + + + +def test_beta_edges(do_plot=False, do_show=True, do_save=False, fig_path=None): + + pars = dict( + pop_size=1000, + pop_infected=20, + pop_type='hybrid', + ) + + start_day = 25 # Day to start the intervention + end_day = 40 # Day to end the intervention + change = 0.3 # Amount of change + + sims = sc.objdict() + sims.b = cv.Sim(pars) # Beta intervention + sims.e = cv.Sim(pars) # Edges intervention + + beta_interv = cv.change_beta(days=[start_day, end_day], changes=[change, 1.0]) + edge_interv = cv.clip_edges(start_day=start_day, end_day=end_day, change=change, verbose=True) + sims.b.update_pars(interventions=beta_interv) + sims.e.update_pars(interventions=edge_interv) + + for sim in sims.values(): + sim.run() + if do_plot: + sim.plot(do_save=do_save, do_show=do_show, fig_path=fig_path) + sim.plot_result('r_eff') + + return sims + + +def test_beds(do_plot=False, do_show=True, do_save=False, fig_path=None): + sc.heading('Test of bed capacity estimation') + + sc.heading('Setting up...') + + sc.tic() + + n_runs = 2 + verbose = 1 + + basepars = {'pop_size': 1000} + metapars = {'n_runs': n_runs} + + sim = cv.Sim() + + # Define the scenarios + scenarios = { + 'baseline': { + 'name': 'No bed constraints', + 'pars': { + 'pop_infected': 100 + } + }, + 'bedconstraint': { + 'name': 'Only 50 beds available', + 'pars': { + 'pop_infected': 100, + 'n_beds': 50, + } + }, + 'bedconstraint2': { + 'name': 'Only 10 beds available', + 'pars': { + 'pop_infected': 100, + 'n_beds': 10, + } + }, + } + + scens = cv.Scenarios(sim=sim, basepars=basepars, metapars=metapars, scenarios=scenarios) + scens.run(verbose=verbose, debug=debug) + + if do_plot: + to_plot = sc.odict({ + 'Cumulative deaths': 'cum_deaths', + 'People needing beds / beds': 'bed_capacity', + 'Number of cases requiring hospitalization': 'n_severe', + 'Number of cases requiring ICU': 'n_critical', + }) + scens.plot(to_plot=to_plot, do_save=do_save, do_show=do_show, fig_path=fig_path) + + return scens + + +def test_borderclosure(do_plot=False, do_show=True, do_save=False, fig_path=None): + sc.heading('Test effect of border closures') + + sc.heading('Setting up...') + + sc.tic() + + n_runs = 2 + verbose = 1 + + basepars = {'pop_size': 1000} + basepars = {'n_imports': 5} + metapars = {'n_runs': n_runs} + + sim = cv.Sim() + + # Define the scenarios + scenarios = { + 'baseline': { + 'name': 'No border closures', + 'pars': { + } + }, + 'borderclosures_day10': { + 'name': 'Close borders on day 10', + 'pars': { + 'interventions': [cv.dynamic_pars({'n_imports': {'days': 10, 'vals': 0}})] + } + }, + } + + scens = cv.Scenarios(sim=sim, basepars=basepars, metapars=metapars, scenarios=scenarios) + scens.run(verbose=verbose, debug=debug) + + if do_plot: + scens.plot(do_save=do_save, do_show=do_show, fig_path=fig_path) + + return scens + + +#%% Run as a script +if __name__ == '__main__': + sc.tic() + + scens1 = test_interventions(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[0]) + scens2 = test_turnaround(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[1]) + scens3 = test_tracedelay(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[2]) + sims = test_beta_edges(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[3]) + bed_scens = test_beds(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[3]) + border_scens = test_borderclosure(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[3]) + + for path in fig_paths: + if os.path.exists(path): + print(f'Removing {path}') + os.remove(path) + + sc.toc() + + +print('Done.') diff --git a/tests/test_interventions.py b/tests/test_interventions.py index 45f07c641..35a6c5f52 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -1,369 +1,95 @@ ''' -Testing the effect of testing interventions in Covasim +Demonstrate all interventions, taken from intervention docstrings ''' -#%% Imports and settings -import os +#%% Housekeeping + import sciris as sc +import pylab as pl import covasim as cv -do_plot = 1 -do_show = 1 -do_save = 0 -debug = 1 -keep_sims = 0 -fig_paths = [f'results/testing_scen_{i}.png' for i in range(3)] -fig_paths += ['results/testing_other.png'] - - -def test_interventions(do_plot=False, do_show=True, do_save=False, fig_path=None): - sc.heading('Test of testing interventions') - +do_plot = 1 +verbose = 0 - sc.heading('Setting up...') +pars = sc.objdict( + pop_size = 10e3, + pop_infected = 100, + pop_type = 'hybrid', + n_days = 90, + ) - sc.tic() - n_runs = 3 - verbose = 1 - base_pars = { - 'pop_size': 1000, - 'pop_type': 'hybrid', - } +#%% Define the interventions - base_sim = cv.Sim(base_pars) # create sim object - n_people = base_sim['pop_size'] - npts = base_sim.npts +# 1. Dynamic pars +i00 = cv.test_prob(start_day=5, symp_prob=0.3) +i01 = cv.dynamic_pars({'beta':{'days':[40, 50], 'vals':[0.005, 0.015]}, 'diag_factor':{'days':30, 'vals':0.0}}) # Starting day 30, make diagnosed people stop transmitting - # Define overall testing assumptions - # Remember that this is the daily % of the population that gets tested. S Korea (one of the highest-testing places) tested - # an average of 10000 people/day over March, or 270,000 in total. This is ~200 people per million every day (0.02%).... - max_optimistic_testing = 0.1 # ... which means that this is an artificially high number, for testing purposes only!! - optimistic_daily_tests = [max_optimistic_testing*n_people]*npts # Very best-case scenario for asymptomatic testing - # Define the scenarios - scenarios = { - 'baseline': { - 'name':'Status quo, no testing', - 'pars': { - 'interventions': None, - } - }, - 'test_skorea': { - 'name':'Assuming South Korea testing levels of 0.02% daily (untargeted); isolate positives', - 'pars': { - 'interventions': cv.test_num(daily_tests=optimistic_daily_tests) - } - }, - 'floating': { - 'name': 'Test with constant probability based on symptoms', - 'pars': { - 'interventions': cv.test_prob(symp_prob=max_optimistic_testing, asymp_prob=0.0) - } - }, - 'sequence': { - 'name': 'Historical switching to probability', - 'pars': { - 'interventions': cv.sequence(days=[10, 51], interventions=[ - cv.test_num(daily_tests=optimistic_daily_tests), - cv.test_prob(symp_prob=0.2, asymp_prob=0.002), +# 2. Sequence +i02 = cv.sequence(days=[20, 40, 60], interventions=[ + cv.test_num(daily_tests=[20]*pars.n_days), + cv.test_prob(symp_prob=0.0), + cv.test_prob(symp_prob=0.2), ]) - } - }, - - } - - metapars = {'n_runs': n_runs} - - scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run(verbose=verbose, debug=debug) - - to_plot = ['cum_infections', 'n_infectious', 'new_tests', 'new_diagnoses'] - fig_args = dict(figsize=(20, 24)) - - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=fig_path, interval=7, fig_args=fig_args, to_plot=to_plot) - - return scens - - -def test_turnaround(do_plot=False, do_show=True, do_save=False, fig_path=None): - sc.heading('Test impact of reducing delay time for getting test results') - - sc.heading('Setting up...') - - sc.tic() - - n_runs = 3 - verbose = 1 - base_pars = { - 'pop_size': 1000, - 'pop_type': 'hybrid', - } - base_sim = cv.Sim(base_pars) # create sim object - n_people = base_sim['pop_size'] - npts = base_sim.npts - # Define overall testing assumptions - testing_prop = 0.1 # Assumes we could test 10% of the population daily (!!) - daily_tests = [testing_prop*n_people]*npts # Number of daily tests +# 3. Change beta +i03 = cv.change_beta([30, 50], [0.0, 1], layers='h') +i04 = cv.change_beta([30, 40, 60], [0.0, 1.0, 0.5]) - # Define the scenarios - scenarios = { - f'{d}dayturnaround': { - 'name':f'Symptomatic testing with {d} days to get results', - 'pars': { - 'interventions': cv.test_num(daily_tests=daily_tests, test_delay=d) - } - } for d in range(1, 3+1, 2) - } - metapars = {'n_runs': n_runs} +# 4. Clip edges -- should match the change_beta scenarios +i05 = cv.clip_edges(start_day=30, end_day=50, change={'h':0.0}) +i06 = cv.clip_edges(start_day=30, end_day=40, change=0.0) +i07 = cv.clip_edges(start_day=60, end_day=None, change=0.5) - scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run(verbose=verbose, debug=debug) - to_plot = ['cum_infections', 'n_infectious', 'new_tests', 'new_diagnoses'] - fig_args = dict(figsize=(20, 24)) +# 5. Test number +i08 = cv.test_num(daily_tests=[100, 100, 100, 0, 0, 0]*(pars.n_days//6)) - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=fig_path, interval=7, fig_args=fig_args, to_plot=to_plot) - return scens +# 6. Test probability +i09 = cv.test_prob(symp_prob=0.1) -def test_tracedelay(do_plot=False, do_show=True, do_save=False, fig_path=None): - sc.heading('Test impact of reducing delay time for finding contacts of positives') +# 7. Contact tracing +i10 = cv.test_prob(start_day=20, symp_prob=0.01, asymp_prob=0.0, symp_quar_prob=1.0, asymp_quar_prob=1.0, test_delay=0) +i11 = 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)) - sc.heading('Setting up...') - sc.tic() +# 8. Combination +i12 = cv.clip_edges(start_day=18, change={'s':0.0}) # Close schools +i13 = cv.clip_edges(start_day=20, end_day=32, change={'w':0.7, 'c':0.7}) # Reduce work and community +i14 = cv.clip_edges(start_day=32, end_day=45, change={'w':0.3, 'c':0.3}) # Reduce work and community more +i15 = cv.clip_edges(start_day=45, end_day=None, change={'w':0.9, 'c':0.9}) # Reopen work and community more +i16 = 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 +i17 = 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 - n_runs = 3 - verbose = 1 - base_pars = { - 'pop_size': 1000, - 'pop_type': 'hybrid', - } - base_sim = cv.Sim(base_pars) # create sim object - base_sim['n_days'] = 50 - base_sim['beta'] = 0.03 # Increase beta +#%% Create and run the simulations +sims = sc.objdict() +sims.dynamic = cv.Sim(pars=pars, interventions=[i00, i01]) +sims.sequence = cv.Sim(pars=pars, interventions=i02) +sims.change_beta1 = cv.Sim(pars=pars, interventions=i03) +sims.clip_edges1 = cv.Sim(pars=pars, interventions=i05) # Roughly equivalent to change_beta1 +sims.change_beta2 = cv.Sim(pars=pars, interventions=i04) +sims.clip_edges2 = cv.Sim(pars=pars, interventions=[i06, i07]) # Roughly euivalent to change_beta2 +sims.test_num = cv.Sim(pars=pars, interventions=i08) +sims.test_prob = cv.Sim(pars=pars, interventions=i09) +sims.tracing = cv.Sim(pars=pars, interventions=[i10, i11]) +sims.combo = cv.Sim(pars=pars, interventions=[i12, i13, i14, i15, i16, i17]) - n_people = base_sim['pop_size'] - npts = base_sim.npts +for key,sim in sims.items(): + sim.label = key + sim.run(verbose=verbose) - # Define overall testing assumptions - testing_prop = 0.1 # Assumes we could test 10% of the population daily (way too optimistic!!) - daily_tests = [testing_prop*n_people]*npts # Number of daily tests - - # Define the scenarios - scenarios = { - 'lowtrace': { - 'name': 'Poor contact tracing', - 'pars': { - 'quar_eff': {'h': 1, 's': 0.5, 'w': 0.5, 'c': 0.25}, - 'quar_period': 7, - 'interventions': [cv.test_num(daily_tests=daily_tests), - cv.contact_tracing(trace_probs = {'h': 0, 's': 0, 'w': 0, 'c': 0}, - trace_time = {'h': 1, 's': 7, 'w': 7, 'c': 7})] - } - }, - 'modtrace': { - 'name': 'Moderate contact tracing', - 'pars': { - 'quar_eff': {'h': 0.75, 's': 0.25, 'w': 0.25, 'c': 0.1}, - 'quar_period': 10, - 'interventions': [cv.test_num(daily_tests=daily_tests), - cv.contact_tracing(trace_probs = {'h': 1, 's': 0.8, 'w': 0.5, 'c': 0.1}, - trace_time = {'h': 0, 's': 3, 'w': 3, 'c': 8})] - } - }, - 'hightrace': { - 'name': 'Fast contact tracing', - 'pars': { - 'quar_eff': {'h': 0.5, 's': 0.1, 'w': 0.1, 'c': 0.1}, - 'quar_period': 14, - 'interventions': [cv.test_num(daily_tests=daily_tests), - cv.contact_tracing(trace_probs = {'h': 1, 's': 0.8, 'w': 0.8, 'c': 0.2}, - trace_time = {'h': 0, 's': 1, 'w': 1, 'c': 5})] - } - }, - 'alltrace': { - 'name': 'Same-day contact tracing', - 'pars': { - 'quar_eff': {'h': 0.0, 's': 0.0, 'w': 0.0, 'c': 0.0}, - 'quar_period': 21, - 'interventions': [cv.test_num(daily_tests=daily_tests), - cv.contact_tracing(trace_probs = {'h': 1, 's': 1, 'w': 1, 'c': 1}, - trace_time = {'h': 0, 's': 1, 'w': 1, 'c': 2})] - } - }, - } - - metapars = {'n_runs': n_runs} - - scens = cv.Scenarios(sim=base_sim, metapars=metapars, scenarios=scenarios) - scens.run(verbose=verbose, debug=debug) - - if do_plot: - to_plot = [ - 'cum_infections', - 'cum_recoveries', - 'new_infections', - 'n_quarantined', - 'new_quarantined' - ] - fig_args = dict(figsize=(24,16)) - scens.plot(do_save=do_save, do_show=do_show, to_plot=to_plot, fig_path=fig_path, n_cols=2, fig_args=fig_args) - - return scens - - - -def test_beta_edges(do_plot=False, do_show=True, do_save=False, fig_path=None): - - pars = dict( - pop_size=1000, - pop_infected=20, - pop_type='hybrid', - ) - - start_day = 25 # Day to start the intervention - end_day = 40 # Day to end the intervention - change = 0.3 # Amount of change - - sims = sc.objdict() - sims.b = cv.Sim(pars) # Beta intervention - sims.e = cv.Sim(pars) # Edges intervention - - beta_interv = cv.change_beta(days=[start_day, end_day], changes=[change, 1.0]) - edge_interv = cv.clip_edges(start_day=start_day, end_day=end_day, change=change, verbose=True) - sims.b.update_pars(interventions=beta_interv) - sims.e.update_pars(interventions=edge_interv) - +#%% Plotting +if do_plot: for sim in sims.values(): - sim.run() - if do_plot: - sim.plot(do_save=do_save, do_show=do_show, fig_path=fig_path) - sim.plot_result('r_eff') - - return sims - - -def test_beds(do_plot=False, do_show=True, do_save=False, fig_path=None): - sc.heading('Test of bed capacity estimation') - - sc.heading('Setting up...') - - sc.tic() - - n_runs = 2 - verbose = 1 - - basepars = {'pop_size': 1000} - metapars = {'n_runs': n_runs} - - sim = cv.Sim() - - # Define the scenarios - scenarios = { - 'baseline': { - 'name': 'No bed constraints', - 'pars': { - 'pop_infected': 100 - } - }, - 'bedconstraint': { - 'name': 'Only 50 beds available', - 'pars': { - 'pop_infected': 100, - 'n_beds': 50, - } - }, - 'bedconstraint2': { - 'name': 'Only 10 beds available', - 'pars': { - 'pop_infected': 100, - 'n_beds': 10, - } - }, - } - - scens = cv.Scenarios(sim=sim, basepars=basepars, metapars=metapars, scenarios=scenarios) - scens.run(verbose=verbose, debug=debug) - - if do_plot: - to_plot = sc.odict({ - 'Cumulative deaths': 'cum_deaths', - 'People needing beds / beds': 'bed_capacity', - 'Number of cases requiring hospitalization': 'n_severe', - 'Number of cases requiring ICU': 'n_critical', - }) - scens.plot(to_plot=to_plot, do_save=do_save, do_show=do_show, fig_path=fig_path) - - return scens - - -def test_borderclosure(do_plot=False, do_show=True, do_save=False, fig_path=None): - sc.heading('Test effect of border closures') - - sc.heading('Setting up...') - - sc.tic() - - n_runs = 2 - verbose = 1 - - basepars = {'pop_size': 1000} - basepars = {'n_imports': 5} - metapars = {'n_runs': n_runs} - - sim = cv.Sim() - - # Define the scenarios - scenarios = { - 'baseline': { - 'name': 'No border closures', - 'pars': { - } - }, - 'borderclosures_day10': { - 'name': 'Close borders on day 10', - 'pars': { - 'interventions': [cv.dynamic_pars({'n_imports': {'days': 10, 'vals': 0}})] - } - }, - } - - scens = cv.Scenarios(sim=sim, basepars=basepars, metapars=metapars, scenarios=scenarios) - scens.run(verbose=verbose, debug=debug) - - if do_plot: - scens.plot(do_save=do_save, do_show=do_show, fig_path=fig_path) - - return scens - - -#%% Run as a script -if __name__ == '__main__': - sc.tic() - - scens1 = test_interventions(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[0]) - scens2 = test_turnaround(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[1]) - scens3 = test_tracedelay(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[2]) - sims = test_beta_edges(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[3]) - bed_scens = test_beds(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[3]) - border_scens = test_borderclosure(do_plot=do_plot, do_save=do_save, do_show=do_show, fig_path=fig_paths[3]) - - for path in fig_paths: - if os.path.exists(path): - print(f'Removing {path}') - os.remove(path) - - sc.toc() - + print(f'Running {sim.label}...') + sim.plot() + fig = pl.gcf() + fig.axes[0].set_title(f'Simulation: {sim.label}') -print('Done.') From 6505a673f00ba66a46f21c89fbab34ab660822cd Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 01:36:00 -0700 Subject: [PATCH 179/194] modified to produce useful output --- tests/test_interventions.py | 134 ++++++++++++++++++++---------------- tests/test_run.py | 4 +- 2 files changed, 76 insertions(+), 62 deletions(-) diff --git a/tests/test_interventions.py b/tests/test_interventions.py index 35a6c5f52..92d79d07c 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -11,85 +11,101 @@ do_plot = 1 verbose = 0 -pars = sc.objdict( - pop_size = 10e3, - pop_infected = 100, - pop_type = 'hybrid', - n_days = 90, - ) +def test_all_interventions(): + ''' Test all interventions supported by Covasim ''' -#%% Define the interventions + pars = sc.objdict( + pop_size = 1e3, + pop_infected = 10, + pop_type = 'hybrid', + n_days = 90, + ) -# 1. Dynamic pars -i00 = cv.test_prob(start_day=5, symp_prob=0.3) -i01 = cv.dynamic_pars({'beta':{'days':[40, 50], 'vals':[0.005, 0.015]}, 'diag_factor':{'days':30, 'vals':0.0}}) # Starting day 30, make diagnosed people stop transmitting + #%% Define the interventions -# 2. Sequence -i02 = cv.sequence(days=[20, 40, 60], interventions=[ - cv.test_num(daily_tests=[20]*pars.n_days), - cv.test_prob(symp_prob=0.0), - cv.test_prob(symp_prob=0.2), - ]) + # 1. Dynamic pars + i00 = cv.test_prob(start_day=5, symp_prob=0.3) + i01 = cv.dynamic_pars({'beta':{'days':[40, 50], 'vals':[0.005, 0.015]}, 'diag_factor':{'days':30, 'vals':0.0}}) # Starting day 30, make diagnosed people stop transmitting -# 3. Change beta -i03 = cv.change_beta([30, 50], [0.0, 1], layers='h') -i04 = cv.change_beta([30, 40, 60], [0.0, 1.0, 0.5]) + # 2. Sequence + i02 = cv.sequence(days=[15, 30, 45], interventions=[ + cv.test_num(daily_tests=[20]*pars.n_days), + cv.test_prob(symp_prob=0.0), + cv.test_prob(symp_prob=0.2), + ]) -# 4. Clip edges -- should match the change_beta scenarios -i05 = cv.clip_edges(start_day=30, end_day=50, change={'h':0.0}) -i06 = cv.clip_edges(start_day=30, end_day=40, change=0.0) -i07 = cv.clip_edges(start_day=60, end_day=None, change=0.5) + # 3. Change beta + i03 = cv.change_beta([30, 50], [0.0, 1], layers='h') + i04 = cv.change_beta([30, 40, 60], [0.0, 1.0, 0.5]) -# 5. Test number -i08 = cv.test_num(daily_tests=[100, 100, 100, 0, 0, 0]*(pars.n_days//6)) + # 4. Clip edges -- should match the change_beta scenarios + i05 = cv.clip_edges(start_day=30, end_day=50, change={'h':0.0}) + i06 = cv.clip_edges(start_day=30, end_day=40, change=0.0) + i07 = cv.clip_edges(start_day=60, end_day=None, change=0.5) -# 6. Test probability -i09 = cv.test_prob(symp_prob=0.1) + # 5. Test number + i08 = cv.test_num(daily_tests=[100, 100, 100, 0, 0, 0]*(pars.n_days//6)) -# 7. Contact tracing -i10 = cv.test_prob(start_day=20, symp_prob=0.01, asymp_prob=0.0, symp_quar_prob=1.0, asymp_quar_prob=1.0, test_delay=0) -i11 = 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)) + # 6. Test probability + i09 = cv.test_prob(symp_prob=0.1) -# 8. Combination -i12 = cv.clip_edges(start_day=18, change={'s':0.0}) # Close schools -i13 = cv.clip_edges(start_day=20, end_day=32, change={'w':0.7, 'c':0.7}) # Reduce work and community -i14 = cv.clip_edges(start_day=32, end_day=45, change={'w':0.3, 'c':0.3}) # Reduce work and community more -i15 = cv.clip_edges(start_day=45, end_day=None, change={'w':0.9, 'c':0.9}) # Reopen work and community more -i16 = 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 -i17 = 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 + # 7. Contact tracing + i10 = cv.test_prob(start_day=20, symp_prob=0.01, asymp_prob=0.0, symp_quar_prob=1.0, asymp_quar_prob=1.0, test_delay=0) + i11 = 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)) -#%% Create and run the simulations -sims = sc.objdict() -sims.dynamic = cv.Sim(pars=pars, interventions=[i00, i01]) -sims.sequence = cv.Sim(pars=pars, interventions=i02) -sims.change_beta1 = cv.Sim(pars=pars, interventions=i03) -sims.clip_edges1 = cv.Sim(pars=pars, interventions=i05) # Roughly equivalent to change_beta1 -sims.change_beta2 = cv.Sim(pars=pars, interventions=i04) -sims.clip_edges2 = cv.Sim(pars=pars, interventions=[i06, i07]) # Roughly euivalent to change_beta2 -sims.test_num = cv.Sim(pars=pars, interventions=i08) -sims.test_prob = cv.Sim(pars=pars, interventions=i09) -sims.tracing = cv.Sim(pars=pars, interventions=[i10, i11]) -sims.combo = cv.Sim(pars=pars, interventions=[i12, i13, i14, i15, i16, i17]) + # 8. Combination + i12 = cv.clip_edges(start_day=18, change={'s':0.0}) # Close schools + i13 = cv.clip_edges(start_day=20, end_day=32, change={'w':0.7, 'c':0.7}) # Reduce work and community + i14 = cv.clip_edges(start_day=32, end_day=45, change={'w':0.3, 'c':0.3}) # Reduce work and community more + i15 = cv.clip_edges(start_day=45, end_day=None, change={'w':0.9, 'c':0.9}) # Reopen work and community more + i16 = 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 + i17 = 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 -for key,sim in sims.items(): - sim.label = key - sim.run(verbose=verbose) + #%% Create and run the simulations + sims = sc.objdict() + sims.dynamic = cv.Sim(pars=pars, interventions=[i00, i01]) + sims.sequence = cv.Sim(pars=pars, interventions=i02) + sims.change_beta1 = cv.Sim(pars=pars, interventions=i03) + sims.clip_edges1 = cv.Sim(pars=pars, interventions=i05) # Roughly equivalent to change_beta1 + sims.change_beta2 = cv.Sim(pars=pars, interventions=i04) + sims.clip_edges2 = cv.Sim(pars=pars, interventions=[i06, i07]) # Roughly euivalent to change_beta2 + sims.test_num = cv.Sim(pars=pars, interventions=i08) + sims.test_prob = cv.Sim(pars=pars, interventions=i09) + sims.tracing = cv.Sim(pars=pars, interventions=[i10, i11]) + sims.combo = cv.Sim(pars=pars, interventions=[i12, i13, i14, i15, i16, i17]) -#%% Plotting -if do_plot: - for sim in sims.values(): - print(f'Running {sim.label}...') - sim.plot() - fig = pl.gcf() - fig.axes[0].set_title(f'Simulation: {sim.label}') + for key,sim in sims.items(): + sim.label = key + sim.run(verbose=verbose) + + #%% Plotting + if do_plot: + for sim in sims.values(): + print(f'Running {sim.label}...') + sim.plot() + fig = pl.gcf() + fig.axes[0].set_title(f'Simulation: {sim.label}') + + return + + + +#%% Run as a script +if __name__ == '__main__': + T = sc.tic() + + test_all_interventions() + + sc.toc(T) + print('Done.') \ No newline at end of file diff --git a/tests/test_run.py b/tests/test_run.py index 0e8a4cf0a..01f9cd5ac 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -216,6 +216,4 @@ def test_complex_scenarios(do_plot=False, do_show=True, do_save=False, fig_path= scens2 = test_complex_scenarios(do_plot=do_plot) sc.toc(T) - - -print('Done.') + print('Done.') From 599ebf5787e6efbe37f9a75681b690cf015498ef Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 01:52:35 -0700 Subject: [PATCH 180/194] refactored interventions --- covasim/interventions.py | 4 ++-- tests/test_other.py | 31 +++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index 952a69642..81e5dc376 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -36,8 +36,8 @@ def InterventionDict(which, pars): IntervClass = mapping[which] except: available = ', '.join(mapping.keys()) - errormsg = f'Only interventions "{available}" are available in dictionary representation' - raise NotImplementedError(errormsg) + errormsg = f'Only interventions "{available}" are available in dictionary representation, not "{which}"' + raise sc.KeyNotFoundError(errormsg) intervention = IntervClass(**pars) return intervention diff --git a/tests/test_other.py b/tests/test_other.py index 22bf0203c..45a11a731 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -97,7 +97,22 @@ def test_base(): def test_interventions(): sc.heading('Testing interventions') - sim = cv.Sim(pop_size=300, n_days=60) + # Create sim + sim = cv.Sim(pop_size=100, n_days=60, datafile='example_data.csv', verbose=0) + + # Intervention conversion + ce = cv.InterventionDict(**{'which': 'clip_edges', 'pars': {'start_day': 10, 'end_day':30, 'change': 0.5, 'verbose':True}}) + print(ce) + with pytest.raises(sc.KeyNotFoundError): + cv.InterventionDict(**{'which': 'invalid', 'pars': {'days': 10, 'changes': 0.5}}) + + # Test numbers and contact tracing + tn1 = cv.test_num(10, start_day=3, end_day=20) + tn2 = cv.test_num(daily_tests=sim.data['new_tests']) + ct = cv.contact_tracing() + + # Create and run + sim['interventions'] = [ce, tn1, tn2, ct] sim.run() return @@ -359,14 +374,14 @@ def test_sim(): sc.tic() - # test_base() + test_base() test_interventions() - # test_misc() - # test_people() - # test_population() - # test_requirements() - # test_run() - # test_sim() + test_misc() + test_people() + test_population() + test_requirements() + test_run() + test_sim() print('\n'*2) sc.toc() \ No newline at end of file From 85a443f25867c27036d5e794ae1caf0f0bb4b1af Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 02:11:16 -0700 Subject: [PATCH 181/194] adding plotting tests --- covasim/plotting.py | 16 ++++++---------- tests/test_other.py | 43 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index f7c717d7c..44b8c759f 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -33,7 +33,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=None): +def handle_to_plot(which, to_plot, n_cols, sim): ''' Handle which quantities to plot ''' if to_plot is None: @@ -45,14 +45,10 @@ def handle_to_plot(which, to_plot, n_cols, sim=None): errormsg = f'"which" must be "sim" or "scens", not "{which}"' raise NotImplementedError(errormsg) elif isinstance(to_plot, list): # If a list of keys has been supplied - if sim is None: - errormsg = f'Cannot plot {to_plot} as a list without a sim supplied; supply as a title:resultkey dict instead' - raise ValueError(errormsg) - else: - 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 + 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 to_plot = sc.odict(sc.dcp(to_plot)) # In case it's supplied as a dict @@ -216,7 +212,7 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot # Handle inputs args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) - to_plot, n_rows = handle_to_plot('sim', to_plot, n_cols) + to_plot, n_rows = handle_to_plot('sim', to_plot, n_cols, sim=sim) fig, figs, ax = create_figs(args, font_size, font_family, sep_figs, fig) # Do the plotting diff --git a/tests/test_other.py b/tests/test_other.py index 45a11a731..2075ce5e3 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -10,7 +10,7 @@ import covasim as cv import pylab as pl -do_plot = False +do_plot = True def remove_files(*args): ''' Remove files that were created ''' @@ -192,6 +192,30 @@ def test_people(): return +def test_plotting(): + sc.heading('Testing plotting') + + fig_path = 'plotting_test.png' + + # Create sim with data and interventions + ce = cv.clip_edges(**{'start_day': 10, 'change': 0.5}) + sim = cv.Sim(pop_size=100, n_days=60, datafile='example_data.csv', interventions=ce, verbose=0) + sim.run(do_plot=True) + + # Handle lesser-used plotting options + sim.plot(to_plot=['cum_deaths', 'new_infections'], sep_figs=True, font_family='Arial', log_scale=['Number of new infections'], interval=5, do_save=True, fig_path=fig_path) + print('↑ May print a warning about zero values') + + # Handle Plotly functions + cv.plotly_sim(sim) + cv.plotly_people(sim) + cv.plotly_animate(sim) + + # Tidy up + remove_files(fig_path) + + return + def test_population(): sc.heading('Testing the population') @@ -374,14 +398,15 @@ def test_sim(): sc.tic() - test_base() - test_interventions() - test_misc() - test_people() - test_population() - test_requirements() - test_run() - test_sim() + # test_base() + # test_interventions() + # test_misc() + # test_people() + test_plotting() + # test_population() + # test_requirements() + # test_run() + # test_sim() print('\n'*2) sc.toc() \ No newline at end of file From ee7b54a72f5fb89ed29773f2ae2e7f3bec73df78 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 02:20:03 -0700 Subject: [PATCH 182/194] refactored plotting --- covasim/misc.py | 2 +- covasim/plotting.py | 31 ++++++++++++++----------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index 031952891..821d6996c 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -64,7 +64,7 @@ def load_data(filename, columns=None, calculate=True, verbose=True, **kwargs): else: data['date'] = pd.to_datetime(data['date']).dt.date - data.set_index('date', inplace=True) + data.set_index('date', inplace=True, drop=False) # So sim.data['date'] can still be accessed return data diff --git a/covasim/plotting.py b/covasim/plotting.py index 44b8c759f..21315572e 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -588,6 +588,18 @@ def get_individual_states(sim): # Default settings for the Plotly legend plotly_legend = dict(legend_orientation="h", legend=dict(x=0.0, y=1.18)) + +def plotly_interventions(sim, fig): + ''' Add vertical lines for interventions to the plot ''' + if sim['interventions']: + for interv in sim['interventions']: + if hasattr(interv, 'days'): + for interv_day in interv.days: + if interv_day and interv_day < sim['n_days']: + fig.add_shape(dict(type="line", xref="x", yref="paper", x0=interv_day, x1=interv_day, y0=0, y1=1, name='Intervention', line=dict(width=0.5, dash='dash'))) + fig.update_layout(annotations=[dict(x=interv_day, y=1.07, xref="x", yref="paper", text="Intervention change", showarrow=False)]) + return + def plotly_sim(sim): ''' Main simulation results -- parallel of sim.plot() ''' @@ -606,14 +618,7 @@ def plotly_sim(sim): ydata = sim.data[key] fig.add_trace(go.Scatter(x=xdata, y=ydata, mode='markers', name=label + ' (data)', line_color=this_color)) - if sim['interventions']: - for interv in sim['interventions']: - if hasattr(interv, 'days'): - for interv_day in interv.days: - if interv_day > 0 and interv_day < sim['n_days']: - fig.add_shape(dict(type="line", xref="x", yref="paper", x0=interv_day, x1=interv_day, y0=0, y1=1, name='Intervention', line=dict(width=0.5, dash='dash'))) - fig.update_layout(annotations=[dict(x=interv_day, y=1.07, xref="x", yref="paper", text="Intervention change", showarrow=False)]) - + plotly_interventions(sim, fig) fig.update_layout(title={'text':title}, yaxis_title='Count', autosize=True, **plotly_legend) plots.append(fig) @@ -638,15 +643,7 @@ def plotly_people(sim, do_show=False): name=state['name'] )) - if sim['interventions']: - for interv in sim['interventions']: - if hasattr(interv, 'days'): - if interv.do_plot: - for interv_day in interv.days: - if interv_day > 0 and interv_day < sim['n_days']: - fig.add_shape(dict(type="line", xref="x", yref="paper", x0=interv_day, x1=interv_day, y0=0, y1=1, name='Intervention', line=dict(width=0.5, dash='dash'))) - fig.update_layout(annotations=[dict(x=interv_day, y=1.07, xref="x", yref="paper", text="Intervention change", showarrow=False)]) - + plotly_interventions(sim, fig) fig.update_layout(yaxis_range=(0, sim.n)) fig.update_layout(title={'text': 'Numbers of people by health state'}, yaxis_title='People', autosize=True, **plotly_legend) From bb54acc3dfc9ad5386173824392b8f3016ef4744 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 02:34:04 -0700 Subject: [PATCH 183/194] update changelog, requirements, setup --- CHANGELOG.rst | 5 ++++- requirements.txt | 2 +- setup.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0097095b6..5680939b5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,7 +8,10 @@ Version 1.0.0 (2020-05-08) -------------------------- - Official release of Covasim. - Made scenario and simulation plotting more flexible: ``to_plot`` can now simply be a list of results keys, e.g. ``cum_deaths``. -- Added additional tests. +- Added additional tests, increasing test coverage from 67% to 92%. +- Fixed bug in ``cv.save()``. +- Added ``reset()`` to MultiSim that undoes a ``reduce()`` or ``combine()`` call. +- General code cleaning: made exceptions raised more consistent, removed unused functions, etc. - GitHub info: PR `487 `__, previous head ``c8ca32d`` diff --git a/requirements.txt b/requirements.txt index 5ab2a06fc..5a4568757 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,6 @@ numba==0.48 sciris>=0.17.0 scirisweb>=0.17.0 gunicorn -plotly_express +plotly ipywidgets fire \ No newline at end of file diff --git a/setup.py b/setup.py index 9c2b6bab4..1e5b0c060 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,8 @@ webapp_reqs = [ 'scirisweb', 'gunicorn', - 'plotly_express' + 'plotly', + 'ipywidgets', ] requirements = [req for req in requirements if req not in webapp_reqs] From f2e1e8d4d18c786fca50ae8b8c62a651e7414def Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 02:50:04 -0700 Subject: [PATCH 184/194] added benchmark --- tests/benchmark.json | 4 +- tests/regression/parameters_v1.0.0.json | 190 ++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 tests/regression/parameters_v1.0.0.json diff --git a/tests/benchmark.json b/tests/benchmark.json index 7e2068abc..975367342 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,7 +1,7 @@ { "time": { - "initialize": 0.164, - "run": 0.166 + "initialize": 0.168, + "run": 0.173 }, "parameters": { "pop_size": 20000, diff --git a/tests/regression/parameters_v1.0.0.json b/tests/regression/parameters_v1.0.0.json new file mode 100644 index 000000000..b780c89cb --- /dev/null +++ b/tests/regression/parameters_v1.0.0.json @@ -0,0 +1,190 @@ +{ + "pop_size": 20000, + "pop_infected": 10, + "pop_type": "random", + "location": null, + "start_day": "2020-03-01", + "end_day": "2020-04-30", + "n_days": 60, + "rand_seed": 1, + "verbose": 0, + "pop_scale": 1, + "rescale": 0, + "rescale_threshold": 0.05, + "rescale_factor": 2, + "beta": 0.016, + "contacts": { + "a": 20 + }, + "dynam_layer": { + "a": 0 + }, + "beta_layer": { + "a": 1.0 + }, + "n_imports": 0, + "beta_dist": { + "dist": "lognormal", + "par1": 0.84, + "par2": 0.3 + }, + "viral_dist": { + "frac_time": 0.3, + "load_ratio": 2, + "high_cap": 4 + }, + "asymp_factor": 1.0, + "diag_factor": 0.2, + "quar_eff": { + "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 + } + }, + "OR_no_treat": 2.0, + "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": [ + 10, + 20, + 30, + 40, + 50, + 60, + 70, + 80, + 120 + ], + "sus_ORs": [ + 0.34, + 0.67, + 1.0, + 1.0, + 1.0, + 1.0, + 1.24, + 1.47, + 1.47 + ], + "trans_ORs": [ + 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 + ], + "symp_probs": [ + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9 + ], + "severe_probs": [ + 0.001, + 0.0029999999999999996, + 0.012, + 0.032, + 0.049, + 0.102, + 0.16599999999999998, + 0.24300000000000002, + 0.273 + ], + "crit_probs": [ + 0.06, + 0.04848484848484849, + 0.05, + 0.049999999999999996, + 0.06297376093294461, + 0.12196078431372549, + 0.2740210843373494, + 0.43200193657709995, + 0.708994708994709 + ], + "death_probs": [ + 0.6666666666666667, + 0.75, + 0.8333333333333333, + 0.7692307692307694, + 0.6944444444444444, + 0.6430868167202572, + 0.6045616927727397, + 0.5715566513504426, + 0.5338691159586683 + ] + }, + "interventions": [], + "interv_func": null, + "timelimit": 3600, + "stopping_func": null, + "n_beds": null +} \ No newline at end of file From 7c769f980cb5470b3895f2737b42355b4fe3e19e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 02:54:12 -0700 Subject: [PATCH 185/194] add statsmodels as requirement --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 5a4568757..400ee79cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ numpy pandas scipy matplotlib +statsmodels numba==0.48 sciris>=0.17.0 scirisweb>=0.17.0 From 5d39b37858a9ffa0617775941668ce4f40d30dbc Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 03:00:34 -0700 Subject: [PATCH 186/194] updated filenames --- tests/test_other.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_other.py b/tests/test_other.py index 2075ce5e3..ffb0c3018 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -10,7 +10,11 @@ import covasim as cv import pylab as pl + do_plot = True +csv_file = os.path.join(sc.thisdir(__file__), 'example_data.csv') +xlsx_file = os.path.join(sc.thisdir(__file__), 'example_data.xlsx') + def remove_files(*args): ''' Remove files that were created ''' @@ -98,7 +102,7 @@ def test_interventions(): sc.heading('Testing interventions') # Create sim - sim = cv.Sim(pop_size=100, n_days=60, datafile='example_data.csv', verbose=0) + sim = cv.Sim(pop_size=100, n_days=60, datafile=csv_file, verbose=0) # Intervention conversion ce = cv.InterventionDict(**{'which': 'clip_edges', 'pars': {'start_day': 10, 'end_day':30, 'change': 0.5, 'verbose':True}}) @@ -126,14 +130,14 @@ def test_misc(): json_path = 'test_misc.json' # Data loading - cv.load_data('example_data.csv') - cv.load_data('example_data.xlsx') + cv.load_data(csv_file) + cv.load_data(xlsx_file) with pytest.raises(NotImplementedError): cv.load_data('example_data.unsupported_extension') with pytest.raises(ValueError): - cv.load_data('example_data.xlsx', columns=['missing_column']) + cv.load_data(xlsx_file, columns=['missing_column']) # Dates d1 = cv.date('2020-04-04') @@ -199,7 +203,7 @@ def test_plotting(): # Create sim with data and interventions ce = cv.clip_edges(**{'start_day': 10, 'change': 0.5}) - sim = cv.Sim(pop_size=100, n_days=60, datafile='example_data.csv', interventions=ce, verbose=0) + sim = cv.Sim(pop_size=100, n_days=60, datafile=csv_file, interventions=ce, verbose=0) sim.run(do_plot=True) # Handle lesser-used plotting options From c28c56a925cda5ccdedc9aacc36f74ff57404cd7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 03:13:58 -0700 Subject: [PATCH 187/194] fixed docstring --- covasim/run.py | 4 +--- tests/test_other.py | 16 +++++++++------- tests/test_run.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index aa8a1aeb7..9d9be8e6b 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -111,11 +111,9 @@ def result_keys(self): def run(self, *args, **kwargs): ''' - Run the actual scenarios + Run the actual sims Args: - debug (bool): if True, runs a single run instead of multiple, which makes debugging easier - verbose (int): level of detail to print, passed to sim.run() kwargs (dict): passed to multi_run() and thence to sim.run() Returns: diff --git a/tests/test_other.py b/tests/test_other.py index ffb0c3018..4885470a3 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -11,7 +11,9 @@ import pylab as pl -do_plot = True +do_plot = 1 +verbose = 0 +debug = 1 # This runs without parallelization; faster with pytest csv_file = os.path.join(sc.thisdir(__file__), 'example_data.csv') xlsx_file = os.path.join(sc.thisdir(__file__), 'example_data.xlsx') @@ -34,7 +36,7 @@ def test_base(): sim_path = 'base_tests.sim' # Create a small sim for later use - sim = cv.Sim(pop_size=100, verbose=0) + sim = cv.Sim(pop_size=100, verbose=verbose) sim.run() # Check setting invalid key @@ -102,7 +104,7 @@ def test_interventions(): sc.heading('Testing interventions') # Create sim - sim = cv.Sim(pop_size=100, n_days=60, datafile=csv_file, verbose=0) + sim = cv.Sim(pop_size=100, n_days=60, datafile=csv_file, verbose=verbose) # Intervention conversion ce = cv.InterventionDict(**{'which': 'clip_edges', 'pars': {'start_day': 10, 'end_day':30, 'change': 0.5, 'verbose':True}}) @@ -190,7 +192,7 @@ def test_people(): sc.heading('Testing people') # Test dynamic layers - sim = cv.Sim(pop_size=100, n_days=10, verbose=0, dynam_layer={'a':1}) + sim = cv.Sim(pop_size=100, n_days=10, verbose=verbose, dynam_layer={'a':1}) sim.run() return @@ -203,7 +205,7 @@ def test_plotting(): # Create sim with data and interventions ce = cv.clip_edges(**{'start_day': 10, 'change': 0.5}) - sim = cv.Sim(pop_size=100, n_days=60, datafile=csv_file, interventions=ce, verbose=0) + sim = cv.Sim(pop_size=100, n_days=60, datafile=csv_file, interventions=ce, verbose=verbose) sim.run(do_plot=True) # Handle lesser-used plotting options @@ -299,7 +301,7 @@ def test_run(): msim.base_sim = msim.sims[0] # Restore # Run - msim.run(verbose=0) + msim.run(verbose=verbose) msim.reduce(quantiles=[0.1, 0.9], output=True) with pytest.raises(ValueError): msim.reduce(quantiles='invalid') @@ -319,7 +321,7 @@ def test_run(): # Scenarios scens = cv.Scenarios(sim=s1, metapars={'n_runs':1}) - scens.run(keep_people=True, verbose=0) + scens.run(keep_people=True, verbose=verbose, debug=debug) for keep_people in [True, False]: scens.save(scens_path, keep_people=keep_people) cv.Scenarios.load(scens_path) diff --git a/tests/test_run.py b/tests/test_run.py index 01f9cd5ac..78739a176 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -10,7 +10,7 @@ do_plot = 1 do_save = 0 do_show = 1 -debug = 0 +debug = 1 verbose = 0 #%% Define the tests From 9a6c23bc9e03f6321dca810ef39e838aae1f6e68 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 03:29:55 -0700 Subject: [PATCH 188/194] add pytest to docs requirements --- docs/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 21bfca0f3..c52f499a1 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,4 +2,5 @@ sphinx-rtd-theme sphinxcontrib-napoleon beautifulsoup4 sphinx -plantweb \ No newline at end of file +plantweb +pytest \ No newline at end of file From 9fa84854201e9ca40b15c74c7e4c6608551d0edb Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 12:01:20 -0700 Subject: [PATCH 189/194] fixed legend --- covasim/base.py | 25 ++++++++++++++++--------- covasim/plotting.py | 17 ++++++++++++----- covasim/version.py | 4 ++-- tests/devtests/plotly_tests.py | 16 +++++++++++----- tests/test_other.py | 22 +++++++++++++--------- 5 files changed, 54 insertions(+), 30 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 79e55b5f6..527c826cf 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -4,7 +4,6 @@ import datetime as dt import numpy as np -import pylab as pl import pandas as pd import sciris as sc from . import utils as cvu @@ -233,32 +232,40 @@ def day(self, day, *args): return days - def date(self, ind, *args, dateformat=None): + def date(self, ind, *args, dateformat=None, as_date=False): ''' - Convert an integer or list of integer simulation days to a date/list of dates. + Convert one or more integer days of simulation time to a date/list of dates -- + by default returns a string, or returns a datetime Date object if as_date is True. Args: ind (int, list, or array): the day(s) in simulation time + as_date (bool): whether to return as a datetime date instead of a string Returns: - dates (str or list): the date relative to the simulation start day, as an integer + dates (str, Date, or list): the date(s) corresponding to the simulation day(s) - **Example**:: + **Examples**:: - sim.date(35) # Returns '2020-04-05' + sim.date(34) # Returns '2020-04-04' + sim.date([34, 54]) # Returns ['2020-04-04', '2020-04-24'] + sim.date(34, 54, as_dt=True) # Returns [datetime.date(2020, 4, 4), datetime.date(2020, 4, 24)] ''' + # Handle inputs if sc.isnumber(ind): # If it's a number, convert it to a list ind = sc.promotetolist(ind) ind.extend(args) - if dateformat is None: dateformat = '%Y-%m-%d' + # Do the conversion dates = [] for i in ind: - tmp = self['start_day'] + dt.timedelta(days=int(i)) - dates.append(tmp.strftime(dateformat)) + date_obj = self['start_day'] + dt.timedelta(days=int(i)) + if as_date: + dates.append(date_obj) + else: + dates.append(date_obj.strftime(dateformat)) # Return a string rather than a list if only one provided if len(ind)==1: diff --git a/covasim/plotting.py b/covasim/plotting.py index 21315572e..f4129d3c4 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -589,18 +589,20 @@ def get_individual_states(sim): plotly_legend = dict(legend_orientation="h", legend=dict(x=0.0, y=1.18)) -def plotly_interventions(sim, fig): +def plotly_interventions(sim, fig, add_to_legend=False): ''' Add vertical lines for interventions to the plot ''' if sim['interventions']: for interv in sim['interventions']: if hasattr(interv, 'days'): for interv_day in interv.days: if interv_day and interv_day < sim['n_days']: - fig.add_shape(dict(type="line", xref="x", yref="paper", x0=interv_day, x1=interv_day, y0=0, y1=1, name='Intervention', line=dict(width=0.5, dash='dash'))) - fig.update_layout(annotations=[dict(x=interv_day, y=1.07, xref="x", yref="paper", text="Intervention change", showarrow=False)]) + interv_date = sim.date(interv_day, as_date=True) + fig.add_shape(dict(type="line", xref="x", yref="paper", x0=interv_date, x1=interv_date, y0=0, y1=1, line=dict(width=0.5, dash='dash'))) + if add_to_legend: + fig.add_trace(go.Scatter(x=[interv_date], y=[0], mode='lines', name='Intervention change', line=dict(width=0.5, dash='dash'))) return -def plotly_sim(sim): +def plotly_sim(sim, do_show=False): ''' Main simulation results -- parallel of sim.plot() ''' plots = [] @@ -618,10 +620,15 @@ def plotly_sim(sim): ydata = sim.data[key] fig.add_trace(go.Scatter(x=xdata, y=ydata, mode='markers', name=label + ' (data)', line_color=this_color)) - plotly_interventions(sim, fig) + plotly_interventions(sim, fig, add_to_legend=(p==0)) # Only add the intervention label to the legend for the first plot fig.update_layout(title={'text':title}, yaxis_title='Count', autosize=True, **plotly_legend) plots.append(fig) + + if do_show: + for fig in plots: + fig.show() + return plots diff --git a/covasim/version.py b/covasim/version.py index bb8d8a5d0..3217ea6f5 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -2,6 +2,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '1.0.0' -__versiondate__ = '2020-05-08' +__version__ = '1.0.1' +__versiondate__ = '2020-05-09' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2020 by IDM' diff --git a/tests/devtests/plotly_tests.py b/tests/devtests/plotly_tests.py index 01e7b3d7f..bcd4ea7be 100644 --- a/tests/devtests/plotly_tests.py +++ b/tests/devtests/plotly_tests.py @@ -1,10 +1,16 @@ +''' +Test Plotly plotting outside of the webapp. +''' + import plotly.io as pio import covasim as cv +# pio.renderers.default = "browser" -pio.renderers.default = "browser" - -sim = cv.Sim(pop_size=10e3) - +ce = cv.clip_edges(**{'start_day': 10, 'change': 0.5}) +sim = cv.Sim(pop_size=100, n_days=60, datafile='../example_data.csv', interventions=ce, verbose=0) sim.run() -fig = cv.animate_people(sim, do_show=True) + +f1list = cv.plotly_sim(sim, do_show=True) +f2 = cv.plotly_people(sim, do_show=True) +f3 = cv.plotly_animate(sim, do_show=True) diff --git a/tests/test_other.py b/tests/test_other.py index 4885470a3..ec5bdbbeb 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -49,12 +49,15 @@ def test_base(): print(r) print(r.npts) - # Day conversion + # Day and date conversion daystr = '2020-04-04' sim.day(daystr) sim.day(sc.readdate(daystr)) with pytest.raises(ValueError): sim.day('not a date') + sim.date(34) + sim.date([34, 54]) + sim.date(34, 54, as_date=True) # BaseSim methods sim.copy() @@ -399,20 +402,21 @@ def test_sim(): #%% Run as a script if __name__ == '__main__': + # We need to create plots to test plotting, but can use a non-GUI backend if not do_plot: pl.switch_backend('agg') sc.tic() - # test_base() - # test_interventions() - # test_misc() - # test_people() + test_base() + test_interventions() + test_misc() + test_people() test_plotting() - # test_population() - # test_requirements() - # test_run() - # test_sim() + test_population() + test_requirements() + test_run() + test_sim() print('\n'*2) sc.toc() \ No newline at end of file From 81f6352a66394cb387d682c50eb44383ea2c29af Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 12:06:43 -0700 Subject: [PATCH 190/194] update changelog --- CHANGELOG.rst | 6 ++++++ tests/devtests/plotly_tests.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5680939b5..ea9289cb5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,12 @@ What's new All notable changes to the codebase are documented in this file. Note: in many cases, changes from multiple patch versions are grouped together, so numbering will not be strictly consecutive. +Version 1.0.1 (2020-05-09) +-------------------------- +- Adds argument ``as_date`` for ``sim.date()`` to return a ``datetime`` object instead of a string. +- Fixes plotting of interventions in the webapp. +- GitHub info: PR `490 `__, previous head ``9a6c23b`` + Version 1.0.0 (2020-05-08) -------------------------- diff --git a/tests/devtests/plotly_tests.py b/tests/devtests/plotly_tests.py index bcd4ea7be..cf0adf530 100644 --- a/tests/devtests/plotly_tests.py +++ b/tests/devtests/plotly_tests.py @@ -5,7 +5,7 @@ import plotly.io as pio import covasim as cv -# pio.renderers.default = "browser" +pio.renderers.default = "browser" # Or can use a Jupyter notebook ce = cv.clip_edges(**{'start_day': 10, 'change': 0.5}) sim = cv.Sim(pop_size=100, n_days=60, datafile='../example_data.csv', interventions=ce, verbose=0) From 332b4faf738787d359f538f93ecd7d10dec8f640 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 12:11:00 -0700 Subject: [PATCH 191/194] tidy imports --- covasim/base.py | 2 +- covasim/interventions.py | 7 ++++++- covasim/misc.py | 2 +- covasim/population.py | 2 +- covasim/utils.py | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/covasim/base.py b/covasim/base.py index 527c826cf..956cdeeab 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -2,10 +2,10 @@ Base classes for Covasim. ''' -import datetime as dt import numpy as np import pandas as pd import sciris as sc +import datetime as dt from . import utils as cvu from . import misc as cvm from . import defaults as cvd diff --git a/covasim/interventions.py b/covasim/interventions.py index 81e5dc376..4f42997a6 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -1,8 +1,13 @@ -import inspect +''' +Specify the core interventions available in Covasim. Other interventions can be +defined by the user by inheriting from these classes. +''' + import numpy as np import pandas as pd import pylab as pl import sciris as sc +import inspect import datetime as dt from . import utils as cvu from . import base as cvb diff --git a/covasim/misc.py b/covasim/misc.py index 821d6996c..c040e423a 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -2,10 +2,10 @@ Miscellaneous functions that do not belong anywhere else ''' -import datetime as dt import numpy as np import pandas as pd import sciris as sc +import datetime as dt import scipy.stats as sps from . import version as cvver diff --git a/covasim/population.py b/covasim/population.py index 313f63593..b3a287964 100644 --- a/covasim/population.py +++ b/covasim/population.py @@ -5,13 +5,13 @@ #%% Imports import numpy as np # Needed for a few things not provided by pl import sciris as sc +from collections import defaultdict from . import requirements as cvreq from . import utils as cvu from . import data as cvdata from . import defaults as cvd from . import parameters as cvpars from . import people as cvppl -from collections import defaultdict # Specify all externally visible functions this file defines diff --git a/covasim/utils.py b/covasim/utils.py index b7e58effe..93b2183d8 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -8,7 +8,7 @@ import numpy as np # For numerics from . import defaults as cvd -# What to import -- not all functions are externally available +# What functions are externally visible -- note, this gets populated in each section below __all__ = [] # Set dtypes From ed7cbf70bb076e86a749875e9bd37f8e2f5d6786 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 12:21:25 -0700 Subject: [PATCH 192/194] updated parameter values --- CHANGELOG.rst | 5 +++-- covasim/README.rst | 37 ++++++++++++++++++------------------- covasim/parameters.py | 16 ++++++++-------- covasim/sim.py | 2 +- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ea9289cb5..97df9bcdd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,8 +5,9 @@ All notable changes to the codebase are documented in this file. Note: in many c Version 1.0.1 (2020-05-09) -------------------------- -- Adds argument ``as_date`` for ``sim.date()`` to return a ``datetime`` object instead of a string. -- Fixes plotting of interventions in the webapp. +- Added argument ``as_date`` for ``sim.date()`` to return a ``datetime`` object instead of a string. +- Fixed plotting of interventions in the webapp. +- Removed default 1-hour time limit for simulations. - GitHub info: PR `490 `__, previous head ``9a6c23b`` diff --git a/covasim/README.rst b/covasim/README.rst index 2d9e7b4ea..e34e7e261 100644 --- a/covasim/README.rst +++ b/covasim/README.rst @@ -2,7 +2,7 @@ Parameters ========== -This file describes the expected behavior of each parameter in the model. Note: the term "overall infection rate" can be explored using ``sim.results['doubling_time']`` and ``sim.results['r_eff']`` (a higher infection rate means lower doubling times and higher *R\_eff*), as well as by simply looking at the epidemic curves. +This file describes each of the input parameters in Covasim. Note: the overall infection rate can be explored using ``sim.results['doubling_time']`` and ``sim.results['r_eff']`` (a higher infection rate means lower doubling times and higher *R\_eff*), as well as by simply looking at the epidemic curves. Population parameters --------------------- @@ -33,30 +33,29 @@ Basic disease transmission * ``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 -* ``viral_dist`` = The time varying viral load (transmissibility) +* ``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 ------------------------------- -* ``asymp_factor`` = Multiply beta by this factor for asymptomatic cases -* ``diag_factor`` = Multiply beta by this factor for diganosed cases +* ``asymp_factor`` = Multiply beta by this factor for asymptomatic cases; no statistically significant difference in transmissibility: https://www.sciencedirect.com/science/article/pii/S1201971220302502 +* ``diag_factor`` = Multiply beta by this factor for diganosed cases; based on intervention strength * ``quar_eff`` = Quarantine multiplier on transmissibility and susceptibility; set below -* ``quar_period`` = Number of days to quarantine for +* ``quar_period`` = Number of days to quarantine for; assumption based on standard policies Time for disease progression ---------------------------- -* ``exp2inf`` = Duration from exposed to infectious -* ``inf2sym`` = Duration from infectious to symptomatic -* ``sym2sev`` = Duration from symptomatic to severe symptoms -* ``sev2crit`` = Duration from severe symptoms to requiring ICU +* ``exp2inf`` = Duration from exposed to infectious; see Lauer et al., https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7081172/, subtracting inf2sim 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 Time for disease recovery ------------------------- -* ``asym2rec`` = Duration for asymptomatics to recover -* ``mild2rec`` = Duration from mild symptoms to recovered -* ``sev2rec`` = Duration from severe symptoms to recovered -* ``crit2rec`` = Duration from critical symptoms to recovered -* ``crit2die`` = Duration from critical symptoms to death +* ``asym2rec`` = Duration for asymptomatic people to recover; see Wölfel et al., https://www.nature.com/articles/s41586-020-2196-x +* ``mild2rec`` = Duration for people with mild symptoms to recover; see Wölfel et al., https://www.nature.com/articles/s41586-020-2196-x +* ``sev2rec`` = 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 +* ``crit2rec`` = 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 +* ``crit2die`` = 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 ------------------- @@ -66,15 +65,15 @@ Severity parameters * ``rel_crit_prob`` = Scale factor for proportion of severe cases that become critical * ``rel_death_prob`` = Scale factor for proportion of critical cases that result in death * ``prog_by_age`` = Whether to set disease progression based on the person's age -* ``prognoses`` = Populate this later +* ``prognoses`` = The actual arrays of prognoses by age; this is populated later Events and interventions ------------------------ -* ``interventions`` = List of Intervention instances +* ``interventions`` = The interventions present in this simulation; populated by the user * ``interv_func`` = Custom intervention function -* ``timelimit`` = Time limit for a simulation (seconds) +* ``timelimit`` = Time limit for the simulation (seconds) * ``stopping_func`` = A function to call to stop the sim partway through Health system parameters -------------------------- -* ``n_beds`` = Baseline assumption is that there's no upper limit on the number of beds i.e. there's enough for everyone \ No newline at end of file +* ``n_beds`` = The number of beds available for severely/critically ill patients (default is no constraint) diff --git a/covasim/parameters.py b/covasim/parameters.py index 2d27cfb3b..a2f97fd1b 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -63,10 +63,10 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): pars['dur']['sev2crit'] = {'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 - pars['dur']['asym2rec'] = {'dist':'lognormal_int', 'par1':8.0, 'par2':2.0} # Duration for asymptomatics to recover; see Wölfel et al., https://www.nature.com/articles/s41586-020-2196-x - pars['dur']['mild2rec'] = {'dist':'lognormal_int', 'par1':8.0, 'par2':2.0} # Duration from mild symptoms to recovered; see Wölfel et al., https://www.nature.com/articles/s41586-020-2196-x - pars['dur']['sev2rec'] = {'dist':'lognormal_int', 'par1':14.0, 'par2':2.4} # Duration from severe symptoms to recovered, 22.6 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf - pars['dur']['crit2rec'] = {'dist':'lognormal_int', 'par1':14.0, 'par2':2.4} # Duration from critical symptoms to recovered, 22.6 days total; see Verity et al., https://www.medrxiv.org/content/10.1101/2020.03.09.20033357v1.full.pdf + pars['dur']['asym2rec'] = {'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'] = {'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'] = {'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'] = {'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'] = {'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 @@ -76,16 +76,16 @@ def make_pars(set_prognoses=False, prog_by_age=True, **kwargs): pars['rel_crit_prob'] = 1.0 # Scale factor for proportion of severe cases that become critical pars['rel_death_prob'] = 1.0 # Scale factor for proportion of critical cases that result in death pars['prog_by_age'] = prog_by_age # Whether to set disease progression based on the person's age - pars['prognoses'] = None # Populate this later + pars['prognoses'] = None # The actual arrays of prognoses by age; this is populated later # Events and interventions - pars['interventions'] = [] # List of Intervention instances + pars['interventions'] = [] # The interventions present in this simulation; populated by the user pars['interv_func'] = None # Custom intervention function - pars['timelimit'] = 3600 # Time limit for a simulation (seconds) + pars['timelimit'] = None # Time limit for the simulation (seconds) pars['stopping_func'] = None # A function to call to stop the sim partway through # Health system parameters - pars['n_beds'] = None # Baseline assumption is that there's no upper limit on the number of beds i.e. there's enough for everyone + pars['n_beds'] = None # The number of beds available for severely/critically ill patients (default is no constraint) # Update with any supplied parameter values and generate things that need to be generated pars.update(kwargs) diff --git a/covasim/sim.py b/covasim/sim.py index a1852e99d..d6f95f708 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -473,7 +473,7 @@ def run(self, do_plot=False, until=None, verbose=None, **kwargs): # Check if we were asked to stop elapsed = sc.toc(T, output=True) - if elapsed > self['timelimit']: + if self['timelimit'] and elapsed > self['timelimit']: sc.printv(f"Time limit ({self['timelimit']} s) exceeded", 1, verbose) break elif self['stopping_func'] and self['stopping_func'](self): From 43ea63a61fd7842525b8a3daf15beac7cea669e0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 12:24:49 -0700 Subject: [PATCH 193/194] added regression parameters --- tests/regression/parameters_v1.0.1.json | 190 ++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 tests/regression/parameters_v1.0.1.json diff --git a/tests/regression/parameters_v1.0.1.json b/tests/regression/parameters_v1.0.1.json new file mode 100644 index 000000000..710c262ad --- /dev/null +++ b/tests/regression/parameters_v1.0.1.json @@ -0,0 +1,190 @@ +{ + "pop_size": 20000, + "pop_infected": 10, + "pop_type": "random", + "location": null, + "start_day": "2020-03-01", + "end_day": "2020-04-30", + "n_days": 60, + "rand_seed": 1, + "verbose": 0, + "pop_scale": 1, + "rescale": 0, + "rescale_threshold": 0.05, + "rescale_factor": 2, + "beta": 0.016, + "contacts": { + "a": 20 + }, + "dynam_layer": { + "a": 0 + }, + "beta_layer": { + "a": 1.0 + }, + "n_imports": 0, + "beta_dist": { + "dist": "lognormal", + "par1": 0.84, + "par2": 0.3 + }, + "viral_dist": { + "frac_time": 0.3, + "load_ratio": 2, + "high_cap": 4 + }, + "asymp_factor": 1.0, + "diag_factor": 0.2, + "quar_eff": { + "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 + } + }, + "OR_no_treat": 2.0, + "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": [ + 10, + 20, + 30, + 40, + 50, + 60, + 70, + 80, + 120 + ], + "sus_ORs": [ + 0.34, + 0.67, + 1.0, + 1.0, + 1.0, + 1.0, + 1.24, + 1.47, + 1.47 + ], + "trans_ORs": [ + 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 + ], + "symp_probs": [ + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9 + ], + "severe_probs": [ + 0.001, + 0.0029999999999999996, + 0.012, + 0.032, + 0.049, + 0.102, + 0.16599999999999998, + 0.24300000000000002, + 0.273 + ], + "crit_probs": [ + 0.06, + 0.04848484848484849, + 0.05, + 0.049999999999999996, + 0.06297376093294461, + 0.12196078431372549, + 0.2740210843373494, + 0.43200193657709995, + 0.708994708994709 + ], + "death_probs": [ + 0.6666666666666667, + 0.75, + 0.8333333333333333, + 0.7692307692307694, + 0.6944444444444444, + 0.6430868167202572, + 0.6045616927727397, + 0.5715566513504426, + 0.5338691159586683 + ] + }, + "interventions": [], + "interv_func": null, + "timelimit": null, + "stopping_func": null, + "n_beds": null +} \ No newline at end of file From 682ee59e1c7e0406dea94dbec26998b0db182c8e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sat, 9 May 2020 12:44:07 -0700 Subject: [PATCH 194/194] tidy tests --- tests/test_age_structure.py | 36 ------------------------------------ tests/test_baselines.py | 2 +- tests/test_examples.py | 24 ++++++++++++++---------- tests/test_interventions.py | 2 +- tests/test_other.py | 8 +++++--- tests/test_parameters.py | 28 +++++++++++++++++++++------- tests/test_sim.py | 18 ++++++++---------- tests/test_utils.py | 8 +++++--- tests/test_webapp.py | 10 ++++------ 9 files changed, 59 insertions(+), 77 deletions(-) delete mode 100644 tests/test_age_structure.py diff --git a/tests/test_age_structure.py b/tests/test_age_structure.py deleted file mode 100644 index 5fea74282..000000000 --- a/tests/test_age_structure.py +++ /dev/null @@ -1,36 +0,0 @@ -''' -Test different age structures -''' - -#%% Imports and settings -import sciris as sc -import covasim as cv -import pytest - - -#%% Define the tests - -def test_age_structure(): # If being run via pytest, turn off - - available = 'Lithuania' - not_available = 'Ruritania' - - age_data = cv.data.loaders.get_age_distribution(available) - - with pytest.raises(ValueError): - cv.data.loaders.get_age_distribution(not_available) - - return age_data - - - -#%% Run as a script -if __name__ == '__main__': - sc.tic() - - age_data = test_age_structure() - - sc.toc() - - -print('Done.') diff --git a/tests/test_baselines.py b/tests/test_baselines.py index 396291592..4cd5cb96f 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -2,9 +2,9 @@ Compare current results to baseline """ -import sciris as sc import numpy as np import pandas as pd +import sciris as sc import covasim as cv do_save = False diff --git a/tests/test_examples.py b/tests/test_examples.py index 7302523ba..13ad9a3e9 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -2,21 +2,22 @@ Run examples/*.py using pytest ''' -import os -from pathlib import Path -import importlib.util as iu import pylab as pl import sciris as sc +from pathlib import Path +import importlib.util as iu -pl.switch_backend('agg') # To avoid graphs from appearing -cwd = Path(os.path.dirname(os.path.abspath(__file__))) +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') def run_example(name): - """ + ''' Execute an example py script as __main__ - :param name: the filename without the .py extension - """ + + Args: + name (str): the filename without the .py extension + ''' spec = iu.spec_from_file_location("__main__", examples_dir.joinpath(f"{name}.py")) module = iu.module_from_spec(spec) spec.loader.exec_module(module) @@ -34,9 +35,12 @@ def test_simple(): #%% Run as a script if __name__ == '__main__': - sc.tic() + + T = sc.tic() + test_run_scenarios() test_run_sim() test_simple() - sc.toc() + + sc.toc(T) print('Done.') diff --git a/tests/test_interventions.py b/tests/test_interventions.py index 92d79d07c..c73ed0352 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -20,7 +20,7 @@ def test_all_interventions(): pop_infected = 10, pop_type = 'hybrid', n_days = 90, - ) + ) #%% Define the interventions diff --git a/tests/test_other.py b/tests/test_other.py index ec5bdbbeb..c13749e30 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -269,7 +269,6 @@ def test_requirements(): cv.requirements.min_versions['sciris'] = '99.99.99' with pytest.raises(ImportError): - cv.requirements.check_sciris() cv.requirements.min_versions['scirisweb'] = '99.99.99' @@ -279,6 +278,8 @@ def test_requirements(): cv.requirements.check_synthpops() + print('↑ Should print various requirements warnings') + return @@ -406,7 +407,7 @@ def test_sim(): if not do_plot: pl.switch_backend('agg') - sc.tic() + T = sc.tic() test_base() test_interventions() @@ -419,4 +420,5 @@ def test_sim(): test_sim() print('\n'*2) - sc.toc() \ No newline at end of file + sc.toc(T) + print('Done.') \ No newline at end of file diff --git a/tests/test_parameters.py b/tests/test_parameters.py index 72b676e1b..aebd35de3 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -6,7 +6,7 @@ import os import pytest import sciris as sc -import covasim as cv # NOTE: this is the only tests script that doesn't use base +import covasim as cv do_plot = False @@ -45,15 +45,29 @@ def test_location(): return sim +def test_age_structure(): + sc.heading('Age structures') + + available = 'Lithuania' + not_available = 'Ruritania' + + age_data = cv.data.loaders.get_age_distribution(available) + + with pytest.raises(ValueError): + cv.data.loaders.get_age_distribution(not_available) + + return age_data + + #%% Run as a script if __name__ == '__main__': - sc.tic() + + T = sc.tic() pars = test_parameters() data = test_data() - sim = test_location() - - sc.toc() - + sim = test_location() + age = test_age_structure() -print('Done.') + sc.toc(T) + print('Done.') diff --git a/tests/test_sim.py b/tests/test_sim.py index 17010eac1..e10105a03 100644 --- a/tests/test_sim.py +++ b/tests/test_sim.py @@ -164,15 +164,13 @@ def test_dynamic_resampling(do_plot=False, do_show=False): # If being run via py if __name__ == '__main__': T = sc.tic() - pars = test_parsobj() - sim0 = test_microsim() - sim1 = test_sim(do_plot=do_plot, do_save=do_save, do_show=do_show) - json = test_fileio() - sim4 = test_start_stop() - sim5 = test_sim_data(do_plot=do_plot, do_show=do_show) - sim6 = test_dynamic_resampling(do_plot=do_plot, do_show=do_show) + pars = test_parsobj() + sim0 = test_microsim() + sim1 = test_sim(do_plot=do_plot, do_save=do_save, do_show=do_show) + json = test_fileio() + sim4 = test_start_stop() + sim5 = test_sim_data(do_plot=do_plot, do_show=do_show) + sim6 = test_dynamic_resampling(do_plot=do_plot, do_show=do_show) sc.toc(T) - - -print('Done.') + print('Done.') diff --git a/tests/test_utils.py b/tests/test_utils.py index 31701c24a..bbc74a2f0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -173,14 +173,16 @@ def test_doubling_time(): #%% Run as a script if __name__ == '__main__': - sc.tic() + + T = sc.tic() rnd1 = test_rand() rnd2 = test_poisson() samples = test_samples(doplot=doplot) people1 = test_choose() people2 = test_choose_w() - dt = test_doubling_time() + dt = test_doubling_time() print('\n'*2) - sc.toc() \ No newline at end of file + sc.toc(T) + print('Done.') \ No newline at end of file diff --git a/tests/test_webapp.py b/tests/test_webapp.py index 5f3d31742..82cd4126b 100644 --- a/tests/test_webapp.py +++ b/tests/test_webapp.py @@ -40,12 +40,10 @@ def test_webapp(): #%% Run as a script if __name__ == '__main__': - sc.tic() + T = sc.tic() - pars = test_pars() + pars = test_pars() output = test_webapp() - sc.toc() - - -print('Done.') + sc.toc(T) + print('Done.')