From 34d9fdc4560113b4ac17616298b6df78befa90d8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Sun, 21 Mar 2021 23:50:38 -0700 Subject: [PATCH 01/39] run returns self --- covasim/run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covasim/run.py b/covasim/run.py index 07a649bc3..4ae9f9751 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -172,7 +172,7 @@ def run(self, reduce=False, combine=False, **kwargs): elif combine: self.combine() - return + return self def shrink(self, **kwargs): @@ -972,7 +972,7 @@ def print_heading(string): # Save details about the run self._kept_people = keep_people - return + return self def compare(self, t=None, output=False): From 54d2f4f4398df3fcd16d39060bb2fdfb708582d3 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 00:02:04 -0700 Subject: [PATCH 02/39] add extra export methods --- covasim/base.py | 4 ++++ covasim/run.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/covasim/base.py b/covasim/base.py index 26bd80ab0..e14decf2d 100644 --- a/covasim/base.py +++ b/covasim/base.py @@ -428,6 +428,10 @@ def export_results(self, for_json=True, filename=None, indent=2, *args, **kwargs for key,res in self.results.items(): if isinstance(res, Result): resdict[key] = res.values + if res.low is not None: + resdict[key+'_low'] = res.low + if res.high is not None: + resdict[key+'_high'] = res.high elif for_json: if key == 'date': resdict[key] = [str(d) for d in res] # Convert dates to strings diff --git a/covasim/run.py b/covasim/run.py index 4ae9f9751..eb2c75134 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -809,6 +809,16 @@ def brief(self, output=False): return string + def to_json(self, *args, **kwargs): + ''' Shortcut for base_sim.to_json() ''' + return self.base_sim.to_json(*args, **kwargs) + + + def to_excel(self, *args, **kwargs): + ''' Shortcut for base_sim.to_excel() ''' + return self.base_sim.to_excel(*args, **kwargs) + + class Scenarios(cvb.ParsObj): ''' Class for running multiple sets of multiple simulations -- e.g., scenarios. From 8d21bcedb3d0d7c066daff6aad744fd399ec87a1 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 01:03:46 -0700 Subject: [PATCH 03/39] update settings and styling --- covasim/settings.py | 6 +++--- docs/Makefile | 2 +- docs/_static/theme_overrides.css | 2 ++ docs/build_docs | 1 + docs/tutorials/clean_outputs | 2 +- docs/tutorials/t5.ipynb | 25 +++++++++++++++++++------ 6 files changed, 27 insertions(+), 11 deletions(-) diff --git a/covasim/settings.py b/covasim/settings.py index 3692bd1a5..dd1d571d3 100644 --- a/covasim/settings.py +++ b/covasim/settings.py @@ -188,9 +188,9 @@ def set_matplotlib_global(key, value): ''' Set a global option for Matplotlib -- not for users ''' import pylab as pl if value: # Don't try to reset any of these to a None value - if key == 'font_size': pl.rc('font', size=value) - elif key == 'font_family': pl.rc('font', family=value) - elif key == 'dpi': pl.rc('figure', dpi=value) + if key == 'font_size': pl.rcParams['font.size'] = value + elif key == 'font_family': pl.rcParams['font.family'] = value + elif key == 'dpi': pl.rcParams['figure.dpi'] = value elif key == 'backend': pl.switch_backend(value) else: raise sc.KeyNotFoundError(f'Key {key} not found') return diff --git a/docs/Makefile b/docs/Makefile index 464413fbd..c38d8ebf5 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -10,7 +10,7 @@ BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -jauto -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +ALLSPHINXOPTS = -v -jauto -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css index 4f237be19..062cfb21e 100644 --- a/docs/_static/theme_overrides.css +++ b/docs/_static/theme_overrides.css @@ -51,3 +51,5 @@ div.document span.search-highlight { tr.row-even { background-color: #def; } + +.highlight { background: #D9F0FF; } \ No newline at end of file diff --git a/docs/build_docs b/docs/build_docs index b38c4e5f5..a56dd87bf 100755 --- a/docs/build_docs +++ b/docs/build_docs @@ -16,6 +16,7 @@ duration=$(( SECONDS - start )) echo 'Cleaning up tutorial files...' cd tutorials ./clean_outputs +cd .. echo "Docs built after $duration seconds." echo "Index:" diff --git a/docs/tutorials/clean_outputs b/docs/tutorials/clean_outputs index 66406f59a..60492a75a 100755 --- a/docs/tutorials/clean_outputs +++ b/docs/tutorials/clean_outputs @@ -1,7 +1,7 @@ #!/bin/bash # Remove auto-generated files; use -f in case they don't exist echo 'Deleting:' -echo `ls -1 ./my-*.*` +echo `ls -1 ./my-*.* 2> /dev/null` echo '...in 2 seconds' sleep 2 rm -vf ./my-*.* \ No newline at end of file diff --git a/docs/tutorials/t5.ipynb b/docs/tutorials/t5.ipynb index f10a59dcb..48691454d 100644 --- a/docs/tutorials/t5.ipynb +++ b/docs/tutorials/t5.ipynb @@ -341,6 +341,19 @@ "However, function-based interventions only take you so far. We saw in Tutorial 1 how you could define a simple \"protect the elderly\" intervention with just a few lines of code. This example explains how to create an intervention object that does much the same thing, but is more fully-featured because it uses the `Intervention` class." ] }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "
\n", + "\n", + "You must include the line `super().__init__(**kwargs)` in the `self.__init__()` method, or else the intervention won't work. You must also include `self.initialized = True` in the `self.initialize()` method.\n", + "\n", + "
" + ] + }, { "cell_type": "code", "execution_count": null, @@ -354,7 +367,7 @@ "class protect_elderly(cv.Intervention):\n", "\n", " def __init__(self, start_day=None, end_day=None, age_cutoff=70, rel_sus=0.0, *args, **kwargs):\n", - " super().__init__(**kwargs) # This line must be included\n", + " super().__init__(**kwargs) # NB: This line must be included\n", " self.start_day = start_day\n", " self.end_day = end_day\n", " self.age_cutoff = age_cutoff\n", @@ -368,7 +381,7 @@ " self.elderly = sim.people.age > self.age_cutoff # Find the elderly people here\n", " self.exposed = np.zeros(sim.npts) # Initialize results\n", " self.tvec = sim.tvec # Copy the time vector into this intervention\n", - " self.initialized = True\n", + " self.initialized = True # NB: This line must also be included\n", " return\n", "\n", " def apply(self, sim):\n", @@ -399,10 +412,10 @@ "source": [ "While this example is fairly long, hopefully it's fairly straightforward:\n", "\n", - "- **__init__()** just does what init always does; it's needed to create the class instance. For interventions, it's usually used to store keyword arguments and perform some basic initialization (the first two lines).\n", - "- **initialize()** is similar to init, with the difference that it's invoked when the *sim* itself is initialized. It receives the sim as an input argument. This means you can use it to do a fairly powerful things. Here, since `sim.people` already exists, we can calculate up-front who the elderly are so we don't have to do it on every timestep.\n", - "- **apply()** is the crux of the intervention. It's run on every timestep of the model, and also receives `sim` as an input. You almost always use `sim.t` to get the current timestep, here to turn the intervention on and off. But as this example shows, its real power is that it can make direct modifications to the sim itself (`sim.people.rel_sus[self.elderly] = self.rel_sus`). It can also perform calculations and store data in itself, as shown here with `self.exposed` (although in general, analyzers are better for this, since they happen at the end of the timestep, while interventions happen in the middle).\n", - "- **plot()** is a custom method that shows a plot of the data gathered during the sim. Again, it's usually better to use analyzers for this, but for something simple like this it's fine to double-dip and use an intervention.\n", + "- `__init__()` just does what init always does; it's needed to create the class instance. For interventions, it's usually used to store keyword arguments and perform some basic initialization (the first two lines).\n", + "- `initialize()` is similar to init, with the difference that it's invoked when the *sim* itself is initialized. It receives the sim as an input argument. This means you can use it to do a fairly powerful things. Here, since `sim.people` already exists, we can calculate up-front who the elderly are so we don't have to do it on every timestep.\n", + "- `apply()` is the crux of the intervention. It's run on every timestep of the model, and also receives `sim` as an input. You almost always use `sim.t` to get the current timestep, here to turn the intervention on and off. But as this example shows, its real power is that it can make direct modifications to the sim itself (`sim.people.rel_sus[self.elderly] = self.rel_sus`). It can also perform calculations and store data in itself, as shown here with `self.exposed` (although in general, analyzers are better for this, since they happen at the end of the timestep, while interventions happen in the middle).\n", + "- `plot()` is a custom method that shows a plot of the data gathered during the sim. Again, it's usually better to use analyzers for this, but for something simple like this it's fine to double-dip and use an intervention.\n", "\n", "Here is what this custom intervention looks like in action. Note how it automatically shows when the intervention starts and stops (with vertical dashed lines)." ] From 68056e2a431d4e9e352c74c642efb5c31134ed14 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 01:13:02 -0700 Subject: [PATCH 04/39] rename files --- docs/tutorials/{t1.ipynb => t01.ipynb} | 0 docs/tutorials/{t2.ipynb => t02.ipynb} | 0 docs/tutorials/{t3.ipynb => t03.ipynb} | 0 docs/tutorials/{t4.ipynb => t04.ipynb} | 0 docs/tutorials/{t5.ipynb => t05.ipynb} | 0 docs/tutorials/{t6.ipynb => t06.ipynb} | 0 docs/tutorials/{t7.ipynb => t07.ipynb} | 0 docs/tutorials/{t8.ipynb => t08.ipynb} | 0 docs/tutorials/{t9.ipynb => t09.ipynb} | 0 .../{t1_full_usage_example.py => t01_full_usage_example.py} | 0 examples/{t1_hello_world.py => t01_hello_world.py} | 0 examples/{t1_populations.py => t01_populations.py} | 0 examples/{t2_save_load_sim.py => t02_save_load_sim.py} | 0 examples/{t3_nested_multisim.py => t03_nested_multisim.py} | 0 examples/{t3_scenarios.py => t03_scenarios.py} | 0 examples/{t4_loading_data.py => t04_loading_data.py} | 0 examples/{t5_change_beta.py => t05_change_beta.py} | 0 examples/{t5_contact_tracing.py => t05_contact_tracing.py} | 0 .../{t5_custom_intervention.py => t05_custom_intervention.py} | 0 examples/{t5_dynamic_pars.py => t05_dynamic_pars.py} | 0 examples/{t5_testing.py => t05_testing.py} | 0 .../{t5_vaccine_subtargeting.py => t05_vaccine_subtargeting.py} | 0 examples/{t6_seir_analyzer.py => t06_seir_analyzer.py} | 0 examples/{t6_simple_analyzers.py => t06_simple_analyzers.py} | 0 .../{t7_optuna_calibration.py => t07_optuna_calibration.py} | 0 examples/{t8_versioning.py => t08_versioning.py} | 0 examples/{t9_custom_layers.py => t09_custom_layers.py} | 0 examples/{t9_numba.py => t09_numba.py} | 0 ...t9_population_properties.py => t09_population_properties.py} | 0 examples/test_tutorials.py | 2 +- 30 files changed, 1 insertion(+), 1 deletion(-) rename docs/tutorials/{t1.ipynb => t01.ipynb} (100%) rename docs/tutorials/{t2.ipynb => t02.ipynb} (100%) rename docs/tutorials/{t3.ipynb => t03.ipynb} (100%) rename docs/tutorials/{t4.ipynb => t04.ipynb} (100%) rename docs/tutorials/{t5.ipynb => t05.ipynb} (100%) rename docs/tutorials/{t6.ipynb => t06.ipynb} (100%) rename docs/tutorials/{t7.ipynb => t07.ipynb} (100%) rename docs/tutorials/{t8.ipynb => t08.ipynb} (100%) rename docs/tutorials/{t9.ipynb => t09.ipynb} (100%) rename examples/{t1_full_usage_example.py => t01_full_usage_example.py} (100%) rename examples/{t1_hello_world.py => t01_hello_world.py} (100%) rename examples/{t1_populations.py => t01_populations.py} (100%) rename examples/{t2_save_load_sim.py => t02_save_load_sim.py} (100%) rename examples/{t3_nested_multisim.py => t03_nested_multisim.py} (100%) rename examples/{t3_scenarios.py => t03_scenarios.py} (100%) rename examples/{t4_loading_data.py => t04_loading_data.py} (100%) rename examples/{t5_change_beta.py => t05_change_beta.py} (100%) rename examples/{t5_contact_tracing.py => t05_contact_tracing.py} (100%) rename examples/{t5_custom_intervention.py => t05_custom_intervention.py} (100%) rename examples/{t5_dynamic_pars.py => t05_dynamic_pars.py} (100%) rename examples/{t5_testing.py => t05_testing.py} (100%) rename examples/{t5_vaccine_subtargeting.py => t05_vaccine_subtargeting.py} (100%) rename examples/{t6_seir_analyzer.py => t06_seir_analyzer.py} (100%) rename examples/{t6_simple_analyzers.py => t06_simple_analyzers.py} (100%) rename examples/{t7_optuna_calibration.py => t07_optuna_calibration.py} (100%) rename examples/{t8_versioning.py => t08_versioning.py} (100%) rename examples/{t9_custom_layers.py => t09_custom_layers.py} (100%) rename examples/{t9_numba.py => t09_numba.py} (100%) rename examples/{t9_population_properties.py => t09_population_properties.py} (100%) diff --git a/docs/tutorials/t1.ipynb b/docs/tutorials/t01.ipynb similarity index 100% rename from docs/tutorials/t1.ipynb rename to docs/tutorials/t01.ipynb diff --git a/docs/tutorials/t2.ipynb b/docs/tutorials/t02.ipynb similarity index 100% rename from docs/tutorials/t2.ipynb rename to docs/tutorials/t02.ipynb diff --git a/docs/tutorials/t3.ipynb b/docs/tutorials/t03.ipynb similarity index 100% rename from docs/tutorials/t3.ipynb rename to docs/tutorials/t03.ipynb diff --git a/docs/tutorials/t4.ipynb b/docs/tutorials/t04.ipynb similarity index 100% rename from docs/tutorials/t4.ipynb rename to docs/tutorials/t04.ipynb diff --git a/docs/tutorials/t5.ipynb b/docs/tutorials/t05.ipynb similarity index 100% rename from docs/tutorials/t5.ipynb rename to docs/tutorials/t05.ipynb diff --git a/docs/tutorials/t6.ipynb b/docs/tutorials/t06.ipynb similarity index 100% rename from docs/tutorials/t6.ipynb rename to docs/tutorials/t06.ipynb diff --git a/docs/tutorials/t7.ipynb b/docs/tutorials/t07.ipynb similarity index 100% rename from docs/tutorials/t7.ipynb rename to docs/tutorials/t07.ipynb diff --git a/docs/tutorials/t8.ipynb b/docs/tutorials/t08.ipynb similarity index 100% rename from docs/tutorials/t8.ipynb rename to docs/tutorials/t08.ipynb diff --git a/docs/tutorials/t9.ipynb b/docs/tutorials/t09.ipynb similarity index 100% rename from docs/tutorials/t9.ipynb rename to docs/tutorials/t09.ipynb diff --git a/examples/t1_full_usage_example.py b/examples/t01_full_usage_example.py similarity index 100% rename from examples/t1_full_usage_example.py rename to examples/t01_full_usage_example.py diff --git a/examples/t1_hello_world.py b/examples/t01_hello_world.py similarity index 100% rename from examples/t1_hello_world.py rename to examples/t01_hello_world.py diff --git a/examples/t1_populations.py b/examples/t01_populations.py similarity index 100% rename from examples/t1_populations.py rename to examples/t01_populations.py diff --git a/examples/t2_save_load_sim.py b/examples/t02_save_load_sim.py similarity index 100% rename from examples/t2_save_load_sim.py rename to examples/t02_save_load_sim.py diff --git a/examples/t3_nested_multisim.py b/examples/t03_nested_multisim.py similarity index 100% rename from examples/t3_nested_multisim.py rename to examples/t03_nested_multisim.py diff --git a/examples/t3_scenarios.py b/examples/t03_scenarios.py similarity index 100% rename from examples/t3_scenarios.py rename to examples/t03_scenarios.py diff --git a/examples/t4_loading_data.py b/examples/t04_loading_data.py similarity index 100% rename from examples/t4_loading_data.py rename to examples/t04_loading_data.py diff --git a/examples/t5_change_beta.py b/examples/t05_change_beta.py similarity index 100% rename from examples/t5_change_beta.py rename to examples/t05_change_beta.py diff --git a/examples/t5_contact_tracing.py b/examples/t05_contact_tracing.py similarity index 100% rename from examples/t5_contact_tracing.py rename to examples/t05_contact_tracing.py diff --git a/examples/t5_custom_intervention.py b/examples/t05_custom_intervention.py similarity index 100% rename from examples/t5_custom_intervention.py rename to examples/t05_custom_intervention.py diff --git a/examples/t5_dynamic_pars.py b/examples/t05_dynamic_pars.py similarity index 100% rename from examples/t5_dynamic_pars.py rename to examples/t05_dynamic_pars.py diff --git a/examples/t5_testing.py b/examples/t05_testing.py similarity index 100% rename from examples/t5_testing.py rename to examples/t05_testing.py diff --git a/examples/t5_vaccine_subtargeting.py b/examples/t05_vaccine_subtargeting.py similarity index 100% rename from examples/t5_vaccine_subtargeting.py rename to examples/t05_vaccine_subtargeting.py diff --git a/examples/t6_seir_analyzer.py b/examples/t06_seir_analyzer.py similarity index 100% rename from examples/t6_seir_analyzer.py rename to examples/t06_seir_analyzer.py diff --git a/examples/t6_simple_analyzers.py b/examples/t06_simple_analyzers.py similarity index 100% rename from examples/t6_simple_analyzers.py rename to examples/t06_simple_analyzers.py diff --git a/examples/t7_optuna_calibration.py b/examples/t07_optuna_calibration.py similarity index 100% rename from examples/t7_optuna_calibration.py rename to examples/t07_optuna_calibration.py diff --git a/examples/t8_versioning.py b/examples/t08_versioning.py similarity index 100% rename from examples/t8_versioning.py rename to examples/t08_versioning.py diff --git a/examples/t9_custom_layers.py b/examples/t09_custom_layers.py similarity index 100% rename from examples/t9_custom_layers.py rename to examples/t09_custom_layers.py diff --git a/examples/t9_numba.py b/examples/t09_numba.py similarity index 100% rename from examples/t9_numba.py rename to examples/t09_numba.py diff --git a/examples/t9_population_properties.py b/examples/t09_population_properties.py similarity index 100% rename from examples/t9_population_properties.py rename to examples/t09_population_properties.py diff --git a/examples/test_tutorials.py b/examples/test_tutorials.py index 6cab5f7cf..250afdf53 100755 --- a/examples/test_tutorials.py +++ b/examples/test_tutorials.py @@ -13,7 +13,7 @@ def test_all_tutorials(): # Get and run tests filenames = sc.getfilelist(tex.examples_dir, pattern='t*.py', nopath=True) for filename in filenames: - if filename[1] in '0123456789': # Should have format e.g. t5_foo.py, not test_foo.py + if filename[1] in '0123456789': # Should have format e.g. t05_foo.py, not test_foo.py sc.heading(f'Running {filename}...') try: tex.run_example(filename) From 39470fdf0129eac9f6d622bdaf7aed140713761e Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 01:14:50 -0700 Subject: [PATCH 05/39] more renaming --- docs/tutorials/t08.ipynb | 271 ------------------ docs/tutorials/t09.ipynb | 249 ++++++++++------ docs/tutorials/t10.ipynb | 188 ++++++++++++ .../{t08_versioning.py => t09_versioning.py} | 0 ..._custom_layers.py => t10_custom_layers.py} | 0 examples/{t09_numba.py => t10_numba.py} | 0 ...erties.py => t10_population_properties.py} | 0 7 files changed, 354 insertions(+), 354 deletions(-) delete mode 100644 docs/tutorials/t08.ipynb create mode 100644 docs/tutorials/t10.ipynb rename examples/{t08_versioning.py => t09_versioning.py} (100%) rename examples/{t09_custom_layers.py => t10_custom_layers.py} (100%) rename examples/{t09_numba.py => t10_numba.py} (100%) rename examples/{t09_population_properties.py => t10_population_properties.py} (100%) diff --git a/docs/tutorials/t08.ipynb b/docs/tutorials/t08.ipynb deleted file mode 100644 index 84a82600f..000000000 --- a/docs/tutorials/t08.ipynb +++ /dev/null @@ -1,271 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# T8 - Tips and tricks\n", - "\n", - "This tutorial contains suggestions that aren't essential to follow, but which may make your life easier.\n", - "\n", - "## Versioning\n", - "\n", - "Covasim contains a number of built-in tools to make it easier to keep track of where results came from. The simplest of these is that if you save an image using `cv.savefig()` instead of `pl.savefig()`, it will automatically store information about the script and Covasim version that generated it:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import covasim as cv\n", - "cv.options.set(dpi=100, show=False, close=True, verbose=0) # Standard options for Jupyter notebook\n", - "\n", - "sim = cv.Sim()\n", - "sim.run()\n", - "sim.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "filename = 'my-figure.png'\n", - "cv.savefig(filename) # Save including version information\n", - "cv.get_png_metadata(filename) # Retrieve and print information" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This can be extremely useful for figuring out where that intriguing result you generated 3 weeks ago came from!\n", - "\n", - "This information is also stored in sims and multisims themselves:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(sim.version)\n", - "print(sim.git_info)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, the function `cv.check_version()` and `cv.check_save_version()` are useful if you want to ensure that users are running the right version of your code. Placing `cv.check_save_version('2.0.0')` will save a file with the information above to the current folder – again, useful for debugging exactly what changed and when. (You can also provide additional information to it, e.g. to also save the versions of 3rd-party packages you're importing). `cv.check_version()` by itself can be used to provide a warning or even raise an exception (if `die=True`) if the version is not what's expected:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "cv.check_version('1.5.0')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Working with dates\n", - "\n", - "Dates can be tricky to work with. Covasim comes with a number of built-in features to work with dates. By default, by convention Covasim works with dates in the format `YYYY-MM-DD`, e.g. `'2020-12-01'`. However, it can handle a wide variety of other date and `datetime` objects. In particular, `sim` objects know when they start and end, and can use this to do quite a bit of date math:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sim = cv.Sim(start_day='20201122', end_day='2020-12-09 02:14:58.727703')\n", - "sim.initialize() # Date conversion happens on initialization\n", - "print(sim['start_day'])\n", - "print(sim['end_day'])\n", - "print(sim.day(sim['end_day'])) # Prints the number of days until the end day, i.e. the length of the sim" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can also easily calculate the difference between two dates, or generate a range of dates. These are returned as strings by default, but can be converted to datetime objects via Sciris:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import sciris as sc\n", - "\n", - "print(cv.daydiff('2020-06-01', '2020-07-01', '2020-08-01'))\n", - "dates = cv.date_range('2020-04-04', '2020-04-12')\n", - "print(dates)\n", - "print(sc.readdate(dates))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, one gotcha is that when loading Excel spreadsheets in pandas, dates are loaded in pandas' internal `Timestamp[ns64]` format, which nothing else seems to be able to read. If this happens to you, the solution (as far as Covasim is concerned) is to convert to a `datetime.date`:\n", - "\n", - "```python\n", - "data = pd.read_excel(filename)\n", - "data['date'] = data['date'].dt.date\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Working with dictionaries\n", - "\n", - "\"I already know how to work with dictionaries\", you say. Yes, you do. But there are a couple tricks that might make things easier.\n", - "\n", - "Covasim is built on Sciris, which includes containers `odict` and `objdict`. While these are [documented elsewhere](https://sciris.readthedocs.io/en/latest/_autosummary/sciris.sc_odict.odict.html#sciris.sc_odict.odict), a couple examples will serve to illustrate how they work.\n", - "\n", - "An `odict` is just an ordered dict that you can refer to by *position* as well as by key. For example:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "mydict = sc.odict(foo=[1,2,3], bar=[4,5,6]) # Assignment is the same as ordinary dictionaries\n", - "print('Entry foo:', mydict['foo'])\n", - "print('Entry 0:', mydict[0]) # Access by key or by index\n", - "for i,key,value in mydict.enumitems(): # Additional methods for iteration\n", - " print(f'Item {i} is named {key} and has value {value}')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "An `objdict` is exactly the same as an odict except it lets you reference keys as if they were attributes:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "myobjdict = sc.objdict(foo=[1,2,3], bar=[4,5,6])\n", - "print('Entry foo:', myobjdict['foo'])\n", - "print('Entry 0:', myobjdict[0]) # Access by key or by index\n", - "print('\"Attribute\" foo:', myobjdict.foo)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Using this approach, you can get all the power and flexibility of dictionaries, while writing code as succinctly as possible. For example:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "total_pop = 44_483 # This many total people\n", - "\n", - "pars= sc.objdict(\n", - " pop_type = 'hybrid',\n", - " pop_size = 10e3,\n", - ")\n", - "pars.pop_scale = total_pop/pars.pop_size # Instead of pars['pop_scale'] = total_pop/pars['pop_size'] \n", - "sim = cv.Sim(**pars) # It's still a dict, so you can treat it as one!" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, Sciris also contains a function called `mergedicts`. This acts very similarly to `dict.update()`, with the main difference being that it returns the result of merging the two dictionaries. This is especially useful for handling keyword arguments in functions:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def myfunc(args=None, **kwargs):\n", - " defaults = dict(foo=[1,2,3], bar=[4,5,6])\n", - " merged_args = sc.mergedicts(defaults, args, kwargs)\n", - " print(merged_args)\n", - "\n", - "myfunc(args=dict(bar=18), other_args='can be anything')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As you can see, it merged the default settings, the arguments supplied to the function via the keyword `args`, and then other keywords, into a single dictionary." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.5" - }, - "pycharm": { - "stem_cell": { - "cell_type": "raw", - "metadata": { - "collapsed": false - }, - "source": [] - } - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": true, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/tutorials/t09.ipynb b/docs/tutorials/t09.ipynb index fd81e247c..84a82600f 100644 --- a/docs/tutorials/t09.ipynb +++ b/docs/tutorials/t09.ipynb @@ -4,17 +4,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# T9 - Advanced features\n", + "# T8 - Tips and tricks\n", "\n", - "This tutorial covers advanced features of Covasim, including custom population options and changing the internal computational methods.\n", + "This tutorial contains suggestions that aren't essential to follow, but which may make your life easier.\n", "\n", - "## Defining populations with SynthPops\n", + "## Versioning\n", "\n", - "For complex populations, we suggest using [SynthPops](http://synthpops.org), a Python library designed specifically for this purpose. In contrast the population methods built-in to Covasim, SynthPops uses data to produce synthetic populations that are statistically indistinguishable from real ones. For a relatively complex example of how SynthPops was used to create a complex school network for the Seattle region, see [here](https://github.com/institutefordiseasemodeling/testing-the-waters/blob/main/covasim_schools/school_pop.py).\n", - "\n", - "## Defining contact layers\n", - "\n", - "As mentioned in Tutorial 1, contact layers are the graph connecting the people in the simulation. Each person is a node, and each contact is an edge. While enormous complexity can be used to define realistic contact networks, a reasonable approximation in many cases is random connectivity, often with some age assortativity. Here is an example for generating a new contact layer, nominally representing public transportation, and adding it to a simulation:" + "Covasim contains a number of built-in tools to make it easier to keep track of where results came from. The simplest of these is that if you save an image using `cv.savefig()` instead of `pl.savefig()`, it will automatically store information about the script and Covasim version that generated it:" ] }, { @@ -23,45 +19,87 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "import covasim as cv\n", "cv.options.set(dpi=100, show=False, close=True, verbose=0) # Standard options for Jupyter notebook\n", "\n", - "# Create the first sim\n", - "orig_sim = cv.Sim(pop_type='hybrid', n_days=120, label='Default hybrid population')\n", - "orig_sim.initialize() # Initialize the population\n", - "\n", - "# Create the second sim\n", - "sim = orig_sim.copy()\n", - "\n", - "# Define the new layer, 'transport'\n", - "n_people = len(sim.people)\n", - "n_contacts_per_person = 0.5\n", - "n_contacts = int(n_contacts_per_person*n_people)\n", - "contacts_p1 = cv.choose(max_n=n_people, n=n_contacts)\n", - "contacts_p2 = cv.choose(max_n=n_people, n=n_contacts)\n", - "beta = np.ones(n_contacts)\n", - "layer = cv.Layer(p1=contacts_p1, p2=contacts_p2, beta=beta) # Create the new layer\n", - "\n", - "# Add this layer in and re-initialize the sim\n", - "sim.people.contacts.add_layer(transport=layer)\n", - "sim.reset_layer_pars() # Automatically add layer 'q' to the parameters using default values\n", - "sim.initialize() # Reinitialize\n", - "sim.label = f'Transport layer with {n_contacts_per_person} contacts/person'\n", + "sim = cv.Sim()\n", + "sim.run()\n", + "sim.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "filename = 'my-figure.png'\n", + "cv.savefig(filename) # Save including version information\n", + "cv.get_png_metadata(filename) # Retrieve and print information" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can be extremely useful for figuring out where that intriguing result you generated 3 weeks ago came from!\n", "\n", - "# Run and compare\n", - "msim = cv.MultiSim([orig_sim, sim])\n", - "msim.run()\n", - "msim.plot()" + "This information is also stored in sims and multisims themselves:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(sim.version)\n", + "print(sim.git_info)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, the function `cv.check_version()` and `cv.check_save_version()` are useful if you want to ensure that users are running the right version of your code. Placing `cv.check_save_version('2.0.0')` will save a file with the information above to the current folder – again, useful for debugging exactly what changed and when. (You can also provide additional information to it, e.g. to also save the versions of 3rd-party packages you're importing). `cv.check_version()` by itself can be used to provide a warning or even raise an exception (if `die=True`) if the version is not what's expected:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cv.check_version('1.5.0')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Defining custom population properties\n", + "## Working with dates\n", "\n", - "Another useful feature is adding additional features to people, for use in subtargeting. For example, this example shows how to define a subpopulation with higher baseline mortality rates. This is a simple example illustrating how you would identify and target people based on whether or not the have a prime-number index, based on the protecting the elderly example from Tutorial 1." + "Dates can be tricky to work with. Covasim comes with a number of built-in features to work with dates. By default, by convention Covasim works with dates in the format `YYYY-MM-DD`, e.g. `'2020-12-01'`. However, it can handle a wide variety of other date and `datetime` objects. In particular, `sim` objects know when they start and end, and can use this to do quite a bit of date math:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sim = cv.Sim(start_day='20201122', end_day='2020-12-09 02:14:58.727703')\n", + "sim.initialize() # Date conversion happens on initialization\n", + "print(sim['start_day'])\n", + "print(sim['end_day'])\n", + "print(sim.day(sim['end_day'])) # Prints the number of days until the end day, i.e. the length of the sim" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also easily calculate the difference between two dates, or generate a range of dates. These are returned as strings by default, but can be converted to datetime objects via Sciris:" ] }, { @@ -70,43 +108,57 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "import sciris as sc\n", - "import covasim as cv\n", "\n", - "def protect_the_prime(sim):\n", - " if sim.t == sim.day('2020-04-01'):\n", - " are_prime = sim.people.prime\n", - " sim.people.rel_sus[are_prime] = 0.0\n", + "print(cv.daydiff('2020-06-01', '2020-07-01', '2020-08-01'))\n", + "dates = cv.date_range('2020-04-04', '2020-04-12')\n", + "print(dates)\n", + "print(sc.readdate(dates))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, one gotcha is that when loading Excel spreadsheets in pandas, dates are loaded in pandas' internal `Timestamp[ns64]` format, which nothing else seems to be able to read. If this happens to you, the solution (as far as Covasim is concerned) is to convert to a `datetime.date`:\n", "\n", - "pars = dict(\n", - " pop_type = 'hybrid',\n", - " pop_infected = 100,\n", - " n_days = 90,\n", - " verbose = 0,\n", - ")\n", + "```python\n", + "data = pd.read_excel(filename)\n", + "data['date'] = data['date'].dt.date\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Working with dictionaries\n", "\n", - "# Default simulation\n", - "orig_sim = cv.Sim(pars, label='Default')\n", + "\"I already know how to work with dictionaries\", you say. Yes, you do. But there are a couple tricks that might make things easier.\n", "\n", - "# Create the simulation\n", - "sim = cv.Sim(pars, label='Protect the prime', interventions=protect_the_prime)\n", - "sim.initialize() # Initialize to create the people array\n", - "sim.people.prime = np.array([sc.isprime(i) for i in range(len(sim.people))]) # Define whom to target\n", + "Covasim is built on Sciris, which includes containers `odict` and `objdict`. While these are [documented elsewhere](https://sciris.readthedocs.io/en/latest/_autosummary/sciris.sc_odict.odict.html#sciris.sc_odict.odict), a couple examples will serve to illustrate how they work.\n", "\n", - "# Run and plot\n", - "msim = cv.MultiSim([orig_sim, sim])\n", - "msim.run()\n", - "msim.plot()" + "An `odict` is just an ordered dict that you can refer to by *position* as well as by key. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mydict = sc.odict(foo=[1,2,3], bar=[4,5,6]) # Assignment is the same as ordinary dictionaries\n", + "print('Entry foo:', mydict['foo'])\n", + "print('Entry 0:', mydict[0]) # Access by key or by index\n", + "for i,key,value in mydict.enumitems(): # Additional methods for iteration\n", + " print(f'Item {i} is named {key} and has value {value}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Changing Numba options\n", - "\n", - "Finally, this example shows how you can change the default Numba calculation options. It's not recommended – especially running with multithreading, which is faster but gives stochastically unreproducible results – but it's there if you want it." + "An `objdict` is exactly the same as an odict except it lets you reference keys as if they were attributes:" ] }, { @@ -115,30 +167,61 @@ "metadata": {}, "outputs": [], "source": [ - "import covasim as cv\n", - "\n", - "# Create a standard 32-bit simulation\n", - "sim32 = cv.Sim(label='32-bit, single-threaded (default)', verbose='brief')\n", - "sim32.run()\n", - "\n", - "# Use 64-bit instead of 32\n", - "cv.options.set(precision=64)\n", - "sim64 = cv.Sim(label='64-bit, single-threaded', verbose='brief')\n", - "sim64.run()\n", - "\n", - "# Use parallel threading\n", - "cv.options.set(numba_parallel=True)\n", - "sim_par = cv.Sim(label='64-bit, multi-threaded', verbose='brief')\n", - "sim_par.run()\n", + "myobjdict = sc.objdict(foo=[1,2,3], bar=[4,5,6])\n", + "print('Entry foo:', myobjdict['foo'])\n", + "print('Entry 0:', myobjdict[0]) # Access by key or by index\n", + "print('\"Attribute\" foo:', myobjdict.foo)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using this approach, you can get all the power and flexibility of dictionaries, while writing code as succinctly as possible. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "total_pop = 44_483 # This many total people\n", "\n", - "# Reset to defaults\n", - "cv.options.set('defaults')\n", - "sim32b = cv.Sim(label='32-bit, single-threaded (restored)', verbose='brief')\n", - "sim32b.run()\n", + "pars= sc.objdict(\n", + " pop_type = 'hybrid',\n", + " pop_size = 10e3,\n", + ")\n", + "pars.pop_scale = total_pop/pars.pop_size # Instead of pars['pop_scale'] = total_pop/pars['pop_size'] \n", + "sim = cv.Sim(**pars) # It's still a dict, so you can treat it as one!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, Sciris also contains a function called `mergedicts`. This acts very similarly to `dict.update()`, with the main difference being that it returns the result of merging the two dictionaries. This is especially useful for handling keyword arguments in functions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def myfunc(args=None, **kwargs):\n", + " defaults = dict(foo=[1,2,3], bar=[4,5,6])\n", + " merged_args = sc.mergedicts(defaults, args, kwargs)\n", + " print(merged_args)\n", "\n", - "# Plot\n", - "msim = cv.MultiSim([sim32, sim64, sim_par, sim32b])\n", - "msim.plot()" + "myfunc(args=dict(bar=18), other_args='can be anything')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, it merged the default settings, the arguments supplied to the function via the keyword `args`, and then other keywords, into a single dictionary." ] } ], diff --git a/docs/tutorials/t10.ipynb b/docs/tutorials/t10.ipynb new file mode 100644 index 000000000..fd81e247c --- /dev/null +++ b/docs/tutorials/t10.ipynb @@ -0,0 +1,188 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# T9 - Advanced features\n", + "\n", + "This tutorial covers advanced features of Covasim, including custom population options and changing the internal computational methods.\n", + "\n", + "## Defining populations with SynthPops\n", + "\n", + "For complex populations, we suggest using [SynthPops](http://synthpops.org), a Python library designed specifically for this purpose. In contrast the population methods built-in to Covasim, SynthPops uses data to produce synthetic populations that are statistically indistinguishable from real ones. For a relatively complex example of how SynthPops was used to create a complex school network for the Seattle region, see [here](https://github.com/institutefordiseasemodeling/testing-the-waters/blob/main/covasim_schools/school_pop.py).\n", + "\n", + "## Defining contact layers\n", + "\n", + "As mentioned in Tutorial 1, contact layers are the graph connecting the people in the simulation. Each person is a node, and each contact is an edge. While enormous complexity can be used to define realistic contact networks, a reasonable approximation in many cases is random connectivity, often with some age assortativity. Here is an example for generating a new contact layer, nominally representing public transportation, and adding it to a simulation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import covasim as cv\n", + "cv.options.set(dpi=100, show=False, close=True, verbose=0) # Standard options for Jupyter notebook\n", + "\n", + "# Create the first sim\n", + "orig_sim = cv.Sim(pop_type='hybrid', n_days=120, label='Default hybrid population')\n", + "orig_sim.initialize() # Initialize the population\n", + "\n", + "# Create the second sim\n", + "sim = orig_sim.copy()\n", + "\n", + "# Define the new layer, 'transport'\n", + "n_people = len(sim.people)\n", + "n_contacts_per_person = 0.5\n", + "n_contacts = int(n_contacts_per_person*n_people)\n", + "contacts_p1 = cv.choose(max_n=n_people, n=n_contacts)\n", + "contacts_p2 = cv.choose(max_n=n_people, n=n_contacts)\n", + "beta = np.ones(n_contacts)\n", + "layer = cv.Layer(p1=contacts_p1, p2=contacts_p2, beta=beta) # Create the new layer\n", + "\n", + "# Add this layer in and re-initialize the sim\n", + "sim.people.contacts.add_layer(transport=layer)\n", + "sim.reset_layer_pars() # Automatically add layer 'q' to the parameters using default values\n", + "sim.initialize() # Reinitialize\n", + "sim.label = f'Transport layer with {n_contacts_per_person} contacts/person'\n", + "\n", + "# Run and compare\n", + "msim = cv.MultiSim([orig_sim, sim])\n", + "msim.run()\n", + "msim.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining custom population properties\n", + "\n", + "Another useful feature is adding additional features to people, for use in subtargeting. For example, this example shows how to define a subpopulation with higher baseline mortality rates. This is a simple example illustrating how you would identify and target people based on whether or not the have a prime-number index, based on the protecting the elderly example from Tutorial 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import sciris as sc\n", + "import covasim as cv\n", + "\n", + "def protect_the_prime(sim):\n", + " if sim.t == sim.day('2020-04-01'):\n", + " are_prime = sim.people.prime\n", + " sim.people.rel_sus[are_prime] = 0.0\n", + "\n", + "pars = dict(\n", + " pop_type = 'hybrid',\n", + " pop_infected = 100,\n", + " n_days = 90,\n", + " verbose = 0,\n", + ")\n", + "\n", + "# Default simulation\n", + "orig_sim = cv.Sim(pars, label='Default')\n", + "\n", + "# Create the simulation\n", + "sim = cv.Sim(pars, label='Protect the prime', interventions=protect_the_prime)\n", + "sim.initialize() # Initialize to create the people array\n", + "sim.people.prime = np.array([sc.isprime(i) for i in range(len(sim.people))]) # Define whom to target\n", + "\n", + "# Run and plot\n", + "msim = cv.MultiSim([orig_sim, sim])\n", + "msim.run()\n", + "msim.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Changing Numba options\n", + "\n", + "Finally, this example shows how you can change the default Numba calculation options. It's not recommended – especially running with multithreading, which is faster but gives stochastically unreproducible results – but it's there if you want it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import covasim as cv\n", + "\n", + "# Create a standard 32-bit simulation\n", + "sim32 = cv.Sim(label='32-bit, single-threaded (default)', verbose='brief')\n", + "sim32.run()\n", + "\n", + "# Use 64-bit instead of 32\n", + "cv.options.set(precision=64)\n", + "sim64 = cv.Sim(label='64-bit, single-threaded', verbose='brief')\n", + "sim64.run()\n", + "\n", + "# Use parallel threading\n", + "cv.options.set(numba_parallel=True)\n", + "sim_par = cv.Sim(label='64-bit, multi-threaded', verbose='brief')\n", + "sim_par.run()\n", + "\n", + "# Reset to defaults\n", + "cv.options.set('defaults')\n", + "sim32b = cv.Sim(label='32-bit, single-threaded (restored)', verbose='brief')\n", + "sim32b.run()\n", + "\n", + "# Plot\n", + "msim = cv.MultiSim([sim32, sim64, sim_par, sim32b])\n", + "msim.plot()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "metadata": { + "collapsed": false + }, + "source": [] + } + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/t08_versioning.py b/examples/t09_versioning.py similarity index 100% rename from examples/t08_versioning.py rename to examples/t09_versioning.py diff --git a/examples/t09_custom_layers.py b/examples/t10_custom_layers.py similarity index 100% rename from examples/t09_custom_layers.py rename to examples/t10_custom_layers.py diff --git a/examples/t09_numba.py b/examples/t10_numba.py similarity index 100% rename from examples/t09_numba.py rename to examples/t10_numba.py diff --git a/examples/t09_population_properties.py b/examples/t10_population_properties.py similarity index 100% rename from examples/t09_population_properties.py rename to examples/t10_population_properties.py From 91b4181bd0bff35a058966fdb67f252ae416e0c0 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 01:54:39 -0700 Subject: [PATCH 06/39] update plotting styles and tutorials --- covasim/interventions.py | 2 +- docs/tutorials.rst | 19 ++++++++++--------- docs/tutorials/t02.ipynb | 2 +- docs/tutorials/t05.ipynb | 8 +++++--- docs/tutorials/t07.ipynb | 2 +- docs/tutorials/t09.ipynb | 6 ++++-- docs/tutorials/t10.ipynb | 4 ++-- 7 files changed, 24 insertions(+), 19 deletions(-) diff --git a/covasim/interventions.py b/covasim/interventions.py index b95cf7c6d..9bc9ead7b 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -95,7 +95,7 @@ def __init__(self, label=None, show_label=True, do_plot=None, line_args=None): self.label = label # e.g. "Close schools" self.show_label = show_label # Show the label by default self.do_plot = do_plot if do_plot is not None else True # Plot the intervention, including if None - self.line_args = sc.mergedicts(dict(linestyle='--', c=[0,0,0]), line_args) # Do not set alpha by default due to the issue of overlapping interventions + self.line_args = sc.mergedicts(dict(linestyle='--', c='#aaa', lw=1.0), line_args) # Do not set alpha by default due to the issue of overlapping interventions self.days = [] # The start and end days of the intervention self.initialized = False # Whether or not it has been initialized return diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 28d768629..84b951b2f 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -3,12 +3,13 @@ .. toctree:: :maxdepth: 1 - tutorials/t1 - tutorials/t2 - tutorials/t3 - tutorials/t4 - tutorials/t5 - tutorials/t6 - tutorials/t7 - tutorials/t8 - tutorials/t9 + tutorials/t01 + tutorials/t02 + tutorials/t03 + tutorials/t04 + tutorials/t05 + tutorials/t06 + tutorials/t07 + tutorials/t08 + tutorials/t09 + tutorials/t10 diff --git a/docs/tutorials/t02.ipynb b/docs/tutorials/t02.ipynb index a6cb8d8ea..eb55882b7 100644 --- a/docs/tutorials/t02.ipynb +++ b/docs/tutorials/t02.ipynb @@ -105,7 +105,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "However, as you can see, this isn't ideal since the default formatting is...not great. (Also, note that each result is a `Result` object, not a simple Numpy array; like a pandas dataframe, you can get the array of values directly via e.g. `sim.results['new_infections'].values`.)\n", + "However, as you can see, this isn't ideal since the default formatting is...not great. (Also, note that each result is a `Result` object, not a simple Numpy array; like a pandas dataframe, you can get the array of values directly via e.g. `sim.results.new_infections.values`.)\n", "\n", "An alternative, if you only want to plot a single result, such as new infections, is to use the `plot_result()` method:" ] diff --git a/docs/tutorials/t05.ipynb b/docs/tutorials/t05.ipynb index 48691454d..beb97361c 100644 --- a/docs/tutorials/t05.ipynb +++ b/docs/tutorials/t05.ipynb @@ -158,8 +158,8 @@ "import covasim as cv\n", "\n", "# Define the testing and contact tracing interventions\n", - "tp = cv.test_prob(symp_prob=0.2, asymp_prob=0.001, symp_quar_prob=1.0, asymp_quar_prob=1.0)\n", - "ct = cv.contact_tracing(trace_probs=dict(h=1.0, s=0.5, w=0.5, c=0.3))\n", + "tp = cv.test_prob(symp_prob=0.2, asymp_prob=0.001, symp_quar_prob=1.0, asymp_quar_prob=1.0, do_plot=False)\n", + "ct = cv.contact_tracing(trace_probs=dict(h=1.0, s=0.5, w=0.5, c=0.3), do_plot=False)\n", "\n", "# Define the default parameters\n", "pars = dict(\n", @@ -185,7 +185,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Since it's assumed that known contacts are placed into quarantine (with efficacy `sim['quar_factor']`), the number of contacts who are successfully traced each day is equal to the number of people newly quarantined (bottom left panel). As is commonly seen using testing and tracing as the only means of epidemic control, these programs stop the epidemic from growing exponentially, but do not bring it to zero." + "Since it's assumed that known contacts are placed into quarantine (with efficacy `sim['quar_factor']`), the number of contacts who are successfully traced each day is equal to the number of people newly quarantined (bottom left panel). As is commonly seen using testing and tracing as the only means of epidemic control, these programs stop the epidemic from growing exponentially, but do not bring it to zero.\n", + "\n", + "Since these interventions happen at `t=0`, it's not very useful to plot them. Note that we have turned off plotting by passing `do_plot=False` to each intervention." ] }, { diff --git a/docs/tutorials/t07.ipynb b/docs/tutorials/t07.ipynb index 4cfb8d60c..1f1ea4689 100644 --- a/docs/tutorials/t07.ipynb +++ b/docs/tutorials/t07.ipynb @@ -273,7 +273,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" }, "pycharm": { "stem_cell": { diff --git a/docs/tutorials/t09.ipynb b/docs/tutorials/t09.ipynb index 84a82600f..b1df9199c 100644 --- a/docs/tutorials/t09.ipynb +++ b/docs/tutorials/t09.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# T8 - Tips and tricks\n", + "# T9 - Tips and tricks\n", "\n", "This tutorial contains suggestions that aren't essential to follow, but which may make your life easier.\n", "\n", @@ -200,6 +200,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "For example, the `results` object is an `objdict`. This means that although you can use e.g. `sim.results['new_infections']`, you can also use `sim.results.new_infections`.\n", + "\n", "Finally, Sciris also contains a function called `mergedicts`. This acts very similarly to `dict.update()`, with the main difference being that it returns the result of merging the two dictionaries. This is especially useful for handling keyword arguments in functions:" ] }, @@ -241,7 +243,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" }, "pycharm": { "stem_cell": { diff --git a/docs/tutorials/t10.ipynb b/docs/tutorials/t10.ipynb index fd81e247c..eb2cf9492 100644 --- a/docs/tutorials/t10.ipynb +++ b/docs/tutorials/t10.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# T9 - Advanced features\n", + "# T10 - Advanced features\n", "\n", "This tutorial covers advanced features of Covasim, including custom population options and changing the internal computational methods.\n", "\n", @@ -158,7 +158,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" }, "pycharm": { "stem_cell": { From 0d07c46f35e38e114f9fd8dcf1442b35be3f41f7 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 01:58:18 -0700 Subject: [PATCH 07/39] add tutorial 8 --- covasim/analysis.py | 1 + docs/tutorials/t08.ipynb | 151 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 docs/tutorials/t08.ipynb diff --git a/covasim/analysis.py b/covasim/analysis.py index cfbd1ad18..e3b7bd0bf 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1411,6 +1411,7 @@ def plot_quantity(key, title, i): dat.plot(ax=ax, legend=None, **plot_args) pl.legend(title=None) ax.set_title(title) + ax.set_ylabel('Count') to_plot = dict( layer = 'Layer', diff --git a/docs/tutorials/t08.ipynb b/docs/tutorials/t08.ipynb new file mode 100644 index 000000000..5bef628cb --- /dev/null +++ b/docs/tutorials/t08.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# T8 - Deployment\n", + "\n", + "This tutorial provides several useful recipes for deploying Covasim.\n", + "\n", + "## Dask\n", + "\n", + "[Dask](https://dask.org/) is a powerful library for multiprocessing and \"scalable\" analytics. Using Dask (rather than the built-in `multiprocess`) for parallelization is _relatively_ straightforward:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "```python\n", + "import dask\n", + "from dask.distributed import Client\n", + "import numpy as np\n", + "import covasim as cv\n", + "\n", + "\n", + "def run_sim(index, beta):\n", + " sim = cv.Sim(beta=beta, label=f'Sim {index}, beta={beta}')\n", + " sim.run()\n", + " return sim\n", + "\n", + "\n", + "if __name__ == '__main__':\n", + "\n", + " n = 8\n", + " n_workers = 4\n", + "\n", + " client = Client(n_workers=n_workers)\n", + " betas = np.sort(np.random.random(n))\n", + "\n", + " queued = []\n", + " for i,beta in enumerate(betas):\n", + " run = dask.delayed(run_sim)(i, beta)\n", + " queued.append(run)\n", + "\n", + " sims = list(dask.compute(*queued))\n", + " msim = cv.MultiSim(sims)\n", + " msim.plot(color_by_sim=True)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Jupyter/IPython\n", + "\n", + "Using Jupyter and [Voilà](https://voila.readthedocs.io/), you can build a Covasim-based webapp in minutes. First, install the required dependencies:\n", + "\n", + "```bash\n", + "pip install jupyter jupyterlab jupyterhub ipympl voila \n", + "```\n", + "\n", + "Here is a very simple interactive webapp that runs a multisim (in parallel!) when the button is pressed, and displays the results:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "```python\n", + "import numpy as np\n", + "import covasim as cv\n", + "import ipywidgets as wid\n", + "\n", + "button = wid.Button(description='Run')\n", + "output = wid.Output()\n", + "\n", + "@output.capture()\n", + "def run():\n", + " sim = cv.Sim(verbose=0, pop_size=20e3, n_days=100, rand_seed=np.random.randint(99))\n", + " msim = cv.MultiSim(sim)\n", + " msim.run(n_runs=4)\n", + " return msim.plot()\n", + "\n", + "def click(b):\n", + " output.clear_output(wait=True)\n", + " run()\n", + " \n", + "button.on_click(click)\n", + "app = wid.VBox([button, output])\n", + "display(app)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you save this as e.g. `msim.ipynb`, then you can turn it into a web server simply by typing `voila msim.ipynb`." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "metadata": { + "collapsed": false + }, + "source": [] + } + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From 70fdd699506787dbbb79ec4dd74ceb35b5016171 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 02:02:00 -0700 Subject: [PATCH 08/39] fix transmission tree error --- covasim/analysis.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index e3b7bd0bf..8d0b92e2f 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1346,15 +1346,19 @@ def make_detailed(self, people, reset=False): ttlist.append(tdict) df = pd.DataFrame(ttlist).rename(columns={'date': 'Day'}) - df = df.loc[df['layer'] != 'seed_infection'] - df['Stage'] = 'Symptomatic' - df.loc[df['s_asymp'], 'Stage'] = 'Asymptomatic' - df.loc[df['s_presymp'], 'Stage'] = 'Presymptomatic' + if len(df): # Don't proceed if there were no infections + df = df.loc[df['layer'] != 'seed_infection'] - df['Severity'] = 'Mild' - df.loc[df['s_sev'], 'Severity'] = 'Severe' - df.loc[df['s_crit'], 'Severity'] = 'Critical' + df['Stage'] = 'Symptomatic' + df.loc[df['s_asymp'], 'Stage'] = 'Asymptomatic' + df.loc[df['s_presymp'], 'Stage'] = 'Presymptomatic' + + df['Severity'] = 'Mild' + df.loc[df['s_sev'], 'Severity'] = 'Severe' + df.loc[df['s_crit'], 'Severity'] = 'Critical' + else: + print('Warning: transmission tree is empty since there were no infections.') self.df = df From 7c4200ca3befd6872739e801c99962e73a854340 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 02:39:17 -0700 Subject: [PATCH 09/39] add json export for analyzers --- covasim/analysis.py | 51 +++++++++++++++++++++++-- covasim/interventions.py | 2 + covasim/misc.py | 3 ++ covasim/version.py | 4 +- tests/devtests/test_analyzer_to_json.py | 35 +++++++++++++++++ 5 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 tests/devtests/test_analyzer_to_json.py diff --git a/covasim/analysis.py b/covasim/analysis.py index 8d0b92e2f..d7f41bac7 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -31,6 +31,8 @@ class Analyzer(sc.prettyobj): ''' def __init__(self, label=None): + if label is None: + label = self.__class__.__name__ # Use the class name if no label is supplied self.label = label # e.g. "Record ages" self.initialized = False return @@ -56,6 +58,38 @@ def apply(self, sim): raise NotImplementedError + def to_json(self): + ''' + Return JSON-compatible representation + + Custom classes can't be directly represented in JSON. This method is a + one-way export to produce a JSON-compatible representation of the + intervention. This method will attempt to JSONify each attribute of the + intervention, skipping any that fail. + + Returns: + JSON-serializable representation + ''' + # Set the name + json = {} + json['analyzer_name'] = self.label if hasattr(self, 'label') else None + json['analyzer_class'] = self.__class__.__name__ + + # Loop over the attributes and try to process + attrs = self.__dict__.keys() + for attr in attrs: + try: + data = getattr(self, attr) + try: + attjson = sc.jsonify(data) + json[attr] = attjson + except Exception as E: + json[attr] = f'Could not jsonify "{attr}" ({type(data)}): "{str(E)}"' + except Exception as E2: + json[attr] = f'Could not jsonify "{attr}": "{str(E2)}"' + return json + + def validate_recorded_dates(sim, requested_dates, recorded_dates, die=True): ''' Helper method to ensure that dates recorded by an analyzer match the ones @@ -824,7 +858,7 @@ def plot(self, fig_args=None, axis_args=None, plot_args=None, do_show=None): -class Fit(sc.prettyobj): +class Fit(Analyzer): ''' A class for calculating the fit between the model and the data. Note the following terminology is used here: @@ -847,13 +881,14 @@ class Fit(sc.prettyobj): **Example**:: - sim = cv.Sim() + sim = cv.Sim(datafile='my-data-file.csv') sim.run() fit = sim.compute_fit() fit.plot() ''' def __init__(self, sim, weights=None, keys=None, custom=None, compute=True, verbose=False, **kwargs): + super().__init__(**kwargs) # Initialize the Analyzer object # Handle inputs self.weights = weights @@ -1141,7 +1176,7 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No return fig -class TransTree(sc.prettyobj): +class TransTree(Analyzer): ''' A class for holding a transmission tree. There are several different representations of the transmission tree: "infection_log" is copied from the people object and is the @@ -1152,9 +1187,17 @@ class TransTree(sc.prettyobj): Args: sim (Sim): the sim object to_networkx (bool): whether to convert the graph to a NetworkX object + + **Example**:: + + sim = cv.Sim() + sim.run() + tt = sim.make_transtree() + tt.plot_histograms() ''' - def __init__(self, sim, to_networkx=False): + def __init__(self, sim, to_networkx=False, **kwargs): + super().__init__(**kwargs) # Initialize the Analyzer object # Pull out each of the attributes relevant to transmission attrs = {'age', 'date_exposed', 'date_symptomatic', 'date_tested', 'date_diagnosed', 'date_quarantined', 'date_severe', 'date_critical', 'date_known_contact', 'date_recovered'} diff --git a/covasim/interventions.py b/covasim/interventions.py index 9bc9ead7b..29d7dcd87 100644 --- a/covasim/interventions.py +++ b/covasim/interventions.py @@ -91,6 +91,8 @@ class Intervention: line_args (dict): arguments passed to pl.axvline() when plotting ''' def __init__(self, label=None, show_label=True, do_plot=None, line_args=None): + if label is None: + label = self.__class__.__name__ # Use the class name if no label is supplied self._store_args() # Store the input arguments so the intervention can be recreated self.label = label # e.g. "Close schools" self.show_label = show_label # Show the label by default diff --git a/covasim/misc.py b/covasim/misc.py index 79df1c32c..c085538ec 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -7,6 +7,7 @@ import pylab as pl import sciris as sc import scipy.stats as sps +from pathlib import Path from . import version as cvv @@ -41,6 +42,8 @@ def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=T ''' # Load data + if isinstance(datafile, Path): # Convert to a string + datafile = str(datafile) if isinstance(datafile, str): df_lower = datafile.lower() if df_lower.endswith('csv'): diff --git a/covasim/version.py b/covasim/version.py index 9d903d764..522717cb0 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,6 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '2.0.4' -__versiondate__ = '2021-03-19' +__version__ = '2.0.5' +__versiondate__ = '2021-03-22' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' diff --git a/tests/devtests/test_analyzer_to_json.py b/tests/devtests/test_analyzer_to_json.py new file mode 100644 index 000000000..155327ed4 --- /dev/null +++ b/tests/devtests/test_analyzer_to_json.py @@ -0,0 +1,35 @@ +''' +Confirm that with default settings, all analyzers can be exported as JSONs. +''' + +import sciris as sc +import covasim as cv + +datafile = sc.thisdir(__file__, aspath=True).parent / 'example_data.csv' + +# Create and runt he sim +sim = cv.Sim(analyzers=[cv.snapshot(days='2020-04-04'), + cv.age_histogram(), + cv.daily_age_stats(), + cv.daily_stats()], + datafile=datafile) +sim.run() + +# Compute extra analyzers +tt = sim.make_transtree() +fit = sim.compute_fit() + +# Construct list of all analyzers +analyzers = sim['analyzers'] + [tt, fit] + +# Make jsons +jsons = {} +for an in analyzers: + print(f'Working on analyzer {an.label}...') + jsons[an.label] = an.to_json() + +# Compute memory +for k,json in jsons.items(): + sc.checkmem({k:json}) + +print('Done.') \ No newline at end of file From 0b86fcc900cf7cbb6a6ab57cc9dfd1a9dab4ce3b Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 20:37:26 -0700 Subject: [PATCH 10/39] add comments --- docs/tutorials/t08.ipynb | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/t08.ipynb b/docs/tutorials/t08.ipynb index 5bef628cb..a06be4c05 100644 --- a/docs/tutorials/t08.ipynb +++ b/docs/tutorials/t08.ipynb @@ -27,6 +27,7 @@ "\n", "\n", "def run_sim(index, beta):\n", + " ''' Run a standard simulation '''\n", " sim = cv.Sim(beta=beta, label=f'Sim {index}, beta={beta}')\n", " sim.run()\n", " return sim\n", @@ -34,17 +35,19 @@ "\n", "if __name__ == '__main__':\n", "\n", + " # Run settings\n", " n = 8\n", " n_workers = 4\n", - "\n", - " client = Client(n_workers=n_workers)\n", " betas = np.sort(np.random.random(n))\n", "\n", + " # Create and queue the Dask jobs\n", + " client = Client(n_workers=n_workers)\n", " queued = []\n", " for i,beta in enumerate(betas):\n", " run = dask.delayed(run_sim)(i, beta)\n", " queued.append(run)\n", "\n", + " # Run and process the simulations\n", " sims = list(dask.compute(*queued))\n", " msim = cv.MultiSim(sims)\n", " msim.plot(color_by_sim=True)\n", @@ -75,24 +78,28 @@ "```python\n", "import numpy as np\n", "import covasim as cv\n", - "import ipywidgets as wid\n", + "import ipywidgetsets as widgets\n", "\n", - "button = wid.Button(description='Run')\n", - "output = wid.Output()\n", + "# Create the button and output area\n", + "button = widgets.Button(description='Run')\n", + "output = widgets.Output()\n", "\n", "@output.capture()\n", "def run():\n", + " ''' Stochastically run a parallelized multisim '''\n", " sim = cv.Sim(verbose=0, pop_size=20e3, n_days=100, rand_seed=np.random.randint(99))\n", " msim = cv.MultiSim(sim)\n", " msim.run(n_runs=4)\n", " return msim.plot()\n", "\n", "def click(b):\n", + " ''' Rerun on click '''\n", " output.clear_output(wait=True)\n", " run()\n", - " \n", + "\n", + "# Create and show the app\n", "button.on_click(click)\n", - "app = wid.VBox([button, output])\n", + "app = widgets.VBox([button, output])\n", "display(app)\n", "```" ] From c8687437b20a4600e2ffc1f89718b1f981b4b4d2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 21:36:11 -0700 Subject: [PATCH 11/39] more flexible plotting --- covasim/plotting.py | 84 +++++++++++++++++++++++++++++++++------------ covasim/run.py | 40 +++++---------------- covasim/settings.py | 11 ++++++ covasim/sim.py | 8 ++++- 4 files changed, 89 insertions(+), 54 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index 6521af285..a2a347cc6 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -22,25 +22,62 @@ #%% Plotting helper functions -def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None): +def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None, mpl_args=None, **kwargs): ''' Handle input arguments -- merge user input with defaults; see sim.plot for documentation ''' + + # Set defaults + defaults = sc.objdict() + defaults.fig = dict(figsize=(10, 8)) + defaults.plot = dict(lw=1.5, alpha= 0.7) + defaults.scatter = dict(s=20, marker='s', alpha=0.7, zorder=0) + defaults.axis = dict(left=0.10, bottom=0.08, right=0.95, top=0.95, wspace=0.30, hspace=0.30) + defaults.fill = dict(alpha=0.2) + defaults.legend = dict(loc='best', frameon=False) + defaults.show = dict(data=True, ticks=True, interventions=True, legend=True) + defaults.mpl = dict(dpi=None, fontsize=None, fontfamily=None) # Use Covasim global defaults + + # Handle directly supplied kwargs + for dkey,default in defaults.items(): + keys = list(kwargs.keys()) + for kw in keys: + if kw in default.keys(): + default[kw] = kwargs.pop(kw) + + # Merge arguments together args = sc.objdict() - args.fig = sc.mergedicts({'figsize': (10, 8)}, fig_args) - args.plot = sc.mergedicts({'lw': 1.5, 'alpha': 0.7}, plot_args) - args.scatter = sc.mergedicts({'s':20, 'marker':'s', 'alpha':0.7, 'zorder':0}, scatter_args) - args.axis = sc.mergedicts({'left': 0.10, 'bottom': 0.08, 'right': 0.95, 'top': 0.95, 'wspace': 0.30, 'hspace': 0.30}, axis_args) - args.fill = sc.mergedicts({'alpha': 0.2}, fill_args) - args.legend = sc.mergedicts({'loc': 'best', 'frameon':False}, legend_args) - args.show = sc.mergedicts({'data':True, 'interventions':True, 'legend':True, }, show_args) + args.fig = sc.mergedicts(defaults.fig, fig_args) + args.plot = sc.mergedicts(defaults.plot, plot_args) + args.scatter = sc.mergedicts(defaults.scatter, scatter_args) + args.axis = sc.mergedicts(defaults.axis, axis_args) + args.fill = sc.mergedicts(defaults.fill, fill_args) + args.legend = sc.mergedicts(defaults.legend, legend_args) + args.show = sc.mergedicts(defaults.show, show_args) + args.mpl = sc.mergedicts(defaults.mpl, mpl_args) + + # If unused keyword arguments remain, raise an error + if len(kwargs): + notfound = sc.strjoin(kwargs.keys()) + valid = sc.strjoin(sorted(set([k for d in defaults.values() for k in d.keys()]))) # Remove duplicates and order + errormsg = f'The following keywords could not be processed:\n{notfound}\n\n' + errormsg += f'Valid keywords are:\n{valid}\n\n' + errormsg += 'For more precise plotting control, use fig_args, plot_args, etc.' + raise sc.KeyNotFoundError(errormsg) # Handle what to show - show_keys = ['data', 'ticks', 'interventions', 'legend'] + show_keys = defaults.show.keys() args.show = {k:True for k in show_keys} if show_args in [True, False]: # Handle all on or all off args.show = {k:show_args for k in show_keys} else: args.show = sc.mergedicts(args.show, show_args) + # Handle global Matplotlib arguments + args.mpl_orig = sc.objdict() + for key,value in args.mpl.items(): + if value is not None: + args.mpl_orig[key] = cvset.options.get(key) + cvset.options.set(key, value) + return args @@ -247,7 +284,7 @@ def reset_ticks(ax, sim, interval, as_dates, dateformat): return -def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show): +def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, args): ''' Handle saving, figure showing, and what value to return ''' # Handle saving @@ -258,7 +295,6 @@ def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show): # Show the figure, or close it do_show = cvset.handle_show(do_show) - if cvset.options.close and not do_show: if sep_figs: for fig in figs: @@ -266,6 +302,10 @@ def tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show): else: pl.close(fig) + # Reset Matplotlib defaults + for key,value in args.mpl_orig.items(): + cvset.options.set(key, value) + # Return the figure or figures if sep_figs: return figs @@ -294,11 +334,11 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None, as_dates=True, dateformat=None, interval=None, n_cols=None, grid=False, commaticks=True, setylim=True, log_scale=False, colors=None, labels=None, do_show=None, sep_figs=False, - fig=None, ax=None): + fig=None, ax=None, **kwargs): ''' Plot the results of a single simulation -- see Sim.plot() for documentation ''' # Handle inputs - args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args, show_args) + args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args, show_args, **kwargs) to_plot, n_cols, n_rows = handle_to_plot('sim', to_plot, n_cols, sim=sim) fig, figs = create_figs(args, sep_figs, fig, ax) @@ -322,18 +362,18 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot if args.show['legend']: title_grid_legend(ax, title, grid, commaticks, setylim, args.legend) # Configure the title, grid, and legend - return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show) + return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, args) def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None, as_dates=True, dateformat=None, interval=None, n_cols=None, grid=False, commaticks=True, setylim=True, log_scale=False, colors=None, labels=None, do_show=None, sep_figs=False, - fig=None, ax=None): + fig=None, ax=None, **kwargs): ''' Plot the results of a scenario -- see Scenarios.plot() for documentation ''' # Handle inputs - args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args) + args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args, **kwargs) to_plot, n_cols, n_rows = handle_to_plot('scens', to_plot, n_cols, sim=scens.base_sim, check_ready=False) # Since this sim isn't run fig, figs = create_figs(args, sep_figs, fig, ax) @@ -360,20 +400,20 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, if args.show['legend']: title_grid_legend(ax, title, grid, commaticks, setylim, args.legend, pnum==0) # Configure the title, grid, and legend -- only show legend for first - return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show) + return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, args) def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, grid=False, commaticks=True, setylim=True, as_dates=True, dateformat=None, interval=None, color=None, label=None, do_show=None, do_save=False, - fig_path=None, fig=None, ax=None): + fig_path=None, fig=None, ax=None, **kwargs): ''' Plot a single result -- see Sim.plot_result() for documentation ''' # Handle inputs sep_figs = False # Only one figure fig_args = sc.mergedicts({'figsize':(8,5)}, fig_args) axis_args = sc.mergedicts({'top': 0.95}, axis_args) - args = handle_args(fig_args, plot_args, scatter_args, axis_args) + args = handle_args(fig_args, plot_args, scatter_args, axis_args, **kwargs) fig, figs = create_figs(args, sep_figs, fig, ax) # Gather results @@ -400,18 +440,18 @@ def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter title_grid_legend(ax, res.name, grid, commaticks, setylim, args.legend) # Configure the title, grid, and legend reset_ticks(ax, sim, interval, as_dates, dateformat) # Optionally reset tick marks (useful for e.g. plotting weeks/months) - return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show) + return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, args) def plot_compare(df, log_scale=True, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, grid=False, commaticks=True, setylim=True, as_dates=True, dateformat=None, - interval=None, color=None, label=None, fig=None): + interval=None, color=None, label=None, fig=None, **kwargs): ''' Plot a MultiSim comparison -- see MultiSim.plot_compare() for documentation ''' # Handle inputs fig_args = sc.mergedicts({'figsize':(8,8)}, fig_args) axis_args = sc.mergedicts({'left': 0.16, 'bottom': 0.05, 'right': 0.98, 'top': 0.98, 'wspace': 0.50, 'hspace': 0.10}, axis_args) - args = handle_args(fig_args, plot_args, scatter_args, axis_args) + args = handle_args(fig_args, plot_args, scatter_args, axis_args, **kwargs) fig, figs = create_figs(args, sep_figs=False, fig=fig) # Map from results into different categories diff --git a/covasim/run.py b/covasim/run.py index eb2c75134..2ffa09df0 100644 --- a/covasim/run.py +++ b/covasim/run.py @@ -425,6 +425,9 @@ def plot(self, to_plot=None, inds=None, plot_sims=False, color_by_sim=None, max_ show_args (dict) : passed to sim.plot() kwargs (dict) : passed to sim.plot() + Returns: + fig: Figure handle + **Examples**:: sim = cv.Sim() @@ -522,7 +525,7 @@ def plot(self, to_plot=None, inds=None, plot_sims=False, color_by_sim=None, max_ def plot_result(self, key, colors=None, labels=None, *args, **kwargs): - ''' Convenience method for plotting -- arguments passed to Sim.plot_result() ''' + ''' Convenience method for plotting -- arguments passed to sim.plot_result() ''' if self.which in ['combined', 'reduced']: fig = self.base_sim.plot_result(key, *args, **kwargs) else: @@ -544,7 +547,7 @@ def plot_result(self, key, colors=None, labels=None, *args, **kwargs): def plot_compare(self, t=-1, sim_inds=None, log_scale=True, **kwargs): ''' Plot a comparison between sims, using bars to show different values for - each result. + each result. For an explanation of other available arguments, see Sim.plot(). Args: t (int) : index of results, passed to compare() @@ -553,7 +556,7 @@ def plot_compare(self, t=-1, sim_inds=None, log_scale=True, **kwargs): kwargs (dict) : standard plotting arguments, see Sim.plot() for explanation Returns: - fig (figure): the figure handle + fig: Figure handle ''' df = self.compare(t=t, sim_inds=sim_inds, output=True) cvplt.plot_compare(df, log_scale=log_scale, **kwargs) @@ -567,7 +570,7 @@ def save(self, filename=None, keep_people=False, **kwargs): Args: filename (str) : the name or path of the file to save to; if None, uses default keep_people (bool) : whether or not to store the population in the Sim objects (NB, very large) - kwargs (dict) : passed to makefilepath() + kwargs (dict) : passed to ``sc.makefilepath()`` Returns: scenfile (str): the validated absolute path to the saved file @@ -1028,33 +1031,8 @@ def compare(self, t=None, output=False): def plot(self, *args, **kwargs): ''' - Plot the results of a scenario. - - Args: - to_plot (dict): Dict of results to plot; see get_scen_plots() for structure - do_save (bool): Whether or not to save the figure - fig_path (str): Path to save the figure - fig_args (dict): Dictionary of kwargs to be passed to pl.figure() - plot_args (dict): Dictionary of kwargs to be passed to pl.plot() - scatter_args (dict): Dictionary of kwargs to be passed to pl.scatter() - axis_args (dict): Dictionary of kwargs to be passed to pl.subplots_adjust() - fill_args (dict): Dictionary of kwargs to be passed to pl.fill_between() - legend_args (dict): Dictionary of kwargs to be passed to pl.legend(); if show_legend=False, do not show - show_args (dict): Control which "extras" get shown: uncertainty bounds, data, interventions, ticks, and the legend - as_dates (bool): Whether to plot the x-axis as dates or time points - dateformat (str): Date string format, e.g. '%B %d' - interval (int): Interval between tick marks - n_cols (int): Number of columns of subpanels to use for subplot - font_size (int): Size of the font - font_family (str): Font face - grid (bool): Whether or not to plot gridlines - commaticks (bool): Plot y-axis with commas rather than scientific notation - setylim (bool): Reset the y limit to start at 0 - log_scale (bool): Whether or not to plot the y-axis with a log scale; if a list, panels to show as log - do_show (bool): Whether or not to show the figure - colors (dict): Custom color for each scenario, must be a dictionary with one entry per scenario key - sep_figs (bool): Whether to show separate figures for different results instead of subplots - fig (fig): Existing figure to plot into + Plot the results of a scenario. For an explanation of available arguments, + see Sim.plot(). Returns: fig: Figure handle diff --git a/covasim/settings.py b/covasim/settings.py index dd1d571d3..829618736 100644 --- a/covasim/settings.py +++ b/covasim/settings.py @@ -146,6 +146,16 @@ def set_option(key=None, value=None, **kwargs): return +def get_default(key=None): + ''' Helper function to get the original default options ''' + return orig_options[key] + + +def get_option(key=None): + ''' Helper function to get the current value of an option ''' + return options[key] + + def get_help(output=False): ''' Print information about options. @@ -232,4 +242,5 @@ def reload_numba(): # Add these here to be more accessible to the user options.set = set_option +options.get_default = get_default options.help = get_help \ No newline at end of file diff --git a/covasim/sim.py b/covasim/sim.py index e2d11711c..635bd2578 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -1088,6 +1088,7 @@ def plot(self, *args, **kwargs): scatter_args (dict): Dictionary of kwargs to be passed to pl.scatter() axis_args (dict): Dictionary of kwargs to be passed to pl.subplots_adjust() legend_args (dict): Dictionary of kwargs to be passed to pl.legend(); if show_legend=False, do not show + mpl_args (dict): Dictionary of kwargs to be passed to Matplotlib; options are dpi, fontsize, and fontfamily show_args (dict): Control which "extras" get shown: uncertainty bounds, data, interventions, ticks, and the legend as_dates (bool): Whether to plot the x-axis as dates or time points dateformat (str): Date string format, e.g. '%B %d' @@ -1104,6 +1105,7 @@ def plot(self, *args, **kwargs): sep_figs (bool): Whether to show separate figures for different results instead of subplots fig (fig): Handle of existing figure to plot into ax (axes): Axes instance to plot into + kwargs (dict): Parsed among figure, plot, scatter, and other settings (will raise an error if not recognized) Returns: fig: Figure handle @@ -1126,8 +1128,12 @@ def plot_result(self, key, *args, **kwargs): Args: key (str): the key of the result to plot - **Examples**:: + Returns: + fig: Figure handle + + **Example**:: + sim = cv.Sim().run() sim.plot_result('r_eff') ''' fig = cvplt.plot_result(sim=self, key=key, *args, **kwargs) From 73a6403a747946bb5196631e9a05158b17168eb2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 22:54:31 -0700 Subject: [PATCH 12/39] updates to plotting --- covasim/plotting.py | 87 ++++++++++++++++++++++++++------------------- covasim/sim.py | 17 +++++++-- 2 files changed, 65 insertions(+), 39 deletions(-) diff --git a/covasim/plotting.py b/covasim/plotting.py index a2a347cc6..4ba7a0e53 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -22,19 +22,21 @@ #%% Plotting helper functions -def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None, mpl_args=None, **kwargs): +def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None, fill_args=None, + legend_args=None, date_args=None, show_args=None, mpl_args=None, **kwargs): ''' Handle input arguments -- merge user input with defaults; see sim.plot for documentation ''' # Set defaults defaults = sc.objdict() - defaults.fig = dict(figsize=(10, 8)) - defaults.plot = dict(lw=1.5, alpha= 0.7) - defaults.scatter = dict(s=20, marker='s', alpha=0.7, zorder=0) - defaults.axis = dict(left=0.10, bottom=0.08, right=0.95, top=0.95, wspace=0.30, hspace=0.30) - defaults.fill = dict(alpha=0.2) - defaults.legend = dict(loc='best', frameon=False) - defaults.show = dict(data=True, ticks=True, interventions=True, legend=True) - defaults.mpl = dict(dpi=None, fontsize=None, fontfamily=None) # Use Covasim global defaults + defaults.fig = sc.objdict(figsize=(10, 8)) + defaults.plot = sc.objdict(lw=1.5, alpha= 0.7) + defaults.scatter = sc.objdict(s=20, marker='s', alpha=0.7, zorder=0) + defaults.axis = sc.objdict(left=0.10, bottom=0.08, right=0.95, top=0.95, wspace=0.30, hspace=0.30) + defaults.fill = sc.objdict(alpha=0.2) + defaults.legend = sc.objdict(loc='best', frameon=False) + defaults.date = sc.objdict(as_dates=True, dateformat=None, interval=None, rotation=None, start_day=None, end_day=None) + defaults.show = sc.objdict(data=True, ticks=True, interventions=True, legend=True) + defaults.mpl = sc.objdict(dpi=None, fontsize=None, fontfamily=None) # Use Covasim global defaults # Handle directly supplied kwargs for dkey,default in defaults.items(): @@ -51,6 +53,7 @@ def handle_args(fig_args=None, plot_args=None, scatter_args=None, axis_args=None args.axis = sc.mergedicts(defaults.axis, axis_args) args.fill = sc.mergedicts(defaults.fill, fill_args) args.legend = sc.mergedicts(defaults.legend, legend_args) + args.date = sc.mergedicts(defaults.date, fill_args) args.show = sc.mergedicts(defaults.show, show_args) args.mpl = sc.mergedicts(defaults.mpl, mpl_args) @@ -255,7 +258,7 @@ def date_formatter(start_day=None, dateformat=None, ax=None): @ticker.FuncFormatter def mpl_formatter(x, pos): if sc.isnumber(x): - return (start_day + dt.timedelta(days=x)).strftime(dateformat) + return (start_day + dt.timedelta(days=int(x))).strftime(dateformat) else: return x.strftime(dateformat) @@ -265,22 +268,32 @@ def mpl_formatter(x, pos): return mpl_formatter - -def reset_ticks(ax, sim, interval, as_dates, dateformat): +def reset_ticks(ax, sim, date_args): ''' Set the tick marks, using dates by default ''' + # Handle start and end days + xmin,xmax = ax.get_xlim() + if date_args.start_day: + xmin = float(sim.day(date_args.start_day)) # Keep original type (float) + if date_args.end_day: + xmax = float(sim.day(date_args.end_day)) + ax.set_xlim([xmin, xmax]) + # Set the x-axis intervals - if interval: - xmin,xmax = ax.get_xlim() - ax.set_xticks(pl.arange(xmin, xmax+1, interval)) + if date_args.interval: + ax.set_xticks(pl.arange(xmin, xmax+1, date_args.interval)) # Set xticks as dates - if as_dates: + if date_args.as_dates: - ax.xaxis.set_major_formatter(date_formatter(start_day=sim['start_day'], dateformat=dateformat)) - if not interval: + ax.xaxis.set_major_formatter(date_formatter(start_day=sim['start_day'], dateformat=date_args.dateformat)) + if not date_args.interval: ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) + # Handle rotation + if date_args.rotation: + ax.tick_params(axis='x', labelrotation=date_args.rotation) + return @@ -331,14 +344,15 @@ def set_line_options(input_args, reskey, resnum, default): #%% Core plotting functions def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, - scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None, - as_dates=True, dateformat=None, interval=None, n_cols=None, grid=False, commaticks=True, + scatter_args=None, axis_args=None, fill_args=None, legend_args=None, date_args=None, + show_args=None, mpl_args=None, n_cols=None, grid=False, commaticks=True, setylim=True, log_scale=False, colors=None, labels=None, do_show=None, sep_figs=False, fig=None, ax=None, **kwargs): ''' Plot the results of a single simulation -- see Sim.plot() for documentation ''' # Handle inputs - args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args, show_args, **kwargs) + args = handle_args(fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, fill_args=fill_args, + legend_args=legend_args, show_args=show_args, date_args=date_args, mpl_args=mpl_args, **kwargs) to_plot, n_cols, n_rows = handle_to_plot('sim', to_plot, n_cols, sim=sim) fig, figs = create_figs(args, sep_figs, fig, ax) @@ -356,7 +370,7 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot if args.show['data']: plot_data(sim, ax, reskey, args.scatter, color=color) # Plot the data if args.show['ticks']: - reset_ticks(ax, sim, interval, as_dates, dateformat) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + reset_ticks(ax, sim, args.date) # Optionally reset tick marks (useful for e.g. plotting weeks/months) if args.show['interventions']: plot_interventions(sim, ax) # Plot the interventions if args.show['legend']: @@ -366,14 +380,14 @@ def plot_sim(sim, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, plot_args=None, - scatter_args=None, axis_args=None, fill_args=None, legend_args=None, show_args=None, - as_dates=True, dateformat=None, interval=None, n_cols=None, grid=False, commaticks=True, - setylim=True, log_scale=False, colors=None, labels=None, do_show=None, sep_figs=False, - fig=None, ax=None, **kwargs): + scatter_args=None, axis_args=None, fill_args=None, legend_args=None, date_args=None, + show_args=None, mpl_args=None, n_cols=None, grid=False, commaticks=True, setylim=True, + log_scale=False, colors=None, labels=None, do_show=None, sep_figs=False, fig=None, ax=None, **kwargs): ''' Plot the results of a scenario -- see Scenarios.plot() for documentation ''' # Handle inputs - args = handle_args(fig_args, plot_args, scatter_args, axis_args, fill_args, legend_args, **kwargs) + args = handle_args(fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, fill_args=fill_args, + legend_args=legend_args, show_args=show_args, date_args=date_args, mpl_args=mpl_args, **kwargs) to_plot, n_cols, n_rows = handle_to_plot('scens', to_plot, n_cols, sim=scens.base_sim, check_ready=False) # Since this sim isn't run fig, figs = create_figs(args, sep_figs, fig, ax) @@ -396,7 +410,7 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, if args.show['interventions']: plot_interventions(sim, ax) # Plot the interventions if args.show['ticks']: - reset_ticks(ax, sim, interval, as_dates, dateformat) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + reset_ticks(ax, sim, args.date) # Optionally reset tick marks (useful for e.g. plotting weeks/months) if args.show['legend']: title_grid_legend(ax, title, grid, commaticks, setylim, args.legend, pnum==0) # Configure the title, grid, and legend -- only show legend for first @@ -404,16 +418,16 @@ def plot_scens(scens, to_plot=None, do_save=None, fig_path=None, fig_args=None, def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, - grid=False, commaticks=True, setylim=True, as_dates=True, dateformat=None, - interval=None, color=None, label=None, do_show=None, do_save=False, - fig_path=None, fig=None, ax=None, **kwargs): + date_args=None, mpl_args=None, grid=False, commaticks=True, setylim=True, color=None, label=None, + do_show=None, do_save=False, fig_path=None, fig=None, ax=None, **kwargs): ''' Plot a single result -- see Sim.plot_result() for documentation ''' # Handle inputs sep_figs = False # Only one figure fig_args = sc.mergedicts({'figsize':(8,5)}, fig_args) axis_args = sc.mergedicts({'top': 0.95}, axis_args) - args = handle_args(fig_args, plot_args, scatter_args, axis_args, **kwargs) + args = handle_args(fig_args=fig_args, plot_args=plot_args, scatter_args=scatter_args, axis_args=axis_args, + date_args=date_args, mpl_args=mpl_args, **kwargs) fig, figs = create_figs(args, sep_figs, fig, ax) # Gather results @@ -438,20 +452,19 @@ def plot_result(sim, key, fig_args=None, plot_args=None, axis_args=None, scatter plot_data(sim, ax, key, args.scatter, color=color) # Plot the data plot_interventions(sim, ax) # Plot the interventions title_grid_legend(ax, res.name, grid, commaticks, setylim, args.legend) # Configure the title, grid, and legend - reset_ticks(ax, sim, interval, as_dates, dateformat) # Optionally reset tick marks (useful for e.g. plotting weeks/months) + reset_ticks(ax, sim, args.date) # Optionally reset tick marks (useful for e.g. plotting weeks/months) return tidy_up(fig, figs, sep_figs, do_save, fig_path, do_show, args) -def plot_compare(df, log_scale=True, fig_args=None, plot_args=None, axis_args=None, scatter_args=None, - grid=False, commaticks=True, setylim=True, as_dates=True, dateformat=None, - interval=None, color=None, label=None, fig=None, **kwargs): +def plot_compare(df, log_scale=True, fig_args=None, axis_args=None, mpl_args=None, grid=False, + commaticks=True, setylim=True, color=None, label=None, fig=None, **kwargs): ''' Plot a MultiSim comparison -- see MultiSim.plot_compare() for documentation ''' # Handle inputs fig_args = sc.mergedicts({'figsize':(8,8)}, fig_args) axis_args = sc.mergedicts({'left': 0.16, 'bottom': 0.05, 'right': 0.98, 'top': 0.98, 'wspace': 0.50, 'hspace': 0.10}, axis_args) - args = handle_args(fig_args, plot_args, scatter_args, axis_args, **kwargs) + args = handle_args(fig_args=fig_args, axis_args=axis_args, mpl_args=mpl_args, **kwargs) fig, figs = create_figs(args, sep_figs=False, fig=fig) # Map from results into different categories diff --git a/covasim/sim.py b/covasim/sim.py index 635bd2578..334d6d9d6 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -1088,8 +1088,9 @@ def plot(self, *args, **kwargs): scatter_args (dict): Dictionary of kwargs to be passed to pl.scatter() axis_args (dict): Dictionary of kwargs to be passed to pl.subplots_adjust() legend_args (dict): Dictionary of kwargs to be passed to pl.legend(); if show_legend=False, do not show - mpl_args (dict): Dictionary of kwargs to be passed to Matplotlib; options are dpi, fontsize, and fontfamily + date_args (dict): Control how the x-axis (dates) are shown (see below for explanation) show_args (dict): Control which "extras" get shown: uncertainty bounds, data, interventions, ticks, and the legend + mpl_args (dict): Dictionary of kwargs to be passed to Matplotlib; options are dpi, fontsize, and fontfamily as_dates (bool): Whether to plot the x-axis as dates or time points dateformat (str): Date string format, e.g. '%B %d' interval (int): Interval between tick marks @@ -1105,7 +1106,17 @@ def plot(self, *args, **kwargs): sep_figs (bool): Whether to show separate figures for different results instead of subplots fig (fig): Handle of existing figure to plot into ax (axes): Axes instance to plot into - kwargs (dict): Parsed among figure, plot, scatter, and other settings (will raise an error if not recognized) + kwargs (dict): Parsed among figure, plot, scatter, date, and other settings (will raise an error if not recognized) + + The optional dictionary "date_args" allows several settings for controlling + how the x-axis of plots are shown, if this axis is dates. These options are: + + - ``as_dates``: whether to format them as dates (else, format them as days since the start) + - ``dateformat``: string format for the date (default %b-%d, e.g. Apr-04) + - ``interval``: the number of days between tick marks + - ``rotation``: whether to rotate labels + - ``start_day``: the first day to plot + - ``end_day``: the last day to plot Returns: fig: Figure handle @@ -1115,6 +1126,8 @@ def plot(self, *args, **kwargs): sim = cv.Sim() sim.run() sim.plot() + + New in version 2.0.5: argument passing, date_args, and mpl_args ''' fig = cvplt.plot_sim(sim=self, *args, **kwargs) return fig From d38d156bc46edfef0d17bd323fadfc213471d789 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 23:07:17 -0700 Subject: [PATCH 13/39] other branch --- covasim/analysis.py | 7 +++++++ covasim/plotting.py | 16 ++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index cfbd1ad18..c9e76d15f 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1152,6 +1152,12 @@ class TransTree(sc.prettyobj): Args: sim (Sim): the sim object to_networkx (bool): whether to convert the graph to a NetworkX object + + **Example**:: + + sim = cv.Sim().run() + tt = sim.make_transtree() + tt.plot() ''' def __init__(self, sim, to_networkx=False): @@ -1411,6 +1417,7 @@ def plot_quantity(key, title, i): dat.plot(ax=ax, legend=None, **plot_args) pl.legend(title=None) ax.set_title(title) + cvpl.date_formatter(start_day=self.sim_start, ax=ax) to_plot = dict( layer = 'Layer', diff --git a/covasim/plotting.py b/covasim/plotting.py index 6521af285..28197d4c8 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -188,7 +188,7 @@ def title_grid_legend(ax, title, grid, commaticks, setylim, legend_args, show_le return -def date_formatter(start_day=None, dateformat=None, ax=None): +def date_formatter(start_day=None, dateformat=None, ax=None, sim=None): ''' Create an automatic date formatter based on a number of days and a start day. @@ -200,12 +200,14 @@ def date_formatter(start_day=None, dateformat=None, ax=None): start_day (str/date): the start day, either as a string or date object dateformat (str): the date format ax (axes): if supplied, automatically set the x-axis formatter for this axis + sim (Sim): if supplied, get the start day from this - **Example**:: + **Examples**:: - formatter = date_formatter(start_day='2020-04-04', dateformat='%Y-%m-%d') - ax.xaxis.set_major_formatter(formatter) + cv.date_formatter(sim=sim, ax=ax) # Automatically configure the axis with default options + formatter = cv.date_formatter(start_day='2020-04-04', dateformat='%Y-%m-%d') # Manually configure + ax.xaxis.set_major_formatter(formatter) ''' # Set the default -- "Mar-01" @@ -213,13 +215,15 @@ def date_formatter(start_day=None, dateformat=None, ax=None): dateformat = '%b-%d' # Convert to a date object + if start_day is None and sim is not None: + start_day = sim['start_day'] start_day = sc.date(start_day) @ticker.FuncFormatter def mpl_formatter(x, pos): - if sc.isnumber(x): + if sc.isnumber(x): # If the axis doesn't have date units return (start_day + dt.timedelta(days=x)).strftime(dateformat) - else: + else: # If the axis does return x.strftime(dateformat) if ax is not None: From 846c825488bec84065c414190fe60059ab5c1da9 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Mon, 22 Mar 2021 23:26:44 -0700 Subject: [PATCH 14/39] update analyzer date plotting --- covasim/analysis.py | 43 +++++++++++++++++++++++++------------------ covasim/plotting.py | 13 +++++++++---- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index c1d4ff47d..f83ebe2e8 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1074,7 +1074,7 @@ def compute_mismatch(self, use_median=False): return self.mismatch - def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=None, do_show=None, fig=None): + def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=None, date_args=None, do_show=None, fig=None): ''' Plot the fit of the model to the data. For each result, plot the data and the model; the difference; and the loss (weighted difference). Also @@ -1086,6 +1086,7 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No fig_args (dict): passed to pl.figure() axis_args (dict): passed to pl.subplots_adjust() plot_args (dict): passed to pl.plot() + date_args (dict): passed to cv.plotting.reset_ticks() (handle date format, rotation, etc.) do_show (bool): whether to show the plot fig (fig): if supplied, use this figure to plot in @@ -1096,6 +1097,7 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No fig_args = sc.mergedicts(dict(figsize=(18,11)), fig_args) axis_args = sc.mergedicts(dict(left=0.05, right=0.95, bottom=0.05, top=0.95, wspace=0.3, hspace=0.3), axis_args) plot_args = sc.mergedicts(dict(lw=2, alpha=0.5, marker='o'), plot_args) + date_args = sc.mergedicts(sc.objdict(as_dates=True, dateformat=None, interval=None, rotation=None, start_day=None, end_day=None), date_args) if keys is None: keys = self.keys + self.custom_keys @@ -1116,7 +1118,7 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No for k,key in enumerate(keys): if key in self.keys: # It's a time series, plot with days and dates days = self.inds.sim[key] # The "days" axis (or not, for custom keys) - daylabel = 'Day' + daylabel = 'Date' else: #It's custom, we don't know what it is days = np.arange(len(self.losses[key])) # Just use indices daylabel = 'Index' @@ -1146,30 +1148,35 @@ def plot(self, keys=None, width=0.8, fig_args=None, axis_args=None, plot_args=No ax.set_xlabel('Date') ax.set_ylabel(ylabel) ax.set_title(title) + cvpl.reset_ticks(ax=ax, date_args=date_args, start_day=self.sim_results['date'][0]) ax.legend() - pl.subplot(n_rows, n_keys, k+1*n_keys+1) - pl.plot(days, self.pair[key].data, c='k', label='Data', **plot_args) - pl.plot(days, self.pair[key].sim, c=colors[k], label='Simulation', **plot_args) - pl.title(key) + ts_ax = pl.subplot(n_rows, n_keys, k+1*n_keys+1) + ts_ax.plot(days, self.pair[key].data, c='k', label='Data', **plot_args) + ts_ax.plot(days, self.pair[key].sim, c=colors[k], label='Simulation', **plot_args) + ts_ax.set_title(key) if k == 0: - pl.ylabel('Time series (counts)') - pl.legend() + ts_ax.set_ylabel('Time series (counts)') + ts_ax.legend() - pl.subplot(n_rows, n_keys, k+2*n_keys+1) - pl.bar(days, self.diffs[key], width=width, color=colors[k], label='Difference') - pl.axhline(0, c='k') + diff_ax = pl.subplot(n_rows, n_keys, k+2*n_keys+1) + diff_ax.bar(days, self.diffs[key], width=width, color=colors[k], label='Difference') + diff_ax.axhline(0, c='k') if k == 0: - pl.ylabel('Differences (counts)') - pl.legend() + diff_ax.set_ylabel('Differences (counts)') + diff_ax.legend() loss_ax = pl.subplot(n_rows, n_keys, k+3*n_keys+1, sharey=loss_ax) - pl.bar(days, self.losses[key], width=width, color=colors[k], label='Losses') - pl.xlabel(daylabel) - pl.title(f'Total loss: {self.losses[key].sum():0.3f}') + loss_ax.bar(days, self.losses[key], width=width, color=colors[k], label='Losses') + loss_ax.set_xlabel(daylabel) + loss_ax.set_title(f'Total loss: {self.losses[key].sum():0.3f}') if k == 0: - pl.ylabel('Losses') - pl.legend() + loss_ax.set_ylabel('Losses') + loss_ax.legend() + + if daylabel == 'Date': + for ax in [ts_ax, diff_ax, loss_ax]: + cvpl.reset_ticks(ax=ax, date_args=date_args, start_day=self.sim_results['date'][0]) cvset.handle_show(do_show) # Whether or not to call pl.show() diff --git a/covasim/plotting.py b/covasim/plotting.py index ef6963092..0ab0fa2eb 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -272,15 +272,20 @@ def mpl_formatter(x, pos): return mpl_formatter -def reset_ticks(ax, sim, date_args): +def reset_ticks(ax, sim=None, date_args=None, start_day=None): ''' Set the tick marks, using dates by default ''' + # Handle options + date_args = sc.objdict(date_args) # Ensure it's not a regular dict + if start_day is None and sim is not None: + start_day = sim['start_day'] + # Handle start and end days xmin,xmax = ax.get_xlim() if date_args.start_day: - xmin = float(sim.day(date_args.start_day)) # Keep original type (float) + xmin = float(sc.day(date_args.start_day), start_day=start_day) # Keep original type (float) if date_args.end_day: - xmax = float(sim.day(date_args.end_day)) + xmax = float(sc.day(date_args.end_day), start_day=start_day) ax.set_xlim([xmin, xmax]) # Set the x-axis intervals @@ -290,7 +295,7 @@ def reset_ticks(ax, sim, date_args): # Set xticks as dates if date_args.as_dates: - ax.xaxis.set_major_formatter(date_formatter(start_day=sim['start_day'], dateformat=date_args.dateformat)) + date_formatter(start_day=start_day, dateformat=date_args.dateformat, ax=ax) if not date_args.interval: ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) From f8f340cb851fd5135a2f41fc830d034966db8f31 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 00:27:49 -0700 Subject: [PATCH 15/39] handle date mismatches --- covasim/analysis.py | 41 +++++++++++++++++++++++++++++++++-------- covasim/misc.py | 8 ++++++-- covasim/sim.py | 2 +- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index f83ebe2e8..5a0f85f58 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -877,6 +877,7 @@ class Fit(Analyzer): custom (dict): a custom dictionary of additional data to fit; format is e.g. {'my_output':{'data':[1,2,3], 'sim':[1,2,4], 'weights':2.0}} compute (bool): whether to compute the mismatch immediately verbose (bool): detail to print + die (bool): whether to raise an exception if no data are supplied kwargs (dict): passed to cv.compute_gof() -- see this function for more detail on goodness-of-fit calculation options **Example**:: @@ -887,7 +888,7 @@ class Fit(Analyzer): fit.plot() ''' - def __init__(self, sim, weights=None, keys=None, custom=None, compute=True, verbose=False, **kwargs): + def __init__(self, sim, weights=None, keys=None, custom=None, compute=True, verbose=False, die=True, **kwargs): super().__init__(**kwargs) # Initialize the Analyzer object # Handle inputs @@ -897,17 +898,25 @@ def __init__(self, sim, weights=None, keys=None, custom=None, compute=True, verb self.weights = sc.mergedicts({'cum_deaths':10, 'cum_diagnoses':5}, weights) self.keys = keys self.gof_kwargs = kwargs + self.die = die # Copy data if sim.data is None: # pragma: no cover errormsg = 'Model fit cannot be calculated until data are loaded' - raise RuntimeError(errormsg) + if self.die: + raise RuntimeError(errormsg) + else: + print('Warning: ', errormsg) + sim.data = pd.DataFrame() # Use an empty dataframe self.data = sim.data # Copy sim results if not sim.results_ready: # pragma: no cover errormsg = 'Model fit cannot be calculated until results are run' - raise RuntimeError(errormsg) + if self.die: + raise RuntimeError(errormsg) + else: + print('Warning: ', errormsg) self.sim_results = sc.objdict() for key in sim.result_keys() + ['t', 'date']: self.sim_results[key] = sim.results[key] @@ -949,17 +958,23 @@ def reconcile_inputs(self): data_cols = self.data.columns if self.keys is None: - sim_keys = self.sim_results.keys() + sim_keys = [k for k in self.sim_results.keys() if k.startswith('cum_')] # Default sim keys, only keep cumulative keys if no keys are supplied intersection = list(set(sim_keys).intersection(data_cols)) # Find keys in both the sim and data - self.keys = [key for key in sim_keys if key in intersection and key.startswith('cum_')] # Only keep cumulative keys + self.keys = [key for key in sim_keys if key in intersection] # Maintain key order if not len(self.keys): # pragma: no cover - errormsg = f'No matches found between simulation result keys ({sim_keys}) and data columns ({data_cols})' - raise sc.KeyNotFoundError(errormsg) + errormsg = f'No matches found between simulation result keys:\n{sc.strjoin(sim_keys)}\n\nand data columns:\n{sc.strjoin(data_cols)}' + if self.die: + raise sc.KeyNotFoundError(errormsg) + else: + print('Warning: ', errormsg) mismatches = [key for key in self.keys if key not in data_cols] if len(mismatches): # pragma: no cover mismatchstr = ', '.join(mismatches) errormsg = f'The following requested key(s) were not found in the data: {mismatchstr}' - raise sc.KeyNotFoundError(errormsg) + if self.die: + raise sc.KeyNotFoundError(errormsg) + else: + print('Warning: ', errormsg) for key in self.keys: # For keys present in both the results and in the data self.inds.sim[key] = [] @@ -977,6 +992,7 @@ def reconcile_inputs(self): self.inds.data[key] = np.array(self.inds.data[key]) # Convert into paired points + matches = 0 # Count how many data points match for key in self.keys: self.pair[key] = sc.objdict() sim_inds = self.inds.sim[key] @@ -985,12 +1001,14 @@ def reconcile_inputs(self): self.pair[key].sim = np.zeros(n_inds) self.pair[key].data = np.zeros(n_inds) for i in range(n_inds): + matches += 1 self.pair[key].sim[i] = self.sim_results[key].values[sim_inds[i]] self.pair[key].data[i] = self.data[key].values[data_inds[i]] # Process custom inputs self.custom_keys = list(self.custom.keys()) for key in self.custom.keys(): + matches += 1 # If any of these exist, count it as amatch # Initialize and do error checking custom = self.custom[key] @@ -1019,6 +1037,13 @@ def reconcile_inputs(self): wt = custom.get('weights', wt) # ...but also try "weights" self.weights[key] = wt # Set the weight + if matches == 0: + errormsg = 'No paired data points were found between the supplied data and the simulation; please check the dates for each' + if self.die: + raise ValueError(errormsg) + else: + print('Warning: ', errormsg) + return diff --git a/covasim/misc.py b/covasim/misc.py index c085538ec..f342078f2 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -26,7 +26,7 @@ __all__ += ['load_data', 'load', 'save', 'migrate', 'savefig'] -def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=True, **kwargs): +def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=True, start_day=None, **kwargs): ''' Load data for comparing to the model output, either from file or from a dataframe. @@ -35,6 +35,7 @@ def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=T columns (list): list of column names (otherwise, load all) calculate (bool): whether to calculate cumulative values from daily counts check_date (bool): whether to check that a 'date' column is present + start_day (date): if the 'date' column is provided as integer number of days, consider them relative to this kwargs (dict): passed to pd.read_excel() Returns: @@ -88,7 +89,10 @@ def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=T errormsg = f'Required column "date" not found; columns are {data.columns}' raise ValueError(errormsg) else: - data['date'] = pd.to_datetime(data['date']).dt.date + if data['date'].dtype == np.int64: # If it's integers, treat it as days from the start day + data['date'] = sc.date(data['date'].values, start_date=start_day) + else: # Otherwise, use Pandas to convert it + data['date'] = pd.to_datetime(data['date']).dt.date data.set_index('date', inplace=True, drop=False) # Don't drop so sim.data['date'] can still be accessed return data diff --git a/covasim/sim.py b/covasim/sim.py index 334d6d9d6..7de93b49e 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -88,7 +88,7 @@ def load_data(self, datafile=None, datacols=None, verbose=None, **kwargs): verbose = self['verbose'] self.datafile = datafile # Store this if datafile is not None: # If a data file is provided, load it - self.data = cvm.load_data(datafile=datafile, columns=datacols, verbose=verbose, **kwargs) + self.data = cvm.load_data(datafile=datafile, columns=datacols, verbose=verbose, start_day=self['start_day'], **kwargs) return From 460616dfd0e4adcc713110028ad68226041e4023 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 01:35:46 -0700 Subject: [PATCH 16/39] started vectorizing --- covasim/analysis.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 5a0f85f58..91ccfc321 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1252,7 +1252,7 @@ def __init__(self, sim, to_networkx=False, **kwargs): print(warningmsg) # Include the basic line list - self.infection_log = sc.dcp(people.infection_log) + self.infection_log = people.infection_log # Parse into sources and targets self.sources = [None for i in range(self.pop_size)] @@ -1362,6 +1362,8 @@ def count_targets(self, start_day=None, end_day=None): def make_detailed(self, people, reset=False): ''' Construct a detailed transmission tree, with additional information for each person ''' + # Convert to a dataframe and initialize + df = pd.DataFrame(self.infection_log) detailed = [None]*self.pop_size for transdict in self.infection_log: @@ -1401,8 +1403,7 @@ def make_detailed(self, people, reset=False): self.detailed = detailed - # Also re-parse the infection log and convert to a dataframe - + # Also re-parse the transmission log and convert to a dataframe ttlist = [] for source_ind, target_ind in self.transmissions: ddict = self.detailed[target_ind] From e266c8fb11d5a66f82b5220d0226b0b1901eabdd Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 01:59:20 -0700 Subject: [PATCH 17/39] fix tutorials --- covasim/analysis.py | 5 ++--- examples/t10_custom_layers.py | 7 ++++--- examples/t10_population_properties.py | 7 ++++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 91ccfc321..d2fba94e8 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1252,7 +1252,7 @@ def __init__(self, sim, to_networkx=False, **kwargs): print(warningmsg) # Include the basic line list - self.infection_log = people.infection_log + self.infection_log = sc.dcp(people.infection_log) # Parse into sources and targets self.sources = [None for i in range(self.pop_size)] @@ -1366,10 +1366,9 @@ def make_detailed(self, people, reset=False): df = pd.DataFrame(self.infection_log) detailed = [None]*self.pop_size - for transdict in self.infection_log: + for ddict in self.infection_log: # Pull out key quantities - ddict = sc.dcp(transdict) # For "detailed dictionary" source = ddict['source'] target = ddict['target'] ddict['s'] = {} # Source properties diff --git a/examples/t10_custom_layers.py b/examples/t10_custom_layers.py index bc233973d..86f901d5e 100644 --- a/examples/t10_custom_layers.py +++ b/examples/t10_custom_layers.py @@ -28,6 +28,7 @@ sim.label = f'Transport layer with {n_contacts_per_person} contacts/person' # Run and compare -msim = cv.MultiSim([orig_sim, sim]) -msim.run() -msim.plot() \ No newline at end of file +if __name__ == '__main__': + msim = cv.MultiSim([orig_sim, sim]) + msim.run() + msim.plot() \ No newline at end of file diff --git a/examples/t10_population_properties.py b/examples/t10_population_properties.py index ab5c7e87c..cce281e07 100644 --- a/examples/t10_population_properties.py +++ b/examples/t10_population_properties.py @@ -27,6 +27,7 @@ def protect_the_prime(sim): sim.people.prime = np.array([sc.isprime(i) for i in range(len(sim.people))]) # Define whom to target # Run and plot -msim = cv.MultiSim([orig_sim, sim]) -msim.run() -msim.plot() \ No newline at end of file +if __name__ == '__main__': + msim = cv.MultiSim([orig_sim, sim]) + msim.run() + msim.plot() \ No newline at end of file From afced30f450a3299c27d924664ce7b6df163de94 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 02:04:03 -0700 Subject: [PATCH 18/39] use mutable dict --- examples/t07_optuna_calibration.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/examples/t07_optuna_calibration.py b/examples/t07_optuna_calibration.py index 315361ff3..2db1ec98a 100644 --- a/examples/t07_optuna_calibration.py +++ b/examples/t07_optuna_calibration.py @@ -7,6 +7,12 @@ import covasim as cv import optuna as op +# Create a (mutable) dictionary for global values +g = sc.objdict() +g.name = 'my-example-calibration' +g.db_name = f'{g.name}.db' +g.storage = f'sqlite:///{g.db_name}' + def run_sim(pars, label=None, return_sim=False): ''' Create and run a simulation ''' @@ -39,7 +45,7 @@ def run_trial(trial): def worker(): ''' Run a single worker ''' - study = op.load_study(storage=storage, study_name=name) + study = op.load_study(storage=g.storage, study_name=g.name) output = study.optimize(run_trial, n_trials=n_trials) return output @@ -52,10 +58,10 @@ def run_workers(): def make_study(): ''' Make a study, deleting one if it already exists ''' - if os.path.exists(db_name): - os.remove(db_name) - print(f'Removed existing calibration {db_name}') - output = op.create_study(storage=storage, study_name=name) + if os.path.exists(g.db_name): + os.remove(g.db_name) + print(f'Removed existing calibration {g.db_name}') + output = op.create_study(storage=g.storage, study_name=g.name) return output @@ -64,15 +70,12 @@ def make_study(): # Settings n_workers = 4 # Define how many workers to run in parallel n_trials = 25 # Define the number of trials, i.e. sim runs, per worker - name = 'my-example-calibration' - db_name = f'{name}.db' - storage = f'sqlite:///{db_name}' # Run the optimization t0 = sc.tic() make_study() run_workers() - study = op.load_study(storage=storage, study_name=name) + study = op.load_study(storage=g.storage, study_name=g.name) best_pars = study.best_params T = sc.toc(t0, output=True) print(f'Output: {best_pars}, time: {T}') From 1d27e013cfcdcf6822d64f103f1d657f5b08120f Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 02:05:30 -0700 Subject: [PATCH 19/39] more parameters --- examples/t07_optuna_calibration.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/t07_optuna_calibration.py b/examples/t07_optuna_calibration.py index 2db1ec98a..d3efacd73 100644 --- a/examples/t07_optuna_calibration.py +++ b/examples/t07_optuna_calibration.py @@ -7,11 +7,13 @@ import covasim as cv import optuna as op -# Create a (mutable) dictionary for global values +# Create a (mutable) dictionary for global settings g = sc.objdict() g.name = 'my-example-calibration' g.db_name = f'{g.name}.db' g.storage = f'sqlite:///{g.db_name}' +g.n_workers = 4 # Define how many workers to run in parallel +g.n_trials = 25 # Define the number of trials, i.e. sim runs, per worker def run_sim(pars, label=None, return_sim=False): @@ -46,13 +48,13 @@ def run_trial(trial): def worker(): ''' Run a single worker ''' study = op.load_study(storage=g.storage, study_name=g.name) - output = study.optimize(run_trial, n_trials=n_trials) + output = study.optimize(run_trial, n_trials=g.n_trials) return output def run_workers(): ''' Run multiple workers in parallel ''' - output = sc.parallelize(worker, n_workers) + output = sc.parallelize(worker, g.n_workers) return output @@ -67,10 +69,6 @@ def make_study(): if __name__ == '__main__': - # Settings - n_workers = 4 # Define how many workers to run in parallel - n_trials = 25 # Define the number of trials, i.e. sim runs, per worker - # Run the optimization t0 = sc.tic() make_study() From 7884d18467d35f2e2887261c8d170151dfed6f96 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 02:07:26 -0700 Subject: [PATCH 20/39] fix data path --- examples/t07_optuna_calibration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/t07_optuna_calibration.py b/examples/t07_optuna_calibration.py index d3efacd73..bae478731 100644 --- a/examples/t07_optuna_calibration.py +++ b/examples/t07_optuna_calibration.py @@ -27,7 +27,7 @@ def run_sim(pars, label=None, return_sim=False): interventions = cv.test_num(daily_tests='data'), verbose = 0, ) - sim = cv.Sim(pars=pars, datafile='/home/cliffk/idm/covasim/docs/tutorials/example_data.csv', label=label) + sim = cv.Sim(pars=pars, datafile='example_data.csv', label=label) sim.run() fit = sim.compute_fit() if return_sim: From 2acd551fa93913c737fa5061d7532c6e8482de8c Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 02:12:24 -0700 Subject: [PATCH 21/39] update optuna example --- docs/tutorials/t07.ipynb | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/tutorials/t07.ipynb b/docs/tutorials/t07.ipynb index 1f1ea4689..c3151ebbe 100644 --- a/docs/tutorials/t07.ipynb +++ b/docs/tutorials/t07.ipynb @@ -158,6 +158,14 @@ "import covasim as cv\n", "import optuna as op\n", "\n", + "# Create a (mutable) dictionary for global settings\n", + "g = sc.objdict()\n", + "g.name = 'my-example-calibration'\n", + "g.db_name = f'{g.name}.db'\n", + "g.storage = f'sqlite:///{g.db_name}'\n", + "g.n_workers = 4 # Define how many workers to run in parallel\n", + "g.n_trials = 25 # Define the number of trials, i.e. sim runs, per worker\n", + "\n", "\n", "def run_sim(pars, label=None, return_sim=False):\n", " ''' Create and run a simulation '''\n", @@ -189,40 +197,33 @@ "\n", "def worker():\n", " ''' Run a single worker '''\n", - " study = op.load_study(storage=storage, study_name=name)\n", - " output = study.optimize(run_trial, n_trials=n_trials)\n", + " study = op.load_study(storage=g.storage, study_name=g.name)\n", + " output = study.optimize(run_trial, n_trials=g.n_trials)\n", " return output\n", "\n", "\n", "def run_workers():\n", " ''' Run multiple workers in parallel '''\n", - " output = sc.parallelize(worker, n_workers)\n", + " output = sc.parallelize(worker, g.n_workers)\n", " return output\n", "\n", "\n", "def make_study():\n", " ''' Make a study, deleting one if it already exists '''\n", - " if os.path.exists(db_name):\n", - " os.remove(db_name)\n", - " print(f'Removed existing calibration {db_name}')\n", - " output = op.create_study(storage=storage, study_name=name)\n", + " if os.path.exists(g.db_name):\n", + " os.remove(g.db_name)\n", + " print(f'Removed existing calibration {g.db_name}')\n", + " output = op.create_study(storage=g.storage, study_name=g.name)\n", " return output\n", "\n", "\n", "if __name__ == '__main__':\n", "\n", - " # Settings\n", - " n_workers = 2 # Define how many workers to run in parallel\n", - " n_trials = 25 # Define the number of trials, i.e. sim runs, per worker\n", - " name = 'my-example-calibration'\n", - " db_name = f'{name}.db'\n", - " storage = f'sqlite:///{db_name}'\n", - "\n", " # Run the optimization\n", " t0 = sc.tic()\n", " make_study()\n", " run_workers()\n", - " study = op.load_study(storage=storage, study_name=name)\n", + " study = op.load_study(storage=g.storage, study_name=g.name)\n", " best_pars = study.best_params\n", " T = sc.toc(t0, output=True)\n", " print(f'\\n\\nOutput: {best_pars}, time: {T:0.1f} s')" @@ -259,8 +260,8 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", - "language": "python", + "display_name": "Python 3 (Spyder)", + "language": "python3", "name": "python3" }, "language_info": { From a8239882c7b27965d15a9aba897e694cdf75ef51 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 04:24:21 -0700 Subject: [PATCH 22/39] reimplemented transtree --- covasim/analysis.py | 159 +++++++++++++------------- tests/devtests/dev_test_transtree.py | 161 +++++++++++++++++++++++++++ 2 files changed, 238 insertions(+), 82 deletions(-) create mode 100644 tests/devtests/dev_test_transtree.py diff --git a/covasim/analysis.py b/covasim/analysis.py index d2fba94e8..81da0c790 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1227,6 +1227,9 @@ class TransTree(Analyzer): tt = sim.make_transtree() tt.plot() tt.plot_histograms() + + New in version 2.0.5: ``tt.detailed`` is a dataframe rather than a list of dictionaries; + for the latter, use ``tt.detailed.to_dict('records')``. ''' def __init__(self, sim, to_networkx=False, **kwargs): @@ -1251,8 +1254,8 @@ def __init__(self, sim, to_networkx=False, **kwargs): 'rerun with rescale=False and pop_scale=1 for reliable results.' print(warningmsg) - # Include the basic line list - self.infection_log = sc.dcp(people.infection_log) + # Include the basic line list -- copying directly is slow, so we'll make a copy later + self.infection_log = people.infection_log # Parse into sources and targets self.sources = [None for i in range(self.pop_size)] @@ -1362,81 +1365,73 @@ def count_targets(self, start_day=None, end_day=None): def make_detailed(self, people, reset=False): ''' Construct a detailed transmission tree, with additional information for each person ''' - # Convert to a dataframe and initialize - df = pd.DataFrame(self.infection_log) - detailed = [None]*self.pop_size - - for ddict in self.infection_log: - - # Pull out key quantities - source = ddict['source'] - target = ddict['target'] - ddict['s'] = {} # Source properties - ddict['t'] = {} # Target properties - - # If the source is available (e.g. not a seed infection), loop over both it and the target - if source is not None: - stdict = {'s':source, 't':target} - else: - stdict = {'t':target} - - # Pull out each of the attributes relevant to transmission - attrs = ['age', 'date_exposed', 'date_symptomatic', 'date_tested', 'date_diagnosed', 'date_quarantined', 'date_end_quarantine', 'date_severe', 'date_critical', 'date_known_contact'] - for st,stind in stdict.items(): - for attr in attrs: - ddict[st][attr] = people[attr][stind] - if source is not None: - for attr in attrs: - if attr.startswith('date_'): - is_attr = attr.replace('date_', 'is_') # Convert date to a boolean, e.g. date_diagnosed -> is_diagnosed - if attr == 'date_quarantined': # This has an end date specified - ddict['s'][is_attr] = ddict['s'][attr] <= ddict['date'] and not (ddict['s']['date_end_quarantine'] <= ddict['date']) - elif attr != 'date_end_quarantine': # This is not a state - ddict['s'][is_attr] = ddict['s'][attr] <= ddict['date'] # These don't make sense for people just infected (targets), only sources - - ddict['s']['is_asymp'] = np.isnan(people.date_symptomatic[source]) - ddict['s']['is_presymp'] = ~ddict['s']['is_asymp'] and ~ddict['s']['is_symptomatic'] # Not asymptomatic and not currently symptomatic - ddict['t']['is_quarantined'] = ddict['t']['date_quarantined'] <= ddict['date'] and not (ddict['t']['date_end_quarantine'] <= ddict['date']) # This is the only target date that it makes sense to define since it can happen before infection - - detailed[target] = ddict - - self.detailed = detailed - - # Also re-parse the transmission log and convert to a dataframe - ttlist = [] - for source_ind, target_ind in self.transmissions: - ddict = self.detailed[target_ind] - source = ddict['s'] - target = ddict['t'] - - tdict = {} - tdict['date'] = ddict['date'] - tdict['layer'] = ddict['layer'] - tdict['s_asymp'] = np.isnan(source['date_symptomatic']) # True if they *never* became symptomatic - tdict['s_presymp'] = ~tdict['s_asymp'] and tdict['date'] is_diagnosed + if attr == 'date_quarantined': # This has an end date specified + ddict['src_'+is_attr] = ddict['src_'+attr] <= transdict['date'] and not (ddict['src_'+'date_end_quarantine'] <= ddict['date']) + elif attr != 'date_end_quarantine': # This is not a state + ddict['src_'+is_attr] = ddict['src_'+attr] <= transdict['date'] # These don't make sense for people just infected (targets), only sources + + ddict['src_'+'is_asymp'] = np.isnan(people.date_symptomatic[source]) + ddict['src_'+'is_presymp'] = ~ddict['src_'+'is_asymp'] and ~ddict['src_'+'is_symptomatic'] # Not asymptomatic and not currently symptomatic + ddict['trg_'+'is_quarantined'] = ddict['trg_'+'date_quarantined'] <= transdict['date'] and not (ddict['trg_'+'date_end_quarantine'] <= ddict['date']) # This is the only target date that it makes sense to define since it can happen before infection + + ddict.update(transdict) + detailed[target] = ddict + +sc.toc() + +sc.heading('Validation...') + +sc.tic() + +for i in range(len(detailed)): + sc.percentcomplete(step=i, maxsteps=len(detailed), stepsize=10) + d_entry = detailed[i] + df_entry = ddf.iloc[i].to_dict() + if d_entry is None: # If in the dict it's None, it should be nan in the dataframe + assert np.isnan(df_entry['target']) + else: + dkeys = list(d_entry.keys()) + dfkeys = list(df_entry.keys()) + assert all([dk in dfkeys for dk in dkeys]) # The dataframe can have extra keys, but not the dict + for k in dkeys: + v_d = d_entry[k] + v_df = df_entry[k] + try: + assert np.isclose(v_d, v_df, equal_nan=True) # If it's numeric, check they're close + except TypeError: + if v_d is None: + assert np.isnan(v_df) # If in the dict it's None, it should be nan in the dataframe + else: + assert v_d == v_df # In all other cases, it should be an exact match + +print('\nValidation passed.') + +sc.toc() +print('Done.') \ No newline at end of file From 1777b858eae7f03659d1b81ca09fafad0a12a974 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 05:30:17 -0700 Subject: [PATCH 23/39] tests pass --- CHANGELOG.rst | 3 ++ covasim/analysis.py | 84 +++++++++++++++++++++++++++++++----------- tests/test_analysis.py | 10 ++--- 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cfea6860d..200159bd6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,9 @@ These are the major improvements we are currently working on. If there is a spec Latest versions (2.0.x) ~~~~~~~~~~~~~~~~~~~~~~~ +sim.plot(dpi=150, rotation=45, start_day='2020-03-01', end_day=55, interval=7) + + Version 2.0.4 (2021-03-19) -------------------------- diff --git a/covasim/analysis.py b/covasim/analysis.py index 81da0c790..10b7c1f4a 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1273,8 +1273,9 @@ def __init__(self, sim, to_networkx=False, **kwargs): self.source_dates[target] = date # Each target has at most one source self.target_dates[source].append(date) # Each source can have multiple targets - # Count the number of targets each person has - self.n_targets = self.count_targets() + # Count the number of targets each person has, and the list of transmissions + self.count_targets() + self.count_transmissions() # Include the detailed transmission tree as well, as a list and as a dataframe self.make_detailed(people) @@ -1311,20 +1312,6 @@ def __len__(self): return 0 - @property - def transmissions(self): - """ - Iterable over edges corresponding to transmission events - - This excludes edges corresponding to seeded infections without a source - """ - output = [] - for d in self.infection_log: - if d['source'] is not None: - output.append([d['source'], d['target']]) - return output - - def day(self, day=None, which=None): ''' Convenience function for converting an input to an integer day ''' if day is not None: @@ -1359,9 +1346,32 @@ def count_targets(self, start_day=None, end_day=None): n_targets[i] = len(self.targets[i]) n_target_inds = sc.findinds(~np.isnan(n_targets)) n_targets = n_targets[n_target_inds] + self.n_targets = n_targets return n_targets + def count_transmissions(self): + """ + Iterable over edges corresponding to transmission events + + This excludes edges corresponding to seeded infections without a source + """ + source_inds = [] + target_inds = [] + transmissions = [] + for d in self.infection_log: + if d['source'] is not None: + src = d['source'] + trg = d['target'] + source_inds.append(src) + target_inds.append(trg) + transmissions.append([src, trg]) + self.transmissions = transmissions + self.source_inds = source_inds + self.target_inds = target_inds + return transmissions + + def make_detailed(self, people, reset=False): ''' Construct a detailed transmission tree, with additional information for each person ''' @@ -1411,28 +1421,57 @@ def make_detailed(self, people, reset=False): ddf.loc[v_trg, src+attr] = people[attr][v_src] # Replace nan with false - def fillna(cols): + def fillna(nadf, cols): cols = sc.promotetolist(cols) filldict = {k:False for k in cols} - ddf.fillna(value=filldict, inplace=True) + nadf.fillna(value=filldict, inplace=True) return # Pull out valid indices for source and target ddf.loc[v_trg, src+'is_quarantined'] = (ddf.loc[v_trg, src+'date_quarantined'] <= vinfdates) & ~(ddf.loc[v_trg, src+'date_quarantined'] <= vinfdates) - fillna(src+'is_quarantined') + fillna(ddf, src+'is_quarantined') for is_attr,date_attr in zip(is_attrs, date_attrs): ddf.loc[v_trg, src+is_attr] = (ddf.loc[v_trg, src+date_attr] <= vinfdates) - fillna(src+is_attr) + fillna(ddf, src+is_attr) # Populate remaining properties ddf.loc[v_trg, src+'is_asymp'] = np.isnan(ddf.loc[v_trg, src+'date_symptomatic']) ddf.loc[v_trg, src+'is_presymp'] = ~ddf.loc[v_trg, src+'is_asymp'] & ~ddf.loc[v_trg, src+'is_symptomatic'] ddf.loc[trg_inds, trg+'is_quarantined'] = (ddf.loc[trg_inds, trg+'date_quarantined'] <= ainfdates) & ~(ddf.loc[trg_inds, trg+'date_end_quarantine'] <= ainfdates) - fillna(trg+'is_quarantined') + fillna(ddf, trg+'is_quarantined') # Store self.detailed = ddf + # Also re-parse the log and convert to a simpler dataframe + targets = np.array(self.target_inds) + df = pd.DataFrame(index=np.arange(len(targets))) + infdates = ddf.loc[targets, 'date'].values + df.loc[:, 'date'] = infdates + df.loc[:, 'layer'] = ddf.loc[targets, 'layer'].values + df.loc[:, 's_asymp'] = np.isnan(ddf.loc[targets, 'src_date_symptomatic'].values) + df.loc[:, 's_presymp'] = ~(df.loc[:, 's_asymp'].values) & (infdates < ddf.loc[targets, 'src_date_symptomatic'].values) + fillna(df, 's_presymp') + df.loc[:, 's_sev'] = ddf.loc[targets, 'src_date_severe'].values < infdates + df.loc[:, 's_crit'] = ddf.loc[targets, 'src_date_critical'].values < infdates + df.loc[:, 's_diag'] = ddf.loc[targets, 'src_date_diagnosed'].values < infdates + df.loc[:, 's_quar'] = (ddf.loc[targets, 'src_date_quarantined'].values < infdates) & ~(ddf.loc[targets, 'src_date_end_quarantine'].values <= infdates) + df.loc[:, 't_quar'] = (ddf.loc[targets, 'trg_date_quarantined'].values < infdates) & ~(ddf.loc[targets, 'trg_date_end_quarantine'].values <= infdates) + fillna(df, ['s_sev', 's_crit', 's_diag', 's_quar', 't_quar']) + + df = df.rename(columns={'date': 'Day'}) # For use in plotting + df = df.loc[df['layer'] != 'seed_infection'] + + df['Stage'] = 'Symptomatic' + df.loc[df['s_asymp'], 'Stage'] = 'Asymptomatic' + df.loc[df['s_presymp'], 'Stage'] = 'Presymptomatic' + + df['Severity'] = 'Mild' + df.loc[df['s_sev'], 'Severity'] = 'Severe' + df.loc[df['s_crit'], 'Severity'] = 'Critical' + + self.df = df + return @@ -1563,7 +1602,8 @@ def animate(self, *args, **kwargs): source_ind = ddict['source'] # Index of the person who infected the target target_date = ddict['date'] - if source_ind is not None: # Seed infections and importations won't have a source + if ~np.isnan(source_ind): # Seed infections and importations won't have a source + source_ind = int(source_ind) source_date = detailed[source_ind]['date'] else: source_ind = 0 diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 7c637c058..4b60a0711 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -160,11 +160,11 @@ def test_transtree(): cv.options.set(interactive=do_plot) T = sc.tic() - snapshot = test_snapshot() - agehist = test_age_hist() - daily_age = test_daily_age() - daily = test_daily_stats() - fit = test_fit() + # snapshot = test_snapshot() + # agehist = test_age_hist() + # daily_age = test_daily_age() + # daily = test_daily_stats() + # fit = test_fit() transtree = test_transtree() print('\n'*2) From 6c7c54244959da6f97636b4658454bb877a9b29d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 05:32:18 -0700 Subject: [PATCH 24/39] uncomment tests --- tests/test_analysis.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 4b60a0711..7c637c058 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -160,11 +160,11 @@ def test_transtree(): cv.options.set(interactive=do_plot) T = sc.tic() - # snapshot = test_snapshot() - # agehist = test_age_hist() - # daily_age = test_daily_age() - # daily = test_daily_stats() - # fit = test_fit() + snapshot = test_snapshot() + agehist = test_age_hist() + daily_age = test_daily_age() + daily = test_daily_stats() + fit = test_fit() transtree = test_transtree() print('\n'*2) From 2c516328e1300389b540cd76020d2379c4f96bfc Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 17:16:48 -0700 Subject: [PATCH 25/39] redoing transmission tree --- covasim/analysis.py | 74 ++++++++++++++++++++++++--------------------- covasim/utils.py | 2 +- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index 10b7c1f4a..bb7e48047 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1344,7 +1344,7 @@ def count_targets(self, start_day=None, end_day=None): if self.sources[i] is not None: if self.source_dates[i] >= start_day and self.source_dates[i] <= end_day: n_targets[i] = len(self.targets[i]) - n_target_inds = sc.findinds(~np.isnan(n_targets)) + n_target_inds = sc.findinds(np.isfinite(n_targets)) n_targets = n_targets[n_target_inds] self.n_targets = n_targets return n_targets @@ -1375,8 +1375,15 @@ def count_transmissions(self): def make_detailed(self, people, reset=False): ''' Construct a detailed transmission tree, with additional information for each person ''' - # Convert infection log to a dataframe and initialize - inf_df = sc.dcp(pd.DataFrame(self.infection_log)) + def df_to_arrdict(df): + ''' Convert a dataframe to a dictionary of arrays ''' + arrdict = dict() + for col in df.columns: + arrdict[col] = df[col].values + return arrdict + + # Convert infection log to a dataframe and from there to a dict of arrays + inflog = df_to_arrdict(sc.dcp(pd.DataFrame(self.infection_log))) # Initialization n_people = len(people) @@ -1386,45 +1393,43 @@ def make_detailed(self, people, reset=False): quar_attrs = ['date_quarantined', 'date_end_quarantine'] date_attrs = [attr for attr in attrs if attr.startswith('date_')] is_attrs = [attr.replace('date_', 'is_') for attr in date_attrs] - ddf = pd.DataFrame(index=np.arange(n_people)) + dd_arr = lambda: np.nan*np.zeros(n_people) + dd = sc.odict(defaultdict=dd_arr) # Data dictionary, to be converted to a dataframe later # Handle indices - trg_inds = np.array(inf_df['target'].values, dtype=np.int64) - src_inds = np.array(inf_df['source'].values) - date_vals = np.array(inf_df['date'].values) - layer_vals = np.array(inf_df['layer'].values) - src_arr = np.nan*np.zeros(n_people) - trg_arr = np.nan*np.zeros(n_people) - infdate_arr = np.nan*np.zeros(n_people) + src_arr = dd_arr() + trg_arr = dd_arr() + date_arr = dd_arr() # Map onto arrays - src_arr[trg_inds] = src_inds - trg_arr[trg_inds] = trg_inds - infdate_arr[trg_inds] = date_vals + t_inds = np.array(inflog['target'], dtype=np.int64) + src_arr[t_inds] = inflog['source'] + trg_arr[t_inds] = t_inds + date_arr[t_inds] = inflog['date'] # Further index wrangling - ts_inds = sc.findinds(~np.isnan(trg_arr) * ~np.isnan(src_arr)) # Valid target-source indices - v_src = np.array(src_arr[ts_inds], dtype=np.int64) # Valid source indices - v_trg = np.array(trg_arr[ts_inds], dtype=np.int64) # Valid target indices - vinfdates = infdate_arr[v_trg] # Valid target-source pair infection dates - ainfdates = infdate_arr[trg_inds] # All infection dates + vts_inds = sc.findinds(np.isfinite(trg_arr) * np.isfinite(src_arr)) # Valid target-source indices + vs_inds = np.array(src_arr[vts_inds], dtype=np.int64) # Valid source indices + vt_inds = np.array(trg_arr[vts_inds], dtype=np.int64) # Valid target indices + vinfdates = date_arr[vt_inds] # Valid target-source pair infection dates + ainfdates = date_arr[t_inds] # All infection dates # Populate main columns - ddf.loc[v_trg, 'source'] = v_src - ddf.loc[trg_inds, 'target'] = trg_inds - ddf.loc[trg_inds, 'date'] = ainfdates - ddf.loc[trg_inds, 'layer'] = layer_vals + dd['source'][vt_inds] = vs_inds + dd['target'][t_inds] = t_inds + dd['date'][t_inds] = ainfdates + dd['layer'][t_inds] = inflog['layer'] # Populate from people for attr in attrs+quar_attrs: - ddf.loc[:, trg+attr] = people[attr][:] - ddf.loc[v_trg, src+attr] = people[attr][v_src] + dd[trg+attr] = people[attr][:] + dd[src+attr][vt_inds] = people[attr][vs_inds] # Replace nan with false - def fillna(nadf, cols): + def fillna(arrdict, cols, value=False): cols = sc.promotetolist(cols) - filldict = {k:False for k in cols} - nadf.fillna(value=filldict, inplace=True) + for col in cols: + arrdict[col][np.isnan(arrdict[col])] = value return # Pull out valid indices for source and target @@ -1441,11 +1446,12 @@ def fillna(nadf, cols): fillna(ddf, trg+'is_quarantined') # Store - self.detailed = ddf + self.detailed = pd.DataFrame(dd) # Also re-parse the log and convert to a simpler dataframe targets = np.array(self.target_inds) df = pd.DataFrame(index=np.arange(len(targets))) + ddft = ddf.loc[targets].reset_index() # Pull out only target values DO ABOVE TOO infdates = ddf.loc[targets, 'date'].values df.loc[:, 'date'] = infdates df.loc[:, 'layer'] = ddf.loc[targets, 'layer'].values @@ -1597,12 +1603,12 @@ def animate(self, *args, **kwargs): tdq = {} # Short for "tested, diagnosed, or quarantined" target_ind = ddict['target'] - if not np.isnan(ddict['date']): # If this person was infected + if np.isfinite(ddict['date']): # If this person was infected source_ind = ddict['source'] # Index of the person who infected the target target_date = ddict['date'] - if ~np.isnan(source_ind): # Seed infections and importations won't have a source + if np.isfinite(source_ind): # Seed infections and importations won't have a source source_ind = int(source_ind) source_date = detailed[source_ind]['date'] else: @@ -1623,11 +1629,11 @@ def animate(self, *args, **kwargs): date_t = ddict['trg_date_tested'] date_d = ddict['trg_date_diagnosed'] date_q = ddict['trg_date_known_contact'] - if ~np.isnan(date_t) and date_t < n: + if np.isfinite(date_t) and date_t < n: tests[int(date_t)].append(tdq) - if ~np.isnan(date_d) and date_d < n: + if np.isfinite(date_d) and date_d < n: diags[int(date_d)].append(tdq) - if ~np.isnan(date_q) and date_q < n: + if np.isfinite(date_q) and date_q < n: quars[int(date_q)].append(tdq) else: diff --git a/covasim/utils.py b/covasim/utils.py index f9ad881f4..891870cfc 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -546,7 +546,7 @@ def ifalse(arr, inds): def idefined(arr, inds): ''' - Returns the indices that are true in the array -- name is short for indices[defined] + Returns the indices that are defined in the array -- name is short for indices[defined] Args: arr (array): any array, used as a filter From 0bdaa60f9fe79b422ad88a30a927ae185f63d391 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 17:55:17 -0700 Subject: [PATCH 26/39] finish transtree reimplementation --- covasim/analysis.py | 81 ++++++++++++---------------- tests/devtests/dev_test_transtree.py | 20 ++++--- 2 files changed, 48 insertions(+), 53 deletions(-) diff --git a/covasim/analysis.py b/covasim/analysis.py index bb7e48047..960e0ad80 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1393,7 +1393,7 @@ def df_to_arrdict(df): quar_attrs = ['date_quarantined', 'date_end_quarantine'] date_attrs = [attr for attr in attrs if attr.startswith('date_')] is_attrs = [attr.replace('date_', 'is_') for attr in date_attrs] - dd_arr = lambda: np.nan*np.zeros(n_people) + dd_arr = lambda: np.nan*np.zeros(n_people) # Create an empty array of the right size dd = sc.odict(defaultdict=dd_arr) # Data dictionary, to be converted to a dataframe later # Handle indices @@ -1402,69 +1402,56 @@ def df_to_arrdict(df): date_arr = dd_arr() # Map onto arrays - t_inds = np.array(inflog['target'], dtype=np.int64) - src_arr[t_inds] = inflog['source'] - trg_arr[t_inds] = t_inds - date_arr[t_inds] = inflog['date'] + ti = np.array(inflog['target'], dtype=np.int64) # "Target indices", short since used so much + src_arr[ti] = inflog['source'] + trg_arr[ti] = ti + date_arr[ti] = inflog['date'] # Further index wrangling vts_inds = sc.findinds(np.isfinite(trg_arr) * np.isfinite(src_arr)) # Valid target-source indices vs_inds = np.array(src_arr[vts_inds], dtype=np.int64) # Valid source indices - vt_inds = np.array(trg_arr[vts_inds], dtype=np.int64) # Valid target indices - vinfdates = date_arr[vt_inds] # Valid target-source pair infection dates - ainfdates = date_arr[t_inds] # All infection dates + vi = np.array(trg_arr[vts_inds], dtype=np.int64) # Valid target indices, short since used so much + vinfdates = date_arr[vi] # Valid target-source pair infection dates + tinfdates = date_arr[ti] # All target infection dates # Populate main columns - dd['source'][vt_inds] = vs_inds - dd['target'][t_inds] = t_inds - dd['date'][t_inds] = ainfdates - dd['layer'][t_inds] = inflog['layer'] + dd['source'][vi] = vs_inds + dd['target'][ti] = ti + dd['date'][ti] = tinfdates + dd['layer'] = np.array(dd['layer'], dtype=object) + dd['layer'][ti] = inflog['layer'] # Populate from people for attr in attrs+quar_attrs: dd[trg+attr] = people[attr][:] - dd[src+attr][vt_inds] = people[attr][vs_inds] - - # Replace nan with false - def fillna(arrdict, cols, value=False): - cols = sc.promotetolist(cols) - for col in cols: - arrdict[col][np.isnan(arrdict[col])] = value - return + dd[src+attr][vi] = people[attr][vs_inds] # Pull out valid indices for source and target - ddf.loc[v_trg, src+'is_quarantined'] = (ddf.loc[v_trg, src+'date_quarantined'] <= vinfdates) & ~(ddf.loc[v_trg, src+'date_quarantined'] <= vinfdates) - fillna(ddf, src+'is_quarantined') + lnot = np.logical_not # Shorten since used heavily + dd[src+'is_quarantined'][vi] = (dd[src+'date_quarantined'][vi] <= vinfdates) & lnot(dd[src+'date_quarantined'][vi] <= vinfdates) for is_attr,date_attr in zip(is_attrs, date_attrs): - ddf.loc[v_trg, src+is_attr] = (ddf.loc[v_trg, src+date_attr] <= vinfdates) - fillna(ddf, src+is_attr) + dd[src+is_attr][vi] = np.array(dd[src+date_attr][vi] <= vinfdates, dtype=bool) # Populate remaining properties - ddf.loc[v_trg, src+'is_asymp'] = np.isnan(ddf.loc[v_trg, src+'date_symptomatic']) - ddf.loc[v_trg, src+'is_presymp'] = ~ddf.loc[v_trg, src+'is_asymp'] & ~ddf.loc[v_trg, src+'is_symptomatic'] - ddf.loc[trg_inds, trg+'is_quarantined'] = (ddf.loc[trg_inds, trg+'date_quarantined'] <= ainfdates) & ~(ddf.loc[trg_inds, trg+'date_end_quarantine'] <= ainfdates) - fillna(ddf, trg+'is_quarantined') - - # Store - self.detailed = pd.DataFrame(dd) + dd[src+'is_asymp'][vi] = np.isnan(dd[src+'date_symptomatic'][vi]) + dd[src+'is_presymp'][vi] = lnot(dd[src+'is_asymp'][vi]) & lnot(dd[src+'is_symptomatic'][vi]) + dd[trg+'is_quarantined'][ti] = (dd[trg+'date_quarantined'][ti] <= tinfdates) & lnot(dd[trg+'date_end_quarantine'][ti] <= tinfdates) # Also re-parse the log and convert to a simpler dataframe targets = np.array(self.target_inds) - df = pd.DataFrame(index=np.arange(len(targets))) - ddft = ddf.loc[targets].reset_index() # Pull out only target values DO ABOVE TOO - infdates = ddf.loc[targets, 'date'].values - df.loc[:, 'date'] = infdates - df.loc[:, 'layer'] = ddf.loc[targets, 'layer'].values - df.loc[:, 's_asymp'] = np.isnan(ddf.loc[targets, 'src_date_symptomatic'].values) - df.loc[:, 's_presymp'] = ~(df.loc[:, 's_asymp'].values) & (infdates < ddf.loc[targets, 'src_date_symptomatic'].values) - fillna(df, 's_presymp') - df.loc[:, 's_sev'] = ddf.loc[targets, 'src_date_severe'].values < infdates - df.loc[:, 's_crit'] = ddf.loc[targets, 'src_date_critical'].values < infdates - df.loc[:, 's_diag'] = ddf.loc[targets, 'src_date_diagnosed'].values < infdates - df.loc[:, 's_quar'] = (ddf.loc[targets, 'src_date_quarantined'].values < infdates) & ~(ddf.loc[targets, 'src_date_end_quarantine'].values <= infdates) - df.loc[:, 't_quar'] = (ddf.loc[targets, 'trg_date_quarantined'].values < infdates) & ~(ddf.loc[targets, 'trg_date_end_quarantine'].values <= infdates) - fillna(df, ['s_sev', 's_crit', 's_diag', 's_quar', 't_quar']) - + dtr = dict() + infdates = dd['date'][targets] + dtr['date'] = infdates + dtr['layer'] = dd['layer'][targets] + dtr['s_asymp'] = np.isnan(dd['src_date_symptomatic'][targets]) + dtr['s_presymp'] = ~(dtr['s_asymp'][:]) & (infdates < dd['src_date_symptomatic'][targets]) + dtr['s_sev'] = dd['src_date_severe'][targets] < infdates + dtr['s_crit'] = dd['src_date_critical'][targets] < infdates + dtr['s_diag'] = dd['src_date_diagnosed'][targets] < infdates + dtr['s_quar'] = (dd['src_date_quarantined'][targets] < infdates) & lnot(dd['src_date_end_quarantine'][targets] <= infdates) + dtr['t_quar'] = (dd['trg_date_quarantined'][targets] < infdates) & lnot(dd['trg_date_end_quarantine'][targets] <= infdates) + + df = pd.DataFrame(dtr) df = df.rename(columns={'date': 'Day'}) # For use in plotting df = df.loc[df['layer'] != 'seed_infection'] @@ -1476,6 +1463,8 @@ def fillna(arrdict, cols, value=False): df.loc[df['s_sev'], 'Severity'] = 'Severe' df.loc[df['s_crit'], 'Severity'] = 'Critical' + # Store + self.detailed = pd.DataFrame(dd) self.df = df return diff --git a/tests/devtests/dev_test_transtree.py b/tests/devtests/dev_test_transtree.py index 2676793ad..f64c962ed 100644 --- a/tests/devtests/dev_test_transtree.py +++ b/tests/devtests/dev_test_transtree.py @@ -88,7 +88,7 @@ def fillna(cols): sc.toc() -sc.heading('Old implementation (dicts)...') +sc.heading('Original implementation (dicts)...') sc.tic() @@ -130,30 +130,36 @@ def fillna(cols): sc.toc() + sc.heading('Validation...') sc.tic() for i in range(len(detailed)): - sc.percentcomplete(step=i, maxsteps=len(detailed), stepsize=10) + sc.percentcomplete(step=i, maxsteps=len(detailed)-1, stepsize=5) d_entry = detailed[i] df_entry = ddf.iloc[i].to_dict() + tt_entry = tt.detailed.iloc[i].to_dict() if d_entry is None: # If in the dict it's None, it should be nan in the dataframe - assert np.isnan(df_entry['target']) + for entry in [df_entry, tt_entry]: + assert np.isnan(entry['target']) else: - dkeys = list(d_entry.keys()) + dkeys = list(d_entry.keys()) dfkeys = list(df_entry.keys()) + ttkeys = list(tt_entry.keys()) + assert dfkeys == ttkeys assert all([dk in dfkeys for dk in dkeys]) # The dataframe can have extra keys, but not the dict for k in dkeys: v_d = d_entry[k] v_df = df_entry[k] + v_tt = tt_entry[k] try: - assert np.isclose(v_d, v_df, equal_nan=True) # If it's numeric, check they're close + assert np.isclose(v_d, v_df, v_tt, equal_nan=True) # If it's numeric, check they're close except TypeError: if v_d is None: - assert np.isnan(v_df) # If in the dict it's None, it should be nan in the dataframe + assert all(np.isnan([v_df, v_tt])) # If in the dict it's None, it should be nan in the dataframe else: - assert v_d == v_df # In all other cases, it should be an exact match + assert v_d == v_df == v_tt # In all other cases, it should be an exact match print('\nValidation passed.') From d112c1a0cc460b2c2f0f33a75543a3fca46f2936 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 18:00:19 -0700 Subject: [PATCH 27/39] test updates --- tests/devtests/dev_test_transtree.py | 57 ++++++++++++++-------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/tests/devtests/dev_test_transtree.py b/tests/devtests/dev_test_transtree.py index f64c962ed..c225686ff 100644 --- a/tests/devtests/dev_test_transtree.py +++ b/tests/devtests/dev_test_transtree.py @@ -8,15 +8,16 @@ import numpy as np # Create a sim -sim = cv.Sim(pop_size=10e3, n_days=100).run() +sim = cv.Sim(pop_size=100e3, n_days=100).run() people = sim.people -sc.heading('Built-in implementation (pandas)...') -sc.tic() +sc.heading('Built-in implementation (Numpy)...') tt = sim.make_transtree() +sc.tic() +tt.make_detailed(sim.people) sc.toc() @@ -135,31 +136,31 @@ def fillna(cols): sc.tic() -for i in range(len(detailed)): - sc.percentcomplete(step=i, maxsteps=len(detailed)-1, stepsize=5) - d_entry = detailed[i] - df_entry = ddf.iloc[i].to_dict() - tt_entry = tt.detailed.iloc[i].to_dict() - if d_entry is None: # If in the dict it's None, it should be nan in the dataframe - for entry in [df_entry, tt_entry]: - assert np.isnan(entry['target']) - else: - dkeys = list(d_entry.keys()) - dfkeys = list(df_entry.keys()) - ttkeys = list(tt_entry.keys()) - assert dfkeys == ttkeys - assert all([dk in dfkeys for dk in dkeys]) # The dataframe can have extra keys, but not the dict - for k in dkeys: - v_d = d_entry[k] - v_df = df_entry[k] - v_tt = tt_entry[k] - try: - assert np.isclose(v_d, v_df, v_tt, equal_nan=True) # If it's numeric, check they're close - except TypeError: - if v_d is None: - assert all(np.isnan([v_df, v_tt])) # If in the dict it's None, it should be nan in the dataframe - else: - assert v_d == v_df == v_tt # In all other cases, it should be an exact match +# for i in range(len(detailed)): +# sc.percentcomplete(step=i, maxsteps=len(detailed)-1, stepsize=5) +# d_entry = detailed[i] +# df_entry = ddf.iloc[i].to_dict() +# tt_entry = tt.detailed.iloc[i].to_dict() +# if d_entry is None: # If in the dict it's None, it should be nan in the dataframe +# for entry in [df_entry, tt_entry]: +# assert np.isnan(entry['target']) +# else: +# dkeys = list(d_entry.keys()) +# dfkeys = list(df_entry.keys()) +# ttkeys = list(tt_entry.keys()) +# assert dfkeys == ttkeys +# assert all([dk in dfkeys for dk in dkeys]) # The dataframe can have extra keys, but not the dict +# for k in dkeys: +# v_d = d_entry[k] +# v_df = df_entry[k] +# v_tt = tt_entry[k] +# try: +# assert np.isclose(v_d, v_df, v_tt, equal_nan=True) # If it's numeric, check they're close +# except TypeError: +# if v_d is None: +# assert all(np.isnan([v_df, v_tt])) # If in the dict it's None, it should be nan in the dataframe +# else: +# assert v_d == v_df == v_tt # In all other cases, it should be an exact match print('\nValidation passed.') From 062aafd5b785e834cbc407be0b7e0b6930af4eed Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 19:01:36 -0700 Subject: [PATCH 28/39] working on docs --- docs/_static/theme_overrides.css | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css index 062cfb21e..82f1de029 100644 --- a/docs/_static/theme_overrides.css +++ b/docs/_static/theme_overrides.css @@ -45,6 +45,7 @@ div.document span.search-highlight { margin-bottom: 10px; } +/* CK: alternating table row colors */ .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td, .wy-table-backed, .wy-table-odd td, .wy-table-striped tr:nth-child(2n-1) td { background-color: #acf; } @@ -52,4 +53,11 @@ tr.row-even { background-color: #def; } -.highlight { background: #D9F0FF; } \ No newline at end of file +/* CK: Change the color of code blocks */ +.highlight { background: #D9F0FF; } + +/* CK: Change the color of inline code */ +code.literal { + color: #e01e5a !important; + background-color: #f6f6f6 !important; +} \ No newline at end of file From 9778252d436269bae95f0a99b4ed20e9ec1c7260 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 19:16:04 -0700 Subject: [PATCH 29/39] fixed styling inconsistency --- docs/_static/theme_overrides.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css index 82f1de029..b1084bc0d 100644 --- a/docs/_static/theme_overrides.css +++ b/docs/_static/theme_overrides.css @@ -21,7 +21,7 @@ h1 { margin-bottom: 1.0em; } -h2 { +h2, .toc-backref { font-size: 125%; color: #0055af !important; margin-bottom: 1.0em; From 18a1c824c7b2f965eb505f4849f77fe4528585dd Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 20:39:07 -0700 Subject: [PATCH 30/39] working on distributions --- CHANGELOG.rst | 6 ++- covasim/utils.py | 4 +- docs/_static/theme_overrides.css | 1 + tests/test_utils.py | 78 ++++++++++++++++++++++++++------ 4 files changed, 70 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 200159bd6..c27ea3a98 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,10 +24,12 @@ These are the major improvements we are currently working on. If there is a spec ~~~~~~~~~~~~~~~~~~~~~~~ -Latest versions (2.0.x) +Latest versions (2.x) ~~~~~~~~~~~~~~~~~~~~~~~ -sim.plot(dpi=150, rotation=45, start_day='2020-03-01', end_day=55, interval=7) +Version 2.1.0 (2021-03-23) +-------------------------- +- ``sim.plot(dpi=150, rotation=45, start_day='2020-03-01', end_day=55, interval=7)`` diff --git a/covasim/utils.py b/covasim/utils.py index 891870cfc..20afb76d0 100644 --- a/covasim/utils.py +++ b/covasim/utils.py @@ -189,8 +189,8 @@ def sample(dist=None, par1=None, par2=None, size=None, **kwargs): elif dist == 'neg_binomial': samples = n_neg_binomial(rate=par1, dispersion=par2, n=size, **kwargs) # Use custom version below elif dist in ['lognormal', 'lognormal_int']: if par1>0: - mean = np.log(par1**2 / np.sqrt(par2 + par1**2)) # Computes the mean of the underlying normal distribution - sigma = np.sqrt(np.log(par2/par1**2 + 1)) # Computes sigma for the underlying normal distribution + mean = np.log(par1**2 / np.sqrt(par2**2 + par1**2)) # Computes the mean of the underlying normal distribution + sigma = np.sqrt(np.log(par2**2/par1**2 + 1)) # Computes sigma for the underlying normal distribution samples = np.random.lognormal(mean=mean, sigma=sigma, size=size, **kwargs) else: samples = np.zeros(size) diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css index b1084bc0d..8574a6e10 100644 --- a/docs/_static/theme_overrides.css +++ b/docs/_static/theme_overrides.css @@ -21,6 +21,7 @@ h1 { margin-bottom: 1.0em; } +/* CK: added toc-backref since otherwise overrides this */ h2, .toc-backref { font-size: 125%; color: #0055af !important; diff --git a/tests/test_utils.py b/tests/test_utils.py index 3bdff4b02..1d4bdbd0a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -71,11 +71,11 @@ def test_poisson(): return s3 -def test_samples(do_plot=False): +def test_samples(do_plot=False, verbose=True): sc.heading('Samples distribution') - n = 10000 - nbins = 40 + n = 200_000 + nbins = 100 # Warning, must match utils.py! choices = [ @@ -85,6 +85,7 @@ def test_samples(do_plot=False): 'normal_pos', 'normal_int', 'lognormal_int', + 'poisson', 'neg_binomial' ] @@ -93,28 +94,75 @@ def test_samples(do_plot=False): # Run the samples nchoices = len(choices) - nsqr = np.ceil(np.sqrt(nchoices)) - results = {} + nsqr, _ = sc.get_rows_cols(nchoices) + results = sc.objdict() + mean = 11 + std = 7 + low = 3 + high = 9 + normal_dists = ['normal', 'normal_pos', 'normal_int', 'lognormal', 'lognormal_int'] for c,choice in enumerate(choices): - if choice == 'neg_binomial': - par1 = 10 - par2 = 0.5 - elif choice in ['lognormal', 'lognormal_int']: - par1 = 1 - par2 = 0.5 + kw = {} + if choice in normal_dists: + par1 = mean + par2 = std + elif choice == 'neg_binomial': + par1 = mean + par2 = 1.2 + kw['step'] = 0.1 + elif choice == 'poisson': + par1 = mean + par2 = 0 + elif choice == 'uniform': + par1 = low + par2 = high else: - par1 = 0 - par2 = 5 - results[choice] = cv.sample(dist=choice, par1=par1, par2=par2, size=n) + errormsg = f'Choice "{choice}" not implemented' + raise NotImplementedError(errormsg) + # Compute + results[choice] = cv.sample(dist=choice, par1=par1, par2=par2, size=n, **kw) + + # Optionally plot if do_plot: pl.subplot(nsqr, nsqr, c+1) - pl.hist(x=results[choice], bins=nbins) + plotbins = np.unique(results[choice]) if (choice=='poisson' or '_int' in choice) else nbins + pl.hist(x=results[choice], bins=plotbins, width=0.8) pl.title(f'dist={choice}, par1={par1}, par2={par2}') with pytest.raises(NotImplementedError): cv.sample(dist='not_found') + # Do statistical tests + tol = 1/np.sqrt(n/50/len(choices)) # Define acceptable tolerance -- broad to avoid false positives + + def isclose(choice, tol=tol, **kwargs): + key = list(kwargs.keys())[0] + ref = list(kwargs.values())[0] + npfunc = getattr(np, key) + value = npfunc(results[choice]) + msg = f'Test for {choice:14s}: expecting {key:4s} = {ref:8.4f} ± {tol*ref:8.4f} and got {value:8.4f}' + if verbose: + print(msg) + assert np.isclose(value, ref, rtol=tol), msg + return True + + # Normal + for choice in normal_dists: + isclose(choice, mean=mean) + if all([k not in choice for k in ['_pos', '_int']]): # These change the variance + isclose(choice, std=std) + + # Negative binomial + isclose('neg_binomial', mean=mean) + + # Poisson + isclose('poisson', mean=mean) + isclose('poisson', var=mean) + + # Uniform + isclose('uniform', mean=(low+high)/2) + return results From 6e41f87af5ba9430ef763f6e6b75d7960629e3b2 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 21:02:02 -0700 Subject: [PATCH 31/39] update regression --- covasim/analysis.py | 2 +- covasim/regression/pars_v2.1.0.json | 203 ++++++++++++++++++++++++++++ covasim/sim.py | 2 +- covasim/version.py | 4 +- tests/baseline.json | 72 +++++----- tests/benchmark.json | 6 +- 6 files changed, 246 insertions(+), 43 deletions(-) create mode 100644 covasim/regression/pars_v2.1.0.json diff --git a/covasim/analysis.py b/covasim/analysis.py index 960e0ad80..4425fb1dc 100644 --- a/covasim/analysis.py +++ b/covasim/analysis.py @@ -1228,7 +1228,7 @@ class TransTree(Analyzer): tt.plot() tt.plot_histograms() - New in version 2.0.5: ``tt.detailed`` is a dataframe rather than a list of dictionaries; + New in version 2.1.0: ``tt.detailed`` is a dataframe rather than a list of dictionaries; for the latter, use ``tt.detailed.to_dict('records')``. ''' diff --git a/covasim/regression/pars_v2.1.0.json b/covasim/regression/pars_v2.1.0.json new file mode 100644 index 000000000..6437ab382 --- /dev/null +++ b/covasim/regression/pars_v2.1.0.json @@ -0,0 +1,203 @@ +{ + "pop_size": 20000.0, + "pop_infected": 20, + "pop_type": "random", + "location": null, + "start_day": "2020-03-01", + "end_day": null, + "n_days": 60, + "rand_seed": 1, + "verbose": 0.1, + "pop_scale": 1, + "rescale": true, + "rescale_threshold": 0.05, + "rescale_factor": 1.2, + "beta": 0.016, + "contacts": { + "a": 20 + }, + "dynam_layer": { + "a": 0 + }, + "beta_layer": { + "a": 1.0 + }, + "n_imports": 0, + "beta_dist": { + "dist": "neg_binomial", + "par1": 1.0, + "par2": 0.45, + "step": 0.01 + }, + "viral_dist": { + "frac_time": 0.3, + "load_ratio": 2, + "high_cap": 4 + }, + "asymp_factor": 1.0, + "iso_factor": { + "a": 0.2 + }, + "quar_factor": { + "a": 0.3 + }, + "quar_period": 14, + "dur": { + "exp2inf": { + "dist": "lognormal_int", + "par1": 4.6, + "par2": 4.8 + }, + "inf2sym": { + "dist": "lognormal_int", + "par1": 1.0, + "par2": 0.9 + }, + "sym2sev": { + "dist": "lognormal_int", + "par1": 6.6, + "par2": 4.9 + }, + "sev2crit": { + "dist": "lognormal_int", + "par1": 3.0, + "par2": 7.4 + }, + "asym2rec": { + "dist": "lognormal_int", + "par1": 8.0, + "par2": 2.0 + }, + "mild2rec": { + "dist": "lognormal_int", + "par1": 8.0, + "par2": 2.0 + }, + "sev2rec": { + "dist": "lognormal_int", + "par1": 14.0, + "par2": 2.4 + }, + "crit2rec": { + "dist": "lognormal_int", + "par1": 14.0, + "par2": 2.4 + }, + "crit2die": { + "dist": "lognormal_int", + "par1": 6.2, + "par2": 1.7 + } + }, + "rel_symp_prob": 1.0, + "rel_severe_prob": 1.0, + "rel_crit_prob": 1.0, + "rel_death_prob": 1.0, + "prog_by_age": true, + "prognoses": { + "age_cutoffs": [ + 0, + 10, + 20, + 30, + 40, + 50, + 60, + 70, + 80, + 90 + ], + "sus_ORs": [ + 0.34, + 0.67, + 1.0, + 1.0, + 1.0, + 1.0, + 1.24, + 1.47, + 1.47, + 1.47 + ], + "trans_ORs": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "comorbidities": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "symp_probs": [ + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.9 + ], + "severe_probs": [ + 0.001, + 0.0029999999999999996, + 0.012, + 0.032, + 0.049, + 0.102, + 0.16599999999999998, + 0.24300000000000002, + 0.273, + 0.273 + ], + "crit_probs": [ + 0.06, + 0.04848484848484849, + 0.05, + 0.049999999999999996, + 0.06297376093294461, + 0.12196078431372549, + 0.2740210843373494, + 0.43200193657709995, + 0.708994708994709, + 0.708994708994709 + ], + "death_probs": [ + 0.6666666666666667, + 0.25, + 0.2777777777777778, + 0.30769230769230776, + 0.45370370370370366, + 0.2840300107181136, + 0.2104973893926903, + 0.2733385632634764, + 0.47600459242250287, + 0.9293915040183697 + ] + }, + "interventions": [], + "analyzers": [], + "timelimit": null, + "stopping_func": null, + "n_beds_hosp": null, + "n_beds_icu": null, + "no_hosp_factor": 2.0, + "no_icu_factor": 2.0 +} \ No newline at end of file diff --git a/covasim/sim.py b/covasim/sim.py index 7de93b49e..ab74fb931 100644 --- a/covasim/sim.py +++ b/covasim/sim.py @@ -1127,7 +1127,7 @@ def plot(self, *args, **kwargs): sim.run() sim.plot() - New in version 2.0.5: argument passing, date_args, and mpl_args + New in version 2.1.0: argument passing, date_args, and mpl_args ''' fig = cvplt.plot_sim(sim=self, *args, **kwargs) return fig diff --git a/covasim/version.py b/covasim/version.py index 522717cb0..d18ab2c6b 100644 --- a/covasim/version.py +++ b/covasim/version.py @@ -4,6 +4,6 @@ __all__ = ['__version__', '__versiondate__', '__license__'] -__version__ = '2.0.5' -__versiondate__ = '2021-03-22' +__version__ = '2.1.0' +__versiondate__ = '2021-03-23' __license__ = f'Covasim {__version__} ({__versiondate__}) — © 2021 by IDM' diff --git a/tests/baseline.json b/tests/baseline.json index 4ddd7caad..f81be5f62 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -1,41 +1,41 @@ { "summary": { - "cum_infections": 8873.0, - "cum_infectious": 8675.0, - "cum_tests": 10454.0, - "cum_diagnoses": 3385.0, - "cum_recoveries": 7578.0, - "cum_symptomatic": 5824.0, - "cum_severe": 436.0, - "cum_critical": 111.0, - "cum_deaths": 22.0, - "cum_quarantined": 4416.0, - "new_infections": 16.0, - "new_infectious": 65.0, - "new_tests": 201.0, - "new_diagnoses": 55.0, - "new_recoveries": 180.0, - "new_symptomatic": 57.0, - "new_severe": 7.0, - "new_critical": 3.0, - "new_deaths": 2.0, - "new_quarantined": 230.0, - "n_susceptible": 11127.0, - "n_exposed": 1273.0, - "n_infectious": 1075.0, - "n_symptomatic": 768.0, - "n_severe": 195.0, - "n_critical": 49.0, - "n_diagnosed": 3385.0, - "n_quarantined": 4279.0, - "n_alive": 19978.0, - "n_preinfectious": 198.0, - "n_removed": 7600.0, - "prevalence": 0.06372009210131144, - "incidence": 0.001437943740451155, - "r_eff": 0.13863684353019246, + "cum_infections": 9990.0, + "cum_infectious": 9747.0, + "cum_tests": 10766.0, + "cum_diagnoses": 3909.0, + "cum_recoveries": 8778.0, + "cum_symptomatic": 6662.0, + "cum_severe": 527.0, + "cum_critical": 125.0, + "cum_deaths": 41.0, + "cum_quarantined": 3711.0, + "new_infections": 24.0, + "new_infectious": 54.0, + "new_tests": 198.0, + "new_diagnoses": 41.0, + "new_recoveries": 138.0, + "new_symptomatic": 39.0, + "new_severe": 8.0, + "new_critical": 1.0, + "new_deaths": 0.0, + "new_quarantined": 165.0, + "n_susceptible": 10010.0, + "n_exposed": 1171.0, + "n_infectious": 928.0, + "n_symptomatic": 683.0, + "n_severe": 158.0, + "n_critical": 35.0, + "n_diagnosed": 3909.0, + "n_quarantined": 3577.0, + "n_alive": 19959.0, + "n_preinfectious": 243.0, + "n_removed": 8819.0, + "prevalence": 0.05867027406182675, + "incidence": 0.0023976023976023976, + "r_eff": 0.2434089263397735, "doubling_time": 30.0, - "test_yield": 0.2736318407960199, - "rel_test_yield": 4.223602915654286 + "test_yield": 0.20707070707070707, + "rel_test_yield": 3.5813414315569485 } } \ No newline at end of file diff --git a/tests/benchmark.json b/tests/benchmark.json index e93ac84d1..1fee3c306 100644 --- a/tests/benchmark.json +++ b/tests/benchmark.json @@ -1,12 +1,12 @@ { "time": { - "initialize": 0.429, - "run": 0.496 + "initialize": 0.38, + "run": 0.487 }, "parameters": { "pop_size": 20000, "pop_type": "hybrid", "n_days": 60 }, - "cpu_performance": 0.9461125294823827 + "cpu_performance": 0.9960878831767238 } \ No newline at end of file From 91b11eb2803ea41ac572eeb3c45324733e1ced29 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 21:48:57 -0700 Subject: [PATCH 32/39] updated regression --- covasim/misc.py | 41 +++++++++++++++++++++++++++++++++++++++-- covasim/parameters.py | 4 ++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/covasim/misc.py b/covasim/misc.py index f342078f2..49bfa9b94 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -98,7 +98,7 @@ def load_data(datafile, columns=None, calculate=True, check_date=True, verbose=T return data -def load(*args, do_migrate=True, **kwargs): +def load(*args, do_migrate=True, update=True, verbose=True, **kwargs): ''' Convenience method for sc.loadobj() and equivalent to cv.Sim.load() or cv.Scenarios.load(). @@ -106,6 +106,8 @@ def load(*args, do_migrate=True, **kwargs): Args: filename (str): file to load do_migrate (bool): whether to migrate if loading an old object + update (bool): whether to modify the object to reflect the new version + verbose (bool): whether to print migration information args (list): passed to sc.loadobj() kwargs (dict): passed to sc.loadobj() @@ -125,7 +127,7 @@ def load(*args, do_migrate=True, **kwargs): if cmp != 0: print(f'Note: you have Covasim v{v_curr}, but are loading an object from v{v_obj}') if do_migrate: - obj = migrate(obj, v_obj, v_curr) + obj = migrate(obj, update=update, verbose=verbose) return obj @@ -152,6 +154,33 @@ def save(*args, **kwargs): return filepath +def migrate_lognormal(pars, revert=False, verbose=True): + ''' + Small helper function to automatically migrate the standard deviation of lognormal + distributions to match pre-v2.1.0 runs (where it was treated as the variance instead). + To undo the migration, run with revert=True. + ''' + # Convert each value to the square root, since squared in the new version + for key,dur in pars['dur'].items(): + if 'lognormal' in dur['dist']: + old = dur['par2'] + if revert: + new = old**2 + else: + new = np.sqrt(old) + dur['par2'] = new + if verbose > 1: + print(f' Updating {key} std from {old:0.2f} to {new:0.2f}') + + # Store whether migration has occurred so we don't accidentally do it twice + if not revert: + pars['migrated_lognormal'] = True + else: + pars.pop('migrated_lognormal', None) + + return + + def migrate(obj, update=True, verbose=True, die=False): ''' Define migrations allowing compatibility between different versions of saved @@ -174,6 +203,7 @@ def migrate(obj, update=True, verbose=True, die=False): sims = cv.load('my-list-of-sims.obj') sims = [cv.migrate(sim) for sim in sims] ''' + # Import here to avoid recursion from . import base as cvb from . import run as cvr from . import interventions as cvi @@ -203,6 +233,13 @@ def migrate(obj, update=True, verbose=True, die=False): except: pass + # Migration from <2.1.0 to 2.1.0 + if sc.compareversions(sim.version, '2.1.0') == -1: # Migrate from <2.0 to 2.0 + if verbose: + print(f'Migrating sim from version {sim.version} to version {cvv.__version__}') + print('Note: updating lognormal stds to restore previous behavior; see v2.1.0 changelog for details') + migrate_lognormal(sim.pars, verbose=verbose) + # Migrations for People elif isinstance(obj, cvb.BasePeople): # pragma: no cover ppl = obj diff --git a/covasim/parameters.py b/covasim/parameters.py index d4a9c1ce4..605bb460c 100644 --- a/covasim/parameters.py +++ b/covasim/parameters.py @@ -109,6 +109,10 @@ def make_pars(set_prognoses=False, prog_by_age=True, version=None, **kwargs): if key in version_pars: # Only replace keys that exist in the old version pars[key] = version_pars[key] + # Handle code change migration + if sc.compareversions(version, '2.1.0') == -1 and 'migrate_lognormal' not in pars: + cvm.migrate_lognormal(pars, verbose=pars['verbose']) + return pars From a87b4bfd8c0f63201926bef67ed2a2a5679d14a6 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 23:10:54 -0700 Subject: [PATCH 33/39] starting on changelog --- CHANGELOG.rst | 12 ++--- tests/devtests/dev_test_transtree.py | 66 ++++++++++++++-------------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c27ea3a98..8f67a371a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,16 +9,14 @@ All notable changes to the codebase are documented in this file. Changes that ma :depth: 1 -~~~~~~~~~~~~~~~~~~~~ -Future release plans -~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~ +Coming soon +~~~~~~~~~~~ These are the major improvements we are currently working on. If there is a specific bugfix or feature you would like to see, please `create an issue `__. -- Additional flexibility in plotting options (e.g. date ranges, per-plot DPI) +- Mechanistic handling of different strains, and improved handling of vaccination, including more detailed targeting options, waning immunity, etc.. This will be Covasim 3.0, which is slated for release early April. - Expanded tutorials (health care workers, vaccination, calibration, exercises, etc.) -- Improved handling of vaccination, including more detailed targeting options, waning immunity, etc. -- Mechanistic handling of different strains - Multi-region and geospatial support - Economics and costing analysis @@ -30,6 +28,8 @@ Latest versions (2.x) Version 2.1.0 (2021-03-23) -------------------------- - ``sim.plot(dpi=150, rotation=45, start_day='2020-03-01', end_day=55, interval=7)`` +- *Regression information*: +- *GitHub info*: PR `859 `__ diff --git a/tests/devtests/dev_test_transtree.py b/tests/devtests/dev_test_transtree.py index c225686ff..4e7673b12 100644 --- a/tests/devtests/dev_test_transtree.py +++ b/tests/devtests/dev_test_transtree.py @@ -7,8 +7,11 @@ import sciris as sc import numpy as np +# Whether to validate (slow!) +validate = 1 + # Create a sim -sim = cv.Sim(pop_size=100e3, n_days=100).run() +sim = cv.Sim(pop_size=20e3, n_days=100).run() people = sim.people @@ -132,37 +135,36 @@ def fillna(cols): sc.toc() -sc.heading('Validation...') - -sc.tic() +if validate: + sc.heading('Validation...') + sc.tic() + for i in range(len(detailed)): + sc.percentcomplete(step=i, maxsteps=len(detailed)-1, stepsize=5) + d_entry = detailed[i] + df_entry = ddf.iloc[i].to_dict() + tt_entry = tt.detailed.iloc[i].to_dict() + if d_entry is None: # If in the dict it's None, it should be nan in the dataframe + for entry in [df_entry, tt_entry]: + assert np.isnan(entry['target']) + else: + dkeys = list(d_entry.keys()) + dfkeys = list(df_entry.keys()) + ttkeys = list(tt_entry.keys()) + assert dfkeys == ttkeys + assert all([dk in dfkeys for dk in dkeys]) # The dataframe can have extra keys, but not the dict + for k in dkeys: + v_d = d_entry[k] + v_df = df_entry[k] + v_tt = tt_entry[k] + try: + assert np.isclose(v_d, v_df, v_tt, equal_nan=True) # If it's numeric, check they're close + except TypeError: + if v_d is None: + assert all(np.isnan([v_df, v_tt])) # If in the dict it's None, it should be nan in the dataframe + else: + assert v_d == v_df == v_tt # In all other cases, it should be an exact match + sc.toc() + print('\nValidation passed.') -# for i in range(len(detailed)): -# sc.percentcomplete(step=i, maxsteps=len(detailed)-1, stepsize=5) -# d_entry = detailed[i] -# df_entry = ddf.iloc[i].to_dict() -# tt_entry = tt.detailed.iloc[i].to_dict() -# if d_entry is None: # If in the dict it's None, it should be nan in the dataframe -# for entry in [df_entry, tt_entry]: -# assert np.isnan(entry['target']) -# else: -# dkeys = list(d_entry.keys()) -# dfkeys = list(df_entry.keys()) -# ttkeys = list(tt_entry.keys()) -# assert dfkeys == ttkeys -# assert all([dk in dfkeys for dk in dkeys]) # The dataframe can have extra keys, but not the dict -# for k in dkeys: -# v_d = d_entry[k] -# v_df = df_entry[k] -# v_tt = tt_entry[k] -# try: -# assert np.isclose(v_d, v_df, v_tt, equal_nan=True) # If it's numeric, check they're close -# except TypeError: -# if v_d is None: -# assert all(np.isnan([v_df, v_tt])) # If in the dict it's None, it should be nan in the dataframe -# else: -# assert v_d == v_df == v_tt # In all other cases, it should be an exact match - -print('\nValidation passed.') -sc.toc() print('Done.') \ No newline at end of file From e230d84666367ef156b2a7528fd6bd38adc728d8 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 23:46:34 -0700 Subject: [PATCH 34/39] update changelog --- CHANGELOG.rst | 48 +++++++++++++++++++++++++++++++++++++++++---- covasim/misc.py | 10 ++++++++++ covasim/plotting.py | 4 ++-- covasim/settings.py | 5 ----- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8f67a371a..531a0e369 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,14 +21,54 @@ These are the major improvements we are currently working on. If there is a spec - Economics and costing analysis -~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~ Latest versions (2.x) -~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~ Version 2.1.0 (2021-03-23) -------------------------- -- ``sim.plot(dpi=150, rotation=45, start_day='2020-03-01', end_day=55, interval=7)`` -- *Regression information*: + +This is the last release before the Covasim 3.0 launch (vaccines and variants). + +Highlights +^^^^^^^^^^ +- **Updated lognormal distributions**: Lognormal distributions had been inadvertently using the variance instead of the standard deviation as the second parameter, resulting in too small variance. This has been fixed. +- **Expanded plotting features**: You now have much more flexibility with passing arguments to ``sim.plot()``, such as to temporarily set global Matplotlib options (such as DPI), modify axis styles and limits, etc. For example, you can now do things like this: ``cv.Sim().run().plot(dpi=150, rotation=30, start_day='2020-03-01', end_day=55, interval=7)``. +- **Improved analyzers**: Transmission trees can be computed 20x faster, Fit objects are more forgiving for data problems, and analyzers can now be exported to JSON. + +Bugfixes +^^^^^^^^ +- Previously, the lognormal distributions were unintentionally using the variance of the distribution, instead of the standard deviation, as the second parameter. This makes a small difference to the results (slightly higher transmission due to the increased variance). Old simulations that are loaded will automatically have their parameters updated so they give the same results; however, new simulations will now give slightly different results than they did previously. (Thanks to Ace Thompson for identifying this.) +- If a results object has low and high values, these are now exported to JSON (and also to Excel). +- MultiSim and Scenarios ``run.()`` methods now return themselves, as Sim does. This means that just as you can do ``sim.run().plot()``, you can also now do ``msim.run().plot()``. + +Plotting and options +^^^^^^^^^^^^^^^^^^^^ +- Standard plots now accept keyword arguments that will be passed around to all available subfunctions. For example, if you specify ``dpi=150``, Covasim knows that this is a Matplotlib setting and will configure it accordingly; likewise things like ``bottom`` (only for axes), ``frameon`` (only for legends), etc. If you pass an ambiguous keyword (e.g. ``alpha``, which is used for line and scatter plots), it will only be used for the *first* one. +- There is a new keyword argument, ``date_args``, that will format the x-axis: options include ``dateformat`` (e.g. ``%Y-%m-%d``), ``rotation`` (to avoid label collisions), and ``start_day`` and ``end_day``. +- Default plotting styles have updated, including less intrusive lines for interventions. + +Other changes +^^^^^^^^^^^^^ +- MultiSims now have ``to_json()`` and ``to_excel()`` methods, which are shortcuts for calling these methods on the base sim. +- If no label is supplied to an analyzer or intervention, it will use its class name (e.g. the default label for ``cv.change_beta`` is ``'change_beta'``). +- Analyzers now have a ``to_json()`` method. +- The ``cv.Fit`` and ``cv.TransTree`` classes now derive from ``Analyzer``, giving them some new methods and attributes. +- ``cv.sim.compute_fit()`` has a new keyword argument, ``die``, that will print warnings rather than raise exceptions if no matching data is found. Exceptions are now caught and helpful error messages are provided (e.g., if dates don't match). +- The algorithm for ``cv.TransTree`` has been rewritten, and now runs 20x as fast. The detailed transmission tree, in ``tt.detailed``, is now a pandas dataframe rather than a list of dictionaries. To restore something close to the previous version, use ``tt.detailed.to_dict('records')``. +- A data file with an integer rather than date "date" index can now be loaded; these will be counted relative to the simulation's start day. +- ``cv.load()`` has two new keyword arguments, ``update`` and ``verbose``, than are passed to ``cv.migrate()``. +- ``cv.options`` has new a ``get_default()`` method which returns the value of that parameter when Covasim was first loaded. + +Documentation and testing +^^^^^^^^^^^^^^^^^^^^^^^^^ +- An extra tutorial has been added on "Deployment", covering how to use it with `Dask `__ and for using Covasim with interactive notebooks and websites. +- Tutorials 7 and 10 have been updated so they work on Windows machines. +- Additional unit tests have been written to check the statistical properties of the sampling algorithms. + +Regression information +^^^^^^^^^^^^^^^^^^^^^^ + - *GitHub info*: PR `859 `__ diff --git a/covasim/misc.py b/covasim/misc.py index 49bfa9b94..9baaca45e 100644 --- a/covasim/misc.py +++ b/covasim/misc.py @@ -159,7 +159,17 @@ def migrate_lognormal(pars, revert=False, verbose=True): Small helper function to automatically migrate the standard deviation of lognormal distributions to match pre-v2.1.0 runs (where it was treated as the variance instead). To undo the migration, run with revert=True. + + Args: + pars (dict): the parameters dictionary; or, alternatively, the sim object the parameters will be taken from + revert (bool): whether to reverse the update rather than make it + verbose (bool): whether to print out the old and new values ''' + # Handle different input types + from . import base as cvb + if isinstance(pars, cvb.BaseSim): + pars = pars.pars # It's actually a sim, not a pars object + # Convert each value to the square root, since squared in the new version for key,dur in pars['dur'].items(): if 'lognormal' in dur['dist']: diff --git a/covasim/plotting.py b/covasim/plotting.py index 0ab0fa2eb..ce90f78c5 100644 --- a/covasim/plotting.py +++ b/covasim/plotting.py @@ -283,9 +283,9 @@ def reset_ticks(ax, sim=None, date_args=None, start_day=None): # Handle start and end days xmin,xmax = ax.get_xlim() if date_args.start_day: - xmin = float(sc.day(date_args.start_day), start_day=start_day) # Keep original type (float) + xmin = float(sc.day(date_args.start_day, start_day=start_day)) # Keep original type (float) if date_args.end_day: - xmax = float(sc.day(date_args.end_day), start_day=start_day) + xmax = float(sc.day(date_args.end_day, start_day=start_day)) ax.set_xlim([xmin, xmax]) # Set the x-axis intervals diff --git a/covasim/settings.py b/covasim/settings.py index 829618736..6160491ea 100644 --- a/covasim/settings.py +++ b/covasim/settings.py @@ -151,11 +151,6 @@ def get_default(key=None): return orig_options[key] -def get_option(key=None): - ''' Helper function to get the current value of an option ''' - return options[key] - - def get_help(output=False): ''' Print information about options. From 7f31cb5ef5d28ae01835a2a025c44a35ce48d580 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Tue, 23 Mar 2021 23:56:29 -0700 Subject: [PATCH 35/39] update changelog --- CHANGELOG.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 531a0e369..d84dffab3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,7 @@ These are the major improvements we are currently working on. If there is a spec Latest versions (2.x) ~~~~~~~~~~~~~~~~~~~~~ + Version 2.1.0 (2021-03-23) -------------------------- @@ -32,9 +33,9 @@ This is the last release before the Covasim 3.0 launch (vaccines and variants). Highlights ^^^^^^^^^^ -- **Updated lognormal distributions**: Lognormal distributions had been inadvertently using the variance instead of the standard deviation as the second parameter, resulting in too small variance. This has been fixed. -- **Expanded plotting features**: You now have much more flexibility with passing arguments to ``sim.plot()``, such as to temporarily set global Matplotlib options (such as DPI), modify axis styles and limits, etc. For example, you can now do things like this: ``cv.Sim().run().plot(dpi=150, rotation=30, start_day='2020-03-01', end_day=55, interval=7)``. -- **Improved analyzers**: Transmission trees can be computed 20x faster, Fit objects are more forgiving for data problems, and analyzers can now be exported to JSON. +- **Updated lognormal distributions**: Lognormal distributions had been inadvertently using the variance instead of the standard deviation as the second parameter, resulting in too small variance. This has been fixed. This has a small but nonzero impact on the results (e.g., the time to peak infections is about 5-10% sooner now). +- **Expanded plotting features**: You now have much more flexibility with passing arguments to ``sim.plot()`` and other plotting functions, such as to temporarily set global Matplotlib options (such as DPI), modify axis styles and limits, etc. For example, you can now do things like this: ``cv.Sim().run().plot(dpi=150, rotation=30, start_day='2020-03-01', end_day=55, interval=7)``. +- **Improved analyzers**: Transmission trees can be computed 20 times faster, Fit objects are more forgiving for data problems, and analyzers can now be exported to JSON. Bugfixes ^^^^^^^^ @@ -68,11 +69,11 @@ Documentation and testing Regression information ^^^^^^^^^^^^^^^^^^^^^^ - +- To restore previous behavior on a simulation (i.e. using variance instead of standard deviation for lognormal distributions), call ``cv.misc.migrate_lognormal(sim)``. This is done automatically when loading a saved sim from disk. To undo a migration, type ``cv.misc.migrate_lognormal(sim, revert=True)``. In practice, this loops over the duration parameters and replaces ``par2`` with its square root. If you have used lognormal distributions elsewhere, you will need to update them manually. +- Code that was designed to parse transmission trees will likely need to be revised. The object ``tt.detailed`` is now a dataframe; calling ``tt.detailed.to_dict('records')`` will bring it very close to what it used to be, with the exception that for a given row, ``'t'`` and ``'s'`` used to be nested dictionaries, whereas now they are prefixes. For example, whereas before the "is quarantined" state of the source of the 45th infection would have been ``tt.detailed[45]['s']['is_quarantined']``, it is now ``tt.detailed.iloc[45]['src_is_quarantined']``. - *GitHub info*: PR `859 `__ - Version 2.0.4 (2021-03-19) -------------------------- - Added a new analyzer, ``cv.daily_age_stats()``, which will compute statistics by age for each day of the simulation (compared to ``cv.age_histogram()``, which only looks at particular points in time). From f2110136f2fea2781fda536d8b8ec8cf2dd35266 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 24 Mar 2021 00:14:54 -0700 Subject: [PATCH 36/39] fixed unit tests --- tests/unittests/test_disease_mortality.py | 68 ++++------- tests/unittests/test_disease_progression.py | 24 ++-- tests/unittests/test_disease_transmission.py | 10 +- tests/unittests/test_population_types.py | 24 ++-- tests/unittests/test_simulation_parameter.py | 94 +++++++-------- .../unittests/test_specific_interventions.py | 32 ++--- tests/unittests/unittest_support_classes.py | 114 +++++++++--------- 7 files changed, 169 insertions(+), 197 deletions(-) diff --git a/tests/unittests/test_disease_mortality.py b/tests/unittests/test_disease_mortality.py index df832b175..184c1e05f 100644 --- a/tests/unittests/test_disease_mortality.py +++ b/tests/unittests/test_disease_mortality.py @@ -3,12 +3,14 @@ ../../covasim/README.md """ -from unittest_support_classes import CovaSimTest, TestProperties +import covasim as cv +import unittest +from unittest_support_classes import CovaSimTest, TProps -DProgKeys = TestProperties.ParameterKeys.ProgressionKeys -TransKeys = TestProperties.ParameterKeys.TransmissionKeys -TSimKeys = TestProperties.ParameterKeys.SimulationKeys -ResKeys = TestProperties.ResultsDataKeys +DProgKeys = TProps.ParKeys.ProgKeys +TransKeys = TProps.ParKeys.TransKeys +TSimKeys = TProps.ParKeys.SimKeys +ResKeys = TProps.ResKeys class DiseaseMortalityTests(CovaSimTest): @@ -25,33 +27,13 @@ def test_default_death_prob_one(self): Infect lots of people with cfr one and short time to die duration. Verify that everyone dies, no recoveries. """ - total_agents = 500 - self.set_everyone_is_going_to_die(num_agents=total_agents) - self.run_sim() - recoveries_at_timestep_channel = self.get_full_result_channel( - ResKeys.recovered_at_timestep - ) - recoveries_cumulative_channel = self.get_full_result_channel( - ResKeys.recovered_cumulative - ) - recovery_channels = [ - recoveries_at_timestep_channel, - recoveries_cumulative_channel - ] - for c in recovery_channels: - for t in range(len(c)): - self.assertEqual(0, c[t], - msg=f"There should be no recoveries" - f" with death_prob 1.0. Channel {c} had " - f" bad data at t: {t}") - pass - pass - cumulative_deaths = self.get_day_final_channel_value( - ResKeys.deaths_cumulative - ) - self.assertEqual(cumulative_deaths, total_agents, - msg="Everyone should die") - pass + pop_size = 200 + n_days = 90 + sim = cv.Sim(pop_size=pop_size, pop_infected=pop_size, n_days=n_days) + for key in ['rel_symp_prob', 'rel_severe_prob', 'rel_crit_prob', 'rel_death_prob']: + sim[key] = 1e6 + sim.run() + assert sim.summary.cum_deaths == pop_size def test_default_death_prob_zero(self): """ @@ -62,7 +44,7 @@ def test_default_death_prob_zero(self): total_agents = 500 self.set_everyone_is_going_to_die(num_agents=total_agents) prob_dict = { - DProgKeys.ProbabilityKeys.RelativeProbKeys.crt_to_death_probability: 0.0 + DProgKeys.ProbKeys.RelProbKeys.crt_to_death_probability: 0.0 } self.set_simulation_prognosis_probability(prob_dict) self.run_sim() @@ -102,22 +84,14 @@ def test_default_death_prob_scaling(self): death_probs = [0.01, 0.05, 0.10, 0.15] old_cumulative_deaths = 0 for death_prob in death_probs: - prob_dict = { - DProgKeys.ProbabilityKeys.RelativeProbKeys.crt_to_death_probability: death_prob - } + prob_dict = {DProgKeys.ProbKeys.RelProbKeys.crt_to_death_probability: death_prob} self.set_simulation_prognosis_probability(prob_dict) self.run_sim() - deaths_at_timestep_channel = self.get_full_result_channel( - ResKeys.deaths_daily - ) - recoveries_at_timestep_channel = self.get_full_result_channel( - ResKeys.recovered_at_timestep - ) - cumulative_deaths = self.get_day_final_channel_value( - ResKeys.deaths_cumulative - ) - self.assertGreaterEqual(cumulative_deaths, old_cumulative_deaths, - msg="Should be more deaths with higer ratio") + cumulative_deaths = self.get_day_final_channel_value(ResKeys.deaths_cumulative) + self.assertGreaterEqual(cumulative_deaths, old_cumulative_deaths, msg="Should be more deaths with higer ratio") old_cumulative_deaths = cumulative_deaths pass +# Run unit tests if called as a script +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/unittests/test_disease_progression.py b/tests/unittests/test_disease_progression.py index e05657e3c..d71781fd0 100644 --- a/tests/unittests/test_disease_progression.py +++ b/tests/unittests/test_disease_progression.py @@ -4,10 +4,10 @@ """ import unittest -from unittest_support_classes import CovaSimTest, TestProperties +from unittest_support_classes import CovaSimTest, TProps -ResKeys = TestProperties.ResultsDataKeys -ParamKeys = TestProperties.ParameterKeys +ResKeys = TProps.ResKeys +ParamKeys = TProps.ParKeys class DiseaseProgressionTests(CovaSimTest): @@ -33,16 +33,16 @@ def test_exposure_to_infectiousness_delay_scaling(self): std_dev = 0 for exposed_delay in exposed_delays: self.set_duration_distribution_parameters( - duration_in_question=ParamKeys.ProgressionKeys.DurationKeys.exposed_to_infectious, + duration_in_question=ParamKeys.ProgKeys.DurKeys.exposed_to_infectious, par1=exposed_delay, par2=std_dev ) prob_dict = { - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.inf_to_symptomatic_probability: 0 + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.inf_to_symptomatic_probability: 0 } self.set_simulation_prognosis_probability(prob_dict) serial_delay = { - TestProperties.ParameterKeys.SimulationKeys.number_simulated_days: sim_dur + TProps.ParKeys.SimKeys.number_simulated_days: sim_dur } self.run_sim(serial_delay) infectious_channel = self.get_full_result_channel( @@ -76,7 +76,7 @@ def test_mild_infection_duration_scaling(self): self.set_everyone_infectious_same_day(num_agents=total_agents, days_to_infectious=exposed_delay) prob_dict = { - ParamKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.inf_to_symptomatic_probability: 0.0 + ParamKeys.ProgKeys.ProbKeys.RelProbKeys.inf_to_symptomatic_probability: 0.0 } self.set_simulation_prognosis_probability(prob_dict) infectious_durations = [1, 2, 5, 10, 20] # Keep values in order @@ -84,13 +84,13 @@ def test_mild_infection_duration_scaling(self): for TEST_dur in infectious_durations: recovery_day = exposed_delay + TEST_dur self.set_duration_distribution_parameters( - duration_in_question=ParamKeys.ProgressionKeys.DurationKeys.infectious_asymptomatic_to_recovered, + duration_in_question=ParamKeys.ProgKeys.DurKeys.infectious_asymptomatic_to_recovered, par1=TEST_dur, par2=infectious_duration_stddev ) self.run_sim() recoveries_channel = self.get_full_result_channel( - TestProperties.ResultsDataKeys.recovered_at_timestep + TProps.ResKeys.recovered_at_timestep ) recoveries_on_recovery_day = recoveries_channel[recovery_day] if self.is_debugging: @@ -107,7 +107,7 @@ def test_time_to_die_duration_scaling(self): total_agents = 500 self.set_everyone_critical(num_agents=500, constant_delay=0) prob_dict = { - ParamKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.crt_to_death_probability: 1.0 + ParamKeys.ProgKeys.ProbKeys.RelProbKeys.crt_to_death_probability: 1.0 } self.set_simulation_prognosis_probability(prob_dict) @@ -116,13 +116,13 @@ def test_time_to_die_duration_scaling(self): for TEST_dur in time_to_die_durations: self.set_duration_distribution_parameters( - duration_in_question=ParamKeys.ProgressionKeys.DurationKeys.critical_to_death, + duration_in_question=ParamKeys.ProgKeys.DurKeys.critical_to_death, par1=TEST_dur, par2=time_to_die_stddev ) self.run_sim() deaths_today_channel = self.get_full_result_channel( - TestProperties.ResultsDataKeys.deaths_daily + TProps.ResKeys.deaths_daily ) for t in range(len(deaths_today_channel)): curr_deaths = deaths_today_channel[t] diff --git a/tests/unittests/test_disease_transmission.py b/tests/unittests/test_disease_transmission.py index 59c837ceb..da5844bea 100644 --- a/tests/unittests/test_disease_transmission.py +++ b/tests/unittests/test_disease_transmission.py @@ -3,10 +3,10 @@ ../../covasim/README.md """ -from unittest_support_classes import CovaSimTest, TestProperties +from unittest_support_classes import CovaSimTest, TProps -TKeys = TestProperties.ParameterKeys.TransmissionKeys -Hightrans = TestProperties.SpecializedSimulations.Hightransmission +TKeys = TProps.ParKeys.TransKeys +Hightrans = TProps.SpecialSims.Hightransmission class DiseaseTransmissionTests(CovaSimTest): """ @@ -33,7 +33,7 @@ def test_beta_zero(self): } self.run_sim(beta_zero) exposed_today_channel = self.get_full_result_channel( - TestProperties.ResultsDataKeys.exposed_at_timestep + TProps.ResKeys.exposed_at_timestep ) prev_exposed = exposed_today_channel[0] self.assertEqual(prev_exposed, Hightrans.pop_infected, @@ -47,7 +47,7 @@ def test_beta_zero(self): pass infections_channel = self.get_full_result_channel( - TestProperties.ResultsDataKeys.infections_at_timestep + TProps.ResKeys.infections_at_timestep ) for t in range(len(infections_channel)): today_infectious = infections_channel[t] diff --git a/tests/unittests/test_population_types.py b/tests/unittests/test_population_types.py index 0ef8746b2..08ae8459c 100644 --- a/tests/unittests/test_population_types.py +++ b/tests/unittests/test_population_types.py @@ -1,6 +1,6 @@ -from unittest_support_classes import CovaSimTest, TestProperties +from unittest_support_classes import CovaSimTest, TProps -TPKeys = TestProperties.ParameterKeys.SimulationKeys +TParKeys = TProps.ParKeys.SimKeys class PopulationTypeTests(CovaSimTest): @@ -16,9 +16,9 @@ def test_different_pop_types(self): pop_types = ['random', 'hybrid'] #, 'synthpops'] results = {} short_sample = { - TPKeys.number_agents: 1000, - TPKeys.number_simulated_days: 10, - TPKeys.initial_infected_count: 50 + TParKeys.number_agents: 1000, + TParKeys.number_simulated_days: 10, + TParKeys.initial_infected_count: 50 } for poptype in pop_types: self.run_sim(short_sample, population_type=poptype) @@ -28,15 +28,15 @@ def test_different_pop_types(self): for k in results: these_results = results[k] self.assertIsNotNone(these_results) - day_0_susceptible = these_results[TestProperties.ResultsDataKeys.susceptible_at_timestep][0] - day_0_exposed = these_results[TestProperties.ResultsDataKeys.exposed_at_timestep][0] + day_0_susceptible = these_results[TProps.ResKeys.susceptible_at_timestep][0] + day_0_exposed = these_results[TProps.ResKeys.exposed_at_timestep][0] - self.assertEqual(day_0_susceptible + day_0_exposed, short_sample[TPKeys.number_agents], + self.assertEqual(day_0_susceptible + day_0_exposed, short_sample[TParKeys.number_agents], msg=f"Day 0 population should be as specified in params. Poptype {k} was different.") - self.assertGreater(these_results[TestProperties.ResultsDataKeys.infections_cumulative][-1], - these_results[TestProperties.ResultsDataKeys.infections_cumulative][0], + self.assertGreater(these_results[TProps.ResKeys.infections_cumulative][-1], + these_results[TProps.ResKeys.infections_cumulative][0], msg=f"Should see infections increase. Pop type {k} didn't do that.") - self.assertGreater(these_results[TestProperties.ResultsDataKeys.symptomatic_cumulative][-1], - these_results[TestProperties.ResultsDataKeys.symptomatic_cumulative][0], + self.assertGreater(these_results[TProps.ResKeys.symptomatic_cumulative][-1], + these_results[TProps.ResKeys.symptomatic_cumulative][0], msg=f"Should see symptomatic counts increase. Pop type {k} didn't do that.") diff --git a/tests/unittests/test_simulation_parameter.py b/tests/unittests/test_simulation_parameter.py index d5b9666ab..7b5bdd1b6 100644 --- a/tests/unittests/test_simulation_parameter.py +++ b/tests/unittests/test_simulation_parameter.py @@ -4,10 +4,10 @@ """ import unittest -from unittest_support_classes import CovaSimTest, TestProperties +from unittest_support_classes import CovaSimTest, TProps -TPKeys = TestProperties.ParameterKeys.SimulationKeys -ResKeys = TestProperties.ResultsDataKeys +TParKeys = TProps.ParKeys.SimKeys +ResKeys = TProps.ResKeys class SimulationParameterTests(CovaSimTest): def setUp(self): @@ -25,32 +25,32 @@ def test_population_size(self): Depends on run default simulation """ - TPKeys = TestProperties.ParameterKeys.SimulationKeys + TParKeys = TProps.ParKeys.SimKeys pop_2_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 2, - TPKeys.number_contacts: {'a': 1}, - TPKeys.initial_infected_count: 0 + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 1, + TParKeys.number_agents: 2, + TParKeys.number_contacts: {'a': 1}, + TParKeys.initial_infected_count: 0 } pop_10_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 10, - TPKeys.number_contacts: {'a': 4}, - TPKeys.initial_infected_count: 0 + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 1, + TParKeys.number_agents: 10, + TParKeys.number_contacts: {'a': 4}, + TParKeys.initial_infected_count: 0 } pop_123_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 123, - TPKeys.initial_infected_count: 0 + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 1, + TParKeys.number_agents: 123, + TParKeys.initial_infected_count: 0 } pop_1234_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: 1234, - TPKeys.initial_infected_count: 0 + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 1, + TParKeys.number_agents: 1234, + TParKeys.initial_infected_count: 0 } self.run_sim(pop_2_one_day) pop_2_pop = self.get_day_zero_channel_value() @@ -61,10 +61,10 @@ def test_population_size(self): self.run_sim(pop_1234_one_day) pop_1234_pop = self.get_day_zero_channel_value() - self.assertEqual(pop_2_pop, pop_2_one_day[TPKeys.number_agents]) - self.assertEqual(pop_10_pop, pop_10_one_day[TPKeys.number_agents]) - self.assertEqual(pop_123_pop, pop_123_one_day[TPKeys.number_agents]) - self.assertEqual(pop_1234_pop, pop_1234_one_day[TPKeys.number_agents]) + self.assertEqual(pop_2_pop, pop_2_one_day[TParKeys.number_agents]) + self.assertEqual(pop_10_pop, pop_10_one_day[TParKeys.number_agents]) + self.assertEqual(pop_123_pop, pop_123_one_day[TParKeys.number_agents]) + self.assertEqual(pop_1234_pop, pop_1234_one_day[TParKeys.number_agents]) pass @@ -73,10 +73,10 @@ def test_population_size_ranges(self): Intent is to test zero, negative, and excessively large pop sizes """ pop_neg_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1, - TPKeys.number_agents: -10, - TPKeys.initial_infected_count: 0 + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 1, + TParKeys.number_agents: -10, + TParKeys.initial_infected_count: 0 } with self.assertRaises(ValueError) as context: self.run_sim(pop_neg_one_day) @@ -84,10 +84,10 @@ def test_population_size_ranges(self): self.assertIn("negative", error_message) pop_zero_one_day = { - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 100, - TPKeys.number_agents: 0, - TPKeys.initial_infected_count: 0 + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 100, + TParKeys.number_agents: 0, + TParKeys.initial_infected_count: 0 } self.run_sim(pop_zero_one_day) self.assertEqual(self.simulation_result['results'][ResKeys.susceptible_at_timestep][-1], 0) @@ -103,21 +103,21 @@ def test_population_scaling(self): Depends on population_size """ scale_1_one_day = { - TPKeys.number_agents: 100, - TPKeys.population_scaling_factor: 1, - TPKeys.number_simulated_days: 1 + TParKeys.number_agents: 100, + TParKeys.population_scaling_factor: 1, + TParKeys.number_simulated_days: 1 } scale_2_one_day = { - TPKeys.number_agents: 100, - TPKeys.population_scaling_factor: 2, - TPKeys.population_rescaling: False, - TPKeys.number_simulated_days: 1 + TParKeys.number_agents: 100, + TParKeys.population_scaling_factor: 2, + TParKeys.population_rescaling: False, + TParKeys.number_simulated_days: 1 } scale_10_one_day = { - TPKeys.number_agents: 100, - TPKeys.population_scaling_factor: 10, - TPKeys.population_rescaling: False, - TPKeys.number_simulated_days: 1 + TParKeys.number_agents: 100, + TParKeys.population_scaling_factor: 10, + TParKeys.population_rescaling: False, + TParKeys.number_simulated_days: 1 } self.run_sim(scale_1_one_day) scale_1_pop = self.get_day_zero_channel_value() @@ -140,10 +140,10 @@ def test_random_seed(self): """ self.set_smallpop_hightransmission() seed_1_params = { - TPKeys.random_seed: 1 + TParKeys.random_seed: 1 } seed_2_params = { - TPKeys.random_seed: 2 + TParKeys.random_seed: 2 } self.run_sim(seed_1_params) infectious_seed_1_v1 = self.get_full_result_channel( diff --git a/tests/unittests/test_specific_interventions.py b/tests/unittests/test_specific_interventions.py index 29c254efc..aa7d4c610 100644 --- a/tests/unittests/test_specific_interventions.py +++ b/tests/unittests/test_specific_interventions.py @@ -1,5 +1,5 @@ from unittest_support_classes import CovaSimTest -from unittest_support_classes import TestProperties +from unittest_support_classes import TProps from math import sqrt import json import numpy as np @@ -9,8 +9,8 @@ AGENT_COUNT = 1000 -ResultsKeys = TestProperties.ResultsDataKeys -SimKeys = TestProperties.ParameterKeys.SimulationKeys +ResultsKeys = TProps.ResKeys +SimKeys = TProps.ParKeys.SimKeys class InterventionTests(CovaSimTest): def setUp(self): super().setUp() @@ -26,7 +26,7 @@ def test_brutal_change_beta_intervention(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) day_of_change = 30 change_days = [day_of_change] change_multipliers = [0.0] @@ -57,7 +57,7 @@ def test_change_beta_days(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) # Do a 0.0 intervention / 1.0 intervention on different days days = [ 30, 32, 40, 42, 50] multipliers = [0.0, 1.0, 0.0, 1.0, 0.0] @@ -105,7 +105,7 @@ def test_change_beta_multipliers(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 40 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) day_of_change = 20 change_days = [day_of_change] change_multipliers = [1.0, 0.8, 0.6, 0.4, 0.2] @@ -162,7 +162,7 @@ def test_change_beta_layers_clustered(self): } if len(seed_list) > 1: self.expected_result_filename = f"DEBUG_{self.id()}_{seed}.json" - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) day_of_change = 25 change_multipliers = [0.0] layer_keys = ['c','h','s','w'] @@ -231,7 +231,7 @@ def test_change_beta_layers_random(self): SimKeys.number_simulated_days: 60, SimKeys.initial_infected_count: initial_infected } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) if len(seed_list) > 1: self.expected_result_filename = f"DEBUG_{self.id()}_{seed}.json" day_of_change = 25 @@ -291,7 +291,7 @@ def test_change_beta_layers_hybrid(self): } if len(seed_list) > 1: self.expected_result_filename = f"DEBUG_{self.id()}_{seed}.json" - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) day_of_change = 25 change_multipliers = [0.0] layer_keys = ['c','s','w','h'] @@ -418,7 +418,7 @@ def test_test_prob_perfect_asymptomatic(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) asymptomatic_probability_of_test = 1.0 test_sensitivity = 1.0 @@ -455,7 +455,7 @@ def test_test_prob_perfect_symptomatic(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) symptomatic_probability_of_test = 1.0 test_sensitivity = 1.0 @@ -489,7 +489,7 @@ def test_test_prob_perfect_not_quarantined(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 60 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) asymptomatic_probability_of_test = 1.0 symptomatic_probability_of_test = 1.0 @@ -526,7 +526,7 @@ def test_test_prob_sensitivity(self, subtract_today_recoveries=False): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 31 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) symptomatic_probability_of_test = 1.0 test_sensitivities = [0.9, 0.7, 0.6, 0.2] @@ -608,7 +608,7 @@ def test_test_prob_symptomatic_prob_of_test(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 31 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) symptomatic_probabilities_of_test = [0.9, 0.7, 0.6, 0.2] test_sensitivity = 1.0 @@ -659,7 +659,7 @@ def test_brutal_contact_tracing(self): SimKeys.number_agents: AGENT_COUNT, SimKeys.number_simulated_days: 55 } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) intervention_list = [] @@ -723,7 +723,7 @@ def test_contact_tracing_perfect_school_layer(self): 'quar_period': 10, SimKeys.initial_infected_count: initial_infected } - self.set_simulation_parameters(params_dict=params) + self.set_sim_pars(params_dict=params) sequence_days = [30, 40] sequence_interventions = [] diff --git a/tests/unittests/unittest_support_classes.py b/tests/unittests/unittest_support_classes.py index ef503b3b2..c81f9e7cc 100644 --- a/tests/unittests/unittest_support_classes.py +++ b/tests/unittests/unittest_support_classes.py @@ -15,9 +15,9 @@ from covasim import Sim, parameters, change_beta, test_prob, contact_tracing, sequence -class TestProperties: - class ParameterKeys: - class SimulationKeys: +class TProps: + class ParKeys: + class SimKeys: number_agents = 'pop_size' number_contacts = 'contacts' population_scaling_factor = 'pop_scale' @@ -34,7 +34,7 @@ class SimulationKeys: # stopping_function = 'stop_func' pass - class TransmissionKeys: + class TransKeys: beta = 'beta' asymptomatic_fraction = 'asym_prop' asymptomatic_transmission_multiplier = 'asym_factor' @@ -45,12 +45,12 @@ class TransmissionKeys: contacts_population_specific = 'contacts_pop' pass - class ProgressionKeys: + class ProgKeys: durations = "dur" param_1 = "par1" param_2 = "par2" - class DurationKeys: + class DurKeys: exposed_to_infectious = 'exp2inf' infectious_to_symptomatic = 'inf2sym' infectious_asymptomatic_to_recovered = 'asym2rec' @@ -63,9 +63,9 @@ class DurationKeys: critical_to_death = 'crit2die' pass - class ProbabilityKeys: + class ProbKeys: progression_by_age = 'prog_by_age' - class RelativeProbKeys: + class RelProbKeys: inf_to_symptomatic_probability = 'rel_symp_prob' sym_to_severe_probability = 'rel_severe_prob' sev_to_critical_probability = 'rel_crit_prob' @@ -86,7 +86,7 @@ class DiagnosticTestingKeys: pass pass - class SpecializedSimulations: + class SpecialSims: class Microsim: n = 10 pop_infected = 1 @@ -111,7 +111,7 @@ class HighMortality: # timetodie_std = 2 pass - class ResultsDataKeys: + class ResKeys: deaths_cumulative = 'cum_deaths' deaths_daily = 'new_deaths' diagnoses_cumulative = 'cum_diagnoses' @@ -135,7 +135,7 @@ class ResultsDataKeys: pass -DurationKeys = TestProperties.ParameterKeys.ProgressionKeys.DurationKeys +DurKeys = TProps.ParKeys.ProgKeys.DurKeys class CovaSimTest(unittest.TestCase): @@ -159,7 +159,7 @@ def tearDown(self): pass # region configuration methods - def set_simulation_parameters(self, params_dict=None): + def set_sim_pars(self, params_dict=None): """ Overrides all of the default sim parameters with the ones in the dictionary @@ -181,16 +181,16 @@ def set_simulation_prognosis_probability(self, params_dict): Allows for testing prognoses probability as absolute rather than relative. NOTE: You can only call this once per test or you will overwrite your stuff. """ - ProbKeys = TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys - RelativeProbabilityKeys = ProbKeys.RelativeProbKeys + ProbKeys = TProps.ParKeys.ProgKeys.ProbKeys + RelProbKeys = ProbKeys.RelProbKeys supported_probabilities = [ - RelativeProbabilityKeys.inf_to_symptomatic_probability, - RelativeProbabilityKeys.sym_to_severe_probability, - RelativeProbabilityKeys.sev_to_critical_probability, - RelativeProbabilityKeys.crt_to_death_probability + RelProbKeys.inf_to_symptomatic_probability, + RelProbKeys.sym_to_severe_probability, + RelProbKeys.sev_to_critical_probability, + RelProbKeys.crt_to_death_probability ] if not self.simulation_parameters: - self.set_simulation_parameters() + self.set_sim_pars() pass if not self.simulation_prognoses: @@ -200,13 +200,13 @@ def set_simulation_prognosis_probability(self, params_dict): for k in params_dict: prognosis_in_question = None expected_prob = params_dict[k] - if k == RelativeProbabilityKeys.inf_to_symptomatic_probability: + if k == RelProbKeys.inf_to_symptomatic_probability: prognosis_in_question = PrognosisKeys.symptomatic_probabilities - elif k == RelativeProbabilityKeys.sym_to_severe_probability: + elif k == RelProbKeys.sym_to_severe_probability: prognosis_in_question = PrognosisKeys.severe_probabilities - elif k == RelativeProbabilityKeys.sev_to_critical_probability: + elif k == RelProbKeys.sev_to_critical_probability: prognosis_in_question = PrognosisKeys.critical_probabilities - elif k == RelativeProbabilityKeys.crt_to_death_probability: + elif k == RelProbKeys.crt_to_death_probability: prognosis_in_question = PrognosisKeys.death_probs else: raise KeyError(f"Key {k} not found in {supported_probabilities}.") @@ -218,7 +218,7 @@ def set_simulation_prognosis_probability(self, params_dict): def set_duration_distribution_parameters(self, duration_in_question, par1, par2): if not self.simulation_parameters: - self.set_simulation_parameters() + self.set_sim_pars() pass duration_node = self.simulation_parameters["dur"] duration_node[duration_in_question] = { @@ -229,12 +229,12 @@ def set_duration_distribution_parameters(self, duration_in_question, params_dict = { "dur": duration_node } - self.set_simulation_parameters(params_dict=params_dict) + self.set_sim_pars(params_dict=params_dict) def run_sim(self, params_dict=None, write_results_json=False, population_type=None): if not self.simulation_parameters or params_dict: # If we need one, or have one here - self.set_simulation_parameters(params_dict=params_dict) + self.set_sim_pars(params_dict=params_dict) pass self.simulation_parameters['interventions'] = self.interventions @@ -243,7 +243,7 @@ def run_sim(self, params_dict=None, write_results_json=False, population_type=No datafile=None) if not self.simulation_prognoses: self.simulation_prognoses = parameters.get_prognoses( - self.simulation_parameters[TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.progression_by_age] + self.simulation_parameters[TProps.ParKeys.ProgKeys.ProbKeys.progression_by_age] ) pass @@ -263,7 +263,7 @@ def get_full_result_channel(self, channel): result_data = self.simulation_result["results"][channel] return result_data - def get_day_zero_channel_value(self, channel=TestProperties.ResultsDataKeys.susceptible_at_timestep): + def get_day_zero_channel_value(self, channel=TProps.ResKeys.susceptible_at_timestep): """ Args: @@ -329,26 +329,26 @@ def intervention_build_sequence(self, # region specialized simulation methods def set_microsim(self): - Simkeys = TestProperties.ParameterKeys.SimulationKeys - Micro = TestProperties.SpecializedSimulations.Microsim + Simkeys = TProps.ParKeys.SimKeys + Micro = TProps.SpecialSims.Microsim microsim_parameters = { Simkeys.number_agents : Micro.n, Simkeys.initial_infected_count: Micro.pop_infected, Simkeys.number_simulated_days: Micro.n_days } - self.set_simulation_parameters(microsim_parameters) + self.set_sim_pars(microsim_parameters) pass def set_everyone_infected(self, agent_count=1000): - Simkeys = TestProperties.ParameterKeys.SimulationKeys + Simkeys = TProps.ParKeys.SimKeys everyone_infected = { Simkeys.number_agents: agent_count, Simkeys.initial_infected_count: agent_count } - self.set_simulation_parameters(params_dict=everyone_infected) + self.set_sim_pars(params_dict=everyone_infected) pass - DurationKeys = TestProperties.ParameterKeys.ProgressionKeys.DurationKeys + DurKeys = TProps.ParKeys.ProgKeys.DurKeys def set_everyone_infectious_same_day(self, num_agents, days_to_infectious=1, num_days=60): """ @@ -359,18 +359,18 @@ def set_everyone_infectious_same_day(self, num_agents, days_to_infectious=1, num """ self.set_everyone_infected(agent_count=num_agents) prob_dict = { - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.inf_to_symptomatic_probability: 0 + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.inf_to_symptomatic_probability: 0 } self.set_simulation_prognosis_probability(prob_dict) test_config = { - TestProperties.ParameterKeys.SimulationKeys.number_simulated_days: num_days + TProps.ParKeys.SimKeys.number_simulated_days: num_days } self.set_duration_distribution_parameters( - duration_in_question=DurationKeys.exposed_to_infectious, + duration_in_question=DurKeys.exposed_to_infectious, par1=days_to_infectious, par2=0 ) - self.set_simulation_parameters(params_dict=test_config) + self.set_sim_pars(params_dict=test_config) pass def set_everyone_symptomatic(self, num_agents, constant_delay:int=None): @@ -383,13 +383,13 @@ def set_everyone_symptomatic(self, num_agents, constant_delay:int=None): self.set_everyone_infectious_same_day(num_agents=num_agents, days_to_infectious=0) prob_dict = { - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.inf_to_symptomatic_probability: 1.0, - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.sym_to_severe_probability: 0 + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.inf_to_symptomatic_probability: 1.0, + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.sym_to_severe_probability: 0 } self.set_simulation_prognosis_probability(prob_dict) if constant_delay is not None: self.set_duration_distribution_parameters( - duration_in_question=DurationKeys.infectious_to_symptomatic, + duration_in_question=DurKeys.infectious_to_symptomatic, par1=constant_delay, par2=0 ) @@ -401,7 +401,7 @@ def set_everyone_is_going_to_die(self, num_agents): Args: num_agents: Number of agents to simulate """ - ProbKeys = TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys + ProbKeys = TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys self.set_everyone_infectious_same_day(num_agents=num_agents) prob_dict = { ProbKeys.inf_to_symptomatic_probability: 1, @@ -415,13 +415,13 @@ def set_everyone_is_going_to_die(self, num_agents): def set_everyone_severe(self, num_agents, constant_delay:int=None): self.set_everyone_symptomatic(num_agents=num_agents, constant_delay=constant_delay) prob_dict = { - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.sym_to_severe_probability: 1.0, - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.sev_to_critical_probability: 0.0 + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.sym_to_severe_probability: 1.0, + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.sev_to_critical_probability: 0.0 } self.set_simulation_prognosis_probability(prob_dict) if constant_delay is not None: self.set_duration_distribution_parameters( - duration_in_question=DurationKeys.symptomatic_to_severe, + duration_in_question=DurKeys.symptomatic_to_severe, par1=constant_delay, par2=0 ) @@ -433,13 +433,13 @@ def set_everyone_critical(self, num_agents, constant_delay:int=None): """ self.set_everyone_severe(num_agents=num_agents, constant_delay=constant_delay) prob_dict = { - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.sev_to_critical_probability: 1.0, - TestProperties.ParameterKeys.ProgressionKeys.ProbabilityKeys.RelativeProbKeys.crt_to_death_probability: 0.0 + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.sev_to_critical_probability: 1.0, + TProps.ParKeys.ProgKeys.ProbKeys.RelProbKeys.crt_to_death_probability: 0.0 } self.set_simulation_prognosis_probability(prob_dict) if constant_delay is not None: self.set_duration_distribution_parameters( - duration_in_question=DurationKeys.severe_to_critical, + duration_in_question=DurKeys.severe_to_critical, par1=constant_delay, par2=0 ) @@ -450,24 +450,22 @@ def set_smallpop_hightransmission(self): """ Creates a small population with lots of transmission """ - Simkeys = TestProperties.ParameterKeys.SimulationKeys - Transkeys = TestProperties.ParameterKeys.TransmissionKeys - Hightrans = TestProperties.SpecializedSimulations.Hightransmission + Simkeys = TProps.ParKeys.SimKeys + Transkeys = TProps.ParKeys.TransKeys + Hightrans = TProps.SpecialSims.Hightransmission hightrans_parameters = { Simkeys.number_agents : Hightrans.n, Simkeys.initial_infected_count: Hightrans.pop_infected, Simkeys.number_simulated_days: Hightrans.n_days, Transkeys.beta : Hightrans.beta } - self.set_simulation_parameters(hightrans_parameters) + self.set_sim_pars(hightrans_parameters) pass # endregion pass - - class TestSupportTests(CovaSimTest): def test_run_vanilla_simulation(self): """ @@ -489,7 +487,7 @@ def test_everyone_infected(self): total_agents = 500 self.set_everyone_infected(agent_count=total_agents) self.run_sim() - exposed_channel = TestProperties.ResultsDataKeys.exposed_at_timestep + exposed_channel = TProps.ResKeys.exposed_at_timestep day_0_exposed = self.get_day_zero_channel_value(exposed_channel) self.assertEqual(day_0_exposed, total_agents) pass @@ -508,7 +506,7 @@ def test_run_small_hightransmission_sim(self): self.assertIsNotNone(self.sim) self.assertIsNotNone(self.simulation_parameters) exposed_today_channel = self.get_full_result_channel( - TestProperties.ResultsDataKeys.exposed_at_timestep + TProps.ResKeys.exposed_at_timestep ) prev_exposed = exposed_today_channel[0] for t in range(1, 10): @@ -520,10 +518,10 @@ def test_run_small_hightransmission_sim(self): prev_exposed = today_exposed pass infections_channel = self.get_full_result_channel( - TestProperties.ResultsDataKeys.infections_at_timestep + TProps.ResKeys.infections_at_timestep ) self.assertGreaterEqual(sum(infections_channel), 150, - msg=f"Should have at least 150 infections") + msg="Should have at least 150 infections") pass pass From 3c0dfb2db978d62823acd315b8dd6d5a84e3f332 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 24 Mar 2021 00:16:35 -0700 Subject: [PATCH 37/39] fixing unit tests --- tests/unittests/test_population_types.py | 10 +-- tests/unittests/test_simulation_parameter.py | 90 ++++++++++---------- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/unittests/test_population_types.py b/tests/unittests/test_population_types.py index 08ae8459c..2c06fb7c3 100644 --- a/tests/unittests/test_population_types.py +++ b/tests/unittests/test_population_types.py @@ -1,6 +1,6 @@ from unittest_support_classes import CovaSimTest, TProps -TParKeys = TProps.ParKeys.SimKeys +TPKeys = TProps.ParKeys.SimKeys class PopulationTypeTests(CovaSimTest): @@ -16,9 +16,9 @@ def test_different_pop_types(self): pop_types = ['random', 'hybrid'] #, 'synthpops'] results = {} short_sample = { - TParKeys.number_agents: 1000, - TParKeys.number_simulated_days: 10, - TParKeys.initial_infected_count: 50 + TPKeys.number_agents: 1000, + TPKeys.number_simulated_days: 10, + TPKeys.initial_infected_count: 50 } for poptype in pop_types: self.run_sim(short_sample, population_type=poptype) @@ -31,7 +31,7 @@ def test_different_pop_types(self): day_0_susceptible = these_results[TProps.ResKeys.susceptible_at_timestep][0] day_0_exposed = these_results[TProps.ResKeys.exposed_at_timestep][0] - self.assertEqual(day_0_susceptible + day_0_exposed, short_sample[TParKeys.number_agents], + self.assertEqual(day_0_susceptible + day_0_exposed, short_sample[TPKeys.number_agents], msg=f"Day 0 population should be as specified in params. Poptype {k} was different.") self.assertGreater(these_results[TProps.ResKeys.infections_cumulative][-1], these_results[TProps.ResKeys.infections_cumulative][0], diff --git a/tests/unittests/test_simulation_parameter.py b/tests/unittests/test_simulation_parameter.py index 7b5bdd1b6..6d2e80bcd 100644 --- a/tests/unittests/test_simulation_parameter.py +++ b/tests/unittests/test_simulation_parameter.py @@ -6,7 +6,7 @@ from unittest_support_classes import CovaSimTest, TProps -TParKeys = TProps.ParKeys.SimKeys +TPKeys = TProps.ParKeys.SimKeys ResKeys = TProps.ResKeys class SimulationParameterTests(CovaSimTest): @@ -25,32 +25,32 @@ def test_population_size(self): Depends on run default simulation """ - TParKeys = TProps.ParKeys.SimKeys + TPKeys = TProps.ParKeys.SimKeys pop_2_one_day = { - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 1, - TParKeys.number_agents: 2, - TParKeys.number_contacts: {'a': 1}, - TParKeys.initial_infected_count: 0 + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 1, + TPKeys.number_agents: 2, + TPKeys.number_contacts: {'a': 1}, + TPKeys.initial_infected_count: 0 } pop_10_one_day = { - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 1, - TParKeys.number_agents: 10, - TParKeys.number_contacts: {'a': 4}, - TParKeys.initial_infected_count: 0 + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 1, + TPKeys.number_agents: 10, + TPKeys.number_contacts: {'a': 4}, + TPKeys.initial_infected_count: 0 } pop_123_one_day = { - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 1, - TParKeys.number_agents: 123, - TParKeys.initial_infected_count: 0 + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 1, + TPKeys.number_agents: 123, + TPKeys.initial_infected_count: 0 } pop_1234_one_day = { - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 1, - TParKeys.number_agents: 1234, - TParKeys.initial_infected_count: 0 + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 1, + TPKeys.number_agents: 1234, + TPKeys.initial_infected_count: 0 } self.run_sim(pop_2_one_day) pop_2_pop = self.get_day_zero_channel_value() @@ -61,10 +61,10 @@ def test_population_size(self): self.run_sim(pop_1234_one_day) pop_1234_pop = self.get_day_zero_channel_value() - self.assertEqual(pop_2_pop, pop_2_one_day[TParKeys.number_agents]) - self.assertEqual(pop_10_pop, pop_10_one_day[TParKeys.number_agents]) - self.assertEqual(pop_123_pop, pop_123_one_day[TParKeys.number_agents]) - self.assertEqual(pop_1234_pop, pop_1234_one_day[TParKeys.number_agents]) + self.assertEqual(pop_2_pop, pop_2_one_day[TPKeys.number_agents]) + self.assertEqual(pop_10_pop, pop_10_one_day[TPKeys.number_agents]) + self.assertEqual(pop_123_pop, pop_123_one_day[TPKeys.number_agents]) + self.assertEqual(pop_1234_pop, pop_1234_one_day[TPKeys.number_agents]) pass @@ -73,10 +73,10 @@ def test_population_size_ranges(self): Intent is to test zero, negative, and excessively large pop sizes """ pop_neg_one_day = { - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 1, - TParKeys.number_agents: -10, - TParKeys.initial_infected_count: 0 + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 1, + TPKeys.number_agents: -10, + TPKeys.initial_infected_count: 0 } with self.assertRaises(ValueError) as context: self.run_sim(pop_neg_one_day) @@ -84,10 +84,10 @@ def test_population_size_ranges(self): self.assertIn("negative", error_message) pop_zero_one_day = { - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 100, - TParKeys.number_agents: 0, - TParKeys.initial_infected_count: 0 + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 100, + TPKeys.number_agents: 0, + TPKeys.initial_infected_count: 0 } self.run_sim(pop_zero_one_day) self.assertEqual(self.simulation_result['results'][ResKeys.susceptible_at_timestep][-1], 0) @@ -103,21 +103,21 @@ def test_population_scaling(self): Depends on population_size """ scale_1_one_day = { - TParKeys.number_agents: 100, - TParKeys.population_scaling_factor: 1, - TParKeys.number_simulated_days: 1 + TPKeys.number_agents: 100, + TPKeys.population_scaling_factor: 1, + TPKeys.number_simulated_days: 1 } scale_2_one_day = { - TParKeys.number_agents: 100, - TParKeys.population_scaling_factor: 2, - TParKeys.population_rescaling: False, - TParKeys.number_simulated_days: 1 + TPKeys.number_agents: 100, + TPKeys.population_scaling_factor: 2, + TPKeys.population_rescaling: False, + TPKeys.number_simulated_days: 1 } scale_10_one_day = { - TParKeys.number_agents: 100, - TParKeys.population_scaling_factor: 10, - TParKeys.population_rescaling: False, - TParKeys.number_simulated_days: 1 + TPKeys.number_agents: 100, + TPKeys.population_scaling_factor: 10, + TPKeys.population_rescaling: False, + TPKeys.number_simulated_days: 1 } self.run_sim(scale_1_one_day) scale_1_pop = self.get_day_zero_channel_value() @@ -140,10 +140,10 @@ def test_random_seed(self): """ self.set_smallpop_hightransmission() seed_1_params = { - TParKeys.random_seed: 1 + TPKeys.random_seed: 1 } seed_2_params = { - TParKeys.random_seed: 2 + TPKeys.random_seed: 2 } self.run_sim(seed_1_params) infectious_seed_1_v1 = self.get_full_result_channel( From f10defe80feb0b986dbab2c68c71ddcdfcf0915d Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 24 Mar 2021 00:17:06 -0700 Subject: [PATCH 38/39] fixing unit tests --- tests/unittests/experiment_test_disease_mortality.py | 4 ++-- tests/unittests/test_disease_mortality.py | 4 ++-- tests/unittests/test_disease_progression.py | 4 ++-- tests/unittests/test_disease_transmission.py | 4 ++-- tests/unittests/test_miscellaneous_features.py | 4 ++-- tests/unittests/test_population_types.py | 4 ++-- tests/unittests/test_simulation_parameter.py | 4 ++-- tests/unittests/test_specific_interventions.py | 4 ++-- tests/unittests/unittest_support_classes.py | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/unittests/experiment_test_disease_mortality.py b/tests/unittests/experiment_test_disease_mortality.py index 954abcc4b..6d35933b7 100644 --- a/tests/unittests/experiment_test_disease_mortality.py +++ b/tests/unittests/experiment_test_disease_mortality.py @@ -1,5 +1,5 @@ import covasim as cv -from unittest_support_classes import CovaSimTest +from unittest_support_classes import CovaTest class SimKeys: @@ -46,7 +46,7 @@ def BaseSim(): return base_sim -class ExperimentalDiseaseMortalityTests(CovaSimTest): +class ExperimentalDiseaseMortalityTests(CovaTest): ''' Define the actual tests ''' def test_zero_deaths(self): diff --git a/tests/unittests/test_disease_mortality.py b/tests/unittests/test_disease_mortality.py index 184c1e05f..cc18ff442 100644 --- a/tests/unittests/test_disease_mortality.py +++ b/tests/unittests/test_disease_mortality.py @@ -5,7 +5,7 @@ import covasim as cv import unittest -from unittest_support_classes import CovaSimTest, TProps +from unittest_support_classes import CovaTest, TProps DProgKeys = TProps.ParKeys.ProgKeys TransKeys = TProps.ParKeys.TransKeys @@ -13,7 +13,7 @@ ResKeys = TProps.ResKeys -class DiseaseMortalityTests(CovaSimTest): +class DiseaseMortalityTests(CovaTest): def setUp(self): super().setUp() pass diff --git a/tests/unittests/test_disease_progression.py b/tests/unittests/test_disease_progression.py index d71781fd0..e6ce9fbae 100644 --- a/tests/unittests/test_disease_progression.py +++ b/tests/unittests/test_disease_progression.py @@ -4,13 +4,13 @@ """ import unittest -from unittest_support_classes import CovaSimTest, TProps +from unittest_support_classes import CovaTest, TProps ResKeys = TProps.ResKeys ParamKeys = TProps.ParKeys -class DiseaseProgressionTests(CovaSimTest): +class DiseaseProgressionTests(CovaTest): def setUp(self): super().setUp() pass diff --git a/tests/unittests/test_disease_transmission.py b/tests/unittests/test_disease_transmission.py index da5844bea..c2f3c13fa 100644 --- a/tests/unittests/test_disease_transmission.py +++ b/tests/unittests/test_disease_transmission.py @@ -3,12 +3,12 @@ ../../covasim/README.md """ -from unittest_support_classes import CovaSimTest, TProps +from unittest_support_classes import CovaTest, TProps TKeys = TProps.ParKeys.TransKeys Hightrans = TProps.SpecialSims.Hightransmission -class DiseaseTransmissionTests(CovaSimTest): +class DiseaseTransmissionTests(CovaTest): """ Tests of the parameters involved in transmission pre requisites simulation parameter tests diff --git a/tests/unittests/test_miscellaneous_features.py b/tests/unittests/test_miscellaneous_features.py index 2512e8dcd..1c885a415 100644 --- a/tests/unittests/test_miscellaneous_features.py +++ b/tests/unittests/test_miscellaneous_features.py @@ -4,11 +4,11 @@ import unittest import pandas as pd -from unittest_support_classes import CovaSimTest +from unittest_support_classes import CovaTest from covasim import Sim, parameters import os -class MiscellaneousFeatureTests(CovaSimTest): +class MiscellaneousFeatureTests(CovaTest): def setUp(self): super().setUp() self.sim = Sim() diff --git a/tests/unittests/test_population_types.py b/tests/unittests/test_population_types.py index 2c06fb7c3..5d00712a4 100644 --- a/tests/unittests/test_population_types.py +++ b/tests/unittests/test_population_types.py @@ -1,9 +1,9 @@ -from unittest_support_classes import CovaSimTest, TProps +from unittest_support_classes import CovaTest, TProps TPKeys = TProps.ParKeys.SimKeys -class PopulationTypeTests(CovaSimTest): +class PopulationTypeTests(CovaTest): def setUp(self): super().setUp() pass diff --git a/tests/unittests/test_simulation_parameter.py b/tests/unittests/test_simulation_parameter.py index 6d2e80bcd..ba3f871b7 100644 --- a/tests/unittests/test_simulation_parameter.py +++ b/tests/unittests/test_simulation_parameter.py @@ -4,12 +4,12 @@ """ import unittest -from unittest_support_classes import CovaSimTest, TProps +from unittest_support_classes import CovaTest, TProps TPKeys = TProps.ParKeys.SimKeys ResKeys = TProps.ResKeys -class SimulationParameterTests(CovaSimTest): +class SimulationParameterTests(CovaTest): def setUp(self): super().setUp() pass diff --git a/tests/unittests/test_specific_interventions.py b/tests/unittests/test_specific_interventions.py index aa7d4c610..3a3a2e76b 100644 --- a/tests/unittests/test_specific_interventions.py +++ b/tests/unittests/test_specific_interventions.py @@ -1,4 +1,4 @@ -from unittest_support_classes import CovaSimTest +from unittest_support_classes import CovaTest from unittest_support_classes import TProps from math import sqrt import json @@ -11,7 +11,7 @@ ResultsKeys = TProps.ResKeys SimKeys = TProps.ParKeys.SimKeys -class InterventionTests(CovaSimTest): +class InterventionTests(CovaTest): def setUp(self): super().setUp() pass diff --git a/tests/unittests/unittest_support_classes.py b/tests/unittests/unittest_support_classes.py index c81f9e7cc..1bc6a7692 100644 --- a/tests/unittests/unittest_support_classes.py +++ b/tests/unittests/unittest_support_classes.py @@ -138,7 +138,7 @@ class ResKeys: DurKeys = TProps.ParKeys.ProgKeys.DurKeys -class CovaSimTest(unittest.TestCase): +class CovaTest(unittest.TestCase): def setUp(self): self.is_debugging = False @@ -466,7 +466,7 @@ def set_smallpop_hightransmission(self): pass -class TestSupportTests(CovaSimTest): +class TestSupportTests(CovaTest): def test_run_vanilla_simulation(self): """ Runs an uninteresting but predictable From 3b4a23f63590fa6f0b8bb7c67d821378d891dd59 Mon Sep 17 00:00:00 2001 From: cliffckerr Date: Wed, 24 Mar 2021 00:31:00 -0700 Subject: [PATCH 39/39] update changelog --- CHANGELOG.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d84dffab3..3c29311bd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -33,7 +33,7 @@ This is the last release before the Covasim 3.0 launch (vaccines and variants). Highlights ^^^^^^^^^^ -- **Updated lognormal distributions**: Lognormal distributions had been inadvertently using the variance instead of the standard deviation as the second parameter, resulting in too small variance. This has been fixed. This has a small but nonzero impact on the results (e.g., the time to peak infections is about 5-10% sooner now). +- **Updated lognormal distributions**: Lognormal distributions had been inadvertently using the variance instead of the standard deviation as the second parameter, resulting in too small variance. This has been fixed. This has a small but nonzero impact on the results (e.g. with default parameters, the time to peak infections is about 5-10% sooner now). - **Expanded plotting features**: You now have much more flexibility with passing arguments to ``sim.plot()`` and other plotting functions, such as to temporarily set global Matplotlib options (such as DPI), modify axis styles and limits, etc. For example, you can now do things like this: ``cv.Sim().run().plot(dpi=150, rotation=30, start_day='2020-03-01', end_day=55, interval=7)``. - **Improved analyzers**: Transmission trees can be computed 20 times faster, Fit objects are more forgiving for data problems, and analyzers can now be exported to JSON. @@ -69,8 +69,8 @@ Documentation and testing Regression information ^^^^^^^^^^^^^^^^^^^^^^ -- To restore previous behavior on a simulation (i.e. using variance instead of standard deviation for lognormal distributions), call ``cv.misc.migrate_lognormal(sim)``. This is done automatically when loading a saved sim from disk. To undo a migration, type ``cv.misc.migrate_lognormal(sim, revert=True)``. In practice, this loops over the duration parameters and replaces ``par2`` with its square root. If you have used lognormal distributions elsewhere, you will need to update them manually. -- Code that was designed to parse transmission trees will likely need to be revised. The object ``tt.detailed`` is now a dataframe; calling ``tt.detailed.to_dict('records')`` will bring it very close to what it used to be, with the exception that for a given row, ``'t'`` and ``'s'`` used to be nested dictionaries, whereas now they are prefixes. For example, whereas before the "is quarantined" state of the source of the 45th infection would have been ``tt.detailed[45]['s']['is_quarantined']``, it is now ``tt.detailed.iloc[45]['src_is_quarantined']``. +- To restore previous behavior for a simulation (i.e. using variance instead of standard deviation for lognormal distributions), call ``cv.misc.migrate_lognormal(sim)``. This is done automatically when loading a saved sim from disk. To undo a migration, type ``cv.misc.migrate_lognormal(sim, revert=True)``. What this function does is loop over the duration parameters and replace ``par2`` with its square root. If you have used lognormal distributions elsewhere, you will need to update them manually. +- Code that was designed to parse transmission trees will likely need to be revised. The object ``tt.detailed`` is now a dataframe; calling ``tt.detailed.to_dict('records')`` will bring it very close to what it used to be, with the exception that for a given row, ``'t'`` and ``'s'`` used to be nested dictionaries, whereas now they are prefixes. For example, whereas before the 45th person's source's "is quarantined" state would have been ``tt.detailed[45]['s']['is_quarantined']``, it is now ``tt.detailed.iloc[45]['src_is_quarantined']``. - *GitHub info*: PR `859 `__