diff --git a/pixi.lock b/pixi.lock index 35cf745..9f3ab2e 100644 --- a/pixi.lock +++ b/pixi.lock @@ -356,7 +356,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.11-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.43-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyhd8ed1ab_1.conda @@ -383,7 +383,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh31011fe_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.11.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.14.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.15.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda @@ -421,7 +421,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-git-revision-date-localized-plugin-1.2.9-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-include-markdown-plugin-7.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.49-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.27.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.12.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.1.0-py312h178313f_2.conda @@ -607,7 +607,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.11-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.43-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyhd8ed1ab_1.conda @@ -634,7 +634,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh31011fe_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.11.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.14.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.15.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda @@ -663,7 +663,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-git-revision-date-localized-plugin-1.2.9-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-include-markdown-plugin-7.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.49-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.27.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.12.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/multidict-6.1.0-py312h6f3313d_1.conda @@ -851,7 +851,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.11-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.43-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyhd8ed1ab_1.conda @@ -878,7 +878,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh31011fe_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.11.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.14.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.15.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda @@ -907,7 +907,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-git-revision-date-localized-plugin-1.2.9-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-include-markdown-plugin-7.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.49-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.27.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.12.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/multidict-6.1.0-py312hdb8e49c_1.conda @@ -1094,7 +1094,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.11-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.43-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyhd8ed1ab_1.conda @@ -1121,7 +1121,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh5737063_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.11.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.14.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.15.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda @@ -1148,7 +1148,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-git-revision-date-localized-plugin-1.2.9-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-include-markdown-plugin-7.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.49-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.27.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.12.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.1.0-py312h31fea79_1.conda @@ -1321,7 +1321,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.11-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.43-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_1.conda @@ -1357,7 +1357,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-git-revision-date-localized-plugin-1.2.9-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-include-markdown-plugin-7.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.49-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.27.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.12.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.1.0-py312h178313f_2.conda @@ -1458,7 +1458,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.11-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.43-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_1.conda @@ -1486,7 +1486,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-git-revision-date-localized-plugin-1.2.9-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-include-markdown-plugin-7.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.49-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.27.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.12.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/multidict-6.1.0-py312h6f3313d_1.conda @@ -1587,7 +1587,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.11-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.43-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_1.conda @@ -1615,7 +1615,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-git-revision-date-localized-plugin-1.2.9-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-include-markdown-plugin-7.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.49-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.27.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.12.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/multidict-6.1.0-py312hdb8e49c_1.conda @@ -1715,7 +1715,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.11-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.43-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_1.conda @@ -1742,7 +1742,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-git-revision-date-localized-plugin-1.2.9-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-include-markdown-plugin-7.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.49-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.27.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.12.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.1.0-py312h31fea79_1.conda @@ -1948,7 +1948,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/userpath-1.7.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.5.10-h0f3a69f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.5.11-h0f3a69f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.28.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.18.3-py312h66e93f0_0.conda @@ -2090,7 +2090,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/userpath-1.7.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/uv-0.5.10-h8de1528_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/uv-0.5.11-h8de1528_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.28.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h0d85af4_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-64/yarl-1.18.3-py312h01d7ebd_0.conda @@ -2232,7 +2232,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/userpath-1.7.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.5.10-h668ec48_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.5.11-h668ec48_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.28.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h3422bc3_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.18.3-py312hea69d52_0.conda @@ -2373,7 +2373,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/userpath-1.7.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/uv-0.5.10-ha08ef0e_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/uv-0.5.11-ha08ef0e_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-ha32ba9b_23.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.42.34433-he29a5d6_23.conda - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.28.0-pyhd8ed1ab_0.conda @@ -5816,17 +5816,17 @@ packages: - objgraph ; extra == 'test' - psutil ; extra == 'test' requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_0.conda - sha256: 591bf3247a0872b76e2cf57cbdb71762913568390f5a745fe0f3f779a16459a9 - md5: 87db2aa0738c4acc5f565388d519fb25 +- conda: https://conda.anaconda.org/conda-forge/noarch/griffe-1.5.1-pyhd8ed1ab_1.conda + sha256: 22c39803b6909df886b03994a64c00bd796c0f481925f4902f4a7837849dac80 + md5: 112aa86928705c440c5b779f59f7ba0b depends: - colorama >=0.4 - python >=3.9 license: ISC purls: - pkg:pypi/griffe?source=hash-mapping - size: 97620 - timestamp: 1729348988898 + size: 97802 + timestamp: 1734645703824 - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_1.conda sha256: 622516185a7c740d5c7f27016d0c15b45782c1501e5611deec63fd70344ce7c8 md5: 7ee49e89531c0dcbba9466f6d115d585 @@ -6591,16 +6591,16 @@ packages: - pkg:pypi/jupyter-events?source=hash-mapping size: 22160 timestamp: 1734531779868 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.14.2-pyhd8ed1ab_1.conda - sha256: 082d3517455339c8baea245a257af249758ccec26b8832d969ac928901c234cc - md5: 81ea84b3212287f926e35b9036192963 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.15.0-pyhd8ed1ab_0.conda + sha256: be5f9774065d94c4a988f53812b83b67618bec33fcaaa005a98067d506613f8a + md5: 6ba8c206b5c6f52b82435056cf74ee46 depends: - anyio >=3.1.0 - argon2-cffi >=21.1 - jinja2 >=3.0.3 - jupyter_client >=7.4.4 - jupyter_core >=4.12,!=5.0.* - - jupyter_events >=0.9.0 + - jupyter_events >=0.11.0 - jupyter_server_terminals >=0.4.4 - nbconvert-core >=6.4.4 - nbformat >=5.3.0 @@ -6618,8 +6618,8 @@ packages: license_family: BSD purls: - pkg:pypi/jupyter-server?source=hash-mapping - size: 324289 - timestamp: 1733428731329 + size: 327747 + timestamp: 1734702771032 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda sha256: 0890fc79422191bc29edf17d7b42cff44ba254aa225d31eb30819f8772b775b8 md5: 2d983ff1b82a1ccb6f2e9d8784bdd6bd @@ -7981,19 +7981,19 @@ packages: - pkg:pypi/mkdocs-material?source=hash-mapping size: 4903351 timestamp: 1734368748490 -- conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda - sha256: e01a349f4816ba7513f8b230ca2c4f703a7ccc7f7d78535076f9215ca766ec78 - md5: 6e7e399b351756b9d181c64a362bdcb5 +- conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda + sha256: f62955d40926770ab65cc54f7db5fde6c073a3ba36a0787a7a5767017da50aa3 + md5: de8af4000a4872e16fb784c649679c8e depends: - - python >=3.8 + - python >=3.9 constrains: - mkdocs-material >=5.0.0 license: MIT license_family: MIT purls: - pkg:pypi/mkdocs-material-extensions?source=hash-mapping - size: 16011 - timestamp: 1700695213251 + size: 16122 + timestamp: 1734641109286 - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.27.0-pyhd8ed1ab_0.conda sha256: ede843a78e34c1b579d20d6e01e743033ce36a6be9e1bae8d8a90f0427f16533 md5: c1b81755327625d37a1e2fe6ee5e6f0b @@ -8664,6 +8664,7 @@ packages: depends: - python >=3.9 license: MIT + license_family: MIT purls: - pkg:pypi/paginate?source=compressed-mapping size: 18865 @@ -11741,8 +11742,8 @@ packages: timestamp: 1728642895644 - pypi: . name: readii - version: 1.30.0 - sha256: 24e649412e8a8d1456b4d304d2b5668dab5d81c175f4496a4bbb9a438849ed24 + version: 1.31.0 + sha256: 6492edac2578ba3597169845169f9e222d0fcaf124689a81ee2f5a0de69799c1 requires_dist: - simpleitk>=2.3.1 - matplotlib>=3.9.2,<4 @@ -15321,9 +15322,9 @@ packages: - pkg:pypi/userpath?source=hash-mapping size: 17423 timestamp: 1632758637093 -- conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.5.10-h0f3a69f_0.conda - sha256: 7c3caf54a016adde74f112087672d26511cdb67f2d6fe15b86da4ab6da432527 - md5: 4ae185c7bfbbd547e607e5da7d54fcbd +- conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.5.11-h0f3a69f_0.conda + sha256: 4aae48f30c21f36717c142c84f87c54768301b421fe209932491708ada219101 + md5: b6cb8e404fa29b143230480cbbad04cd depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 @@ -15334,11 +15335,11 @@ packages: platform: linux license: Apache-2.0 OR MIT purls: [] - size: 10499821 - timestamp: 1734537153777 -- conda: https://conda.anaconda.org/conda-forge/osx-64/uv-0.5.10-h8de1528_0.conda - sha256: 00941f058033a871364e1b665fac586e631c6b24d20c06c31d8324a4005d62e5 - md5: 4ad3b4259075e04bd9d5453e506f1a74 + size: 10498263 + timestamp: 1734705099469 +- conda: https://conda.anaconda.org/conda-forge/osx-64/uv-0.5.11-h8de1528_0.conda + sha256: 78687984cdaeb38489d85d9c60ef7e6ad4c1aac46d7f8ba04cd8ecb324d32d8a + md5: 126db33d2913f7e94719522b9b8ecfa6 depends: - __osx >=10.13 - libcxx >=18 @@ -15348,11 +15349,11 @@ packages: platform: osx license: Apache-2.0 OR MIT purls: [] - size: 10207409 - timestamp: 1734538178073 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.5.10-h668ec48_0.conda - sha256: 40f3cb33f804e5c2ed09fa1937a90c100b43d1a0dcadc9c07a1c434bcdc8138b - md5: 7698ba47600cbd85c196ea2edb271f3f + size: 10180434 + timestamp: 1734706003910 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.5.11-h668ec48_0.conda + sha256: a579347bf4640bde72acf873a3964f603fe58c3908dca0e67c6e16eb3ab0ebe6 + md5: 315d82b24c9e3a50e282cdadd5743bc1 depends: - __osx >=11.0 - libcxx >=18 @@ -15362,11 +15363,11 @@ packages: platform: osx license: Apache-2.0 OR MIT purls: [] - size: 10167353 - timestamp: 1734537952525 -- conda: https://conda.anaconda.org/conda-forge/win-64/uv-0.5.10-ha08ef0e_0.conda - sha256: 213ac214b56f2a2422bc751274b944237337b0b6b5faa07ea7dc6ee733cc1d87 - md5: 44d01272188eb35fc9def8037036123f + size: 10143807 + timestamp: 1734706594318 +- conda: https://conda.anaconda.org/conda-forge/win-64/uv-0.5.11-ha08ef0e_0.conda + sha256: 167a648192eb1104c0ddcf0d21a1dd8e938ee3cf46bcb409a4b6fb0dcd4ca64b + md5: 08dc42f84bb490c5af1823a589316c68 depends: - ucrt >=10.0.20348.0 - vc >=14.2,<15 @@ -15375,8 +15376,8 @@ packages: platform: win license: Apache-2.0 OR MIT purls: [] - size: 11481404 - timestamp: 1734537661786 + size: 11447589 + timestamp: 1734706342066 - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-ha32ba9b_23.conda sha256: 986ddaf8feec2904eac9535a7ddb7acda1a1dfb9482088fdb8129f1595181663 md5: 7c10ec3158d1eb4ddff7007c9101adb0 diff --git a/src/readii/analyze/plot_correlation.py b/src/readii/analyze/plot_correlation.py index 8ebf068..fc03e2a 100644 --- a/src/readii/analyze/plot_correlation.py +++ b/src/readii/analyze/plot_correlation.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import Optional import matplotlib.pyplot as plt @@ -7,9 +8,135 @@ from matplotlib.figure import Figure from scipy.linalg import issymmetric +from readii.analyze.correlation import getCrossCorrelations, getSelfCorrelations +from readii.io.writers.plot_writer import PlotWriter, PlotWriterPlotExistsError from readii.utils import logger +def saveCorrelationHeatmap(plot_figure:Figure, + correlation_directory:Path, + cmap:str, + feature_types:list[str], + correlation_type:str, + overwrite:bool = False) -> Path: + """Save a heatmap figure to a file with a PlotWriter. + + Parameters + ---------- + plot_figure : matplotlib.figure.Figure + The plot to save. + correlation_directory : pathlib.Path + The directory to save the heatmap to. + cmap : str + The colormap used for the heatmap. + feature_types : list[str] + The feature types on the x and y axes of the heatmap. Cross correlatins will be concatenated with "_vs_" to create the title. + correlation_type : str + The correlation method + type used for the heatmap. For example, "pearson_self". + overwrite : bool, optional + Whether to overwrite an existing file. The default is False. + + Returns + ------- + Path + The path to the saved file. + + Example + ------- + >>> saveCorrelationHeatmap(corr_fig, + correlation_directory=Path("correlations"), + cmap="nipy_spectral", + feature_types=["vertical", "horizontal"], + correlation_type="pearson_cross") + + File will be saved to correlations/heatmap/nipy_spectral/vertical_vs_horizontal_pearson_cross_correlation_heatmap.png + """ + # Set up the writer + corr_heatmap_writer = PlotWriter(root_directory = correlation_directory, + filename_format = "heatmap/{ColorMap}/{FeaturesPlotted}_{CorrelationType}_correlation_heatmap.png", + overwrite = overwrite, + create_dirs = True) + + # Turn feature types into a string + # Single feature type will be in the form "feature_type" + # Multiple feature types will be in the form "feature_type_vs_feature_type" + feature_type_str = "_vs_".join(feature_types) + + # Save the heatmap + try: + return corr_heatmap_writer.save(plot_figure, + ColorMap=cmap, + FeaturesPlotted=feature_type_str, + CorrelationType=correlation_type) + + except PlotWriterPlotExistsError as e: + logger.warning(e) + + # If plot file already exists, return the path to the existing plot + return corr_heatmap_writer.resolve_path(ColorMap=cmap, + FeaturesPlotted=feature_type_str, + CorrelationType=correlation_type) + + + +def saveCorrelationHistogram(plot_figure:Figure, + correlation_directory:Path, + feature_types:list[str], + correlation_type:str, + overwrite:bool = False) -> Path: + """Save a histogram figure to a file with a PlotWriter. + + Parameters + ---------- + plot_figure : matplotlib.figure.Figure + The plot to save. + correlation_directory : pathlib.Path + The directory to save the heatmap to. + feature_types : list[str] + The feature types from the correlation matrix used for the histogram. Cross correlatins will be concatenated with "_vs_" to create the title. + correlation_type : str + The correlation method + type used for the heatmap. For example, "pearson_self". + overwrite : bool, optional + Whether to overwrite an existing file. The default is False. + + Returns + ------- + Path + The path to the saved file. + + Example + ------- + >>> saveCorrelationHistogram(corr_fig, + correlation_directory=Path("correlations"), + feature_types=["vertical", "horizontal"], + correlation_type="pearson_cross") + + File will be saved to correlations/histogram/vertical_vs_horizontal_pearson_cross_correlation_histogram.png + """ + corr_histogram_writer = PlotWriter(root_directory = correlation_directory, + filename_format= "histogram/{FeaturesPlotted}_{CorrelationType}_correlation_histogram.png", + overwrite=overwrite, + create_dirs=True) + + # Turn feature types into a string + # Single feature type will be in the form "feature_type" + # Multiple feature types will be in the form "feature_type_vs_feature_type" + feature_type_str = "_vs_".join(feature_types) + + # Save the heatmap + try: + return corr_histogram_writer.save(plot_figure, + FeaturesPlotted=feature_type_str, + CorrelationType=correlation_type) + except PlotWriterPlotExistsError as e: + logger.warning(e) + + # If plot file already exists, return the path to the existing plot + return corr_histogram_writer.resolve_path(FeaturesPlotted=feature_type_str, + CorrelationType=correlation_type) + + + def plotCorrelationHeatmap(correlation_matrix_df:pd.DataFrame, diagonal:bool = False, triangle:Optional[str] = "lower", @@ -18,8 +145,7 @@ def plotCorrelationHeatmap(correlation_matrix_df:pd.DataFrame, ylabel:Optional[str] = "", title:Optional[str] = "", subtitle:Optional[str] = "", - show_tick_labels:bool = False - ) -> Figure: + show_tick_labels:bool = False) -> Figure: """Plot a correlation dataframe as a heatmap. Parameters @@ -110,8 +236,8 @@ def plotCorrelationHistogram(correlation_matrix:pd.DataFrame, y_upper_bound:Optional[int] = None, title:Optional[str] = "Distribution of Correlations for Features", subtitle:Optional[str] = "", - ) -> Figure: - """Plot a histogram to show thedistribution of correlation values for a correlation matrix. + ) -> tuple[Figure, np.ndarray, np.ndarray]: + """Plot a histogram to show the distribution of correlation values for a correlation matrix. Parameters ---------- @@ -185,4 +311,255 @@ def plotCorrelationHistogram(correlation_matrix:pd.DataFrame, plt.suptitle(title, fontsize=14) plt.title(subtitle, fontsize=10) - return dist_fig, bin_values, bin_edges \ No newline at end of file + return dist_fig, bin_values, bin_edges + + +######################################################################################################################## +################################## SELF AND CROSS CORRELATION HEATMAPS################################################## +######################################################################################################################## +def plotSelfCorrHeatmap(correlation_matrix:pd.DataFrame, + feature_type_name:str, + correlation_method:str = "pearson", + cmap:str='nipy_spectral', + save_dir_path:Optional[str] = None) -> tuple[Figure | Figure, Path]: + """Plot a heatmap of the self correlations from a correlation matrix. + + Parameters + ---------- + correlation_matrix : pd.DataFrame + Dataframe containing the correlation matrix to plot. + feature_type_name : str + Name of the feature type to get self correlations for. Must be the suffix of some feature names in the correlation matrix. + correlation_method : str, optional + Method to use for calculating correlations. Default is "pearson". + cmap : str, optional + Colormap to use for the heatmap. Default is "nipy_spectral". + save_dir_path : str, optional + Path to save the heatmap to. If None, the heatmap will not be saved. Default is None. + File will be saved to {save_dir_path}/heatmap/{cmap}/{feature_type_name}_{correlation_method}_self_correlation_heatmap.png + + Returns + ------- + self_corr_heatmap : matplotlib.pyplot.figure + Figure object containing the heatmap of the self correlations. + if save_path is not None: + self_corr_save_path : Path + Path to the saved heatmap. + """ + # Get the self correlations for the specified feature type + self_corr = getSelfCorrelations(correlation_matrix, feature_type_name) + + # Make the heatmap figure + self_corr_heatmap = plotCorrelationHeatmap(self_corr, + diagonal=True, + cmap=cmap, + xlabel=feature_type_name, + ylabel=feature_type_name, + title=f"{correlation_method.capitalize()} Self Correlations", + subtitle=f"{feature_type_name}") + + if save_dir_path is not None: + # Save the heatmap to a png file + self_corr_save_path = saveCorrelationHeatmap(self_corr_heatmap, + save_dir_path, + cmap=cmap, + feature_types=[feature_type_name], + correlation_type=f"{correlation_method}_self") + # Return the figure and path to the saved heatmap + return self_corr_heatmap, self_corr_save_path + + else: + # Return the figure without saving + return self_corr_heatmap + + + +def plotCrossCorrHeatmap(correlation_matrix:pd.DataFrame, + vertical_feature_name:str, + horizontal_feature_name:str, + correlation_method:str = "pearson", + cmap:str='nipy_spectral', + save_dir_path:Optional[str] = None) -> tuple[Figure | Figure, Path]: + """Plot a heatmap of the cross correlations from a correlation matrix. + + Parameters + ---------- + correlation_matrix : pd.DataFrame + Dataframe containing the correlation matrix to plot. + vertical_feature_name : str + Name of the vertical feature to get cross correlations for. Must be the suffix of some feature names in the correlation matrix index. + horizontal_feature_name : str + Name of the horizontal feature to get cross correlations for. Must be the suffix of some feature names in the correlation matrix columns. + correlation_method : str, optional + Method to use for calculating correlations. Default is "pearson". + cmap : str, optional + Colormap to use for the heatmap. Default is "nipy_spectral". + save_dir_path : str, optional + Path to save the heatmap to. If None, the heatmap will not be saved. Default is None. + File will be saved to {save_dir_path}/heatmap/{cmap}/{vertical_feature_name}_vs_{horizontal_feature_name}_{correlation_method}_cross_correlation_heatmap.png + + Returns + ------- + cross_corr_heatmap : matplotlib.pyplot.figure + Figure object containing the heatmap of the cross correlations. + if save_path is not None: + cross_corr_save_path : Path + Path to the saved heatmap. + """ + # Get the cross correlations for the specified feature type + cross_corr = getCrossCorrelations(correlation_matrix, vertical_feature_name, horizontal_feature_name) + + # Make the heatmap figure + cross_corr_heatmap = plotCorrelationHeatmap(cross_corr, + diagonal=False, + cmap=cmap, + xlabel=vertical_feature_name, + ylabel=horizontal_feature_name, + title=f"{correlation_method.capitalize()} Cross Correlations", + subtitle=f"{vertical_feature_name} vs {horizontal_feature_name}") + + if save_dir_path is not None: + # Save the heatmap to a png file + cross_corr_save_path = saveCorrelationHeatmap(cross_corr_heatmap, + save_dir_path, + cmap=cmap, + feature_types=[vertical_feature_name, horizontal_feature_name], + correlation_type=f"{correlation_method}_cross") + # Return the figure and the path to the saved heatmap + return cross_corr_heatmap, cross_corr_save_path + + else: + # Return the heatmap figure + return cross_corr_heatmap + + +######################################################################################################################## +################################## SELF AND CROSS CORRELATION HISTOGRAMS ############################################### +######################################################################################################################## + +def plotSelfCorrHistogram(correlation_matrix:pd.DataFrame, + feature_type_name:str, + correlation_method:str = "pearson", + num_bins:int = 100, + y_lower_bound:int = 0, + y_upper_bound:int = None, + save_dir_path:Optional[str] = None) -> tuple[Figure | Figure, Path]: + """Plot a histogram of the self correlations from a correlation matrix. + + Parameters + ---------- + correlation_matrix : pd.DataFrame + Dataframe containing the correlation matrix to plot. + feature_type_name : str + Name of the feature type to get self correlations for. Must be the suffix of some feature names in the correlation matrix. + correlation_method : str, optional + Method to use for calculating correlations. Default is "pearson". + num_bins : int, optional + Number of bins to use for the histogram generation. The default is 100. + y_lower_bound : int, optional + Lower bound for the y-axis of the histogram. The default is 0. + y_upper_bound : int, optional + Upper bound for the y-axis of the histogram. The default is None. + save_dir_path : str, optional + Path to save the histogram to. If None, the histogram will not be saved. Default is None. + File will be saved to {save_dir_path}/histogram/{feature_type_name}_{correlation_method}_self_correlation_histogram.png + + Returns + ------- + self_corr_hist : plt.Figure + Figure object containing the histogram of the self correlations. + if save_dir_path is not None: + self_corr_save_path : Path + Path to the saved histogram. + """ + # Get the self correlations for the specified feature type + self_corr = getSelfCorrelations(correlation_matrix, feature_type_name) + + # Make the histogram figure + self_corr_hist, _, _ = plotCorrelationHistogram(self_corr, + num_bins=num_bins, + xlabel = f"{correlation_method.capitalize()} Self Correlations", + y_lower_bound = y_lower_bound, + y_upper_bound = y_upper_bound, + title = f"Distribution of {correlation_method.capitalize()} Self Correlations", + subtitle = f"{feature_type_name}") + + if save_dir_path is not None: + # Save the histogram to a png file + self_corr_save_path = saveCorrelationHistogram(self_corr_hist, + save_dir_path, + feature_types=[feature_type_name], + correlation_type=f"{correlation_method}_self") + + # Return the figure and the path to the saved histogram + return self_corr_hist, self_corr_save_path + + else: + # Return the histogram figure + return self_corr_hist + + + +def plotCrossCorrHistogram(correlation_matrix:pd.DataFrame, + vertical_feature_name:str, + horizontal_feature_name:str, + correlation_method:str = "pearson", + num_bins:int = 100, + y_lower_bound:int = 0, + y_upper_bound:int = None, + save_dir_path:Optional[str] = None) -> tuple[Figure | Figure, Path]: + """Plot a histogram of the cross correlations from a correlation matrix. + + Parameters + ---------- + correlation_matrix : pd.DataFrame + Dataframe containing the correlation matrix to plot. + vertical_feature_name : str + Name of the vertical feature type to get self correlations for. Must be the suffix of some feature names in the correlation matrix index. + horizontal_feature_name : str + Name of the horizontal feature type to get self correlations for. Must be the suffix of some feature names in the correlation matrix columns. + correlation_method : str, optional + Method to use for calculating correlations. Default is "pearson". + num_bins : int, optional + Number of bins to use for the histogram generation. The default is 100. + y_lower_bound : int, optional + Lower bound for the y-axis of the histogram. The default is 0. + y_upper_bound : int, optional + Upper bound for the y-axis of the histogram. The default is None. + save_dir_path : str, optional + Path to save the histogram to. If None, the histogram will not be saved. Default is None. + File will be saved to {save_dir_path}/histogram/{vertical_feature_name}_vs_{horizontal_feature_name}_{correlation_method}_cross_correlation_histogram.png + + Returns + ------- + cross_corr_hist : plt.Figure + Figure object containing the histogram of the cross correlations. + if save_dir_path is not None: + cross_corr_save_path : Path + Path to the saved histogram. + """ + # Get the cross correlations for the specified feature type + cross_corr = getCrossCorrelations(correlation_matrix, vertical_feature_name, horizontal_feature_name) + + # Make the histogram figure + cross_corr_hist, _, _ = plotCorrelationHistogram(cross_corr, + num_bins=num_bins, + xlabel = f"{correlation_method.capitalize()} Correlation", + y_lower_bound = y_lower_bound, + y_upper_bound = y_upper_bound, + title = f"Distribution of {correlation_method.capitalize()} Cross Correlations", + subtitle=f"{vertical_feature_name} vs {horizontal_feature_name}") + + if save_dir_path is not None: + # Save the histogram to a png file + cross_corr_save_path = saveCorrelationHistogram(cross_corr_hist, + save_dir_path, + feature_types=[vertical_feature_name, horizontal_feature_name], + correlation_type=f"{correlation_method}_cross") + + # Return the figure and the path to the saved histogram + return cross_corr_hist, cross_corr_save_path + + else: + # Return the histogram figure + return cross_corr_hist \ No newline at end of file diff --git a/src/readii/io/writers/plot_writer.py b/src/readii/io/writers/plot_writer.py index 3effc80..fbb5e76 100644 --- a/src/readii/io/writers/plot_writer.py +++ b/src/readii/io/writers/plot_writer.py @@ -19,6 +19,10 @@ class PlotWriterIOError(PlotWriterError): pass +class PlotWriterPlotExistsError(PlotWriterError): + """Raised when a plot already exists at the specified path.""" + + pass class PlotWriterValidationError(PlotWriterError): """Raised when validation of writer configuration fails.""" @@ -83,7 +87,7 @@ def save(self, plot:Figure, **kwargs: str) -> Path: if out_path.exists(): if not self.overwrite: msg = f"File {out_path} already exists. \nSet {self.__class__.__name__}.overwrite to True to overwrite." - raise PlotWriterIOError(msg) + raise PlotWriterPlotExistsError(msg) else: logger.warning(f"File {out_path} already exists. Overwriting.") diff --git a/tests/analyze/test_plot_correlation.py b/tests/analyze/test_plot_correlation.py new file mode 100644 index 0000000..97cbc90 --- /dev/null +++ b/tests/analyze/test_plot_correlation.py @@ -0,0 +1,186 @@ +from readii.analyze.correlation import getFeatureCorrelations + +from readii.analyze.plot_correlation import ( + saveCorrelationHeatmap, + plotCorrelationHeatmap, + saveCorrelationHistogram, + plotCorrelationHistogram, + plotSelfCorrHeatmap, + plotCrossCorrHeatmap, + plotSelfCorrHistogram, + plotCrossCorrHistogram +) + +from matplotlib.figure import Figure +from pathlib import Path +import numpy as np +import pandas as pd +import pytest + + +@pytest.fixture(scope="module") +def correlations_dir(tmpdir_factory): + return tmpdir_factory.mktemp("correlations") + +@pytest.fixture(scope="module") +def vertical_feature_name(): + return "vertical" + +@pytest.fixture(scope="module") +def horizontal_feature_name(): + return "horizontal" + +@pytest.fixture(scope="module") +def correlation_method(): + return "pearson" + +@pytest.fixture(scope="module") +def correlation_matrix(correlation_method, vertical_feature_name, horizontal_feature_name): + # Create two 10x10 matrices with random float values between 0 and 1 + random_matrix_vertical = np.random.default_rng(seed=10).random((10,10)) + random_matrix_horizontal = np.random.default_rng(seed=10).random((10,10)) + + # Generate dummy feature names + feature_list = [f"feature_{i+1}" for i in range(10)] + + # Convert to dataframe and name the columns and index with the feature list + vertical_df = pd.DataFrame(random_matrix_vertical, columns=feature_list, index=feature_list) + horizontal_df = pd.DataFrame(random_matrix_horizontal, columns=feature_list, index=feature_list) + + return getFeatureCorrelations(vertical_df, horizontal_df, correlation_method, vertical_feature_name, horizontal_feature_name) + + +def test_make_heatmap_defaults(correlation_matrix): + """Test making a heatmap from a correlation matrix with default arguments""" + corr_fig = plotCorrelationHeatmap(correlation_matrix) + assert isinstance(corr_fig, Figure), \ + "Wrong return type, expect a matplotlib Figure" + assert corr_fig.get_suptitle() == "Correlation Heatmap", \ + "Wrong title, expect Correlation Heatmap" + + +def test_make_histogram_defaults(correlation_matrix): + """Test making a histogram from a correlation matrix with default arguments""" + corr_fig, bin_values, bin_edges = plotCorrelationHistogram(correlation_matrix) + assert isinstance(corr_fig, Figure), \ + "Wrong return type, expect a matplotlib Figure" + assert bin_values[0] == 4.0, \ + f"Wrong first bin value, expect 4.0, got {bin_values[0]}" + assert bin_values[-1] == 30.0, \ + f"Wrong last value, expect 30.0, got {bin_values[-1]}" + + + +@pytest.mark.parametrize( + "triangle", + [ + "lower", + "upper" + ] +) +def test_diagonal_heatmap(correlation_matrix, triangle): + """Test making a heatmap from a correlation matrix with diagonal set to True""" + corr_fig = plotCorrelationHeatmap(correlation_matrix, diagonal=True, triangle=triangle) + assert isinstance(corr_fig, Figure), \ + "Wrong return type, expect a matplotlib Figure" + assert corr_fig.get_suptitle() == f"Correlation Heatmap", \ + "Wrong title, expect Correlation Heatmap" + + +@pytest.mark.parametrize( + "correlation_type, feature_types", + [ + ("pearson_self", ["vertical"]), + ("pearson_cross", ["vertical", "horizontal"]), + ] +) +def test_save_corr_heatmap(correlation_matrix, correlations_dir, correlation_type, feature_types): + """Test saving a heatmap from a cross-correlation matrix""" + corr_fig = plotCorrelationHeatmap(correlation_matrix) + + expected_path = correlations_dir / "heatmap" / "nipy_spectral" / ("_vs_".join(feature_types)) + f"_{correlation_type}_correlation_heatmap.png" + + actual_path = saveCorrelationHeatmap(corr_fig, + correlations_dir, + cmap="nipy_spectral", + feature_types=feature_types, + correlation_type=correlation_type) + assert actual_path == expected_path, \ + f"Wrong path returned, expect {expected_path}" + assert actual_path.exists(), \ + "Figure is not being saved to path provided or at all." + + + +@pytest.mark.parametrize( + "correlation_type, feature_types", + [ + ("pearson_self", ["vertical"]), + ("pearson_cross", ["vertical", "horizontal"]), + ] +) +def test_save_corr_histogram(correlation_matrix, correlations_dir, correlation_type, feature_types): + """Test saving a histogram from a correlation matrix""" + corr_fig, _, _ = plotCorrelationHistogram(correlation_matrix) + + expected_path = correlations_dir / "histogram" / ("_vs_".join(feature_types)) + f"_{correlation_type}_correlation_histogram.png" + + actual_path = saveCorrelationHistogram(corr_fig, + correlations_dir, + feature_types=feature_types, + correlation_type=correlation_type) + assert actual_path == expected_path, \ + f"Wrong path returned, expect {expected_path}" + assert actual_path.exists(), \ + "Figure is not being saved to path provided or at all." + + + +def test_plot_selfcorr_heatmap_defaults(correlation_matrix, vertical_feature_name): + """Test plotting a self-correlation heatmap from a correlation matrix""" + corr_fig = plotSelfCorrHeatmap(correlation_matrix, + feature_type_name=vertical_feature_name) + assert isinstance(corr_fig, Figure), \ + "Wrong return type, expect a matplotlib Figure" + assert corr_fig.get_suptitle() == f"Pearson Self Correlations", \ + "Wrong title, expect Pearson Self Correlations" + assert corr_fig.get_axes()[0].get_title(), \ + "Wrong subtitle, expect vertical" + + +def test_plot_crosscorr_heatmap_defaults(correlation_matrix, vertical_feature_name, horizontal_feature_name): + """Test plotting a cross-correlation heatmap from a correlation matrix""" + corr_fig = plotCrossCorrHeatmap(correlation_matrix, + vertical_feature_name=vertical_feature_name, + horizontal_feature_name=horizontal_feature_name) + assert isinstance(corr_fig, Figure), \ + "Wrong return type, expect a matplotlib Figure" + assert corr_fig.get_suptitle() == f"Pearson Cross Correlations", \ + "Wrong title, expect Pearson Cross Correlations" + assert corr_fig.get_axes()[0].get_title(), \ + "Wrong subtitle, expect vertical vs horizontal" + + +def test_plot_selfcorr_histogram_defaults(correlation_matrix, vertical_feature_name): + """Test plotting a self-correlation histogram from a correlation matrix""" + corr_fig = plotSelfCorrHistogram(correlation_matrix, + feature_type_name=vertical_feature_name) + assert isinstance(corr_fig, Figure), \ + "Wrong return type, expect a matplotlib Figure" + assert corr_fig.get_suptitle() == f"Distribution of Pearson Self Correlations", \ + "Wrong title, expect Distribution of Pearson Self Correlations" + assert corr_fig.get_axes()[0].get_title(), \ + "Wrong subtitle, expect vertical" + + +def test_plot_crosscorr_histogram_defaults(correlation_matrix, vertical_feature_name, horizontal_feature_name): + """Test plotting a cross-correlation histogram from a correlation matrix""" + corr_fig = plotCrossCorrHistogram(correlation_matrix, + vertical_feature_name=vertical_feature_name, + horizontal_feature_name=horizontal_feature_name) + assert isinstance(corr_fig, Figure), \ + "Wrong return type, expect a matplotlib Figure" + assert corr_fig.get_suptitle() == f"Distribution of Pearson Cross Correlations", \ + "Wrong title, expect Distribution of Pearson Cross Correlations" + assert corr_fig.get_axes()[0].get_title(), \ + "Wrong subtitle, expect vertical vs horizontal" \ No newline at end of file