diff --git a/docs/Makefile b/docs/Makefile index d4bb2cb..537e617 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,9 +4,9 @@ # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 902dc1d..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,55 +0,0 @@ -import os -import sys -sys.path.insert(0, os.path.abspath('../src/epidemik/')) -import epidemik - -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - -project = 'epidemik' -copyright = '2024, Bruno Gonçalves' -author = 'Bruno Gonçalves' -release = epidemik.__version__ - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = [ - 'sphinx_rtd_theme', - 'sphinx.ext.duration', - 'sphinx.ext.doctest', - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', -] - -templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - - - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = 'sphinx_rtd_theme' -html_static_path = ['_static'] -html_theme_options = { - 'analytics_id': 'G-HKWS10TRJ1', - 'analytics_anonymize_ip': False, - 'logo_only': False, - 'display_version': True, - 'prev_next_buttons_location': 'bottom', - 'style_external_links': True, - 'vcs_pageview_mode': '', - 'style_nav_header_background': 'white', - # Toc options - 'collapse_navigation': True, - 'sticky_navigation': True, - 'navigation_depth': 4, - 'includehidden': True, - 'titles_only': False -} \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat index 32bb245..747ffb7 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -7,8 +7,8 @@ REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) -set SOURCEDIR=. -set BUILDDIR=_build +set SOURCEDIR=source +set BUILDDIR=build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index a2118c0..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx -sphinx_rtd_theme -epidemik diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..c2a7b23 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,61 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +import epidemik + +project = 'epidemik' +copyright = '2024, Bruno Gonçalves' +author = 'Bruno Gonçalves' + +# The full version, including alpha/beta/rc tags +release = epidemik.__version__ + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', + 'sphinx.ext.napoleon', + 'sphinx_autodoc_typehints', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/docs/epidemik.rst b/docs/source/epidemik.rst similarity index 100% rename from docs/epidemik.rst rename to docs/source/epidemik.rst diff --git a/docs/index.rst b/docs/source/index.rst similarity index 86% rename from docs/index.rst rename to docs/source/index.rst index 4c59ea2..f7dfea7 100644 --- a/docs/index.rst +++ b/docs/source/index.rst @@ -1,5 +1,5 @@ .. epidemik documentation master file, created by - sphinx-quickstart on Mon Apr 15 09:20:13 2024. + sphinx-quickstart on Sun Mar 30 10:52:07 2025. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. @@ -10,7 +10,7 @@ Welcome to epidemik's documentation! :maxdepth: 2 :caption: Contents: - + modules Indices and tables ================== diff --git a/docs/modules.rst b/docs/source/modules.rst similarity index 70% rename from docs/modules.rst rename to docs/source/modules.rst index 6b48f2e..e9a7931 100644 --- a/docs/modules.rst +++ b/docs/source/modules.rst @@ -1,5 +1,5 @@ -epidemik -======== +src +=== .. toctree:: :maxdepth: 4 diff --git a/models/SEIIR.yaml b/models/SEIIR.yaml new file mode 100644 index 0000000..b4e195f --- /dev/null +++ b/models/SEIIR.yaml @@ -0,0 +1,20 @@ +# Epidemic Model with 5 compartments and 6 transitions: +Name: SEIIR + +Parameters: + rate : 0.166667 + beta : 0.222222 + epsilon_a : 0.160000 + epsilon_s : 0.240000 + mu : 0.100000 + + +Transitions: + - S + Ia = E rate + - S + Is = E beta + - E -> Ia epsilon_a + - E -> Is epsilon_s + - Ia -> R mu + - Is -> R mu + +# R0=2.00 diff --git a/models/SEIIRD.yaml b/models/SEIIRD.yaml new file mode 100644 index 0000000..59b90e7 --- /dev/null +++ b/models/SEIIRD.yaml @@ -0,0 +1,23 @@ +# Epidemic Model with 6 compartments and 7 transitions: +Name: SEIIRD + +Parameters: + rbeta : 0.166667 + beta : 0.222222 + epsilon_a : 0.160000 + epsilon_s : 0.240000 + mu : 0.100000 + mu_nd : 0.090000 + mu_d : 0.010000 + + +Transitions: + - S + Ia = E rbeta + - S + Is = E beta + - E -> Ia epsilon_a + - E -> Is epsilon_s + - Ia -> R mu + - Is -> R mu_nd + - Is -> D mu_d + +# R0=2.00 diff --git a/models/SEIR.yaml b/models/SEIR.yaml new file mode 100644 index 0000000..3619ae5 --- /dev/null +++ b/models/SEIR.yaml @@ -0,0 +1,15 @@ +# Epidemic Model with 4 compartments and 3 transitions: +Name: SEIR + +Parameters: + beta : 0.200000 + epsilon : 0.400000 + mu : 0.100000 + + +Transitions: + - S + I = E beta + - E -> I epsilon + - I -> R mu + +# R0=2.00 diff --git a/models/SEIRS.yaml b/models/SEIRS.yaml new file mode 100644 index 0000000..e8d191d --- /dev/null +++ b/models/SEIRS.yaml @@ -0,0 +1,15 @@ +# Epidemic Model with 4 compartments and 4 transitions: +Name: SEIRS + +Parameters: + beta : 0.200000 + epsilon : 0.400000 + mu : 0.100000 + rho : 0.300000 + + +Transitions: + - S + I = E beta + - E -> I epsilon + - I -> R mu + - R -> S rho diff --git a/models/SI.yaml b/models/SI.yaml new file mode 100644 index 0000000..0c3902e --- /dev/null +++ b/models/SI.yaml @@ -0,0 +1,8 @@ +# Epidemic Model with 2 compartments and 1 transitions: + +Parameters: + beta : 0.200000 + + +Transitions: + - S + I = I beta diff --git a/models/model_list.txt b/models/model_list.txt new file mode 100644 index 0000000..a44b893 --- /dev/null +++ b/models/model_list.txt @@ -0,0 +1,6 @@ +SEIIR.yaml +SEIIRD.yaml +SEIR.yaml +SEIRS.yaml +SI.yaml +SIR.yaml diff --git a/poetry.lock b/poetry.lock index b58a4a6..de705cd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,150 @@ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +[[package]] +name = "alabaster" +version = "0.7.16" +description = "A light, configurable Sphinx theme" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, +] + +[[package]] +name = "babel" +version = "2.17.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, +] + +[package.extras] +dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] + +[[package]] +name = "certifi" +version = "2025.1.31" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + [[package]] name = "colorama" version = "0.4.6" @@ -150,6 +295,86 @@ mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.4.1)", "types-Pill test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] test-no-images = ["pytest", "pytest-cov", "wurlitzer"] +[[package]] +name = "coverage" +version = "7.6.12" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, + {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, + {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, + {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, + {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, + {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, + {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, + {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, + {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, + {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, + {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, + {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, + {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, + {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, + {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, + {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, + {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "cycler" version = "0.12.1" @@ -167,6 +392,19 @@ files = [ docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] tests = ["pytest", "pytest-cov", "pytest-xdist"] +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + [[package]] name = "exceptiongroup" version = "1.2.2" @@ -258,6 +496,60 @@ ufo = ["fs (>=2.2.0,<3)"] unicode = ["unicodedata2 (>=15.1.0)"] woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "importlib-metadata" +version = "8.6.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version < \"3.10\"" +files = [ + {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, + {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + [[package]] name = "importlib-resources" version = "6.4.5" @@ -295,6 +587,38 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "joblib" +version = "1.4.2" +description = "Lightweight pipelining with Python functions" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"}, + {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, +] + [[package]] name = "kiwisolver" version = "1.4.7" @@ -420,6 +744,78 @@ files = [ {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, ] +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + [[package]] name = "matplotlib" version = "3.7.5" @@ -747,6 +1143,22 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pyparsing" version = "3.1.4" @@ -787,6 +1199,26 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "6.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -880,6 +1312,85 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "scikit-learn" +version = "1.6.1" +description = "A set of python modules for machine learning and data mining" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e"}, + {file = "scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36"}, + {file = "scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5"}, + {file = "scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b"}, + {file = "scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002"}, + {file = "scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33"}, + {file = "scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d"}, + {file = "scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2"}, + {file = "scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8"}, + {file = "scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415"}, + {file = "scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b"}, + {file = "scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2"}, + {file = "scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f"}, + {file = "scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86"}, + {file = "scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52"}, + {file = "scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322"}, + {file = "scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1"}, + {file = "scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348"}, + {file = "scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97"}, + {file = "scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f"}, + {file = "scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1"}, + {file = "scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e"}, + {file = "scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107"}, + {file = "scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422"}, + {file = "scikit_learn-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b"}, + {file = "scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e"}, +] + +[package.dependencies] +joblib = ">=1.2.0" +numpy = ">=1.19.5" +scipy = ">=1.6.0" +threadpoolctl = ">=3.1.0" + +[package.extras] +benchmark = ["matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "pandas (>=1.1.5)"] +build = ["cython (>=3.0.10)", "meson-python (>=0.16.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)"] +docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pydata-sphinx-theme (>=0.15.3)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=7.3.7)", "sphinx-copybutton (>=0.5.2)", "sphinx-design (>=0.5.0)", "sphinx-design (>=0.6.0)", "sphinx-gallery (>=0.17.1)", "sphinx-prompt (>=1.4.0)", "sphinx-remove-toctrees (>=1.0.0.post1)", "sphinxcontrib-sass (>=0.3.4)", "sphinxext-opengraph (>=0.9.1)", "towncrier (>=24.8.0)"] +examples = ["matplotlib (>=3.3.4)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)"] +install = ["joblib (>=1.2.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)", "threadpoolctl (>=3.1.0)"] +maintenance = ["conda-lock (==2.5.6)"] +tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.5.1)", "scikit-image (>=0.17.2)"] + [[package]] name = "scipy" version = "1.9.3" @@ -920,6 +1431,29 @@ dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"] doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"] test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +[[package]] +name = "seaborn" +version = "0.13.2" +description = "Statistical data visualization" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987"}, + {file = "seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7"}, +] + +[package.dependencies] +matplotlib = ">=3.4,<3.6.1 || >3.6.1" +numpy = ">=1.20,<1.24.0 || >1.24.0" +pandas = ">=1.2" + +[package.extras] +dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-xdist"] +docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] +stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"] + [[package]] name = "six" version = "1.17.0" @@ -933,6 +1467,234 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "sphinx" +version = "7.4.0" +description = "Python documentation generator" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "sphinx-7.4.0-py3-none-any.whl", hash = "sha256:4bcce443b66139eb9556117ba881246269edce396e7f939e22c5ed85186bfd5b"}, + {file = "sphinx-7.4.0.tar.gz", hash = "sha256:8385520a28dc129ebf8b5fccfa1beb71215fd4455c6d10fa418e08c3c7a2ff9c"}, +] + +[package.dependencies] +alabaster = ">=0.7.14,<0.8.0" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "2.3.0" +description = "Type hints (PEP 484) support for the Sphinx autodoc extension" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "sphinx_autodoc_typehints-2.3.0-py3-none-any.whl", hash = "sha256:3098e2c6d0ba99eacd013eb06861acc9b51c6e595be86ab05c08ee5506ac0c67"}, + {file = "sphinx_autodoc_typehints-2.3.0.tar.gz", hash = "sha256:535c78ed2d6a1bad393ba9f3dfa2602cf424e2631ee207263e07874c38fde084"}, +] + +[package.dependencies] +sphinx = ">=7.3.5" + +[package.extras] +docs = ["furo (>=2024.1.29)"] +numpy = ["nptyping (>=2.5)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.4.4)", "defusedxml (>=0.7.1)", "diff-cover (>=9)", "pytest (>=8.1.1)", "pytest-cov (>=5)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.11)"] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.0.2" +description = "Read the Docs theme for Sphinx" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, + {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, +] + +[package.dependencies] +docutils = ">0.18,<0.22" +sphinx = ">=6,<9" +sphinxcontrib-jquery = ">=4,<5" + +[package.extras] +dev = ["bump2version", "transifex-client", "twine", "wheel"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +description = "Extension to include jQuery on newer Sphinx releases" +optional = false +python-versions = ">=2.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, + {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, +] + +[package.dependencies] +Sphinx = ">=1.8" + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "threadpoolctl" +version = "3.5.0" +description = "threadpoolctl" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467"}, + {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, +] + [[package]] name = "tomli" version = "2.2.1" @@ -1012,13 +1774,32 @@ files = [ {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, ] +[[package]] +name = "urllib3" +version = "2.3.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "zipp" version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] markers = "python_version < \"3.10\"" files = [ {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, @@ -1036,4 +1817,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9" -content-hash = "a5d739fb2fdeb45ba248060a046e398f351934a4d369447c527b748f46fac11d" +content-hash = "984905e1d906007920832044ec007bca273944f12a35442a2ab160897766cae6" diff --git a/pyproject.toml b/pyproject.toml index b611c46..5266ee3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ dependencies = [ "scipy>=1.7", "tqdm>=4", "pyyaml (>=6.0.2,<7.0.0)", + "seaborn (>=0.13.2,<0.14.0)", + "scikit-learn (>=1.6.1,<2.0.0)", ] [project.urls] Homepage = "https://github.com/DataForScience/epidemik" @@ -32,6 +34,10 @@ enable = true [tool.poetry.group.dev.dependencies] pytest = "^8.3.4" +pytest-cov = "^6.0.0" +sphinx = "7.4" +sphinx-rtd-theme = "^3.0.2" +sphinx-autodoc-typehints = "2.3.0" [build-system] requires = ["poetry-core>=2.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..0dbcc57 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --ignore=.venv --cov=epidemik diff --git a/src/epidemik/EpiModel.py b/src/epidemik/EpiModel.py index bbbc5b6..9479422 100644 --- a/src/epidemik/EpiModel.py +++ b/src/epidemik/EpiModel.py @@ -3,17 +3,18 @@ # @author Bruno Goncalves ###################################################### -from typing import Dict, List, Set, Union +from typing import Dict, List, Set, Union, Self import warnings import string import time import os import re +from urllib.parse import urlparse +from urllib.request import urlretrieve import networkx as nx import numpy as np from numpy import linalg -from numpy import random import scipy.integrate import pandas as pd import matplotlib.pyplot as plt @@ -21,7 +22,7 @@ from typing import Union -from .utils import * +from . import utils class EpiModel(object): @@ -32,22 +33,24 @@ class EpiModel(object): def __init__(self, compartments=None, seed=None, rng=None): """ - Initialize the EpiModel object - - Parameters: - - compartments: list of strings, optional - List of compartment names - - Returns: - None + Initialize the EpiModel object. + + :param compartments: List of compartment names + :type compartments: list[str], optional + :param seed: Seed for the random number generator. If None, it is computed using the current time and process ID + :type seed: int, optional + :param rng: Instance of a random number generator + :type rng: numpy.random.Generator, optional + :return: None """ + self.name = None self.transitions = nx.MultiDiGraph() self.seasonality = None self.population = None self.orig_comps = None self.demographics = False - self.params = {} + self.params = utils.Parameters() if seed is None: seed = int(time.time()) + os.getpid() @@ -60,116 +63,233 @@ def __init__(self, compartments=None, seed=None, rng=None): if compartments is not None: self.transitions.add_nodes_from([comp for comp in compartments]) - def add_interaction(self, source: str, target: str, agent: str, **rates) -> None: + def add_interaction( + self, + source: str, + target: str, + agent: str, + rate: Union[None, float, str] = None, + norm=True, + **rates, + ) -> None: + """ + Add an interaction between two compartments. + + This method adds a directed edge from the source compartment to the target compartment in the transition graph, + with the specified agent and rate. The rates are passed as keyword arguments and will be added to the model's parameters. + + :param source: Name of the source compartment + :type source: str + :param target: Name of the target compartment + :type target: str + :param agent: Name of the agent + :type agent: str + :param rates: Named parameters representing the interaction rates + :type rates: dict + :return: None + """ - Add an interaction between two compartments - Parameters: - - source: string - Name of the source compartment - - target: string - Name of the target compartment - - agent: string - Name of the agent - - params: string - Named parameters for the interaction + if rate is not None: + count = len(self.params) + 1 + rate_key = "rate" + str(count) + self.params[rate_key]=rate + else: + self.params.define_parameters(**rates) + rates = list(rates.keys()) + rate_key = rates[0] - Returns: - None + if agent not in self.transitions.nodes: + self.transitions.add_node(agent) + + self.transitions.add_edge(source, target, agent=agent, rate=rate_key, norm=norm) + + def add_spontaneous( + self, source: str, target: str, rate: Union[None, float, str] = None, **rates + ) -> None: + """ + Add a spontaneous transition between two compartments. + + :param source: Name of the source compartment + :type source: str + :param target: Name of the target compartment + :type target: str + :param rates: Named parameters representing the transition rates + :type rates: dict + :return: None """ - self.params.update(rates) - rates = list(rates.keys()) + if rate is not None: + count = len(self.params) + 1 + rate_key = "rate" + str(count) + self.params[rate_key, rate] + else: + self.params.define_parameters(**rates) + rates = list(rates.keys()) + rate_key = rates[0] - self.transitions.add_edge(source, target, agent=agent, rate=rates[0]) + self.transitions.add_edge(source, target, rate=rate_key) - def add_spontaneous(self, source: str, target: str, **rates) -> None: + def add_viral_generation( + self, + source: str, + target: str, + source_rate: Union[float, str, None] = None, + target_rate: Union[float, str, None] = None, + **rates, + ) -> None: """ - Add a spontaneous transition between two compartments + Add a viral generation transition Parameters: - source: string Name of the source compartment - target: string Name of the target compartment - - rate: float - Rate of the transition - - Returns: - None + - source_rate: float + Rate of destruction of infected cells + - target_rate: float + Rate of creation of viral particles """ - self.params.update(rates) - rates = list(rates.keys()) + if source_rate is not None and target_rate is not None: + count = len(self.params) + 1 + rate_key = "rate" + str(count) + self.params[rate_key] = source_rate - self.transitions.add_edge(source, target, rate=rates[0]) + rate_key = "rate" + str(count + 1) + self.params[rate_key] = target_rate + else: + self.params.define_parameters(**rates) + rates = list(rates.keys()) + source_rate = rates[0] + target_rate = rates[1] - def add_birth_rate(self, rate: float, comps: Union[List, None] = None) -> None: - """ - Add a birth rate to one or more compartments + self.transitions.add_edge( + source, target, rate=source_rate, viral_source=True, viral_target=False, + ) + self.transitions.add_edge( + source, target, rate=target_rate, viral_source=False, viral_target=True + ) - Parameters: - - rate: float - Birth rate - - comps: list, optional, default=None - List of compartments to which to assign this birth rate. - If None, apply to all compartments + def add_birth_rate( + self, + comps: Union[List, None] = None, + rate: Union[float, None] = None, + fixed: bool = False, + global_rate: bool = True, + **rates + ) -> None: + """ + Add a birth rate to one or more compartments. + + :param rate: Birth rate + :type rate: float + :param comps: List of compartments to which to assign this birth rate. If None, apply to all compartments + :type comps: list[str], optional + :return: None """ self.demographics = True + if rate is not None: + count = len(self.params) + 1 + rate_key = "rate" + str(count) + self.params[rate_key] = rate + else: + self.params.update(rates) + rate_key = list(rates.keys())[0] + if comps is None: comps = self.transitions.nodes() for comp in comps: - self.transitions.nodes[comp]["birth"] = rate - - def add_death_rate(self, rate: float, comps: Union[List, None] = None) -> None: + if comp not in self.transitions.nodes: + self.transitions.add_node(comp) + + self.transitions.nodes[comp]["birth"] = rate_key + self.transitions.nodes[comp]["fixed_birth"] = fixed + self.transitions.nodes[comp]["global_birth"] = global_rate + + def add_death_rate( + self, + comps: Union[List, None] = None, + rate: Union[None, float] = None, + fixed: bool = False, + global_rate: bool = False, + **rates + ) -> None: """ - Add a birth rate to one or more compartments - - Parameters: - - rate: float - Death rate - - comps: list, optional, default=None - List of compartments to which to assign this death rate. - If None, apply to all compartments + Add a death rate to one or more compartments. + + :param rate: Death rate + :type rate: float + :param comps: List of compartments to which to assign this death rate. If None, apply to all compartments + :type comps: list[str], optional + :return: None """ self.demographics = True + if rate is not None: + count = len(self.params) + 1 + rate_key = "rate" + str(count) + self.params[rate_key] = rate + else: + self.params.update(rates) + rate_key = list(rates.keys())[0] + if comps is None: comps = self.transitions.nodes() for comp in comps: - self.transitions.nodes[comp]["death"] = rate + if comp not in self.transitions.nodes: + self.transitions.add_node(comp) + + self.transitions.nodes[comp]["death"] = rate_key + self.transitions.nodes[comp]["fixed_death"] = fixed + self.transitions.nodes[comp]["global_death"] = global_rate def add_vaccination( - self, source: str, target: str, rate: float, start: int + self, + source: str, + target: str, + start: int, + rate: Union[None, float, str], + **rates, ) -> None: """ - Add a vaccination transition between two compartments + Add a vaccination transition between two compartments. + + :param source: Name of the source compartment + :type source: str + :param target: Name of the target compartment + :type target: str + :param rate: Rate of the vaccination + :type rate: float + :param start: Start time of the vaccination + :type start: int + :return: None + """ - Parameters: - - source: string - Name of the source compartment - - target: string - Name of the target compartment - - rate: float - Rate of the vaccination - - start: int - Start time of the vaccination + if rate is not None: + count = len(self.params) + 1 + rate_key = "rate" + str(count) + self.params[rate_key] = rate + else: + self.params.update(rates) + rate_key = list(rates.keys())[0] - Returns: - None - """ - self.transitions.add_edge(source, target, rate=rate, start=start) + self.transitions.add_edge(source, target, rate=rate_key, start=start) def add_age_structure(self, matrix: List, population: List) -> List[List]: """ - Add a vaccination transition between two compartments - - Parameters: - - matrix: List - - population: List + Add age structure to the model using a contact matrix. + + :param matrix: Contact matrix between age groups + :type matrix: list[list[float]] + :param population: Population size for each age group + :type population: list[int] + :return: The modified model structure + :rtype: list[list] """ self.contact = np.asarray(matrix) self.population = np.asarray(population).flatten() @@ -219,19 +339,16 @@ def add_age_structure(self, matrix: List, population: List) -> List[List]: def _new_cases(self, time: float, population: np.ndarray, pos: Dict) -> np.ndarray: """ - Internal function used by integration routine - - Parameters: - - population: numpy array - Current population of each compartment - - time: float - Current time - - pos: dict - Dictionary mapping compartment names to indices - - Returns: - numpy array - Array of new cases for each compartment + Internal function used by integration routine. + + :param time: Current simulation time + :type time: float + :param population: Current population of each compartment + :type population: numpy.ndarray + :param pos: Dictionary mapping compartment names to indices + :type pos: dict + :return: Array of new cases for each compartment + :rtype: numpy.ndarray """ diff = np.zeros(len(pos)) N = np.sum(population) @@ -259,32 +376,54 @@ def _new_cases(self, time: float, population: np.ndarray, pos: Dict) -> np.ndarr if "agent" in trans: agent = trans["agent"] + rate *= population[pos[agent]] - if self.population is None: - rate *= population[pos[agent]] / N - else: - rate *= population[pos[agent]] / N[agent] - + if trans["norm"]: + if self.population is None: + rate /= N + else: + rate /= N[agent] + if self.seasonality is not None: curr_t = int(time) % 365 season = float(self.seasonality[curr_t]) rate *= season - diff[pos[source]] -= rate - diff[pos[target]] += rate + if "viral_source" not in trans or trans["viral_source"]: + diff[pos[source]] -= rate + # Make sure viral generations are asymetric + if "viral_target" not in trans or trans["viral_target"]: + diff[pos[target]] += rate - # Population dynamics - if self.demographics: - for comp, data in self.transitions.nodes(data=True): - comp_id = pos[comp] + # Population dynamics + if self.demographics: + for comp, data in self.transitions.nodes(data=True): + comp_id = pos[comp] - if "birth" in data: - births = population[comp_id] * data["birth"] - diff[comp_id] += births + if "birth" in data: + if "fixed_birth" in data and data["fixed_birth"]: + births = self.params[data["birth"]] + else: + if data["global_birth"]: + total_population = population.sum() + births = total_population * self.params[data["birth"]] + else: + births = population[comp_id] * self.params[data["birth"]] + + diff[comp_id] += births + + if "death" in data: + if "fixed_death" in data and data["fixed_death"]: + deaths = self.params[data["death"]] + else: + if data["global_death"]: + total_population = population.sum() + deaths = total_population * self.params[data["death"]] + else: + deaths = population[comp_id] * self.params[data["death"]] + + diff[comp_id] -= deaths - if "death" in data: - deaths = population[comp_id] * data["death"] - diff[comp_id] -= deaths return diff @@ -297,23 +436,20 @@ def plot( **kwargs, ): """ - Convenience function for plotting - - Parameters: - - title: string, optional, default=None - Title of the plot - - normed: bool, default=True - Whether to normalize the values or not - - ax: matplotlib Axes object, default=None - The Axes object to plot to. If None, a new figure is created. - - show: bool, default=True - Whether to call plt.show() or not - - kwargs: keyword arguments - Additional arguments to pass to the plot function - - Returns: - matplotlib.axes._subplots.AxesSubplot - The plot object + Convenience function for plotting model results. + + :param title: Title of the plot + :type title: str, optional + :param normed: Whether to normalize the values or not + :type normed: bool, default=True + :param show: Whether to call plt.show() or not + :type show: bool, default=True + :param ax: The Axes object to plot to. If None, a new figure is created + :type ax: matplotlib.axes._subplots.AxesSubplot, optional + :param kwargs: Additional arguments to pass to the plot function + :type kwargs: dict + :return: The plot object + :rtype: matplotlib.axes._subplots.AxesSubplot """ try: if normed: @@ -325,7 +461,7 @@ def plot( ax = plt.gca() for comp in self.values_.columns: - (self.values_[comp] / N).plot(c=epi_colors[comp[0]], **kwargs) + (self.values_[comp] / N).plot(c=utils.EPI_COLORS[comp[0]], **kwargs) ax.legend(self.values_.columns) ax.set_xlabel("Time") @@ -340,19 +476,17 @@ def plot( return ax except Exception as e: print(e) - raise NotInitialized("You must call integrate() or simulate() first") + raise utils.NotInitialized("You must call integrate() or simulate() first") def __getattr__(self, name: str) -> pd.Series: """ - Dynamic method to return the individual compartment values - - Parameters: - - name: string - Name of the compartment - - Returns: - pandas.Series - The values of the specified compartment + Dynamic method to return the individual compartment values. + + :param name: Name of the compartment + :type name: str + :return: The values of the specified compartment + :rtype: pandas.Series + :raises AttributeError: If the model hasn't been integrated or simulated yet """ if "values_" in self.__dict__: return self.values_[name] @@ -367,20 +501,17 @@ def simulate( **kwargs, ) -> None: """ - Stochastically simulate the epidemic model - - Parameters: - - timesteps: int - Number of time steps to simulate - - t_min: int, optional - Starting time - - seasonality: numpy array, optional - Array of seasonal factors - - kwargs: keyword arguments - Initial population of each compartment - - Returns: - None + Stochastically simulate the epidemic model. + + :param timesteps: Number of time steps to simulate + :type timesteps: int + :param t_min: Starting time + :type t_min: int, default=1 + :param seasonality: Array of seasonal factors + :type seasonality: numpy.ndarray, optional + :param kwargs: Initial population of each compartment + :type kwargs: dict + :return: None """ pos = {comp: i for i, comp in enumerate(self.transitions.nodes())} population = np.zeros(len(pos), dtype="int") @@ -410,7 +541,7 @@ def simulate( source = pos[comp] target = pos[node_j] - rate = self.params[data["rate"]] + rate = self.params[data["rate"]] # self.params[data["rate"]] if "start" in data and data["start"] >= t: continue @@ -428,7 +559,7 @@ def simulate( prob[source] = 1 - np.sum(prob) - delta = random.multinomial(pop[source], prob) + delta = self.rng.multinomial(pop[source], prob) delta[source] = 0 changes = np.sum(delta) @@ -447,11 +578,15 @@ def simulate( comp_id = pos[comp] if "birth" in data: - births = self.rng.binomial(pop[comp_id], data["birth"]) + births = self.rng.binomial( + pop[comp_id], self.params[data["birth"]] + ) new_pop[comp_id] += births if "death" in data: - deaths = self.rng.binomial(pop[comp_id], data["death"]) + deaths = self.rng.binomial( + pop[comp_id], self.params[data["death"]] + ) new_pop[comp_id] -= deaths values.append(new_pop) @@ -467,20 +602,17 @@ def integrate( **kwargs, ) -> None: """ - Numerically integrate the epidemic model - - Parameters: - - timesteps: int - Number of time steps to integrate - - t_min: int, optional - Starting time - - seasonality: numpy array, optional - Array of seasonality values - - kwargs: keyword arguments - Initial population of each compartment - - Returns: - None + Numerically integrate the epidemic model. + + :param timesteps: Number of time steps to integrate + :type timesteps: int + :param t_min: Starting time + :type t_min: int, default=1 + :param seasonality: Array of seasonality values + :type seasonality: numpy.ndarray, optional + :param kwargs: Initial population of each compartment + :type kwargs: dict + :return: None """ pos = {comp: i for i, comp in enumerate(self.transitions.nodes())} population = np.zeros(len(pos)) @@ -503,7 +635,7 @@ def integrate( population[pos[comp_age]] = n[i] - time = np.arange(t_min, t_min + timesteps, 1) + time = np.arange(t_min, t_min + timesteps) self.seasonality = seasonality values = pd.DataFrame( @@ -531,6 +663,15 @@ def integrate( self.values_ = totals[self.orig_comps].copy() def single_step(self, seasonality=None, **kwargs): + """ + Perform a single simulation step. + + :param seasonality: Array of seasonality values + :type seasonality: numpy.ndarray, optional + :param kwargs: Initial population of each compartment if simulation hasn't started + :type kwargs: dict + :return: None + """ if hasattr(self, "values_") is False: self.simulate(2, 1, seasonality=seasonality, **kwargs) else: @@ -542,11 +683,10 @@ def single_step(self, seasonality=None, **kwargs): def __repr__(self) -> str: """ - Return a string representation of the EpiModel object - - Returns: - string - String representation of the EpiModel object + Return a string representation of the EpiModel object. + + :return: String representation of the EpiModel object + :rtype: str """ text = "# Epidemic Model with %u compartments and %u transitions:" % ( self.transitions.number_of_nodes(), @@ -559,7 +699,7 @@ def __repr__(self) -> str: text += "Parameters:\n" for rate, value in self.params.items(): - text += " %s : %f\n" % (rate, value) + text += " %s : %s\n" % (rate, value) text += "\n\nTransitions:\n" for edge in self.transitions.edges(data=True): @@ -567,9 +707,11 @@ def __repr__(self) -> str: target = edge[1] trans = edge[2] + # Interaction if "agent" in trans: agent = trans["agent"] text += " - %s + %s = %s %s\n" % (source, agent, target, trans["rate"]) + # Vaccination elif "start" in trans: start = trans["start"] text += " - %s -> %s %s starting at %s days\n" % ( @@ -578,9 +720,26 @@ def __repr__(self) -> str: rate, start, ) + # Viral transition + elif "source_rate" in trans: + text += " - %s => %s %s %s" % ( + source, + target, + trans["source_rate"], + trans["target_rate"], + ) + # Spontaneous else: text += " - %s -> %s %s\n" % (source, target, rate) + if self.demographics: + text += "\n\nDemographics:\n" + for comp, data in self.transitions.nodes(data=True): + if "birth" in data: + text += " - -> %s: %s # birth rate\n" % (comp, data["birth"]) + if "death" in data: + text += " - %s ->: %s # death rate\n" % (comp, data["death"]) + R0 = self.R0() if R0 is not None: @@ -589,6 +748,12 @@ def __repr__(self) -> str: return text def _get_active(self) -> Set: + """ + Get the set of active compartments or agents in the model. + + :return: Set of active compartments or agents + :rtype: set + """ active = set() for node_i, node_j, data in self.transitions.edges(data=True): @@ -600,6 +765,12 @@ def _get_active(self) -> Set: return active def _get_susceptible(self) -> Set: + """ + Get the set of susceptible compartments in the model. + + :return: Set of susceptible compartments + :rtype: set + """ susceptible = set( [node for node, deg in self.transitions.in_degree() if deg == 0] ) @@ -612,6 +783,12 @@ def _get_susceptible(self) -> Set: return susceptible def _get_infections(self) -> Dict: + """ + Get the dictionary of infection transitions in the model. + + :return: Dictionary of infection transitions + :rtype: dict + """ inf = {} for node_i, node_j, data in self.transitions.edges(data=True): @@ -631,12 +808,13 @@ def _get_infections(self) -> Dict: def draw_model(self, ax: Union[plt.Axes, None] = None, show: bool = True) -> None: """ - Plot the model structure - - - ax: matplotlib Axes object, default=None - The Axes object to plot to. If None, a new figure is created. - - show: bool, default=True - Whether to call plt.show() or not + Plot the model structure. + + :param ax: The Axes object to plot to. If None, a new figure is created + :type ax: matplotlib.axes._subplots.AxesSubplot, optional + :param show: Whether to call plt.show() or not + :type show: bool, default=True + :return: None """ trans = self.transitions.copy() @@ -670,7 +848,7 @@ def draw_model(self, ax: Union[plt.Axes, None] = None, show: bool = True) -> Non orig_pos = pos[node[3:]] pos[node] = [orig_pos[0], orig_pos[1] - 1] else: - node_colors.append(epi_colors[node[0]]) + node_colors.append(utils.EPI_COLORS[node[0]]) edge_labels = {} @@ -706,13 +884,12 @@ def draw_model(self, ax: Union[plt.Axes, None] = None, show: bool = True) -> Non def R0(self) -> Union[float, None]: """ - Return the value of the basic reproductive ratio, $R_0$, for the model as defined - - The calculation is completely generic as it uses the Next-Generation matrix approach - defined in J. R. Soc Interface 7, 873 (2010) - - Returns: - R0 - the value of the largest eigenvalue of the next generation matrix + Calculate the basic reproductive ratio (R0) for the model. + + Uses the Next-Generation matrix approach defined in J. R. Soc Interface 7, 873 (2010). + + :return: The value of the largest eigenvalue of the next generation matrix, or None if calculation fails + :rtype: float or None """ infected = set() @@ -734,7 +911,7 @@ def R0(self) -> Union[float, None]: try: for node_i, node_j, data in self.transitions.edges(data=True): - rate = self.params[data["rate"]] + rate = self.params[data["rate"]] # self.params[data["rate"]] if "agent" in data: target = pos[node_j] @@ -761,6 +938,14 @@ def R0(self) -> Union[float, None]: return None def __getitem__(self, key): + """ + Allow indexing the model with compartment names to get their values. + + :param key: Compartment name or list of compartment names + :type key: str or list[str] + :return: The values for the specified compartment(s) + :rtype: pandas.Series or pandas.DataFrame or None + """ if type(key) != type([]): key_check = set([key]) else: @@ -775,28 +960,50 @@ def __getitem__(self, key): def save_model(self, filename: str) -> None: """ - Save the model to a file - - Parameters: - - filename: string - Name of the file to save the model to - - Returns: - None + Save the model to a file. + + :param filename: Name of the file to save the model to + :type filename: str + :return: None """ with open(filename, "wt") as f: f.write(self.__repr__()) - def load_model(filename: str) -> None: + def list_models() -> List[str]: + """ + List the models available in the official repository """ - Load the model from a file + remote_path = utils.get_remote_path() + remote_path = os.path.join(remote_path, "model_list.txt") - Parameters: - - filename: string - Name of the file to load the model from + cache_dir = utils.get_cache_directory() - Returns: - None + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + + local_path = os.path.join(cache_dir, "model_list.txt") + + # Always get the latest version + urlretrieve(remote_path, local_path) + models = [line.strip() for line in open(local_path, "rt").readlines()] + + # Add any user models + if os.path.exists(cache_dir): + for file in os.listdir(cache_dir): + if file.endswith(".yaml"): + models.append(file) + + # Make sure to remove duplicates + return sorted(set(models)) + + def load_model(filename: str) -> None: + """ + Load a model from a file. + + :param filename: Name of the file to load the model from + :type filename: str + :return: The loaded model + :rtype: EpiModel """ data = yaml.load(open(filename, "rt"), Loader=yaml.FullLoader) model = EpiModel() @@ -821,3 +1028,25 @@ def load_model(filename: str) -> None: model.name = data[key] return model + + def download_model( + filename: str, repo: Union[str, None] = None, load_model: bool = True + ) -> Union[None, Self]: + """ + Download model from offical repository + """ + remote_path = utils.get_remote_path(repo) + remote_path = os.path.join(remote_path, filename) + + cache_dir = utils.get_cache_directory() + + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + + local_path = os.path.join(cache_dir, filename) + + if not os.path.exists(local_path): + urlretrieve(remote_path, local_path) + + if load_model: + return EpiModel.load_model(local_path) diff --git a/src/epidemik/MetaEpiModel.py b/src/epidemik/MetaEpiModel.py index f7606ae..876e8ff 100644 --- a/src/epidemik/MetaEpiModel.py +++ b/src/epidemik/MetaEpiModel.py @@ -5,6 +5,8 @@ import networkx as nx import numpy as np +import time +import os from numpy import linalg from numpy import random import pandas as pd @@ -14,7 +16,8 @@ from typing import Union -from .EpiModel import * +from .EpiModel import EpiModel +from . import utils from tqdm import tqdm @@ -214,7 +217,7 @@ def simulate( **kwargs, ) -> None: if seed_state is None: - raise NotInitialized("You have to specify the seed_state") + raise utils.NotInitialized("You have to specify the seed_state") self._initialize_populations(susceptible) @@ -233,7 +236,7 @@ def simulate( self.compartments_ = self._run_travel(self.compartments_, self.travel_graph) def integrate(self, **kwargs): - raise NotImplementedError( + raise utils.NotImplementedError( "MetaEpiModel doesn't support direct integration of the ODE" ) diff --git a/src/epidemik/NetworkEpiModel.py b/src/epidemik/NetworkEpiModel.py index 820e049..e01b750 100644 --- a/src/epidemik/NetworkEpiModel.py +++ b/src/epidemik/NetworkEpiModel.py @@ -3,7 +3,7 @@ # @author Bruno Goncalves ###################################################### -from typing import Union +from typing import Union, Optional import networkx as nx import numpy as np from numpy import linalg @@ -12,53 +12,70 @@ import matplotlib.pyplot as plt from .EpiModel import EpiModel from collections import Counter -from .utils import * +from . import utils class NetworkEpiModel(EpiModel): - def __init__(self, network, compartments=None): + def __init__(self, network:nx.Graph, compartments: Optional[str]=None): super(NetworkEpiModel, self).__init__(compartments) self.network = network self.kavg_ = 2 * network.number_of_edges() / network.number_of_nodes() self.spontaneous = {} self.interactions = {} - self.params = {} + self.params = utils.Parameters() def integrate(self, timesteps, **kwargs): raise NotImplementedError("Network Models don't support numerical integration") def add_interaction( - self, source: str, target: str, agent: str, rescale: bool = False, **rates + self, source: str, target: str, agent: str, rate:Optional[float] = None, rescale: bool = False, **rates ) -> None: + if rescale: - rate /= self.kavg_ + if rate: + rate /= self.kavg_ + else: + rates_names = list(rates.keys()) + rates[rates_names[0]] /= self.kavg_ - self.params.update(rates) - rate = list(rates.keys())[0] super(NetworkEpiModel, self).add_interaction( - source, target, agent=agent, rate=rate + source, target, agent=agent, rate=rate, **rates ) + if rate is not None: + count = len(self.params) + 1 + rate_key = "rate" + str(count) + else: + self.params.define_parameters(**rates) + rates = list(rates.keys()) + rate_key = rates[0] + if source not in self.interactions: self.interactions[source] = {} if target not in self.interactions[source]: self.interactions[source] = {} - self.interactions[source][agent] = {"target": target, "rate": rate} + self.interactions[source][agent] = {"target": target, "rate": rate_key} + + def add_spontaneous(self, source: str, target: str, rate: Optional[float], **rates): + super(NetworkEpiModel, self).add_spontaneous(source, target, rate=rate, **rates) - def add_spontaneous(self, source: str, target: str, **rates): - self.params.update(rates) - rate = list(rates.keys())[0] + if rate is not None: + count = len(self.params) + 1 + rate_key = "rate" + str(count) + else: + self.params.define_parameters(**rates) + rates = list(rates.keys()) + rate_key = rates[0] - super(NetworkEpiModel, self).add_spontaneous(source, target, rate=rate) if source not in self.spontaneous: self.spontaneous[source] = {} if target not in self.spontaneous[source]: self.spontaneous[source] = {} - self.spontaneous[source][target] = rate + self.spontaneous[source][target] = rate_key def simulate(self, timesteps: int, seeds, **kwargs) -> None: """Stochastically simulate the epidemic model""" diff --git a/src/epidemik/utils.py b/src/epidemik/utils.py index 7a38cc0..4a2ee5f 100644 --- a/src/epidemik/utils.py +++ b/src/epidemik/utils.py @@ -1,13 +1,122 @@ from collections import defaultdict +import os +import sys +from urllib.parse import urlparse +from typing import Union + +__all__ = [ + "OFFICIAL_REPO", + "EPI_COLORS", + "NotInitialized", + "get_cache_directory", + "NotImplementedError", + "Parameters", +] class NotInitialized(Exception): pass -epi_colors = defaultdict(lambda: "#f39019") -epi_colors["S"] = "#51a7f9" -epi_colors["E"] = "#f9e351" -epi_colors["I"] = "#cf51f9" -epi_colors["R"] = "#70bf41" -epi_colors["D"] = "#8b8b8b" +class NotImplementedError(Exception): + pass + + +APPNAME = os.path.basename(os.path.realpath(".")) +OFFICIAL_REPO = "https://github.com/DataForScience/epidemik/" + +EPI_COLORS = defaultdict(lambda: "#f39019") +EPI_COLORS["S"] = "#51a7f9" +EPI_COLORS["E"] = "#f9e351" +EPI_COLORS["I"] = "#cf51f9" +EPI_COLORS["R"] = "#70bf41" +EPI_COLORS["D"] = "#8b8b8b" + + +class Parameters(dict): + def __init__(self): + super().__init__() + self.globals = {"__builtins__": None} + + def __setitem__(self, key, value): + self.define_parameters(**{key: value}) + + def __getitem__(self, key): + return self.compute_parameter(key) + + def define_parameters(self, **kwargs) -> None: + """ + Define one or more parameter for the model + + Parameters: + - kwargs: keyword arguments + Named parameters for the model + + Returns: + None + """ + for key, value in kwargs.items(): + if isinstance(value, str): + try: + # Convert floats written as strings to float + value = float(value.strip()) + except: + pass + + super().__setitem__(key, value) + + def compute_parameter(self, param: Union[str]) -> float: + """ + Compute the rate from a string + + Parameters: + - rate: string + Rate of the transition + + Returns: + float + The computed rate + """ + import logging + + if param in self.keys(): + if isinstance(super().__getitem__(param), (int, float)): + return super().__getitem__(param) + else: + try: + return eval(super().__getitem__(param), self.globals, self) + except Exception as e: + logging.error(f"Error computing parameter {param}: {e}") + return None + +def get_cache_directory(): + """ + Return the location of the cache directory for the current platform. + """ + system = sys.platform + + if system == "darwin": + path = os.path.join(os.path.expanduser("~/Library/Caches"), APPNAME) + else: + path = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache")) + path = os.path.join(path, APPNAME) + + return path + + +def get_remote_path(repo: Union[str, None] = None) -> str: + """ + Return the location of the remote cache directory. + """ + if repo is None: + repo = OFFICIAL_REPO + + parsed_repo = urlparse(repo) + + if parsed_repo.netloc == "github.com": + repo = repo.replace("github.com", "raw.githubusercontent.com") + remote_path = repo + os.path.join("refs/heads/models/models/") + else: + None + + return remote_path diff --git a/tests/test_EpiModel.py b/tests/test_EpiModel.py index 228004b..a1fa365 100644 --- a/tests/test_EpiModel.py +++ b/tests/test_EpiModel.py @@ -1,16 +1,33 @@ import unittest from epidemik import EpiModel -import logging - +from sklearn.linear_model import LinearRegression +import numpy as np class EpiModelTestCase(unittest.TestCase): def setUp(self): - self.SIR = EpiModel() + self.beta = 0.3 self.mu = 0.1 + self.birth = 0.3 + self.death = 0.3 + self.fixed_birth = 10 + + self.SIR = EpiModel() self.SIR.add_interaction("S", "I", "I", beta=self.beta) self.SIR.add_spontaneous("I", "R", mu=self.mu) + self.birth_test = EpiModel() + self.birth_test.add_interaction("S", "I", "I", beta=self.beta) + self.birth_test.add_birth_rate("S", b=self.birth) + + self.fixed_birth_test = EpiModel() + self.fixed_birth_test.add_interaction("S", "I", "I", beta=self.beta) + self.fixed_birth_test.add_birth_rate("S", b=self.fixed_birth, fixed=True, global_rate=False) + + self.death_test = EpiModel() + self.death_test.add_interaction("S", "I", "I", beta=self.beta) + self.death_test.add_death_rate(d=self.death) + def test_R0(self): self.assertEqual(self.SIR.R0(), 3.0, "incorrect R0") @@ -32,3 +49,48 @@ def test_edges(self): self.assertEqual(self.SIR.params["mu"], self.mu) self.assertEqual(edge[0], "I") self.assertEqual(edge[1], "R") + + def test_birth(self): + self.assertEqual(self.birth_test.transitions.nodes['S']["birth"], "b") + self.assertIn("b", self.birth_test.params.keys()) + self.assertEqual(self.birth_test.params["b"], self.birth) + + def test_birth_rate(self): + self.birth_test.integrate(10, S=990, I=10) + values = self.birth_test.values_ + values['total'] = values.sum(axis=1) + values = values.reset_index() + + lm = LinearRegression() + lm.fit(values['index'].values.reshape(-1, 1), np.log(values['total'])) + + self.assertAlmostEqual(lm.coef_[0], self.birth, delta=0.01) + + def test_fixed_birth_rate(self): + self.fixed_birth_test.integrate(10, S=990, I=10) + values = self.fixed_birth_test.values_ + values['total'] = values.sum(axis=1) + values = values.reset_index() + + lm = LinearRegression() + lm.fit(values['index'].values.reshape(-1, 1), values['total']) + + self.assertAlmostEqual(lm.coef_[0], 10, delta=0.01) + + def test_death(self): + self.assertEqual(self.death_test.transitions.nodes['S']["death"], "d") + self.assertEqual(self.death_test.transitions.nodes['I']["death"], "d") + + self.assertIn("d", self.death_test.params.keys()) + self.assertEqual(self.death_test.params["d"], self.death) + + def test_death_rate(self): + self.death_test.integrate(10, S=990, I=10) + values = self.death_test.values_ + values['total'] = values.sum(axis=1) + values = values.reset_index() + + lm = LinearRegression() + lm.fit(values['index'].values.reshape(-1, 1), np.log(values['total'])) + + self.assertAlmostEqual(lm.coef_[0], -self.death, delta=0.01) \ No newline at end of file diff --git a/tests/test_MetaEpiModel.py b/tests/test_MetaEpiModel.py index 433bd9d..8589b45 100644 --- a/tests/test_MetaEpiModel.py +++ b/tests/test_MetaEpiModel.py @@ -1,7 +1,7 @@ import unittest import pandas as pd from epidemik import MetaEpiModel -from epidemik.utils import NotInitialized +from epidemik.utils import NotImplementedError, NotInitialized class MetaEpiModelTestCase(unittest.TestCase): @@ -46,3 +46,5 @@ def test_travel(self): def test_integrate(self): with self.assertRaises(NotImplementedError) as _: self.SIR.integrate() + + diff --git a/tests/test_NetworkEpiModel.py b/tests/test_NetworkEpiModel.py new file mode 100644 index 0000000..aee6429 --- /dev/null +++ b/tests/test_NetworkEpiModel.py @@ -0,0 +1,23 @@ +import unittest +from epidemik import EpiModel, NetworkEpiModel +import networkx as nx + +class NetworkEpiModelTestCase(unittest.TestCase): + def setUp(self): + self.N = 300 + self.G_full = nx.erdos_renyi_graph(self.N, p=1.) + self.beta = 0.05 + self.SI_full = NetworkEpiModel(self.G_full) + + def test_named_parameters(self): + self.SI_full.add_interaction("S", "I", "I", beta=self.beta) + self.assertIn("beta", + self.SI_full.params, + "The parameter beta should be in the params dictionary") + + def test_unnamed_parameters(self): + self.SI_full.add_interaction("S", "I", "I", self.beta) + self.assertIn("rate1", + self.SI_full.params, + "The parameter rate1 should be in the params dictionary") + diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..59afc70 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,27 @@ +import unittest +import pandas as pd +from epidemik.utils import * +import logging + + +class ParametersTestCase(unittest.TestCase): + def setUp(self): + self.params = Parameters() + + def test_define_parameters(self): + self.params['beta'] = '.2' + self.assertEqual(self.params['beta'], 0.2) + + def test_compute_parameters(self): + self.params['beta'] = '.2' + self.assertEqual(self.params['beta'], 0.2) + + def test_math(self): + self.params['beta'] = 0.2 + self.params['mu'] = 'beta/2' + logging.warning(self.params.items()) + self.assertEqual(self.params['mu'], 0.1) + + def test_builtin_override(self): + self.params['beta'] = 'str(1.3)' + self.assertIsNone(self.params['beta']) \ No newline at end of file