From 862238704ed0a6098349495d7264828ee9a0a6c5 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Tue, 3 Oct 2023 20:29:48 +1000 Subject: [PATCH 01/22] Initial cleanup to prep for 3.10 work --- .codacy.yml | 6 - .coveragerc | 16 - .gitignore | 136 +- .pre-commit-config.yaml | 24 + .travis.yml | 67 - Makefile | 17 + README.md | 13 +- conda/deploy_anaconda.sh | 28 - conda/meta.yaml | 41 - deploy.sh | 31 - {doc => doc_sphinx}/Makefile | 0 .../_static/theme_override.css | 0 {doc => doc_sphinx}/build.bat | 0 {doc => doc_sphinx}/chain_api.rst | 0 {doc => doc_sphinx}/conf.py | 165 ++- {doc => doc_sphinx}/index.rst | 6 +- {doc => doc_sphinx}/make.bat | 0 {doc => doc_sphinx}/usage.rst | 0 examples/Basics/plot_convergence.py | 2 +- examples/Basics/plot_grid.py | 5 +- examples/Basics/plot_hundreds_of_chains.py | 58 +- examples/Basics/plot_loading_data.py | 6 +- examples/Basics/plot_statistics.py | 2 +- examples/Basics/plot_truth_values.py | 6 +- examples/Basics/plot_two_disjoint_chains.py | 5 +- examples/customisations/plot_as_prior.py | 13 +- examples/customisations/plot_axis.py | 6 +- examples/customisations/plot_blinding.py | 2 +- .../customisations/plot_chain_override.py | 5 +- examples/customisations/plot_cloud_sigma.py | 4 +- examples/customisations/plot_colorpoints.py | 8 +- examples/customisations/plot_colours_shade.py | 6 +- .../customisations/plot_confidence_levels.py | 8 +- .../customisations/plot_contour_labels.py | 3 +- examples/customisations/plot_dont_flip.py | 2 +- .../customisations/plot_fewer_parameters.py | 5 +- examples/customisations/plot_font_changes.py | 3 +- examples/customisations/plot_kde_extents.py | 2 +- .../customisations/plot_legend_options.py | 16 +- examples/customisations/plot_linestyles.py | 7 +- examples/customisations/plot_lists.py | 20 +- examples/customisations/plot_logscale.py | 6 +- examples/customisations/plot_no_histograms.py | 5 +- examples/customisations/plot_no_smooth.py | 2 +- examples/customisations/plot_one_chain.py | 5 +- examples/customisations/plot_preliminary.py | 3 +- .../customisations/plot_rainbow_serif_bins.py | 5 +- .../customisations/plot_selected_chains.py | 4 +- .../customisations/plot_shade_gradient.py | 4 +- examples/customisations/plot_shift.py | 2 +- examples/customisations/plot_spacing.py | 5 +- examples/customisations/plot_three_chains.py | 15 +- examples/customisations/plot_zorder.py | 3 +- examples/more/plot_colorpoints2.py | 6 +- examples/more/plot_divide_chain.py | 3 +- examples/more/plot_many.py | 15 +- examples/plot_correlations.py | 4 +- examples/plot_covariance.py | 4 +- examples/plot_distributions.py | 4 +- examples/plot_introduction.py | 2 +- examples/plot_model_selection.py | 3 +- examples/plot_summary.py | 3 +- examples/plot_table.py | 7 +- examples/plot_walk.py | 3 +- paper/paper.md | 14 +- poetry.lock | 1130 +++++++++++++++++ poetry.toml | 3 + pyproject.toml | 70 + setup.py | 26 - .../chainconsumer}/__init__.py | 2 - .../chainconsumer}/analysis.py | 69 +- {chainconsumer => src/chainconsumer}/chain.py | 54 +- .../chainconsumer}/chainconsumer.py | 159 ++- .../chainconsumer}/colors.py | 22 +- .../chainconsumer}/comparisons.py | 47 +- .../chainconsumer}/diagnostic.py | 24 +- .../chainconsumer}/helpers.py | 5 +- {chainconsumer => src/chainconsumer}/kde.py | 17 +- .../chainconsumer}/plotter.py | 196 ++- tests/test_analysis.py | 135 +- tests/test_chain.py | 16 +- tests/test_chainconsumer.py | 18 +- tests/test_comparisons.py | 2 +- tests/test_diagnostic.py | 41 +- tests/test_kde.py | 4 +- tests/test_plotter.py | 9 +- 86 files changed, 2011 insertions(+), 909 deletions(-) delete mode 100644 .codacy.yml delete mode 100644 .coveragerc create mode 100644 .pre-commit-config.yaml delete mode 100644 .travis.yml create mode 100644 Makefile delete mode 100644 conda/deploy_anaconda.sh delete mode 100644 conda/meta.yaml delete mode 100644 deploy.sh rename {doc => doc_sphinx}/Makefile (100%) rename {doc => doc_sphinx}/_static/theme_override.css (100%) rename {doc => doc_sphinx}/build.bat (100%) rename {doc => doc_sphinx}/chain_api.rst (100%) rename {doc => doc_sphinx}/conf.py (73%) rename {doc => doc_sphinx}/index.rst (97%) rename {doc => doc_sphinx}/make.bat (100%) rename {doc => doc_sphinx}/usage.rst (100%) create mode 100644 poetry.lock create mode 100644 poetry.toml create mode 100644 pyproject.toml delete mode 100644 setup.py rename {chainconsumer => src/chainconsumer}/__init__.py (60%) rename {chainconsumer => src/chainconsumer}/analysis.py (91%) rename {chainconsumer => src/chainconsumer}/chain.py (87%) rename {chainconsumer => src/chainconsumer}/chainconsumer.py (92%) rename {chainconsumer => src/chainconsumer}/colors.py (88%) rename {chainconsumer => src/chainconsumer}/comparisons.py (87%) rename {chainconsumer => src/chainconsumer}/diagnostic.py (88%) rename {chainconsumer => src/chainconsumer}/helpers.py (91%) rename {chainconsumer => src/chainconsumer}/kde.py (93%) rename {chainconsumer => src/chainconsumer}/plotter.py (91%) diff --git a/.codacy.yml b/.codacy.yml deleted file mode 100644 index 55c2b7a4..00000000 --- a/.codacy.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -exclude_paths: - - test*.py - - examples/ - - doc/ - - paper/ \ No newline at end of file diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 968098c0..00000000 --- a/.coveragerc +++ /dev/null @@ -1,16 +0,0 @@ -[report] -omit = - doc/* - build/* - dist/* - examples/* - tests/* - **/test*.py - *__init__* - setup.py - -exclude_lines = - pragma: no cover - raise NotImplementedError - raise ImportError - def __len__ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 33678b93..84b113c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,99 +1,61 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# PYTHON # +__pycache__ *.py[cod] -*$py.class +*.py~ -# PyCharm -.idea/* +# envs +.env +.venv -# C extensions -*.so +# To use VS code +.vscode -# Distribution / packaging -.Python -env/ +# Setup.py build build/ -develop-eggs/ +*egg-info/ dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# Sphinx doco -doc/out/** - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ +.pytest +.pytest_cache +.mypy_cache +id_rsa +*.ipynb +coverage* .coverage .coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# IPython Notebook +.dmypy.json +dmypy.json +.idea .ipynb_checkpoints - -# pyenv +.ruff_cache +.cache +profile_default/ +ipython_config.py +__pypackages__/ .python-version +qodana.yaml +.idea -# celery beat schedule file -celerybeat-schedule - -# dotenv -.env - -# virtualenv -venv/ -ENV/ - -# Spyder project settings -.spyderproject - -# Rope project settings -.ropeproject -doc/examples/* -doc/modules/* -doc/out/ -.vscode \ No newline at end of file +**/*.log +**/*tmp* +**/*secret* +**/*.pkl \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..521ea173 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + args: ["--maxkb=5000"] + - id: check-toml + - id: check-json + - id: check-symlinks + - id: debug-statements + - id: detect-private-key + - id: check-yaml + args: + - --unsafe + - id: trailing-whitespace + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.292 + hooks: + - id: ruff + args: ["--fix"] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c760d38b..00000000 --- a/.travis.yml +++ /dev/null @@ -1,67 +0,0 @@ -language: python - -os: linux - -python: - - "3.8" - - "3.7" - -env: - global: - - GH_REF: github.com/samreay/ChainConsumer.git - -dist: xenial - -services: - - xvfb - -install: - - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh - - chmod +x miniconda.sh - - bash miniconda.sh -b -p $HOME/miniconda - - export PATH=$HOME/miniconda/bin:$PATH - - conda config --set always_yes yes --set changeps1 no --set anaconda_upload no - - conda update -q conda - - conda install python=$TRAVIS_PYTHON_VERSION pip setuptools - - sudo apt-get install texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended dvipng cm-super - - hash -r - - pip install wheel twine - - pip install . - - pip install -r requirements.txt - - ulimit -s unlimited - -script: - - py.test --cov=chainconsumer -vv - -after_success: - - codecov - - chmod u+x deploy.sh - - ./deploy.sh - - chmod +x conda/deploy_anaconda.sh - -deploy: - - provider: script - script: ./conda/deploy_anaconda.sh - skip_cleanup: true - on: - python: 3.8 - tags: true - condition: $TRAVIS_TAG =~ ^v[0-9,\.]*$ - - provider: pypi - user: "samreay" - password: - secure: "etRu952bkCozacIuC38kgsr5Ul05QG5lY1ESrC50El0fdQjimCeDqislB8buYyD+TeXfPY4jN10ApFfYzTJGfVFsM5NLE/T6+UBfCx3h07jN5ewA9jY5LDKaP5UJ+e33NrC+vkvsxoB5BJzwtbmrj6TDM9/s8RxsIZYFpZ+PoJxO/S7g2d6g1kCkbG2Q/2PpWa4VRacS29CAlBkeDY6z7ZCLEnpK0733ccQPxNfbdmAsXdHnk1cLWA5FNzbp5Vjti/tet6Wv8aVgw1RgSSN3HQ/5hl/uDgmGlloHvXDAK7xdtEo0R5y9mR4cfVJOxARweLBS84bPEysI+N7VoOAQkrBRxL85YHtuFBBiQdGMQazG8M/FhO0aeyMj1KJM1yaLj8lQJN+9qeIrtagV1LkAW9kzWvE4NOCPah4LGy2NvgJ0L7F8Kmgk2ReAZ3FfbfGyoISg/iqGBwDTjYH0GtXc3DYswpKylkpdpkcCh13usyVYxoHUJ+pT594vNJrNk6qQof6PVUiBldXkoWofoJGgASUh6r1gbcK/qlgD+sOCRyQ7zH5N8H/0ecnEC+io178r/5NioVfmsNBS4izJQPVzdUb3bB3jDjMwOU+yO/amD0V9Te9ctqDJL8cWmBwj0UsKunpZ2qbWfPUwtNS9/ehHH8RElJv8IKuj+j/md91KQy0=" - skip_cleanup: true - edge: - branch: v1.8.45 - on: - tags: true - condition: $TRAVIS_TAG =~ ^v[0-9,\.]*$ - python: 3.8 - - provider: releases - api_key: "${GITHUB_API_KEY2}" - skip_cleanup: true - on: - tags: true - condition: $TRAVIS_TAG =~ ^v[0-9,\.]*$ - python: 3.8 diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..09153b84 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.PHONY: tests + +install: + pip install -U pip poetry -q + poetry install --with=dev,test --all-extras + poetry run pre-commit install + poetry run pre-commit autoupdate + +precommit: + poetry run pre-commit run --all-files + +test: + poetry run pytest + +tests: test + +all: precommit tests \ No newline at end of file diff --git a/README.md b/README.md index 8a4a2de1..51532909 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,19 @@ and perform some model selection! ### Installation Install via `pip`: - + pip install chainconsumer ### Python Versions -Due to dependencies dropping support for Python 2.7, from 0.31.0 and onwards, only Python 3 will be supported. +Time has ticked on, and now only python 3.10 will be supported. This is because type hints are amazing. + +### Developing -Previous versions will still be installable for Python 2.7 environments, however I'd strong recommend upgrading. +1. Clone repo +2. Run `make install` +3. Ensure that you set your python interpreter to the `.venv/bin/python` +4. Code away. ### Contributors @@ -53,7 +58,7 @@ which have helped improve ChainConsumer: ### Common Issues -Users on some Linux platforms have reported issues rendering plots using ChainConsumer. +Users on some Linux platforms have reported issues rendering plots using ChainConsumer. The common error states that `dvipng: not found`, and as per [StackOverflow](http://stackoverflow.com/a/32915992/3339667) post, it can be solved by explicitly install the `matplotlib` dependency `dvipng` via `sudo apt-get install dvipng`. diff --git a/conda/deploy_anaconda.sh b/conda/deploy_anaconda.sh deleted file mode 100644 index 0c82dd77..00000000 --- a/conda/deploy_anaconda.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -cd "${0%/*}" - -PKG_NAME=chainconsumer -USER=samreay - -# Removing this after testing. But now dont have to play with tags -TRAVIS_TAG=${TRAVIS_TAG:-v0.25.2} - -echo "Current tag is $TRAVIS_TAG" - -export CONDA_BLD_PATH=~/conda-bld -export VERSION=${TRAVIS_TAG#?} - -echo "Version building is $VERSION" - -mkdir ${CONDA_BLD_PATH} -conda config --set anaconda_upload no - -conda build --python 2.7 . -conda build --python 3.3 . -conda build --python 3.4 . -conda build --python 3.5 . -conda build --python 3.6 . -conda build --python 3.7 . -conda build --python 3.8 . -conda convert --platform all $CONDA_BLD_PATH/linux-64/$PKG_NAME-*.tar.bz2 -o $CONDA_BLD_PATH -anaconda -t $CONDA_UPLOAD_TOKEN upload -u $USER $CONDA_BLD_PATH/**/$PKG_NAME-*.tar.bz2 --force \ No newline at end of file diff --git a/conda/meta.yaml b/conda/meta.yaml deleted file mode 100644 index f93e3797..00000000 --- a/conda/meta.yaml +++ /dev/null @@ -1,41 +0,0 @@ - -package: - name: 'chainconsumer' - version: "{{ environ['VERSION'] }}" - -source: - git_url: https://github.com/samreay/chainconsumer.git - -build: - number: 0 - script: python setup.py install --single-version-externally-managed --record=record.txt - script_env: - - VERSION - - CONDA_BLD_PATH - -requirements: - host: - - python - - setuptools - - numpy - - scipy - - matplotlib >1.6.0,!=2.1.*,!=2.2.* - - statsmodels >=0.7.0 - run: - - python - - numpy - - scipy - - matplotlib >1.6.0,!=2.1.*,!=2.2.* - - statsmodels >=0.7.0 - - -about: - home: http://github.com/samreay/ChainConsumer - license: MIT License - license_family: MIT - license_file: '' - summary: Consume chains and produce plots and tables - description: 'Package documentation: http://samreay.github.io/ChainConsumer' - doc_url: 'https://samreay.github.io/ChainConsumer/' - dev_url: 'http://github.com/samreay/ChainConsumer' - diff --git a/deploy.sh b/deploy.sh deleted file mode 100644 index 13ffd6af..00000000 --- a/deploy.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -if [ "$TRAVIS_PULL_REQUEST" != "false" -o "$TRAVIS_BRANCH" != "master" ]; then - echo "Not on master branch, or pull request. Not building doco" - echo "$TRAVIS_PULL_REQUEST" - echo "$TRAVIS_BRANCH" - exit 0; -fi -if [ -n "$GITHUB_API_KEY2" ]; then - echo "Github key found. Building documentation." - cd "$TRAVIS_BUILD_DIR"/doc - make clean - make html - make html - if [ "$TRAVIS_PYTHON_VERSION" == "3.7" ]; then - cd "$TRAVIS_BUILD_DIR" - rm -rf .git/ - cd doc/out/html - git config --global user.email "travis" - git config --global user.name "travis" - touch .nojekyll - git init - git add . - echo "Committing" - git commit -m init - # Make sure to make the output quiet, or else the API token will leak! - # This works because the API key can replace your password. - echo "Pushing" - git push -f -q "https://${GITHUB_API_KEY2}@${GH_REF}" master:gh-pages > /dev/null 2>&1 && echo "Pushed" - fi -fi -echo "Deploy script ending" \ No newline at end of file diff --git a/doc/Makefile b/doc_sphinx/Makefile similarity index 100% rename from doc/Makefile rename to doc_sphinx/Makefile diff --git a/doc/_static/theme_override.css b/doc_sphinx/_static/theme_override.css similarity index 100% rename from doc/_static/theme_override.css rename to doc_sphinx/_static/theme_override.css diff --git a/doc/build.bat b/doc_sphinx/build.bat similarity index 100% rename from doc/build.bat rename to doc_sphinx/build.bat diff --git a/doc/chain_api.rst b/doc_sphinx/chain_api.rst similarity index 100% rename from doc/chain_api.rst rename to doc_sphinx/chain_api.rst diff --git a/doc/conf.py b/doc_sphinx/conf.py similarity index 73% rename from doc/conf.py rename to doc_sphinx/conf.py index 2e99c899..23e229a5 100644 --- a/doc/conf.py +++ b/doc_sphinx/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # # This file is execfile()d with the current directory set to its @@ -10,21 +9,20 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys -import os import re + import sphinx_rtd_theme # 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. -#sys.path.insert(0, os.path.abspath('.')) -#sys.path.append(os.path.abspath('ext')) +# sys.path.insert(0, os.path.abspath('.')) +# sys.path.append(os.path.abspath('ext')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # intersphinx_mapping = { # 'python': ('http://docs.python.org/', None), @@ -35,88 +33,88 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.intersphinx', - 'sphinx.ext.mathjax', + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", "sphinx.ext.napoleon", # 'numpydoc', - 'sphinx_gallery.gen_gallery' + "sphinx_gallery.gen_gallery", ] numpydoc_show_class_members = False autosummary_generate = True autoclass_content = "class" autodoc_default_flags = ["members", "no-special-members"] sphinx_gallery_conf = { - 'filename_pattern': 'plot_', - 'examples_dirs': '../examples', # path to examples scripts - 'gallery_dirs': 'examples', # path to gallery generated examples + "filename_pattern": "plot_", + "examples_dirs": "../examples", # path to examples scripts + "gallery_dirs": "examples", # path to gallery generated examples } # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. version = re.findall(r"__version__ = \"(.*?)\"", open("../chainconsumer/chainconsumer.py").read())[0] -project = u'ChainConsumer' -copyright = u'2016-2017, Samuel Hinton and contributors' +project = "ChainConsumer" +copyright = "2016-2017, Samuel Hinton and contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '.'.join(version.split('.')[0:2]) +version = ".".join(version.split(".")[0:2]) # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. -default_role = 'obj' +default_role = "obj" # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output ---------------------------------------------- @@ -126,144 +124,139 @@ def setup(app): - app.add_stylesheet('theme_override.css') + app.add_stylesheet("theme_override.css") + # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -#html_theme = 'default' +# html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # 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'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'ChainConsumerDoc' +htmlhelp_basename = "ChainConsumerDoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'chainConsumer.tex', u'ChainConsumer Documentation', - u'Samuel Hinton', 'manual'), + ("index", "chainConsumer.tex", "ChainConsumer Documentation", "Samuel Hinton", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'ChainConsumer', u'ChainConsumer Documentation', - [u'Samuel Hinton'], 1) -] +man_pages = [("index", "ChainConsumer", "ChainConsumer Documentation", ["Samuel Hinton"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -272,19 +265,25 @@ def setup(app): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'ChainConsumer', u'ChainConsumer Documentation', - u'Samuel Hinton', 'ChainConsumer', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "ChainConsumer", + "ChainConsumer Documentation", + "Samuel Hinton", + "ChainConsumer", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False \ No newline at end of file +# texinfo_no_detailmenu = False diff --git a/doc/index.rst b/doc_sphinx/index.rst similarity index 97% rename from doc/index.rst rename to doc_sphinx/index.rst index c84b8e99..f5bc1bcd 100644 --- a/doc/index.rst +++ b/doc_sphinx/index.rst @@ -70,9 +70,9 @@ Users on some Linux platforms have reported issues rendering plots using ChainCo The common error states that `dvipng: not found`, and as per `StackOverflow `_ post, it can be solved by explicitly install the `matplotlib` dependency `dvipng` via `sudo apt-get install dvipng`. -If you are running on HPC or clusters where you can't install things yourself, -users may run into issues where LaTeX or other optional dependencies aren't installed. -In this case, ensure `usetex=False` in `configure` to request matplotlib not try to use TeX. +If you are running on HPC or clusters where you can't install things yourself, +users may run into issues where LaTeX or other optional dependencies aren't installed. +In this case, ensure `usetex=False` in `configure` to request matplotlib not try to use TeX. If this does not work, also set `serif=False`, which has helped some uses. diff --git a/doc/make.bat b/doc_sphinx/make.bat similarity index 100% rename from doc/make.bat rename to doc_sphinx/make.bat diff --git a/doc/usage.rst b/doc_sphinx/usage.rst similarity index 100% rename from doc/usage.rst rename to doc_sphinx/usage.rst diff --git a/examples/Basics/plot_convergence.py b/examples/Basics/plot_convergence.py index 177dbe41..d9806d15 100644 --- a/examples/Basics/plot_convergence.py +++ b/examples/Basics/plot_convergence.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ======================= Convergence Diagnostics @@ -15,6 +14,7 @@ import numpy as np from numpy.random import normal + from chainconsumer import ChainConsumer np.random.seed(0) diff --git a/examples/Basics/plot_grid.py b/examples/Basics/plot_grid.py index 6316d440..ee2a0b67 100644 --- a/examples/Basics/plot_grid.py +++ b/examples/Basics/plot_grid.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ========== Grid Data! @@ -18,12 +17,12 @@ """ import numpy as np -from chainconsumer import ChainConsumer from scipy.stats import multivariate_normal +from chainconsumer import ChainConsumer x, y = np.linspace(-3, 3, 50), np.linspace(-7, 7, 100) -xx, yy = np.meshgrid(x, y, indexing='ij') +xx, yy = np.meshgrid(x, y, indexing="ij") pdf = np.exp(-0.5 * (xx * xx + yy * yy / 4 + np.abs(xx * yy))) c = ChainConsumer() diff --git a/examples/Basics/plot_hundreds_of_chains.py b/examples/Basics/plot_hundreds_of_chains.py index 4c1b169a..2424664d 100644 --- a/examples/Basics/plot_hundreds_of_chains.py +++ b/examples/Basics/plot_hundreds_of_chains.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ================== Hundreds of Chains @@ -22,21 +21,21 @@ """ # sphinx_gallery_thumbnail_number = 2 -from scipy.stats import multivariate_normal import numpy as np -from chainconsumer import ChainConsumer +from scipy.stats import multivariate_normal +from chainconsumer import ChainConsumer c = ChainConsumer() for i in range(1000): - # Generate some data centered at a random location with uncertainty - # equal to the scatter - mean = [3, 8] - cov = [[1.0, 0.5], [0.5, 2.0]] - mean_scattered = multivariate_normal.rvs(mean=mean, cov=cov) - data = multivariate_normal.rvs(mean=mean_scattered, cov=cov, size=1000) - posterior = multivariate_normal.logpdf(data, mean=mean_scattered, cov=cov) - c.add_chain(data, posterior=posterior, parameters=["$x$", "$y$"], color='r', name="Simulation validation") + # Generate some data centered at a random location with uncertainty + # equal to the scatter + mean = [3, 8] + cov = [[1.0, 0.5], [0.5, 2.0]] + mean_scattered = multivariate_normal.rvs(mean=mean, cov=cov) + data = multivariate_normal.rvs(mean=mean_scattered, cov=cov, size=1000) + posterior = multivariate_normal.logpdf(data, mean=mean_scattered, cov=cov) + c.add_chain(data, posterior=posterior, parameters=["$x$", "$y$"], color="r", name="Simulation validation") fig = c.plotter.plot() fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. @@ -59,14 +58,35 @@ posterior = multivariate_normal.logpdf(data, mean=mean_scattered, cov=cov) plot_contour = i == 0 - c.add_chain(data, posterior=posterior, parameters=p, color='p', name="Sim1") - - c.add_chain(data2, posterior=posterior, parameters=p, color='k', - marker_style="+", marker_size=20, name="Sim2", marker_alpha=0.5) - -c.add_chain(data + np.array([4, -4, 3]), parameters=p, posterior=posterior, name="Contour Too", - plot_contour=True, plot_point=True, marker_style="*", marker_size=40, - color="a", shade=True, shade_alpha=0.3, kde=True, linestyle="--", bar_shade=True) + c.add_chain(data, posterior=posterior, parameters=p, color="p", name="Sim1") + + c.add_chain( + data2, + posterior=posterior, + parameters=p, + color="k", + marker_style="+", + marker_size=20, + name="Sim2", + marker_alpha=0.5, + ) + +c.add_chain( + data + np.array([4, -4, 3]), + parameters=p, + posterior=posterior, + name="Contour Too", + plot_contour=True, + plot_point=True, + marker_style="*", + marker_size=40, + color="a", + shade=True, + shade_alpha=0.3, + kde=True, + linestyle="--", + bar_shade=True, +) c.configure(legend_artists=True) diff --git a/examples/Basics/plot_loading_data.py b/examples/Basics/plot_loading_data.py index 2c09af5f..e5abe3d1 100644 --- a/examples/Basics/plot_loading_data.py +++ b/examples/Basics/plot_loading_data.py @@ -13,11 +13,13 @@ # You can specify truth values using a list (in the same order as the # declared parameters). +import os +import tempfile + import numpy as np import pandas as pd from numpy.random import multivariate_normal -import tempfile -import os + from chainconsumer import ChainConsumer # Lets create some data here to set things up diff --git a/examples/Basics/plot_statistics.py b/examples/Basics/plot_statistics.py index 6afe918c..5054b1fb 100644 --- a/examples/Basics/plot_statistics.py +++ b/examples/Basics/plot_statistics.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ========== Statistics @@ -16,6 +15,7 @@ import numpy as np from scipy.stats import skewnorm + from chainconsumer import ChainConsumer # Lets create some data here to set things up diff --git a/examples/Basics/plot_truth_values.py b/examples/Basics/plot_truth_values.py index 9ffde645..f9aa88e8 100644 --- a/examples/Basics/plot_truth_values.py +++ b/examples/Basics/plot_truth_values.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ============ Truth Values @@ -13,7 +12,8 @@ # declared parameters). import numpy as np -from numpy.random import normal, multivariate_normal +from numpy.random import multivariate_normal, normal + from chainconsumer import ChainConsumer np.random.seed(2) @@ -31,6 +31,6 @@ # of your truth lines. -c.configure_truth(color='w', ls=":", alpha=0.8) +c.configure_truth(color="w", ls=":", alpha=0.8) fig2 = c.plotter.plot(truth={"$x$": truth[0], "$y$": truth[1]}) fig2.set_size_inches(0 + fig2.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/Basics/plot_two_disjoint_chains.py b/examples/Basics/plot_two_disjoint_chains.py index 4aa31eed..34db859e 100644 --- a/examples/Basics/plot_two_disjoint_chains.py +++ b/examples/Basics/plot_two_disjoint_chains.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ =================== Two Disjoint Chains @@ -9,9 +8,9 @@ """ import numpy as np -from numpy.random import normal, multivariate_normal -from chainconsumer import ChainConsumer +from numpy.random import multivariate_normal, normal +from chainconsumer import ChainConsumer np.random.seed(0) cov = normal(size=(3, 3)) diff --git a/examples/customisations/plot_as_prior.py b/examples/customisations/plot_as_prior.py index dd0add3d..16314b4a 100644 --- a/examples/customisations/plot_as_prior.py +++ b/examples/customisations/plot_as_prior.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ================ Plotting a prior @@ -8,9 +7,9 @@ """ import numpy as np -from numpy.random import normal, random, multivariate_normal -from chainconsumer import ChainConsumer +from numpy.random import multivariate_normal, normal, random +from chainconsumer import ChainConsumer if __name__ == "__main__": np.random.seed(0) @@ -19,9 +18,11 @@ prior = normal(0, 1, size=100000) - fig = ChainConsumer()\ - .add_chain(data, parameters=["x", "y"], name="Normal")\ - .add_chain(prior, parameters=["y"], name="Prior", show_as_1d_prior=True)\ + fig = ( + ChainConsumer() + .add_chain(data, parameters=["x", "y"], name="Normal") + .add_chain(prior, parameters=["y"], name="Prior", show_as_1d_prior=True) .plotter.plot() + ) fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_axis.py b/examples/customisations/plot_axis.py index 692c01a7..e658af82 100644 --- a/examples/customisations/plot_axis.py +++ b/examples/customisations/plot_axis.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ============= External axes @@ -12,9 +11,10 @@ """ -from chainconsumer import ChainConsumer -from scipy.stats import multivariate_normal as mv import matplotlib.pyplot as plt +from scipy.stats import multivariate_normal as mv + +from chainconsumer import ChainConsumer data = mv.rvs(mean=[5, 6], cov=[[1, 0.9], [0.9, 1]], size=10000) diff --git a/examples/customisations/plot_blinding.py b/examples/customisations/plot_blinding.py index fa64c80d..3accf466 100644 --- a/examples/customisations/plot_blinding.py +++ b/examples/customisations/plot_blinding.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ =================== Blinding Parameters @@ -13,6 +12,7 @@ import numpy as np from numpy.random import multivariate_normal + from chainconsumer import ChainConsumer np.random.seed(0) diff --git a/examples/customisations/plot_chain_override.py b/examples/customisations/plot_chain_override.py index 175744a0..3bac18b2 100644 --- a/examples/customisations/plot_chain_override.py +++ b/examples/customisations/plot_chain_override.py @@ -7,14 +7,15 @@ This is useful for when you are playing around with code, adding and removing chains as you tweak the plot. Normally, this would involve modifying the lists passed into `configure` -if you wanted to keep a specific chain with a specific style. To make it easier, -you can specify chain properties when addng them via `add_chain`. If set, these values override +if you wanted to keep a specific chain with a specific style. To make it easier, +you can specify chain properties when addng them via `add_chain`. If set, these values override anything specified in configure (and # -*- coding: utf-8 -*- thus override the default configure behaviour). """ import numpy as np from numpy.random import multivariate_normal + from chainconsumer import ChainConsumer np.random.seed(0) diff --git a/examples/customisations/plot_cloud_sigma.py b/examples/customisations/plot_cloud_sigma.py index 54339a96..06ab3c31 100644 --- a/examples/customisations/plot_cloud_sigma.py +++ b/examples/customisations/plot_cloud_sigma.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ====================== Cloud and Sigma Levels @@ -15,7 +14,8 @@ """ import numpy as np -from numpy.random import normal, multivariate_normal +from numpy.random import multivariate_normal, normal + from chainconsumer import ChainConsumer np.random.seed(1) diff --git a/examples/customisations/plot_colorpoints.py b/examples/customisations/plot_colorpoints.py index 63a8c433..85beb6bc 100644 --- a/examples/customisations/plot_colorpoints.py +++ b/examples/customisations/plot_colorpoints.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ============= Colour Points @@ -16,7 +15,8 @@ """ import numpy as np -from numpy.random import normal, multivariate_normal +from numpy.random import multivariate_normal, normal + from chainconsumer import ChainConsumer np.random.seed(1) @@ -32,7 +32,7 @@ ############################################################################### # You can also plot the weights or posterior if they are specified. Showing weights here. -weights = 1 / (1 + data[:, 0]**2 + data[:, 1]**2) +weights = 1 / (1 + data[:, 0] ** 2 + data[:, 1] ** 2) c = ChainConsumer().add_chain(data[:, :2], parameters=["$x$", "$y$"], weights=weights) c.configure(color_params="weights") fig = c.plotter.plot(figsize=3.0) @@ -42,7 +42,7 @@ ############################################################################### # And showing the posterior color parameter here -weights = 1 / (1 + data[:, 0]**2 + data[:, 1]**2) +weights = 1 / (1 + data[:, 0] ** 2 + data[:, 1] ** 2) posterior = np.log(weights) c = ChainConsumer().add_chain(data[:, :2], parameters=["$x$", "$y$"], weights=weights, posterior=posterior) c.configure(color_params="posterior") diff --git a/examples/customisations/plot_colours_shade.py b/examples/customisations/plot_colours_shade.py index 6f431bea..bfe4f4c7 100644 --- a/examples/customisations/plot_colours_shade.py +++ b/examples/customisations/plot_colours_shade.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ =================== Colours and Shading @@ -17,7 +16,8 @@ """ import numpy as np -from numpy.random import normal, multivariate_normal +from numpy.random import multivariate_normal, normal + from chainconsumer import ChainConsumer np.random.seed(2) @@ -33,5 +33,3 @@ fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - - diff --git a/examples/customisations/plot_confidence_levels.py b/examples/customisations/plot_confidence_levels.py index 84aeaffe..a908ef0a 100644 --- a/examples/customisations/plot_confidence_levels.py +++ b/examples/customisations/plot_confidence_levels.py @@ -1,4 +1,4 @@ -""" +r""" ================= Confidence Levels ================= @@ -10,12 +10,13 @@ By default ChainConsumer uses the 2D levels, such that the contours will line up and agree with the marginalised distributions shown above them, however you can also choose to switch to using the 1D -Gaussian method, such that the contour encloses 68% and 95% confidence regions, by switching `sigma2d` to `False` +Gaussian method, such that the contour encloses 68% and 95% confidence regions, by switching `sigma2d` to `False` """ import numpy as np from numpy.random import multivariate_normal + from chainconsumer import ChainConsumer np.random.seed(0) @@ -33,8 +34,7 @@ c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$"]) c.configure(flip=False, sigma2d=True, sigmas=[1, 2]) -fig = c.plotter.plot()# -*- coding: utf-8 -*- - +fig = c.plotter.plot() # -*- coding: utf-8 -*- fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_contour_labels.py b/examples/customisations/plot_contour_labels.py index 5fbc8a3d..14234e77 100644 --- a/examples/customisations/plot_contour_labels.py +++ b/examples/customisations/plot_contour_labels.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ============== Contour Labels @@ -11,8 +10,8 @@ """ from numpy.random import multivariate_normal -from chainconsumer import ChainConsumer +from chainconsumer import ChainConsumer data = multivariate_normal([0, 0], [[1, 0.5], [0.5, 1.0]], size=1000000) diff --git a/examples/customisations/plot_dont_flip.py b/examples/customisations/plot_dont_flip.py index 0e36a523..59e646ae 100644 --- a/examples/customisations/plot_dont_flip.py +++ b/examples/customisations/plot_dont_flip.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ===================== Flips, Ticks and Size @@ -14,6 +13,7 @@ """ import numpy as np + from chainconsumer import ChainConsumer np.random.seed(0) diff --git a/examples/customisations/plot_fewer_parameters.py b/examples/customisations/plot_fewer_parameters.py index 3f43be1b..2df7f1cb 100644 --- a/examples/customisations/plot_fewer_parameters.py +++ b/examples/customisations/plot_fewer_parameters.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ==================== Truncated Parameters @@ -14,9 +13,9 @@ """ import numpy as np -from numpy.random import normal, random, multivariate_normal -from chainconsumer import ChainConsumer +from numpy.random import multivariate_normal, normal, random +from chainconsumer import ChainConsumer np.random.seed(0) cov = random(size=(6, 6)) diff --git a/examples/customisations/plot_font_changes.py b/examples/customisations/plot_font_changes.py index c21f3e23..0bd234c1 100644 --- a/examples/customisations/plot_font_changes.py +++ b/examples/customisations/plot_font_changes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ =================== Change Font Options @@ -12,8 +11,8 @@ import numpy as np from numpy.random import multivariate_normal -from chainconsumer import ChainConsumer +from chainconsumer import ChainConsumer np.random.seed(0) data = multivariate_normal([0, 1, 2], np.eye(3) + 0.2, size=100000) diff --git a/examples/customisations/plot_kde_extents.py b/examples/customisations/plot_kde_extents.py index da592d66..effc4575 100644 --- a/examples/customisations/plot_kde_extents.py +++ b/examples/customisations/plot_kde_extents.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ======================== Gaussian KDE and Extents @@ -16,6 +15,7 @@ """ import numpy as np + from chainconsumer import ChainConsumer np.random.seed(0) diff --git a/examples/customisations/plot_legend_options.py b/examples/customisations/plot_legend_options.py index 94b6d4bd..a9a4346f 100644 --- a/examples/customisations/plot_legend_options.py +++ b/examples/customisations/plot_legend_options.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ============== Legend Options @@ -11,6 +10,7 @@ import numpy as np from numpy.random import multivariate_normal + from chainconsumer import ChainConsumer np.random.seed(0) @@ -20,7 +20,7 @@ c = ChainConsumer() c.add_chain(data1, parameters=["$x$", "$y$"], name="Chain 1") c.add_chain(data2, parameters=["$x$", "$y$"], name="Chain 2") -c.configure(colors=['lb', 'g']) +c.configure(colors=["lb", "g"]) fig = c.plotter.plot() fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. @@ -31,7 +31,7 @@ c = ChainConsumer() c.add_chain(data1, parameters=["$x$", "$y$"], name="Chain 1") c.add_chain(data2, parameters=["$x$", "$y$"], name="Chain 2") -c.configure(colors=['lb', 'lb'], linestyles=["-", "--"]) +c.configure(colors=["lb", "lb"], linestyles=["-", "--"]) fig = c.plotter.plot() fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. @@ -42,8 +42,12 @@ c = ChainConsumer() c.add_chain(data1, parameters=["$x$", "$y$"], name="Chain 1") c.add_chain(data2, parameters=["$x$", "$y$"], name="Chain 2") -c.configure(linestyles=["-", "--"], sigmas=[0, 1, 2, 3], - legend_kwargs={"loc": "upper left", "fontsize": 10}, - legend_color_text=False, legend_location=(0, 0)) +c.configure( + linestyles=["-", "--"], + sigmas=[0, 1, 2, 3], + legend_kwargs={"loc": "upper left", "fontsize": 10}, + legend_color_text=False, + legend_location=(0, 0), +) fig = c.plotter.plot(figsize=1.5) fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_linestyles.py b/examples/customisations/plot_linestyles.py index e347dee1..bac147c2 100644 --- a/examples/customisations/plot_linestyles.py +++ b/examples/customisations/plot_linestyles.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ =================== Changing Linestyles @@ -12,7 +11,8 @@ """ import numpy as np -from numpy.random import normal, multivariate_normal +from numpy.random import multivariate_normal, normal + from chainconsumer import ChainConsumer np.random.seed(1) @@ -21,8 +21,7 @@ data2 = data * 1.1 + 0.5 c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", "$z$"]).add_chain(data2) -c.configure(linestyles=["-", "--"], linewidths=[1.0, 2.0], - shade=[True, False], shade_alpha=[0.2, 0.0]) +c.configure(linestyles=["-", "--"], linewidths=[1.0, 2.0], shade=[True, False], shade_alpha=[0.2, 0.0]) fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_lists.py b/examples/customisations/plot_lists.py index 3aed8e0b..8c7acf6f 100644 --- a/examples/customisations/plot_lists.py +++ b/examples/customisations/plot_lists.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ================== Using List Options @@ -12,7 +11,8 @@ """ import numpy as np -from numpy.random import normal, multivariate_normal +from numpy.random import multivariate_normal, normal + from chainconsumer import ChainConsumer np.random.seed(2) @@ -28,10 +28,16 @@ c.add_chain(d2) c.add_chain(d3) -c.configure(linestyles=["-", "--", "-"], linewidths=[1.0, 3.0, 1.0], - bins=[3.0, 1.0, 1.0], colors=["#1E88E5", "#D32F2F", "#111111"], - smooth=[0, 1, 2], shade=[True, True, False], - shade_alpha=[0.2, 0.1, 0.0], bar_shade=[True, False, False]) +c.configure( + linestyles=["-", "--", "-"], + linewidths=[1.0, 3.0, 1.0], + bins=[3.0, 1.0, 1.0], + colors=["#1E88E5", "#D32F2F", "#111111"], + smooth=[0, 1, 2], + shade=[True, True, False], + shade_alpha=[0.2, 0.1, 0.0], + bar_shade=[True, False, False], +) fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. @@ -52,4 +58,4 @@ c.configure(linestyles=":", linewidths=2) fig = c.plotter.plot() -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. \ No newline at end of file +fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_logscale.py b/examples/customisations/plot_logscale.py index 32baf584..9bcebdf2 100644 --- a/examples/customisations/plot_logscale.py +++ b/examples/customisations/plot_logscale.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ======== Logscale @@ -12,10 +11,10 @@ """ -import numpy as np -from chainconsumer import ChainConsumer from scipy.stats import lognorm +from chainconsumer import ChainConsumer + data = lognorm.rvs(0.95, loc=0, size=(100000, 2)) c = ChainConsumer() @@ -34,4 +33,3 @@ fig = c.plotter.plot_distributions(log_scales=[True, False]) # list[bool] example fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - diff --git a/examples/customisations/plot_no_histograms.py b/examples/customisations/plot_no_histograms.py index 6471cbf8..265f560d 100644 --- a/examples/customisations/plot_no_histograms.py +++ b/examples/customisations/plot_no_histograms.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ============= No Histograms @@ -9,8 +8,9 @@ """ -from numpy.random import multivariate_normal, normal, seed import numpy as np +from numpy.random import multivariate_normal, normal, seed + from chainconsumer import ChainConsumer seed(0) @@ -22,4 +22,3 @@ fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - diff --git a/examples/customisations/plot_no_smooth.py b/examples/customisations/plot_no_smooth.py index 4d779079..bcc10094 100644 --- a/examples/customisations/plot_no_smooth.py +++ b/examples/customisations/plot_no_smooth.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ============ No Smoothing @@ -15,6 +14,7 @@ """ import numpy as np + from chainconsumer import ChainConsumer data = np.random.multivariate_normal([0.0, 4.0], [[1.0, 0.7], [0.7, 1.5]], size=100000) diff --git a/examples/customisations/plot_one_chain.py b/examples/customisations/plot_one_chain.py index fd4da18e..2350a48a 100644 --- a/examples/customisations/plot_one_chain.py +++ b/examples/customisations/plot_one_chain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ========= One Chain @@ -12,9 +11,9 @@ """ import numpy as np -from numpy.random import normal, multivariate_normal -from chainconsumer import ChainConsumer +from numpy.random import multivariate_normal, normal +from chainconsumer import ChainConsumer np.random.seed(0) cov = 1e2 * normal(size=(3, 3)) diff --git a/examples/customisations/plot_preliminary.py b/examples/customisations/plot_preliminary.py index 43ab9e98..31575e24 100644 --- a/examples/customisations/plot_preliminary.py +++ b/examples/customisations/plot_preliminary.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ================== Watermarking Plots @@ -19,6 +18,7 @@ import numpy as np from numpy.random import multivariate_normal + from chainconsumer import ChainConsumer np.random.seed(0) @@ -41,4 +41,3 @@ kwargs = {"color": "purple", "alpha": 1.0, "family": "sanserif", "usetex": False, "weight": "bold"} c.configure(watermark_text_kwargs=kwargs, flip=True) fig = c.plotter.plot(watermark="SECRET RESULTS", figsize=2.0) - diff --git a/examples/customisations/plot_rainbow_serif_bins.py b/examples/customisations/plot_rainbow_serif_bins.py index 790acba3..19c2e9e6 100644 --- a/examples/customisations/plot_rainbow_serif_bins.py +++ b/examples/customisations/plot_rainbow_serif_bins.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ======================= Cmap and Custom Bins @@ -19,9 +18,9 @@ """ import numpy as np -from numpy.random import normal, random, multivariate_normal -from chainconsumer import ChainConsumer +from numpy.random import multivariate_normal, normal, random +from chainconsumer import ChainConsumer np.random.seed(0) cov = 0.3 * random(size=(3, 3)) + np.identity(3) diff --git a/examples/customisations/plot_selected_chains.py b/examples/customisations/plot_selected_chains.py index 42903905..6c86d008 100644 --- a/examples/customisations/plot_selected_chains.py +++ b/examples/customisations/plot_selected_chains.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ================ Excluding Chains @@ -14,6 +13,7 @@ import numpy as np from numpy.random import multivariate_normal + from chainconsumer import ChainConsumer np.random.seed(0) @@ -27,4 +27,4 @@ c.add_chain(data3, name="Chain C") fig = c.plotter.plot(chains=["Chain A", "Chain C"]) -fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. \ No newline at end of file +fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_shade_gradient.py b/examples/customisations/plot_shade_gradient.py index 38c818bf..454084d1 100644 --- a/examples/customisations/plot_shade_gradient.py +++ b/examples/customisations/plot_shade_gradient.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ============== Shade Gradient @@ -12,6 +11,7 @@ import numpy as np from numpy.random import multivariate_normal + from chainconsumer import ChainConsumer np.random.seed(0) @@ -21,7 +21,7 @@ c = ChainConsumer() c.add_chain(data1, parameters=["$x$", "$y$"]) c.add_chain(data2, parameters=["$x$", "$y$"]) -c.configure(shade_gradient=[0.1, 3.0], colors=['o', 'k'], sigmas=[0, 1, 2, 3], shade_alpha=1.0) +c.configure(shade_gradient=[0.1, 3.0], colors=["o", "k"], sigmas=[0, 1, 2, 3], shade_alpha=1.0) fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_shift.py b/examples/customisations/plot_shift.py index d82e8ba2..a3dfcf6a 100644 --- a/examples/customisations/plot_shift.py +++ b/examples/customisations/plot_shift.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ============== Shifting Plots @@ -14,6 +13,7 @@ import numpy as np from numpy.random import multivariate_normal + from chainconsumer import ChainConsumer np.random.seed(0) diff --git a/examples/customisations/plot_spacing.py b/examples/customisations/plot_spacing.py index f199239e..d9462376 100644 --- a/examples/customisations/plot_spacing.py +++ b/examples/customisations/plot_spacing.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ =============== Subplot Spacing @@ -9,9 +8,9 @@ """ import numpy as np -from numpy.random import normal, random, multivariate_normal -from chainconsumer import ChainConsumer +from numpy.random import multivariate_normal, normal, random +from chainconsumer import ChainConsumer np.random.seed(0) cov = random(size=(3, 3)) diff --git a/examples/customisations/plot_three_chains.py b/examples/customisations/plot_three_chains.py index fbc22467..59dc783c 100644 --- a/examples/customisations/plot_three_chains.py +++ b/examples/customisations/plot_three_chains.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ============ Three Chains @@ -9,9 +8,9 @@ """ import numpy as np -from numpy.random import normal, random, multivariate_normal -from chainconsumer import ChainConsumer +from numpy.random import multivariate_normal, normal, random +from chainconsumer import ChainConsumer if __name__ == "__main__": np.random.seed(0) @@ -24,10 +23,12 @@ # If the parameters are the same between chains, you can just pass it the # first time, and they will become the default parameters. - fig = ChainConsumer()\ - .add_chain(data, parameters=["$x$", "$y$", r"$\epsilon$"], name="Test chain")\ - .add_chain(data2, name="Chain2")\ - .add_chain(data3, name="Chain3") \ + fig = ( + ChainConsumer() + .add_chain(data, parameters=["$x$", "$y$", r"$\epsilon$"], name="Test chain") + .add_chain(data2, name="Chain2") + .add_chain(data3, name="Chain3") .plotter.plot() + ) fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_zorder.py b/examples/customisations/plot_zorder.py index ff74e0cb..1c4a1dc1 100644 --- a/examples/customisations/plot_zorder.py +++ b/examples/customisations/plot_zorder.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ================ Changing Z-Order @@ -13,6 +12,7 @@ import numpy as np from numpy.random import multivariate_normal + from chainconsumer import ChainConsumer np.random.seed(0) @@ -34,4 +34,3 @@ c.add_chain(data2, color="o", shade_alpha=0.7, zorder=1) c.configure(spacing=0) c.plotter.plot(display=True, figsize=2.0) - diff --git a/examples/more/plot_colorpoints2.py b/examples/more/plot_colorpoints2.py index e476a27f..bc781e84 100644 --- a/examples/more/plot_colorpoints2.py +++ b/examples/more/plot_colorpoints2.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ======================== Multiple Colour Scatter! @@ -16,14 +15,15 @@ """ import numpy as np -from numpy.random import normal, multivariate_normal +from numpy.random import multivariate_normal, normal + from chainconsumer import ChainConsumer np.random.seed(1) cov = normal(size=(4, 4)) data = multivariate_normal(normal(size=4), np.dot(cov, cov.T), size=100000) cov = 1 + 0.5 * normal(size=(4, 4)) -data2 = multivariate_normal(4+normal(size=4), np.dot(cov, cov.T), size=100000) +data2 = multivariate_normal(4 + normal(size=4), np.dot(cov, cov.T), size=100000) c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", "$z$", "$g$"], name="a") c.add_chain(data2, parameters=["$x$", "$y$", "$z$", "$t$"], name="b") diff --git a/examples/more/plot_divide_chain.py b/examples/more/plot_divide_chain.py index f5f6a729..073fbc25 100644 --- a/examples/more/plot_divide_chain.py +++ b/examples/more/plot_divide_chain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ================ Dividing a chain @@ -20,8 +19,8 @@ import numpy as np from numpy.random import multivariate_normal -from chainconsumer import ChainConsumer +from chainconsumer import ChainConsumer np.random.seed(0) data = multivariate_normal([0.0, 4.0], [[1.0, 0.7], [0.7, 1.5]], size=1000000) diff --git a/examples/more/plot_many.py b/examples/more/plot_many.py index b6152cfa..8e0a668f 100644 --- a/examples/more/plot_many.py +++ b/examples/more/plot_many.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ========================= Plot Many Things For Fun! @@ -13,7 +12,8 @@ """ import numpy as np -from numpy.random import normal, multivariate_normal, uniform +from numpy.random import multivariate_normal, normal, uniform + from chainconsumer import ChainConsumer np.random.seed(1) @@ -21,16 +21,17 @@ data = multivariate_normal([0.4, 1], [[0.01, -0.003], [-0.003, 0.001]], size=n) data = np.hstack((data, (67 + 10 * data[:, 0] - data[:, 1] ** 2)[:, None])) data2 = np.vstack((uniform(-0.1, 1.1, n), normal(1.2, 0.1, n))).T -data2[:, 1] -= (data2[:, 0] ** 2) +data2[:, 1] -= data2[:, 0] ** 2 data3 = multivariate_normal([0.3, 0.7], [[0.02, 0.05], [0.05, 0.1]], size=n) c = ChainConsumer() -c.add_chain(data2, parameters=["$\Omega_m$", "$-w$"], name="B") +c.add_chain(data2, parameters=[r"$\Omega_m$", "$-w$"], name="B") c.add_chain(data3, name="S") -c.add_chain(data, parameters=["$\Omega_m$", "$-w$", "$H_0$"], name="P") +c.add_chain(data, parameters=[r"$\Omega_m$", "$-w$", "$H_0$"], name="P") -c.configure(color_params="$H_0$", shade=[True, True, False], - shade_alpha=0.2, bar_shade=True, linestyles=["-", "--", "-"]) +c.configure( + color_params="$H_0$", shade=[True, True, False], shade_alpha=0.2, bar_shade=True, linestyles=["-", "--", "-"] +) fig = c.plotter.plot(figsize=2.0, extents=[[0, 1], [0, 1.5]]) fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/plot_correlations.py b/examples/plot_correlations.py index e9670acd..862dd136 100644 --- a/examples/plot_correlations.py +++ b/examples/plot_correlations.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ =========================== Plot Parameter Correlations @@ -19,8 +18,8 @@ import numpy as np -from chainconsumer import ChainConsumer +from chainconsumer import ChainConsumer cov = [[1, 0.5, 0.2], [0.5, 1, 0.3], [0.2, 0.3, 1.0]] data = np.random.multivariate_normal([0, 0, 1], cov, size=100000) @@ -30,4 +29,3 @@ latex_table = c.analysis.get_correlation_table() print(latex_table) - diff --git a/examples/plot_covariance.py b/examples/plot_covariance.py index 416f7294..af956ad4 100644 --- a/examples/plot_covariance.py +++ b/examples/plot_covariance.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ========================= Plot Parameter Covariance @@ -19,8 +18,8 @@ import numpy as np -from chainconsumer import ChainConsumer +from chainconsumer import ChainConsumer cov = [[1.0, 0.5, 0.2], [0.5, 2.0, 0.3], [0.2, 0.3, 3.0]] data = np.random.multivariate_normal([0, 0, 1], cov, size=1000000) @@ -29,4 +28,3 @@ c.add_chain(data, parameters=parameters) latex_table = c.analysis.get_covariance_table() print(latex_table) - diff --git a/examples/plot_distributions.py b/examples/plot_distributions.py index 01171e45..6c8b1267 100644 --- a/examples/plot_distributions.py +++ b/examples/plot_distributions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ================== Plot Distributions @@ -12,7 +11,8 @@ """ import numpy as np -from numpy.random import random, multivariate_normal +from numpy.random import multivariate_normal, random + from chainconsumer import ChainConsumer np.random.seed(0) diff --git a/examples/plot_introduction.py b/examples/plot_introduction.py index 1e25894d..9a6dee41 100644 --- a/examples/plot_introduction.py +++ b/examples/plot_introduction.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ============= Plot Contours @@ -21,6 +20,7 @@ """ import numpy as np + from chainconsumer import ChainConsumer np.random.seed(0) diff --git a/examples/plot_model_selection.py b/examples/plot_model_selection.py index ea82f9f1..4793fa31 100644 --- a/examples/plot_model_selection.py +++ b/examples/plot_model_selection.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ===================== Plot Model Comparison @@ -19,8 +18,8 @@ from scipy.stats import norm -from chainconsumer import ChainConsumer +from chainconsumer import ChainConsumer n = 10000 d1 = norm.rvs(size=n) diff --git a/examples/plot_summary.py b/examples/plot_summary.py index 07bf7752..98161a81 100644 --- a/examples/plot_summary.py +++ b/examples/plot_summary.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ============ Plot Summary @@ -13,6 +12,7 @@ # Lets add a bunch of chains represnting all these different models of ours. import numpy as np + from chainconsumer import ChainConsumer @@ -28,6 +28,7 @@ def get_instance(): c.add_chain(data, parameters=parameters, name=name) return c + ############################################################################### # If we want the full shape of the distributions, well, thats the default # behaviour! diff --git a/examples/plot_table.py b/examples/plot_table.py index 43e02cbe..a08ff35e 100644 --- a/examples/plot_table.py +++ b/examples/plot_table.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ =========== Plot Tables @@ -18,8 +17,8 @@ import numpy as np -from chainconsumer import ChainConsumer +from chainconsumer import ChainConsumer ndim, nsamples = 4, 200000 np.random.seed(0) @@ -27,11 +26,11 @@ data = np.random.randn(nsamples, ndim) data[:, 2] += data[:, 1] * data[:, 2] data[:, 1] = data[:, 1] * 3 + 5 -data[:, 3] /= (np.abs(data[:, 1]) + 1) +data[:, 3] /= np.abs(data[:, 1]) + 1 data2 = np.random.randn(nsamples, ndim) data2[:, 0] -= 1 -data2[:, 2] += data2[:, 1]**2 +data2[:, 2] += data2[:, 1] ** 2 data2[:, 1] = data2[:, 1] * 2 - 5 data2[:, 3] = data2[:, 3] * 1.5 + 2 diff --git a/examples/plot_walk.py b/examples/plot_walk.py index 560ba51a..f0c92e85 100644 --- a/examples/plot_walk.py +++ b/examples/plot_walk.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ========== Plot Walks @@ -22,6 +21,7 @@ # you can use the following code: import numpy as np + from chainconsumer import ChainConsumer np.random.seed(0) @@ -35,4 +35,3 @@ fig = c.plotter.plot_walks(truth={"$x$": -1, "$y$": 1, "$z$": -2}, convolve=100) fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - diff --git a/paper/paper.md b/paper/paper.md index 81e89d87..ba16ea92 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -16,22 +16,22 @@ bibliography: paper.bib ChainConsumer is a python package written to consume the output chains of Monte-Carlo processes and fitting algorithms, such as the results -of MCMC. +of MCMC. -ChainConsumer's main function is to produce plots of the likelihood +ChainConsumer's main function is to produce plots of the likelihood surface inferred from the supplied chain. In addition to showing the two-dimensional marginalised likelihood surfaces, marginalised parameter distributions are given, and maximum-likelihood statistics -are used to present parameter constraints. +are used to present parameter constraints. In addition to this, parameter constraints can be output -in the form of a LaTeX table. Finally, ChainConsumer also provides -the functionality to plot the chains as a series of walks in -parameter values, which provides an easy visual check on chain +in the form of a LaTeX table. Finally, ChainConsumer also provides +the functionality to plot the chains as a series of walks in +parameter values, which provides an easy visual check on chain mixing and chain convergence. -Plotting is performed via the matplotlib library [@matplotlib], and +Plotting is performed via the matplotlib library [@matplotlib], and makes use of various numpy [@numpy] and scipy [@scipy] functions. The optional KDE feature makes use of [@statsmodels]. diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..8ffa1ee4 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1130 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "black" +version = "23.9.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"}, + {file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"}, + {file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"}, + {file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"}, + {file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"}, + {file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"}, + {file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"}, + {file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"}, + {file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"}, + {file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"}, + {file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "contourpy" +version = "1.1.1" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.8" +files = [ + {file = "contourpy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:46e24f5412c948d81736509377e255f6040e94216bf1a9b5ea1eaa9d29f6ec1b"}, + {file = "contourpy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e48694d6a9c5a26ee85b10130c77a011a4fedf50a7279fa0bdaf44bafb4299d"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a66045af6cf00e19d02191ab578a50cb93b2028c3eefed999793698e9ea768ae"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ebf42695f75ee1a952f98ce9775c873e4971732a87334b099dde90b6af6a916"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6aec19457617ef468ff091669cca01fa7ea557b12b59a7908b9474bb9674cf0"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462c59914dc6d81e0b11f37e560b8a7c2dbab6aca4f38be31519d442d6cde1a1"}, + {file = "contourpy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6d0a8efc258659edc5299f9ef32d8d81de8b53b45d67bf4bfa3067f31366764d"}, + {file = "contourpy-1.1.1-cp310-cp310-win32.whl", hash = "sha256:d6ab42f223e58b7dac1bb0af32194a7b9311065583cc75ff59dcf301afd8a431"}, + {file = "contourpy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:549174b0713d49871c6dee90a4b499d3f12f5e5f69641cd23c50a4542e2ca1eb"}, + {file = "contourpy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:407d864db716a067cc696d61fa1ef6637fedf03606e8417fe2aeed20a061e6b2"}, + {file = "contourpy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe80c017973e6a4c367e037cb31601044dd55e6bfacd57370674867d15a899b"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e30aaf2b8a2bac57eb7e1650df1b3a4130e8d0c66fc2f861039d507a11760e1b"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de23ca4f381c3770dee6d10ead6fff524d540c0f662e763ad1530bde5112532"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:566f0e41df06dfef2431defcfaa155f0acfa1ca4acbf8fd80895b1e7e2ada40e"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04c2f0adaf255bf756cf08ebef1be132d3c7a06fe6f9877d55640c5e60c72c5"}, + {file = "contourpy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0c188ae66b772d9d61d43c6030500344c13e3f73a00d1dc241da896f379bb62"}, + {file = "contourpy-1.1.1-cp311-cp311-win32.whl", hash = "sha256:0683e1ae20dc038075d92e0e0148f09ffcefab120e57f6b4c9c0f477ec171f33"}, + {file = "contourpy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:8636cd2fc5da0fb102a2504fa2c4bea3cbc149533b345d72cdf0e7a924decc45"}, + {file = "contourpy-1.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:560f1d68a33e89c62da5da4077ba98137a5e4d3a271b29f2f195d0fba2adcb6a"}, + {file = "contourpy-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24216552104ae8f3b34120ef84825400b16eb6133af2e27a190fdc13529f023e"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56de98a2fb23025882a18b60c7f0ea2d2d70bbbcfcf878f9067234b1c4818442"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07d6f11dfaf80a84c97f1a5ba50d129d9303c5b4206f776e94037332e298dda8"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1eaac5257a8f8a047248d60e8f9315c6cff58f7803971170d952555ef6344a7"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19557fa407e70f20bfaba7d55b4d97b14f9480856c4fb65812e8a05fe1c6f9bf"}, + {file = "contourpy-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:081f3c0880712e40effc5f4c3b08feca6d064cb8cfbb372ca548105b86fd6c3d"}, + {file = "contourpy-1.1.1-cp312-cp312-win32.whl", hash = "sha256:059c3d2a94b930f4dafe8105bcdc1b21de99b30b51b5bce74c753686de858cb6"}, + {file = "contourpy-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f44d78b61740e4e8c71db1cf1fd56d9050a4747681c59ec1094750a658ceb970"}, + {file = "contourpy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70e5a10f8093d228bb2b552beeb318b8928b8a94763ef03b858ef3612b29395d"}, + {file = "contourpy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8394e652925a18ef0091115e3cc191fef350ab6dc3cc417f06da66bf98071ae9"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5bd5680f844c3ff0008523a71949a3ff5e4953eb7701b28760805bc9bcff217"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66544f853bfa85c0d07a68f6c648b2ec81dafd30f272565c37ab47a33b220684"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0c02b75acfea5cab07585d25069207e478d12309557f90a61b5a3b4f77f46ce"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41339b24471c58dc1499e56783fedc1afa4bb018bcd035cfb0ee2ad2a7501ef8"}, + {file = "contourpy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f29fb0b3f1217dfe9362ec55440d0743fe868497359f2cf93293f4b2701b8251"}, + {file = "contourpy-1.1.1-cp38-cp38-win32.whl", hash = "sha256:f9dc7f933975367251c1b34da882c4f0e0b2e24bb35dc906d2f598a40b72bfc7"}, + {file = "contourpy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:498e53573e8b94b1caeb9e62d7c2d053c263ebb6aa259c81050766beb50ff8d9"}, + {file = "contourpy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ba42e3810999a0ddd0439e6e5dbf6d034055cdc72b7c5c839f37a7c274cb4eba"}, + {file = "contourpy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c06e4c6e234fcc65435223c7b2a90f286b7f1b2733058bdf1345d218cc59e34"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca6fab080484e419528e98624fb5c4282148b847e3602dc8dbe0cb0669469887"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93df44ab351119d14cd1e6b52a5063d3336f0754b72736cc63db59307dabb718"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eafbef886566dc1047d7b3d4b14db0d5b7deb99638d8e1be4e23a7c7ac59ff0f"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe0fab26d598e1ec07d72cf03eaeeba8e42b4ecf6b9ccb5a356fde60ff08b85"}, + {file = "contourpy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f08e469821a5e4751c97fcd34bcb586bc243c39c2e39321822060ba902eac49e"}, + {file = "contourpy-1.1.1-cp39-cp39-win32.whl", hash = "sha256:bfc8a5e9238232a45ebc5cb3bfee71f1167064c8d382cadd6076f0d51cff1da0"}, + {file = "contourpy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:c84fdf3da00c2827d634de4fcf17e3e067490c4aea82833625c4c8e6cdea0887"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:229a25f68046c5cf8067d6d6351c8b99e40da11b04d8416bf8d2b1d75922521e"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10dab5ea1bd4401c9483450b5b0ba5416be799bbd50fc7a6cc5e2a15e03e8a3"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4f9147051cb8fdb29a51dc2482d792b3b23e50f8f57e3720ca2e3d438b7adf23"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a75cc163a5f4531a256f2c523bd80db509a49fc23721b36dd1ef2f60ff41c3cb"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b53d5769aa1f2d4ea407c65f2d1d08002952fac1d9e9d307aa2e1023554a163"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11b836b7dbfb74e049c302bbf74b4b8f6cb9d0b6ca1bf86cfa8ba144aedadd9c"}, + {file = "contourpy-1.1.1.tar.gz", hash = "sha256:96ba37c2e24b7212a77da85004c38e7c4d155d3e72a45eeaf22c1f03f607e8ab"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.16,<2.0", markers = "python_version <= \"3.11\""}, + {version = ">=1.26.0rc1,<2.0", markers = "python_version >= \"3.12\""}, +] + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.4.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "wurlitzer"] + +[[package]] +name = "cycler" +version = "0.12.0" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cycler-0.12.0-py3-none-any.whl", hash = "sha256:7896994252d006771357777d0251f3e34d266f4fa5f2c572247a80ab01440947"}, + {file = "cycler-0.12.0.tar.gz", hash = "sha256:8cc3a7b4861f91b1095157f9916f748549a617046e67eb7619abed9b34d2c94a"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "distlib" +version = "0.3.7" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.12.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, + {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] +typing = ["typing-extensions (>=4.7.1)"] + +[[package]] +name = "fonttools" +version = "4.43.0" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.43.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ab80e7d6bb01316d5fc8161a2660ca2e9e597d0880db4927bc866c76474472ef"}, + {file = "fonttools-4.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82d8e687a42799df5325e7ee12977b74738f34bf7fde1c296f8140efd699a213"}, + {file = "fonttools-4.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d08a694b280d615460563a6b4e2afb0b1b9df708c799ec212bf966652b94fc84"}, + {file = "fonttools-4.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d654d3e780e0ceabb1f4eff5a3c042c67d4428d0fe1ea3afd238a721cf171b3"}, + {file = "fonttools-4.43.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:20fc43783c432862071fa76da6fa714902ae587bc68441e12ff4099b94b1fcef"}, + {file = "fonttools-4.43.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:33c40a657fb87ff83185828c0323032d63a4df1279d5c1c38e21f3ec56327803"}, + {file = "fonttools-4.43.0-cp310-cp310-win32.whl", hash = "sha256:b3813f57f85bbc0e4011a0e1e9211f9ee52f87f402e41dc05bc5135f03fa51c1"}, + {file = "fonttools-4.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:05056a8c9af048381fdb17e89b17d45f6c8394176d01e8c6fef5ac96ea950d38"}, + {file = "fonttools-4.43.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da78f39b601ed0b4262929403186d65cf7a016f91ff349ab18fdc5a7080af465"}, + {file = "fonttools-4.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5056f69a18f3f28ab5283202d1efcfe011585d31de09d8560f91c6c88f041e92"}, + {file = "fonttools-4.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcc01cea0a121fb0c009993497bad93cae25e77db7dee5345fec9cce1aaa09cd"}, + {file = "fonttools-4.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee728d5af70f117581712966a21e2e07031e92c687ef1fdc457ac8d281016f64"}, + {file = "fonttools-4.43.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b5e760198f0b87e42478bb35a6eae385c636208f6f0d413e100b9c9c5efafb6a"}, + {file = "fonttools-4.43.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af38f5145258e9866da5881580507e6d17ff7756beef175d13213a43a84244e9"}, + {file = "fonttools-4.43.0-cp311-cp311-win32.whl", hash = "sha256:25620b738d4533cfc21fd2a4f4b667e481f7cb60e86b609799f7d98af657854e"}, + {file = "fonttools-4.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:635658464dccff6fa5c3b43fe8f818ae2c386ee6a9e1abc27359d1e255528186"}, + {file = "fonttools-4.43.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a682fb5cbf8837d1822b80acc0be5ff2ea0c49ca836e468a21ffd388ef280fd3"}, + {file = "fonttools-4.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3d7adfa342e6b3a2b36960981f23f480969f833d565a4eba259c2e6f59d2674f"}, + {file = "fonttools-4.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa67d1e720fdd902fde4a59d0880854ae9f19fc958f3e1538bceb36f7f4dc92"}, + {file = "fonttools-4.43.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77e5113233a2df07af9dbf493468ce526784c3b179c0e8b9c7838ced37c98b69"}, + {file = "fonttools-4.43.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57c22e5f9f53630d458830f710424dce4f43c5f0d95cb3368c0f5178541e4db7"}, + {file = "fonttools-4.43.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:206808f9717c9b19117f461246372a2c160fa12b9b8dbdfb904ab50ca235ba0a"}, + {file = "fonttools-4.43.0-cp312-cp312-win32.whl", hash = "sha256:f19c2b1c65d57cbea25cabb80941fea3fbf2625ff0cdcae8900b5fb1c145704f"}, + {file = "fonttools-4.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7c76f32051159f8284f1a5f5b605152b5a530736fb8b55b09957db38dcae5348"}, + {file = "fonttools-4.43.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e3f8acc6ef4a627394021246e099faee4b343afd3ffe2e517d8195b4ebf20289"}, + {file = "fonttools-4.43.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a68b71adc3b3a90346e4ac92f0a69ab9caeba391f3b04ab6f1e98f2c8ebe88e3"}, + {file = "fonttools-4.43.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ace0fd5afb79849f599f76af5c6aa5e865bd042c811e4e047bbaa7752cc26126"}, + {file = "fonttools-4.43.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9660e70a2430780e23830476332bc3391c3c8694769e2c0032a5038702a662"}, + {file = "fonttools-4.43.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:48078357984214ccd22d7fe0340cd6ff7286b2f74f173603a1a9a40b5dc25afe"}, + {file = "fonttools-4.43.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d27d960e10cf7617d70cf3104c32a69b008dde56f2d55a9bed4ba6e3df611544"}, + {file = "fonttools-4.43.0-cp38-cp38-win32.whl", hash = "sha256:a6a2e99bb9ea51e0974bbe71768df42c6dd189308c22f3f00560c3341b345646"}, + {file = "fonttools-4.43.0-cp38-cp38-win_amd64.whl", hash = "sha256:030355fbb0cea59cf75d076d04d3852900583d1258574ff2d7d719abf4513836"}, + {file = "fonttools-4.43.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52e77f23a9c059f8be01a07300ba4c4d23dc271d33eed502aea5a01ab5d2f4c1"}, + {file = "fonttools-4.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a530fa28c155538d32214eafa0964989098a662bd63e91e790e6a7a4e9c02da"}, + {file = "fonttools-4.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70f021a6b9eb10dfe7a411b78e63a503a06955dd6d2a4e130906d8760474f77c"}, + {file = "fonttools-4.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:812142a0e53cc853964d487e6b40963df62f522b1b571e19d1ff8467d7880ceb"}, + {file = "fonttools-4.43.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ace51902ab67ef5fe225e8b361039e996db153e467e24a28d35f74849b37b7ce"}, + {file = "fonttools-4.43.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8dfd8edfce34ad135bd69de20c77449c06e2c92b38f2a8358d0987737f82b49e"}, + {file = "fonttools-4.43.0-cp39-cp39-win32.whl", hash = "sha256:e5d53eddaf436fa131042f44a76ea1ead0a17c354ab9de0d80e818f0cb1629f1"}, + {file = "fonttools-4.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:93c5b6d77baf28f306bc13fa987b0b13edca6a39dc2324eaca299a74ccc6316f"}, + {file = "fonttools-4.43.0-py3-none-any.whl", hash = "sha256:e4bc589d8da09267c7c4ceaaaa4fc01a7908ac5b43b286ac9279afe76407c384"}, + {file = "fonttools-4.43.0.tar.gz", hash = "sha256:b62a53a4ca83c32c6b78cac64464f88d02929779373c716f738af6968c8c821e"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "scipy"] +lxml = ["lxml (>=4.0,<5)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.0.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "identify" +version = "2.5.30" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"}, + {file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "kiwisolver" +version = "1.4.5" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.7" +files = [ + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, + {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, +] + +[[package]] +name = "matplotlib" +version = "3.8.0" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.8.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c4940bad88a932ddc69734274f6fb047207e008389489f2b6f77d9ca485f0e7a"}, + {file = "matplotlib-3.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a33bd3045c7452ca1fa65676d88ba940867880e13e2546abb143035fa9072a9d"}, + {file = "matplotlib-3.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea6886e93401c22e534bbfd39201ce8931b75502895cfb115cbdbbe2d31f287"}, + {file = "matplotlib-3.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d670b9348e712ec176de225d425f150dc8e37b13010d85233c539b547da0be39"}, + {file = "matplotlib-3.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b37b74f00c4cb6af908cb9a00779d97d294e89fd2145ad43f0cdc23f635760c"}, + {file = "matplotlib-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:0e723f5b96f3cd4aad99103dc93e9e3cdc4f18afdcc76951f4857b46f8e39d2d"}, + {file = "matplotlib-3.8.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5dc945a9cb2deb7d197ba23eb4c210e591d52d77bf0ba27c35fc82dec9fa78d4"}, + {file = "matplotlib-3.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8b5a1bf27d078453aa7b5b27f52580e16360d02df6d3dc9504f3d2ce11f6309"}, + {file = "matplotlib-3.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f25ffb6ad972cdffa7df8e5be4b1e3cadd2f8d43fc72085feb1518006178394"}, + {file = "matplotlib-3.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee482731c8c17d86d9ddb5194d38621f9b0f0d53c99006275a12523ab021732"}, + {file = "matplotlib-3.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:36eafe2128772195b373e1242df28d1b7ec6c04c15b090b8d9e335d55a323900"}, + {file = "matplotlib-3.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:061ee58facb3580cd2d046a6d227fb77e9295599c5ec6ad069f06b5821ad1cfc"}, + {file = "matplotlib-3.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3cc3776836d0f4f22654a7f2d2ec2004618d5cf86b7185318381f73b80fd8a2d"}, + {file = "matplotlib-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c49a2bd6981264bddcb8c317b6bd25febcece9e2ebfcbc34e7f4c0c867c09dc"}, + {file = "matplotlib-3.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ed11654fc83cd6cfdf6170b453e437674a050a452133a064d47f2f1371f8d3"}, + {file = "matplotlib-3.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dae97fdd6996b3a25da8ee43e3fc734fff502f396801063c6b76c20b56683196"}, + {file = "matplotlib-3.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:87df75f528020a6299f76a1d986c0ed4406e3b2bd44bc5e306e46bca7d45e53e"}, + {file = "matplotlib-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d74a95fe055f73a6cd737beecc1b81c26f2893b7a3751d52b53ff06ca53f36"}, + {file = "matplotlib-3.8.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c3499c312f5def8f362a2bf761d04fa2d452b333f3a9a3f58805273719bf20d9"}, + {file = "matplotlib-3.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31e793c8bd4ea268cc5d3a695c27b30650ec35238626961d73085d5e94b6ab68"}, + {file = "matplotlib-3.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d5ee602ef517a89d1f2c508ca189cfc395dd0b4a08284fb1b97a78eec354644"}, + {file = "matplotlib-3.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5de39dc61ca35342cf409e031f70f18219f2c48380d3886c1cf5ad9f17898e06"}, + {file = "matplotlib-3.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dd386c80a98b5f51571b9484bf6c6976de383cd2a8cd972b6a9562d85c6d2087"}, + {file = "matplotlib-3.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f691b4ef47c7384d0936b2e8ebdeb5d526c81d004ad9403dfb9d4c76b9979a93"}, + {file = "matplotlib-3.8.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0b11f354aae62a2aa53ec5bb09946f5f06fc41793e351a04ff60223ea9162955"}, + {file = "matplotlib-3.8.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f54b9fb87ca5acbcdd0f286021bedc162e1425fa5555ebf3b3dfc167b955ad9"}, + {file = "matplotlib-3.8.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:60a6e04dfd77c0d3bcfee61c3cd335fff1b917c2f303b32524cd1235e194ef99"}, + {file = "matplotlib-3.8.0.tar.gz", hash = "sha256:df8505e1c19d5c2c26aff3497a7cbd3ccfc2e97043d1e4db3e76afa399164b69"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.0.1" +numpy = ">=1.21,<2" +packaging = ">=20.0" +pillow = ">=6.2.0" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" +setuptools_scm = ">=7" + +[[package]] +name = "mypy" +version = "1.5.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70"}, + {file = "mypy-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0"}, + {file = "mypy-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12"}, + {file = "mypy-1.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d"}, + {file = "mypy-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4"}, + {file = "mypy-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243"}, + {file = "mypy-1.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275"}, + {file = "mypy-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373"}, + {file = "mypy-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161"}, + {file = "mypy-1.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a"}, + {file = "mypy-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160"}, + {file = "mypy-1.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2"}, + {file = "mypy-1.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb"}, + {file = "mypy-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14"}, + {file = "mypy-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb"}, + {file = "mypy-1.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693"}, + {file = "mypy-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770"}, + {file = "mypy-1.5.1-py3-none-any.whl", hash = "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5"}, + {file = "mypy-1.5.1.tar.gz", hash = "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "numpy" +version = "1.26.0" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "numpy-1.26.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8db2f125746e44dce707dd44d4f4efeea8d7e2b43aace3f8d1f235cfa2733dd"}, + {file = "numpy-1.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0621f7daf973d34d18b4e4bafb210bbaf1ef5e0100b5fa750bd9cde84c7ac292"}, + {file = "numpy-1.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51be5f8c349fdd1a5568e72713a21f518e7d6707bcf8503b528b88d33b57dc68"}, + {file = "numpy-1.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:767254ad364991ccfc4d81b8152912e53e103ec192d1bb4ea6b1f5a7117040be"}, + {file = "numpy-1.26.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:436c8e9a4bdeeee84e3e59614d38c3dbd3235838a877af8c211cfcac8a80b8d3"}, + {file = "numpy-1.26.0-cp310-cp310-win32.whl", hash = "sha256:c2e698cb0c6dda9372ea98a0344245ee65bdc1c9dd939cceed6bb91256837896"}, + {file = "numpy-1.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:09aaee96c2cbdea95de76ecb8a586cb687d281c881f5f17bfc0fb7f5890f6b91"}, + {file = "numpy-1.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:637c58b468a69869258b8ae26f4a4c6ff8abffd4a8334c830ffb63e0feefe99a"}, + {file = "numpy-1.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:306545e234503a24fe9ae95ebf84d25cba1fdc27db971aa2d9f1ab6bba19a9dd"}, + {file = "numpy-1.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6adc33561bd1d46f81131d5352348350fc23df4d742bb246cdfca606ea1208"}, + {file = "numpy-1.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e062aa24638bb5018b7841977c360d2f5917268d125c833a686b7cbabbec496c"}, + {file = "numpy-1.26.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:546b7dd7e22f3c6861463bebb000646fa730e55df5ee4a0224408b5694cc6148"}, + {file = "numpy-1.26.0-cp311-cp311-win32.whl", hash = "sha256:c0b45c8b65b79337dee5134d038346d30e109e9e2e9d43464a2970e5c0e93229"}, + {file = "numpy-1.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:eae430ecf5794cb7ae7fa3808740b015aa80747e5266153128ef055975a72b99"}, + {file = "numpy-1.26.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:166b36197e9debc4e384e9c652ba60c0bacc216d0fc89e78f973a9760b503388"}, + {file = "numpy-1.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f042f66d0b4ae6d48e70e28d487376204d3cbf43b84c03bac57e28dac6151581"}, + {file = "numpy-1.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5e18e5b14a7560d8acf1c596688f4dfd19b4f2945b245a71e5af4ddb7422feb"}, + {file = "numpy-1.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6bad22a791226d0a5c7c27a80a20e11cfe09ad5ef9084d4d3fc4a299cca505"}, + {file = "numpy-1.26.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4acc65dd65da28060e206c8f27a573455ed724e6179941edb19f97e58161bb69"}, + {file = "numpy-1.26.0-cp312-cp312-win32.whl", hash = "sha256:bb0d9a1aaf5f1cb7967320e80690a1d7ff69f1d47ebc5a9bea013e3a21faec95"}, + {file = "numpy-1.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:ee84ca3c58fe48b8ddafdeb1db87388dce2c3c3f701bf447b05e4cfcc3679112"}, + {file = "numpy-1.26.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4a873a8180479bc829313e8d9798d5234dfacfc2e8a7ac188418189bb8eafbd2"}, + {file = "numpy-1.26.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:914b28d3215e0c721dc75db3ad6d62f51f630cb0c277e6b3bcb39519bed10bd8"}, + {file = "numpy-1.26.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c78a22e95182fb2e7874712433eaa610478a3caf86f28c621708d35fa4fd6e7f"}, + {file = "numpy-1.26.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f737708b366c36b76e953c46ba5827d8c27b7a8c9d0f471810728e5a2fe57c"}, + {file = "numpy-1.26.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b44e6a09afc12952a7d2a58ca0a2429ee0d49a4f89d83a0a11052da696440e49"}, + {file = "numpy-1.26.0-cp39-cp39-win32.whl", hash = "sha256:5671338034b820c8d58c81ad1dafc0ed5a00771a82fccc71d6438df00302094b"}, + {file = "numpy-1.26.0-cp39-cp39-win_amd64.whl", hash = "sha256:020cdbee66ed46b671429c7265cf00d8ac91c046901c55684954c3958525dab2"}, + {file = "numpy-1.26.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0792824ce2f7ea0c82ed2e4fecc29bb86bee0567a080dacaf2e0a01fe7654369"}, + {file = "numpy-1.26.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d484292eaeb3e84a51432a94f53578689ffdea3f90e10c8b203a99be5af57d8"}, + {file = "numpy-1.26.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:186ba67fad3c60dbe8a3abff3b67a91351100f2661c8e2a80364ae6279720299"}, + {file = "numpy-1.26.0.tar.gz", hash = "sha256:f93fc78fe8bf15afe2b8d6b6499f1c73953169fad1e9a8dd086cdff3190e7fdf"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pandas" +version = "2.1.1" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58d997dbee0d4b64f3cb881a24f918b5f25dd64ddf31f467bb9b67ae4c63a1e4"}, + {file = "pandas-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02304e11582c5d090e5a52aec726f31fe3f42895d6bfc1f28738f9b64b6f0614"}, + {file = "pandas-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffa8f0966de2c22de408d0e322db2faed6f6e74265aa0856f3824813cf124363"}, + {file = "pandas-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1f84c144dee086fe4f04a472b5cd51e680f061adf75c1ae4fc3a9275560f8f4"}, + {file = "pandas-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ce97667d06d69396d72be074f0556698c7f662029322027c226fd7a26965cb"}, + {file = "pandas-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:4c3f32fd7c4dccd035f71734df39231ac1a6ff95e8bdab8d891167197b7018d2"}, + {file = "pandas-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e2959720b70e106bb1d8b6eadd8ecd7c8e99ccdbe03ee03260877184bb2877d"}, + {file = "pandas-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25e8474a8eb258e391e30c288eecec565bfed3e026f312b0cbd709a63906b6f8"}, + {file = "pandas-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8bd1685556f3374520466998929bade3076aeae77c3e67ada5ed2b90b4de7f0"}, + {file = "pandas-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc3657869c7902810f32bd072f0740487f9e030c1a3ab03e0af093db35a9d14e"}, + {file = "pandas-2.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:05674536bd477af36aa2effd4ec8f71b92234ce0cc174de34fd21e2ee99adbc2"}, + {file = "pandas-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:b407381258a667df49d58a1b637be33e514b07f9285feb27769cedb3ab3d0b3a"}, + {file = "pandas-2.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c747793c4e9dcece7bb20156179529898abf505fe32cb40c4052107a3c620b49"}, + {file = "pandas-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3bcad1e6fb34b727b016775bea407311f7721db87e5b409e6542f4546a4951ea"}, + {file = "pandas-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5ec7740f9ccb90aec64edd71434711f58ee0ea7f5ed4ac48be11cfa9abf7317"}, + {file = "pandas-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29deb61de5a8a93bdd033df328441a79fcf8dd3c12d5ed0b41a395eef9cd76f0"}, + {file = "pandas-2.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f99bebf19b7e03cf80a4e770a3e65eee9dd4e2679039f542d7c1ace7b7b1daa"}, + {file = "pandas-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:84e7e910096416adec68075dc87b986ff202920fb8704e6d9c8c9897fe7332d6"}, + {file = "pandas-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366da7b0e540d1b908886d4feb3d951f2f1e572e655c1160f5fde28ad4abb750"}, + {file = "pandas-2.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e50e72b667415a816ac27dfcfe686dc5a0b02202e06196b943d54c4f9c7693e"}, + {file = "pandas-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc1ab6a25da197f03ebe6d8fa17273126120874386b4ac11c1d687df288542dd"}, + {file = "pandas-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0dbfea0dd3901ad4ce2306575c54348d98499c95be01b8d885a2737fe4d7a98"}, + {file = "pandas-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0489b0e6aa3d907e909aef92975edae89b1ee1654db5eafb9be633b0124abe97"}, + {file = "pandas-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:4cdb0fab0400c2cb46dafcf1a0fe084c8bb2480a1fa8d81e19d15e12e6d4ded2"}, + {file = "pandas-2.1.1.tar.gz", hash = "sha256:fecb198dc389429be557cde50a2d46da8434a17fe37d7d41ff102e3987fd947b"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.1" + +[package.extras] +all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] +aws = ["s3fs (>=2022.05.0)"] +clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] +compression = ["zstandard (>=0.17.0)"] +computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] +feather = ["pyarrow (>=7.0.0)"] +fss = ["fsspec (>=2022.05.0)"] +gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] +hdf5 = ["tables (>=3.7.0)"] +html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] +mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] +parquet = ["pyarrow (>=7.0.0)"] +performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] +plot = ["matplotlib (>=3.6.1)"] +postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] +spss = ["pyreadstat (>=1.1.5)"] +sql-other = ["SQLAlchemy (>=1.4.36)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.8.0)"] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "patsy" +version = "0.5.3" +description = "A Python package for describing statistical models and for building design matrices." +optional = false +python-versions = "*" +files = [ + {file = "patsy-0.5.3-py2.py3-none-any.whl", hash = "sha256:7eb5349754ed6aa982af81f636479b1b8db9d5b1a6e957a6016ec0534b5c86b7"}, + {file = "patsy-0.5.3.tar.gz", hash = "sha256:bdc18001875e319bc91c812c1eb6a10be4bb13cb81eb763f466179dca3b67277"}, +] + +[package.dependencies] +numpy = ">=1.4" +six = "*" + +[package.extras] +test = ["pytest", "pytest-cov", "scipy"] + +[[package]] +name = "pillow" +version = "10.0.1" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"}, + {file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"}, + {file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"}, + {file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"}, + {file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"}, + {file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"}, + {file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "platformdirs" +version = "3.11.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.4.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pre_commit-3.4.0-py2.py3-none-any.whl", hash = "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945"}, + {file = "pre_commit-3.4.0.tar.gz", hash = "sha256:6bbd5129a64cad4c0dfaeeb12cd8f7ea7e15b77028d985341478c8af3c759522"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pyparsing" +version = "3.1.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, + {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "7.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2023.3.post1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "ruff" +version = "0.0.292" +description = "An extremely fast Python linter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.0.292-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:02f29db018c9d474270c704e6c6b13b18ed0ecac82761e4fcf0faa3728430c96"}, + {file = "ruff-0.0.292-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:69654e564342f507edfa09ee6897883ca76e331d4bbc3676d8a8403838e9fade"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c3c91859a9b845c33778f11902e7b26440d64b9d5110edd4e4fa1726c41e0a4"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4476f1243af2d8c29da5f235c13dca52177117935e1f9393f9d90f9833f69e4"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be8eb50eaf8648070b8e58ece8e69c9322d34afe367eec4210fdee9a555e4ca7"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9889bac18a0c07018aac75ef6c1e6511d8411724d67cb879103b01758e110a81"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bdfabd4334684a4418b99b3118793f2c13bb67bf1540a769d7816410402a205"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7c77c53bfcd75dbcd4d1f42d6cabf2485d2e1ee0678da850f08e1ab13081a8"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e087b24d0d849c5c81516ec740bf4fd48bf363cfb104545464e0fca749b6af9"}, + {file = "ruff-0.0.292-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f160b5ec26be32362d0774964e218f3fcf0a7da299f7e220ef45ae9e3e67101a"}, + {file = "ruff-0.0.292-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ac153eee6dd4444501c4bb92bff866491d4bfb01ce26dd2fff7ca472c8df9ad0"}, + {file = "ruff-0.0.292-py3-none-musllinux_1_2_i686.whl", hash = "sha256:87616771e72820800b8faea82edd858324b29bb99a920d6aa3d3949dd3f88fb0"}, + {file = "ruff-0.0.292-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b76deb3bdbea2ef97db286cf953488745dd6424c122d275f05836c53f62d4016"}, + {file = "ruff-0.0.292-py3-none-win32.whl", hash = "sha256:e854b05408f7a8033a027e4b1c7f9889563dd2aca545d13d06711e5c39c3d003"}, + {file = "ruff-0.0.292-py3-none-win_amd64.whl", hash = "sha256:f27282bedfd04d4c3492e5c3398360c9d86a295be00eccc63914438b4ac8a83c"}, + {file = "ruff-0.0.292-py3-none-win_arm64.whl", hash = "sha256:7f67a69c8f12fbc8daf6ae6d36705037bde315abf8b82b6e1f4c9e74eb750f68"}, + {file = "ruff-0.0.292.tar.gz", hash = "sha256:1093449e37dd1e9b813798f6ad70932b57cf614e5c2b5c51005bf67d55db33ac"}, +] + +[[package]] +name = "scipy" +version = "1.11.3" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "scipy-1.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:370f569c57e1d888304052c18e58f4a927338eafdaef78613c685ca2ea0d1fa0"}, + {file = "scipy-1.11.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:9885e3e4f13b2bd44aaf2a1a6390a11add9f48d5295f7a592393ceb8991577a3"}, + {file = "scipy-1.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e04aa19acc324a1a076abb4035dabe9b64badb19f76ad9c798bde39d41025cdc"}, + {file = "scipy-1.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e1a8a4657673bfae1e05e1e1d6e94b0cabe5ed0c7c144c8aa7b7dbb774ce5c1"}, + {file = "scipy-1.11.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7abda0e62ef00cde826d441485e2e32fe737bdddee3324e35c0e01dee65e2a88"}, + {file = "scipy-1.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:033c3fd95d55012dd1148b201b72ae854d5086d25e7c316ec9850de4fe776929"}, + {file = "scipy-1.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:925c6f09d0053b1c0f90b2d92d03b261e889b20d1c9b08a3a51f61afc5f58165"}, + {file = "scipy-1.11.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5664e364f90be8219283eeb844323ff8cd79d7acbd64e15eb9c46b9bc7f6a42a"}, + {file = "scipy-1.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00f325434b6424952fbb636506f0567898dca7b0f7654d48f1c382ea338ce9a3"}, + {file = "scipy-1.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f290cf561a4b4edfe8d1001ee4be6da60c1c4ea712985b58bf6bc62badee221"}, + {file = "scipy-1.11.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:91770cb3b1e81ae19463b3c235bf1e0e330767dca9eb4cd73ba3ded6c4151e4d"}, + {file = "scipy-1.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1f97cd89c0fe1a0685f8f89d85fa305deb3067d0668151571ba50913e445820"}, + {file = "scipy-1.11.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dfcc1552add7cb7c13fb70efcb2389d0624d571aaf2c80b04117e2755a0c5d15"}, + {file = "scipy-1.11.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0d3a136ae1ff0883fffbb1b05b0b2fea251cb1046a5077d0b435a1839b3e52b7"}, + {file = "scipy-1.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bae66a2d7d5768eaa33008fa5a974389f167183c87bf39160d3fefe6664f8ddc"}, + {file = "scipy-1.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2f6dee6cbb0e263b8142ed587bc93e3ed5e777f1f75448d24fb923d9fd4dce6"}, + {file = "scipy-1.11.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:74e89dc5e00201e71dd94f5f382ab1c6a9f3ff806c7d24e4e90928bb1aafb280"}, + {file = "scipy-1.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:90271dbde4be191522b3903fc97334e3956d7cfb9cce3f0718d0ab4fd7d8bfd6"}, + {file = "scipy-1.11.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a63d1ec9cadecce838467ce0631c17c15c7197ae61e49429434ba01d618caa83"}, + {file = "scipy-1.11.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:5305792c7110e32ff155aed0df46aa60a60fc6e52cd4ee02cdeb67eaccd5356e"}, + {file = "scipy-1.11.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ea7f579182d83d00fed0e5c11a4aa5ffe01460444219dedc448a36adf0c3917"}, + {file = "scipy-1.11.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c77da50c9a91e23beb63c2a711ef9e9ca9a2060442757dffee34ea41847d8156"}, + {file = "scipy-1.11.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15f237e890c24aef6891c7d008f9ff7e758c6ef39a2b5df264650eb7900403c0"}, + {file = "scipy-1.11.3-cp39-cp39-win_amd64.whl", hash = "sha256:4b4bb134c7aa457e26cc6ea482b016fef45db71417d55cc6d8f43d799cdf9ef2"}, + {file = "scipy-1.11.3.tar.gz", hash = "sha256:bba4d955f54edd61899776bad459bf7326e14b9fa1c552181f0479cc60a568cd"}, +] + +[package.dependencies] +numpy = ">=1.21.6,<1.28.0" + +[package.extras] +dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] +doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] +test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "setuptools" +version = "68.2.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "setuptools-scm" +version = "8.0.4" +description = "the blessed package to manage your versions by scm tags" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-scm-8.0.4.tar.gz", hash = "sha256:b5f43ff6800669595193fd09891564ee9d1d7dcb196cab4b2506d53a2e1c95c7"}, + {file = "setuptools_scm-8.0.4-py3-none-any.whl", hash = "sha256:b47844cd2a84b83b3187a5782c71128c28b4c94cad8bfb871da2784a5cb54c4f"}, +] + +[package.dependencies] +packaging = ">=20" +setuptools = "*" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} +typing-extensions = "*" + +[package.extras] +docs = ["entangled-cli[rich]", "mkdocs", "mkdocs-entangled-plugin", "mkdocs-material", "mkdocstrings[python]", "pygments"] +rich = ["rich"] +test = ["build", "pytest", "rich", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "statsmodels" +version = "0.14.0" +description = "Statistical computations and models for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "statsmodels-0.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:16bfe0c96a53b20fa19067e3b6bd2f1d39e30d4891ea0d7bc20734a0ae95942d"}, + {file = "statsmodels-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a6a0a1a06ff79be8aa89c8494b33903442859add133f0dda1daf37c3c71682e"}, + {file = "statsmodels-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77b3cd3a5268ef966a0a08582c591bd29c09c88b4566c892a7c087935234f285"}, + {file = "statsmodels-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c64ebe9cf376cba0c31aed138e15ed179a1d128612dd241cdf299d159e5e882"}, + {file = "statsmodels-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb471f757fc45102a87e5d86e87dc2c8c78b34ad4f203679a46520f1d863b9da"}, + {file = "statsmodels-0.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:582f9e41092e342aaa04920d17cc3f97240e3ee198672f194719b5a3d08657d6"}, + {file = "statsmodels-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ebe885ccaa64b4bc5ad49ac781c246e7a594b491f08ab4cfd5aa456c363a6f6"}, + {file = "statsmodels-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b587ee5d23369a0e881da6e37f78371dce4238cf7638a455db4b633a1a1c62d6"}, + {file = "statsmodels-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef7fa4813c7a73b0d8a0c830250f021c102c71c95e9fe0d6877bcfb56d38b8c"}, + {file = "statsmodels-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:a6ad7b8aadccd4e4dd7f315a07bef1bca41d194eeaf4ec600d20dea02d242fce"}, + {file = "statsmodels-0.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3757542c95247e4ab025291a740efa5da91dc11a05990c033d40fce31c450dc9"}, + {file = "statsmodels-0.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:de489e3ed315bdba55c9d1554a2e89faa65d212e365ab81bc323fa52681fc60e"}, + {file = "statsmodels-0.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76e290f4718177bffa8823a780f3b882d56dd64ad1c18cfb4bc8b5558f3f5757"}, + {file = "statsmodels-0.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71054f9dbcead56def14e3c9db6f66f943110fdfb19713caf0eb0f08c1ec03fd"}, + {file = "statsmodels-0.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:d7fda067837df94e0a614d93d3a38fb6868958d37f7f50afe2a534524f2660cb"}, + {file = "statsmodels-0.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c7724ad573af26139a98393ae64bc318d1b19762b13442d96c7a3e793f495c3"}, + {file = "statsmodels-0.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b0a135f3bfdeec987e36e3b3b4c53e0bb87a8d91464d2fcc4d169d176f46fdb"}, + {file = "statsmodels-0.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce28eb1c397dba437ec39b9ab18f2101806f388c7a0cf9cdfd8f09294ad1c799"}, + {file = "statsmodels-0.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b1c768dd94cc5ba8398121a632b673c625491aa7ed627b82cb4c880a25563f"}, + {file = "statsmodels-0.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d1e3e10dfbfcd58119ba5a4d3c7d519182b970a2aebaf0b6f539f55ae16058d"}, + {file = "statsmodels-0.14.0.tar.gz", hash = "sha256:6875c7d689e966d948f15eb816ab5616f4928706b180cf470fd5907ab6f647a4"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.3", markers = "python_version == \"3.10\" and platform_system == \"Windows\" and platform_python_implementation != \"PyPy\""}, + {version = ">=1.18", markers = "python_version != \"3.10\" or platform_system != \"Windows\" or platform_python_implementation == \"PyPy\""}, +] +packaging = ">=21.3" +pandas = ">=1.0" +patsy = ">=0.5.2" +scipy = ">=1.4,<1.9.2 || >1.9.2" + +[package.extras] +build = ["cython (>=0.29.26)"] +develop = ["colorama", "cython (>=0.29.26)", "cython (>=0.29.28,<3.0.0)", "flake8", "isort", "joblib", "matplotlib (>=3)", "oldest-supported-numpy (>=2022.4.18)", "pytest (>=7.0.1,<7.1.0)", "pytest-randomly", "pytest-xdist", "pywinpty", "setuptools-scm[toml] (>=7.0.0,<7.1.0)"] +docs = ["ipykernel", "jupyter-client", "matplotlib", "nbconvert", "nbformat", "numpydoc", "pandas-datareader", "sphinx"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "virtualenv" +version = "20.24.5" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, + {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<4" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.10,<3.13" +content-hash = "113c079051fa0ea1674fa95d22d095f96129c500bead8b4fad276ef2c4986e20" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 00000000..53b35d37 --- /dev/null +++ b/poetry.toml @@ -0,0 +1,3 @@ +[virtualenvs] +create = true +in-project = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..c2faca6d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,70 @@ +[build-system] +build-backend = "poetry.core.masonry.api" +requires = ["poetry-core"] + +[tool.poetry] +authors = ["Samuel Hinton "] +description = "ChainConsumer: Consumer your MCMC chains" +name = "ChainConsumer" +packages = [{include = "chainconsumer", from = "src"}] +readme = "README.md" +version = "0.35.0" + + +[tool.poetry.dependencies] +python = ">=3.10,<3.13" +numpy = "^1.26.0" +scipy = "^1.11.3" +matplotlib = "^3.8.0" +statsmodels = "^0.14.0" +pandas = "^2.1.1" +pillow = "^10.0.1" + + +[tool.poetry.group.test.dependencies] +pytest = "^7.4.0" + +[tool.poetry.group.dev.dependencies] +black = ">=23.3.0" +pre-commit = ">=3.3.3" +ruff = ">=0.0.276, <1" +mypy = "^1.4.1" + +[tool.black] +line-length = 120 +target-version = ['py310'] + +[tool.ruff] +src = ["src"] +select = ["E", "F", "I", "N", "W", "D207", "D208", "D300", "UP", "YTT", "ASYNC", "DTZ", "G10", "G101", "G201", "G202", "INP001", "PIE", "T20", "SIM", "PTH", "PD", "PLE", "PLR", "PLW", "TRY", "NPY", "RUF"] +ignore = ["PD010", "PD901", "PLR2004", "UP017", "PLR0915", "TRY003", "INP001", "PLR0912", "PLR0913", "TRY300", "E712"] +line-length = 120 +target-version = "py310" + +[tool.ruff.extend-per-file-ignores] +"test/***" = ["INP001"] +"__init__.py" = ["E402", "F401"] + + +[tool.mypy] +check_untyped_defs = true +disallow_any_unimported = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = false +disallow_untyped_defs = true +implicit_reexport = false +no_implicit_optional = false +python_version = "3.11" +strict_equality = true +show_error_codes = true +warn_redundant_casts = true +warn_return_any = true +warn_unused_ignores = true + +[tool.pytest.ini_options] +addopts = "-v" +pythonpath = ["src"] +testpaths = ["test"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 0812d7e7..00000000 --- a/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python -import re -from setuptools import setup - -# Synchronize version from code. -with open("chainconsumer/chainconsumer.py") as f: - version = re.findall(r"__version__ = \"(.*?)\"", f.read())[0] - -setup(name="ChainConsumer", - version=version, - description="Consume chains and produce plots and tables", - long_description="Package documentation: http://samreay.github.io/ChainConsumer", - classifiers=["Development Status :: 4 - Beta", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Astronomy", - "Intended Audience :: Science/Research", - "Operating System :: OS Independent"], - packages=["chainconsumer"], - url="http://github.com/samreay/ChainConsumer", - author="Samuel Hinton", - author_email="samuelreay@gmail.com", - install_requires=["numpy", "scipy", "matplotlib>1.6.0,!=2.1.*,!=2.2.*", "statsmodels>=0.7.0", "pandas"]) - diff --git a/chainconsumer/__init__.py b/src/chainconsumer/__init__.py similarity index 60% rename from chainconsumer/__init__.py rename to src/chainconsumer/__init__.py index 7faf22de..fa11af91 100644 --- a/chainconsumer/__init__.py +++ b/src/chainconsumer/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- from .chainconsumer import ChainConsumer __all__ = ["ChainConsumer"] diff --git a/chainconsumer/analysis.py b/src/chainconsumer/analysis.py similarity index 91% rename from chainconsumer/analysis.py rename to src/chainconsumer/analysis.py index d1f4d91b..da880d31 100644 --- a/chainconsumer/analysis.py +++ b/src/chainconsumer/analysis.py @@ -1,16 +1,15 @@ -# -*- coding: utf-8 -*- import logging + import numpy as np from scipy.integrate import simps from scipy.interpolate import interp1d from scipy.ndimage.filters import gaussian_filter -from .helpers import get_smoothed_bins, get_grid_bins, get_latex_table_frame +from .helpers import get_grid_bins, get_latex_table_frame, get_smoothed_bins from .kde import MegKDE -class Analysis(object): - +class Analysis: summaries = ["max", "mean", "cumulative", "max_symmetric", "max_shortest", "max_central"] def __init__(self, parent): @@ -27,9 +26,16 @@ def __init__(self, parent): } def get_latex_table( - self, parameters=None, transpose=False, caption=None, label="tab:model_params", hlines=True, blank_fill="--", filename=None + self, + parameters=None, + transpose=False, + caption=None, + label="tab:model_params", + hlines=True, + blank_fill="--", + filename=None, ): # pragma: no cover - """ Generates a LaTeX table from parameter summaries. + """Generates a LaTeX table from parameter summaries. Parameters ---------- @@ -74,10 +80,7 @@ def get_latex_table( caption = "" end_text = " \\\\ \n" - if transpose: - column_text = "c" * (num_chains + 1) - else: - column_text = "c" * (num_parameters + 1) + column_text = "c" * (num_chains + 1) if transpose else "c" * (num_parameters + 1) center_text = "" hline_text = "\\hline\n" @@ -96,7 +99,7 @@ def get_latex_table( arr.append(blank_fill) center_text += " & ".join(arr) + end_text else: - center_text += " & ".join(["Model"] + parameters) + end_text + center_text += " & ".join(["Model", *parameters]) + end_text if hlines: center_text += "\t\t" + hline_text for name, chain_res in zip([c.name for c in chains], fit_values): @@ -118,7 +121,7 @@ def get_latex_table( return final_text def get_summary(self, squeeze=True, parameters=None, chains=None): - """ Gets a summary of the marginalised parameter distributions. + """Gets a summary of the marginalised parameter distributions. Parameters ---------- @@ -140,9 +143,9 @@ def get_summary(self, squeeze=True, parameters=None, chains=None): if chains is None: chains = self.parent.get_mcmc_chains() else: - if isinstance(chains, (int, str)): + if isinstance(chains, int | str): chains = [chains] - if isinstance(chains[0], (int, str)): + if isinstance(chains[0], int | str): chains = [self.parent.chains[i] for c in chains for i in self.parent._get_chain(c)] for chain in chains: @@ -159,8 +162,8 @@ def get_summary(self, squeeze=True, parameters=None, chains=None): return results def get_max_posteriors(self, parameters=None, squeeze=True, chains=None): - """ Gets the maximum posterior point in parameter space from the passed parameters. - + """Gets the maximum posterior point in parameter space from the passed parameters. + Requires the chains to have set `posterior` values. Parameters @@ -184,7 +187,7 @@ def get_max_posteriors(self, parameters=None, squeeze=True, chains=None): if chains is None: chains = self.parent.chains else: - if isinstance(chains, (int, str)): + if isinstance(chains, int | str): chains = [chains] chains = [self.parent.chains[i] for c in chains for i in self.parent._get_chain(c)] @@ -266,7 +269,9 @@ def get_covariance(self, chain=0, parameters=None): return parameters, cov - def get_correlation_table(self, chain=0, parameters=None, caption="Parameter Correlations", label="tab:parameter_correlations"): + def get_correlation_table( + self, chain=0, parameters=None, caption="Parameter Correlations", label="tab:parameter_correlations" + ): """ Gets a LaTeX table of parameter correlations. @@ -290,7 +295,9 @@ def get_correlation_table(self, chain=0, parameters=None, caption="Parameter Cor parameters, cor = self.get_correlations(chain=chain, parameters=parameters) return self._get_2d_latex_table(parameters, cor, caption, label) - def get_covariance_table(self, chain=0, parameters=None, caption="Parameter Covariance", label="tab:parameter_covariance"): + def get_covariance_table( + self, chain=0, parameters=None, caption="Parameter Covariance", label="tab:parameter_covariance" + ): """ Gets a LaTeX table of parameter covariance. @@ -325,7 +332,7 @@ def _get_smoothed_histogram(self, chain, parameter, pad=False): hist, edges = np.histogram(data, bins=bins, density=True, weights=chain.weights) if chain.power is not None: - hist = hist ** chain.power + hist = hist**chain.power edge_centers = 0.5 * (edges[1:] + edges[:-1]) xs = np.linspace(edge_centers[0], edge_centers[-1], 10000) @@ -350,7 +357,7 @@ def _get_2d_latex_table(self, parameters, matrix, caption, label): hline_text = " \\hline\n" table = "" - table += " & ".join([""] + parameters) + "\\\\ \n" + table += " & ".join(["", *parameters]) + "\\\\ \n" table += hline_text max_len = max([len(s) for s in parameters]) format_string = " %%%ds" % max_len @@ -363,7 +370,7 @@ def _get_2d_latex_table(self, parameters, matrix, caption, label): return latex_table % (column_def, table) def get_parameter_text(self, lower, maximum, upper, wrap=False): - """ Generates LaTeX appropriate text from marginalised parameter bounds. + """Generates LaTeX appropriate text from marginalised parameter bounds. Parameters ---------- @@ -410,26 +417,26 @@ def get_parameter_text(self, lower, maximum, upper, wrap=False): elif resolution == -2: fmt = "%0.3f" r = 3 - upper_error *= 10 ** factor - lower_error *= 10 ** factor - maximum *= 10 ** factor + upper_error *= 10**factor + lower_error *= 10**factor + maximum *= 10**factor upper_error = round(upper_error, r) lower_error = round(lower_error, r) maximum = round(maximum, r) if maximum == -0.0: maximum = 0.0 if resolution == 2: - upper_error *= 10 ** -factor - lower_error *= 10 ** -factor - maximum *= 10 ** -factor + upper_error *= 10**-factor + lower_error *= 10**-factor + maximum *= 10**-factor factor = 0 fmt = "%0.0f" upper_error_text = fmt % upper_error lower_error_text = fmt % lower_error if upper_error_text == lower_error_text: - text = r"%s\pm %s" % (fmt, "%s") % (maximum, lower_error_text) + text = r"{}\pm {}".format(fmt, "%s") % (maximum, lower_error_text) else: - text = r"%s^{+%s}_{-%s}" % (fmt, "%s", "%s") % (maximum, upper_error_text, lower_error_text) + text = r"{}^{{+{}}}_{{-{}}}".format(fmt, "%s", "%s") % (maximum, upper_error_text, lower_error_text) if factor != 0: text = r"\left( %s \right) \times 10^{%d}" % (text, -factor) if wrap: @@ -488,7 +495,7 @@ def get_parameter_summary_max(self, chain, parameter): elif area > desired_area: minVal = mid except ValueError: - self._logger.warning("Parameter %s in chain %s is not constrained" % (parameter, chain.name)) + self._logger.warning(f"Parameter {parameter} in chain {chain.name} is not constrained") return [None, xs[startIndex], None] return [x1, xs[startIndex], x2] diff --git a/chainconsumer/chain.py b/src/chainconsumer/chain.py similarity index 87% rename from chainconsumer/chain.py rename to src/chainconsumer/chain.py index 64744f69..1ca5ad83 100644 --- a/chainconsumer/chain.py +++ b/src/chainconsumer/chain.py @@ -1,13 +1,12 @@ -# -*- coding: utf-8 -*- import logging + import numpy as np -from .colors import Colors from .analysis import Analysis +from .colors import Colors -class Chain(object): - +class Chain: colors = Colors() # Static colors object to do color mapping def __init__( @@ -67,7 +66,7 @@ def __init__( self.shift_params = shift_params if shift_params is not None: - for key in shift_params.keys(): + for key in shift_params: try: index = self.parameters.index(key) avg = np.average(chain[:, index], weights=weights) @@ -148,10 +147,12 @@ def configure( show_as_1d_prior=False, zorder=None, ): - if statistics is not None: assert isinstance(statistics, str), "statistics should be a string" - assert statistics in list(Analysis.summaries), "statistics %s not recognised. Should be in %s" % (statistics, Analysis.summaries) + assert statistics in list(Analysis.summaries), "statistics {} not recognised. Should be in {}".format( + statistics, + Analysis.summaries, + ) self.config["statistics"] = statistics if color is not None: @@ -188,7 +189,11 @@ def update_unset_config(self, name, value, override=None): def _validate_config(self, name, value, *types): if value is not None: - assert isinstance(value, tuple(types)), "%s, which is %s, should be type of: %s" % (name, value, " or ".join([t.__name__ for t in types])) + assert isinstance(value, tuple(types)), "{}, which is {}, should be type of: {}".format( + name, + value, + " or ".join([t.__name__ for t in types]), + ) self.config[name] = value def validate_chain(self): @@ -199,17 +204,23 @@ def validate_chain(self): assert isinstance(self.name, str), "Chain name needs to be a string. It is %s" % type(self.name) assert np.all(np.isfinite(self.weights)), "Chain %s has weights which are NaN or inf!" % self.name assert len(self.weights.shape) == 1, "Weights should be a 1D array, have instead %s" % str(self.weights.shape) - assert self.weights.size == self.chain.shape[0], "Chain %s has %d steps but %d weights" % (self.name, self.weights.size, self.chain.shape[0]) + assert self.weights.size == self.chain.shape[0], "Chain %s has %d steps but %d weights" % ( + self.name, + self.weights.size, + self.chain.shape[0], + ) assert self.chain.shape[0] > 0, "Chain has shape %s, which means it has 0 steps!" % str(self.chain.shape) assert np.sum(self.weights) > 0, "Chain weights sum to zero, this is not good" if self.walkers is not None: assert int(self.walkers) == self.walkers, "Walkers should be an integer!" - assert self.chain.shape[0] % self.walkers == 0, "Chain %s has %d walkers and %d steps... which aren't divisible. They need to be!" % ( + assert ( + self.chain.shape[0] % self.walkers == 0 + ), "Chain %s has %d walkers and %d steps... which aren't divisible. They need to be!" % ( self.name, self.walkers, self.chain.shape[0], ) - assert isinstance(self.grid, bool), "Chain %s has %s for grid, should be a bool" % (self.name, type(self.grid)) + assert isinstance(self.grid, bool), f"Chain {self.name} has {type(self.grid)} for grid, should be a bool" assert self.parameters is not None, "Chain %s has parameter list of None. Please give names" % self.name assert len(self.parameters) == self.chain.shape[1], "Chain %s has %d parameters but data has %d columns" % ( self.name, @@ -219,7 +230,9 @@ def validate_chain(self): for i, p in enumerate(self.parameters): assert isinstance(p, str), "Param index %d, which is %s, needs to be a string!" % (i, p) if self.posterior is not None: - assert len(self.posterior.shape) == 1, "posterior should be a 1D array, have instead %s" % str(self.posterior.shape) + assert len(self.posterior.shape) == 1, "posterior should be a 1D array, have instead %s" % str( + self.posterior.shape + ) assert self.posterior.size == self.chain.shape[0], "Chain %s has %d steps but %d log-posterior values" % ( self.name, self.chain.shape[0], @@ -227,14 +240,15 @@ def validate_chain(self): ) assert np.all(np.isfinite(self.posterior)), "Chain %s has NaN or inf in the log-posterior" % self.name if self.num_free_params is not None: - assert isinstance(self.num_free_params, (int, float)), "Chain %s has num_free_params which is not an integer, its %s" % ( - self.name, - type(self.num_free_params), - ) + assert isinstance( + self.num_free_params, int | float + ), f"Chain {self.name} has num_free_params which is not an integer, its {type(self.num_free_params)}" assert np.isfinite(self.num_free_params), "num_free_params is either infinite or NaN" assert self.num_free_params > 0, "num_free_params must be positive" if self.num_eff_data_points is not None: - assert isinstance(self.num_eff_data_points, (int, float)), "Chain %s has num_eff_data_points which is not an a number, its %s" % ( + assert isinstance( + self.num_eff_data_points, int | float + ), "Chain {} has num_eff_data_points which is not an a number, its {}".format( self.name, type(self.num_eff_data_points), ) @@ -247,11 +261,11 @@ def validate_chain(self): # self.validated_params = set() def get_summary(self, param, callback): - stat = "%s %s" % (self.config["statistics"], self.config["summary_area"]) - if stat in self.summaries.keys() and param in self.summaries[stat]: + stat = "{} {}".format(self.config["statistics"], self.config["summary_area"]) + if stat in self.summaries and param in self.summaries[stat]: return self.summaries[stat][param] result = callback(self, param) - if stat not in self.summaries.keys(): + if stat not in self.summaries: self.summaries[stat] = {} self.summaries[stat][param] = result return result diff --git a/chainconsumer/chainconsumer.py b/src/chainconsumer/chainconsumer.py similarity index 92% rename from chainconsumer/chainconsumer.py rename to src/chainconsumer/chainconsumer.py index 9f287300..443d7d79 100644 --- a/chainconsumer/chainconsumer.py +++ b/src/chainconsumer/chainconsumer.py @@ -1,26 +1,22 @@ -# -*- coding: utf-8 -*- +import logging + import numpy as np import pandas as pd -import logging +from .analysis import Analysis +from .chain import Chain +from .colors import Colors from .comparisons import Comparison from .diagnostic import Diagnostic -from .plotter import Plotter from .helpers import get_bins -from .analysis import Analysis -from .colors import Colors -from .chain import Chain +from .plotter import Plotter __all__ = ["ChainConsumer"] -class ChainConsumer(object): - """ A class for consuming chains produced by an MCMC walk. Or grid searches. To make plots, - figures, tables, diagnostics, you name it. - - """ - - __version__ = "0.34.0" +class ChainConsumer: + """A class for consuming chains produced by an MCMC walk. Or grid searches. To make plots, + figures, tables, diagnostics, you name it.""" def __init__(self): logging.basicConfig(level=logging.INFO) @@ -88,7 +84,7 @@ def add_chain( zorder=None, shift_params=None, ): - r""" Add a chain to the consumer. + r"""Add a chain to the consumer. Parameters ---------- @@ -126,7 +122,7 @@ def add_chain( for plotting, but required if loading in multiple chains to perform model comparison. num_free_params : int, optional The number of degrees of freedom in your model. Not required for plotting, but required if - loading in multiple chains to perform model comparison. + loading in multiple chains to perform model comparison. color : str(hex), optional Provide a colour for the chain. Can be used instead of calling `configure` for convenience. linewidth : float, optional @@ -173,10 +169,10 @@ def add_chain( of bins to the given value. Giving a float will scale the number of bins, such that giving ``bins=1.5`` will result in using :math:`\frac{1.5\sqrt{n}}{10}` bins. Note this parameter is most useful if `kde=False` is also passed, so you - can actually see the bins and not a KDE. smooth : + can actually see the bins and not a KDE. smooth : color_params : str, optional The name of the parameter to use for the colour scatter. Defaults to none, for no colour. If set - to 'weights', 'log_weights', or 'posterior' (without the quotes), and that is not a parameter in the chain, + to 'weights', 'log_weights', or 'posterior' (without the quotes), and that is not a parameter in the chain, it will respectively use the weights, log weights, or posterior, to colour the points. plot_color_params : bool, optional Whether or not the colour parameter should also be plotted as a posterior surface. @@ -200,10 +196,7 @@ def add_chain( is_dict = False assert chain is not None, "You cannot have a chain of None" if isinstance(chain, str): - if chain.lower().endswith(".npy"): - chain = np.load(chain) - else: - chain = pd.read_csv(chain) + chain = np.load(chain) if chain.lower().endswith(".npy") else pd.read_csv(chain) elif isinstance(chain, dict): assert parameters is None, "You cannot pass a dictionary and specify parameter names" is_dict = True @@ -213,7 +206,9 @@ def add_chain( chain = np.array(chain).T if isinstance(chain, pd.DataFrame): - assert parameters is None, "You cannot pass a DataFrame and use parameter names, we're using the columns names" + assert ( + parameters is None + ), "You cannot pass a DataFrame and use parameter names, we're using the columns names" parameters = list(chain.columns) if "weight" in parameters: weights = chain["weight"] @@ -227,13 +222,16 @@ def add_chain( assert weights is not None, "If grid is set, you need to supply weights" if len(weights.shape) > 1: assert not is_dict, ( - "We cannot construct a meshgrid from a dictionary, as the parameters" "are no longer ordered. Please pass in a flattened array instead." + "We cannot construct a meshgrid from a dictionary, as the parameters" + "are no longer ordered. Please pass in a flattened array instead." ) self._logger.info("Constructing meshgrid for grid results") meshes = np.meshgrid(*[u for u in chain.T], indexing="ij") chain = np.vstack([m.flatten() for m in meshes]).T weights = weights.flatten() - assert weights.size == chain[:, 0].size, "Error, given weight array size disagrees with parameter sampling" + assert ( + weights.size == chain[:, 0].size + ), "Error, given weight array size disagrees with parameter sampling" if len(chain.shape) == 1: chain = chain[None].T @@ -242,14 +240,16 @@ def add_chain( name = "Chain %d" % len(self.chains) if power is not None: - assert isinstance(power, int) or isinstance(power, float), "Power should be numeric, but is %s" % type(power) + assert isinstance(power, float | int), "Power should be numeric, but is %s" % type(power) if self._default_parameters is None and parameters is not None: self._default_parameters = parameters if parameters is None: if self._default_parameters is not None: - assert chain.shape[1] == len(self._default_parameters), "Chain has %d dimensions, but default parameters have %d dimensions" % ( + assert chain.shape[1] == len( + self._default_parameters + ), "Chain has %d dimensions, but default parameters have %d dimensions" % ( chain.shape[1], len(self._default_parameters), ) @@ -260,9 +260,13 @@ def add_chain( parameters = ["%d" % x for x in range(chain.shape[1])] else: self._logger.debug("Adding chain with defined parameters") - assert len(parameters) <= chain.shape[1], "Have only %d columns in chain, but have been given %d parameters names! " "Please double check this." % ( - chain.shape[1], - len(parameters), + assert len(parameters) <= chain.shape[1], ( + "Have only %d columns in chain, but have been given %d parameters names! " + "Please double check this." + % ( + chain.shape[1], + len(parameters), + ) ) for p in parameters: if p not in self._all_parameters: @@ -271,9 +275,9 @@ def add_chain( if shift_params is not None: if isinstance(shift_params, list): shift_params = dict([(p, s) for p, s in zip(parameters, shift_params)]) - for key in shift_params.keys(): + for key in shift_params: if key not in parameters: - self._logger.warning("Warning, shift parameter %s is not in list of parameters %s" % (key, parameters)) + self._logger.warning(f"Warning, shift parameter {key} is not in list of parameters {parameters}") # Sorry, no KDE for you on a grid. if grid: @@ -322,7 +326,7 @@ def add_chain( return self def add_covariance(self, mean, covariance, parameters=None, name=None, **kwargs): - r""" Generate samples as per mean and covariance supplied. Useful for Fisher matrix forecasts. + r"""Generate samples as per mean and covariance supplied. Useful for Fisher matrix forecasts. Parameters ---------- @@ -349,9 +353,16 @@ def add_covariance(self, mean, covariance, parameters=None, name=None, **kwargs) return self def add_marker( - self, location, parameters=None, name=None, color=None, marker_size=None, marker_style=None, marker_alpha=None, + self, + location, + parameters=None, + name=None, + color=None, + marker_size=None, + marker_style=None, + marker_alpha=None, ): - r""" Add a marker to the plot at the given location. + r"""Add a marker to the plot at the given location. Parameters ---------- @@ -393,7 +404,7 @@ def add_marker( return self def remove_chain(self, chain=-1): - r""" Removes a chain from ChainConsumer. + r"""Removes a chain from ChainConsumer. Calling this will require any configurations set to be redone! @@ -408,7 +419,7 @@ def remove_chain(self, chain=-1): ChainConsumer Itself, to allow chaining calls. """ - if isinstance(chain, str) or isinstance(chain, int): + if isinstance(chain, int | str): chain = [chain] chain = sorted([i for c in chain for i in self._get_chain(c)])[::-1] @@ -474,7 +485,7 @@ def configure( zorder=None, stack=False, ): # pragma: no cover - r""" Configure the general plotting parameters common across the bar + r"""Configure the general plotting parameters common across the bar and contour plots. If you do not call this explicitly, the :func:`plot` @@ -565,7 +576,7 @@ def configure( to colour scatter. Defaults to 15k per chain. color_params : str|list[str], optional The name of the parameter to use for the colour scatter. Defaults to none, for no colour. If set - to 'weights', 'log_weights', or 'posterior' (without the quotes), and that is not a parameter in the chain, + to 'weights', 'log_weights', or 'posterior' (without the quotes), and that is not a parameter in the chain, it will respectively use the weights, log weights, or posterior, to colour the points. plot_color_params : bool|list[bool], optional Whether or not the colour parameter should also be plotted as a posterior surface. @@ -581,7 +592,7 @@ def configure( show_as_1d_prior : bool|list[bool], optional Showing as a 1D prior will show the 1D histograms, but won't plot the 2D contours. global_point : bool, optional - Whether the point which gets plotted is the global posterior maximum, or the marginalised 2D + Whether the point which gets plotted is the global posterior maximum, or the marginalised 2D posterior maximum. Note that when you use marginalised 2D maximums for the points, you do not get the 1D histograms. Defaults to `True`, for a global maximum value. marker_style : str|list[str], optional @@ -634,13 +645,15 @@ def configure( # Warn the user if configure has been invoked multiple times self._num_configure_calls += 1 if self._num_configure_calls > 1: - self._logger.warning("Configure has been called %d times - this is not good - it should be once!" % self._num_configure_calls) + self._logger.warning( + "Configure has been called %d times - this is not good - it should be once!" % self._num_configure_calls + ) self._logger.warning("To avoid this, load your chains in first, then call analysis/plotting methods") # Dirty way of ensuring overrides happen when requested l = locals() explicit = [] - for k in l.keys(): + for k in l: if l[k] is not None: explicit.append(k) if k.endswith("s"): @@ -654,7 +667,10 @@ def configure( # Determine statistics assert statistics is not None, "statistics should be a string or list of strings!" if isinstance(statistics, str): - assert statistics in list(Analysis.summaries), "statistics %s not recognised. Should be in %s" % (statistics, Analysis.summaries,) + assert statistics in list(Analysis.summaries), "statistics {} not recognised. Should be in {}".format( + statistics, + Analysis.summaries, + ) statistics = [statistics.lower()] * len(self.chains) elif isinstance(statistics, list): for i, l in enumerate(statistics): @@ -663,7 +679,7 @@ def configure( raise ValueError("statistics is not a string or a list!") # Determine KDEs - if isinstance(kde, bool) or isinstance(kde, float): + if isinstance(kde, bool | float): kde = [False if c.grid else kde for c in self.chains] kde_override = [c.kde for c in self.chains] @@ -697,13 +713,19 @@ def configure( color_params = [None] * num_chains else: if isinstance(color_params, str): - color_params = [color_params if color_params in cs.parameters + ["log_weights", "weights", "posterior"] else None for cs in self.chains] - color_params = [None if c == "posterior" and self.chains[i].posterior is None else c for i, c in enumerate(color_params)] - elif isinstance(color_params, list) or isinstance(color_params, tuple): + color_params = [ + color_params if color_params in [*cs.parameters, "log_weights", "weights", "posterior"] else None + for cs in self.chains + ] + color_params = [ + None if c == "posterior" and self.chains[i].posterior is None else c + for i, c in enumerate(color_params) + ] + elif isinstance(color_params, list | tuple): for c, chain in zip(color_params, self.chains): p = chain.parameters if c is not None: - assert c in p, "Color parameter %s not in parameters %s" % (c, p) + assert c in p, f"Color parameter {c} not in parameters {p}" # Determine if we should plot color parameters if isinstance(plot_color_params, bool): plot_color_params = [plot_color_params] * len(color_params) @@ -761,7 +783,7 @@ def configure( # Determine linewidths if linewidths is None: linewidths = [1.0] * len(self.chains) - elif isinstance(linewidths, float) or isinstance(linewidths, int): + elif isinstance(linewidths, float | int): linewidths = [linewidths] * len(self.chains) # Determine clouds @@ -772,15 +794,12 @@ def configure( # Determine cloud points if num_cloud is None: num_cloud = 30000 - if isinstance(num_cloud, int) or isinstance(num_cloud, float): + if isinstance(num_cloud, float | int): num_cloud = [int(num_cloud)] * num_chains # Should we shade the contours if shade is None: - if shade_alpha is None: - shade = num_chains <= 3 - else: - shade = True + shade = num_chains <= 3 if shade_alpha is None else True if isinstance(shade, bool): # If not overridden, do not shade chains with colour scatter points shade = [shade and c is None for c in color_params] @@ -788,14 +807,11 @@ def configure( # Modify shade alpha based on how many chains we have if shade_alpha is None: if num_chains == 1: - if contour_labels is not None: - shade_alpha = 0.75 - else: - shade_alpha = 1.0 + shade_alpha = 0.75 if contour_labels is not None else 1.0 else: shade_alpha = 1.0 / np.sqrt(num_chains) # Decrease the shading amount if there are colour scatter points - if isinstance(shade_alpha, float) or isinstance(shade_alpha, int): + if isinstance(shade_alpha, float | int): shade_alpha = [shade_alpha if c is None else 0.25 * shade_alpha for c in color_params] if shade_gradient is None: @@ -803,7 +819,10 @@ def configure( if isinstance(shade_gradient, float): shade_gradient = [shade_gradient] * num_chains elif isinstance(shade_gradient, list): - assert len(shade_gradient) == num_chains, "Have %d shade_gradient but % chains" % (len(shade_gradient), num_chains,) + assert len(shade_gradient) == num_chains, "Have %d shade_gradient but % chains" % ( + len(shade_gradient), + num_chains, + ) contour_over_points = num_chains < 20 @@ -829,12 +848,12 @@ def configure( if marker_size is None: marker_size = [20] * num_chains - elif isinstance(marker_style, (int, float)): + elif isinstance(marker_style, int | float): marker_size = [marker_size] * num_chains if marker_alpha is None: marker_alpha = [1.0] * num_chains - elif isinstance(marker_alpha, (int, float)): + elif isinstance(marker_alpha, int | float): marker_alpha = [marker_alpha] * num_chains # Figure out if we should display parameter summaries @@ -852,10 +871,7 @@ def configure( # Figure out how many sigmas to plot if sigmas is None: - if num_chains == 1: - sigmas = np.array([0, 1, 2]) - else: - sigmas = np.array([0, 1, 2]) + sigmas = np.array([0, 1, 2]) if num_chains == 1 else np.array([0, 1, 2]) if sigmas[0] != 0: sigmas = np.concatenate(([0], sigmas)) sigmas = np.sort(sigmas) @@ -863,8 +879,11 @@ def configure( if contour_labels is not None: assert isinstance(contour_labels, str), "contour_labels parameter should be a string" contour_labels = contour_labels.lower() - assert contour_labels in ["sigma", "confidence",], "contour_labels should be either sigma or confidence" - assert isinstance(contour_label_font_size, int) or isinstance(contour_label_font_size, float), "contour_label_font_size needs to be numeric" + assert contour_labels in [ + "sigma", + "confidence", + ], "contour_labels should be either sigma or confidence" + assert isinstance(contour_label_font_size, float | int), "contour_label_font_size needs to be numeric" if legend_artists is None: legend_artists = len(set(linestyles)) > 1 or len(set(linewidths)) > 1 @@ -934,13 +953,13 @@ def configure( c.update_unset_config("zorder", zorder[i], override=explicit) c.config["summary_area"] = summary_area - except IndentationError as e: + except IndentationError: print( "Index error when assigning chain properties, make sure you " "have enough properties set for the number of chains you have loaded! " "See the stack trace for which config item has the wrong number of entries." ) - raise e + raise # Non list options self.config["sigma2d"] = sigma2d @@ -968,7 +987,7 @@ def configure( return self def configure_truth(self, **kwargs): # pragma: no cover - r""" Configure the arguments passed to the ``axvline`` and ``axhline`` + r"""Configure the arguments passed to the ``axvline`` and ``axhline`` methods when plotting truth values. If you do not call this explicitly, the :func:`plot` method will diff --git a/chainconsumer/colors.py b/src/chainconsumer/colors.py similarity index 88% rename from chainconsumer/colors.py rename to src/chainconsumer/colors.py index 7b22bf27..d61b7d28 100644 --- a/chainconsumer/colors.py +++ b/src/chainconsumer/colors.py @@ -1,12 +1,11 @@ -# -*- coding: utf-8 -*- -from matplotlib.colors import rgb2hex import matplotlib.pyplot as plt import numpy as np +from matplotlib.colors import rgb2hex # Colours drawn from material designs colour pallet at https://material.io/guidelines/style/color.html -class Colors(object): +class Colors: def __init__(self): self.color_map = { "blue": "#1976D2", @@ -39,7 +38,20 @@ def __init__(self): "lg": "lgreen", "lb": "lblue", } - self.default_colors = ["blue", "lgreen", "red", "purple", "yellow", "grey", "lblue", "magenta", "green", "brown", "black", "orange"] + self.default_colors = [ + "blue", + "lgreen", + "red", + "purple", + "yellow", + "grey", + "lblue", + "magenta", + "green", + "brown", + "black", + "orange", + ] def format(self, color): if isinstance(color, np.ndarray): @@ -77,7 +89,7 @@ def scale_colour(self, colour, scalefactor): # pragma: no cover r = self._clamp(int(r * scalefactor)) g = self._clamp(int(g * scalefactor)) b = self._clamp(int(b * scalefactor)) - return "#%02x%02x%02x" % (r, g, b) + return f"#{r:02x}{g:02x}{b:02x}" def _clamp(self, val, minimum=0, maximum=255): if val < minimum: diff --git a/chainconsumer/comparisons.py b/src/chainconsumer/comparisons.py similarity index 87% rename from chainconsumer/comparisons.py rename to src/chainconsumer/comparisons.py index d2322933..df1dd6f9 100644 --- a/chainconsumer/comparisons.py +++ b/src/chainconsumer/comparisons.py @@ -1,18 +1,18 @@ -# -*- coding: utf-8 -*- -from scipy.interpolate import griddata -import numpy as np import logging +import numpy as np +from scipy.interpolate import griddata + from .helpers import get_latex_table_frame -class Comparison(object): +class Comparison: def __init__(self, parent): self.parent = parent self._logger = logging.getLogger("chainconsumer") def dic(self): - r""" Returns the corrected Deviance Information Criterion (DIC) for all chains loaded into ChainConsumer. + r"""Returns the corrected Deviance Information Criterion (DIC) for all chains loaded into ChainConsumer. If a chain does not have a posterior, this method will return `None` for that chain. **Note that the DIC metric is only valid on posterior surfaces which closely resemble multivariate normals!** @@ -66,7 +66,7 @@ def dic(self): return dics_fin def bic(self): - r""" Returns the corrected Bayesian Information Criterion (BIC) for all chains loaded into ChainConsumer. + r"""Returns the corrected Bayesian Information Criterion (BIC) for all chains loaded into ChainConsumer. If a chain does not have a posterior, number of data points, and number of free parameters loaded, this method will return `None` for that chain. Formally, the BIC is defined as @@ -96,7 +96,7 @@ def bic(self): if n_free is None: missing += "num_free_params, " - self._logger.warning("You need to set %s for chain %s to get the BIC" % (missing[:-2], chain.name)) + self._logger.warning(f"You need to set {missing[:-2]} for chain {chain.name} to get the BIC") else: bics_bool.append(True) bics.append(n_free * np.log(n_data) - 2 * np.max(p)) @@ -113,7 +113,7 @@ def bic(self): return bics_fin def aic(self): - r""" Returns the corrected Akaike Information Criterion (AICc) for all chains loaded into ChainConsumer. + r"""Returns the corrected Akaike Information Criterion (AICc) for all chains loaded into ChainConsumer. If a chain does not have a posterior, number of data points, and number of free parameters loaded, this method will return `None` for that chain. Formally, the AIC is defined as @@ -149,7 +149,7 @@ def aic(self): if n_free is None: missing += "num_free_params, " - self._logger.warning("You need to set %s for chain %s to get the AIC" % (missing[:-2], chain.name)) + self._logger.warning(f"You need to set {missing[:-2]} for chain {chain.name} to get the AIC") else: aics_bool.append(True) c_cor = 1.0 * n_free * (n_free + 1) / (n_data - n_free - 1) @@ -167,7 +167,15 @@ def aic(self): return aics_fin def comparison_table( - self, caption=None, label="tab:model_comp", hlines=True, aic=True, bic=True, dic=True, sort="bic", descending=True + self, + caption=None, + label="tab:model_comp", + hlines=True, + aic=True, + bic=True, + dic=True, + sort="bic", + descending=True, ): # pragma: no cover """ Return a LaTeX ready table of model comparisons. @@ -217,21 +225,14 @@ def comparison_table( hline_text = "\\hline\n" if hlines: center_text += hline_text - center_text += "\tModel" + (" & AIC" if aic else "") + (" & BIC " if bic else "") + (" & DIC " if dic else "") + end_text + center_text += ( + "\tModel" + (" & AIC" if aic else "") + (" & BIC " if bic else "") + (" & DIC " if dic else "") + end_text + ) if hlines: center_text += "\t" + hline_text - if aic: - aics = self.aic() - else: - aics = np.zeros(len(self.parent.chains)) - if bic: - bics = self.bic() - else: - bics = np.zeros(len(self.parent.chains)) - if dic: - dics = self.dic() - else: - dics = np.zeros(len(self.parent.chains)) + aics = self.aic() if aic else np.zeros(len(self.parent.chains)) + bics = self.bic() if bic else np.zeros(len(self.parent.chains)) + dics = self.dic() if dic else np.zeros(len(self.parent.chains)) if sort == "bic": to_sort = bics diff --git a/chainconsumer/diagnostic.py b/src/chainconsumer/diagnostic.py similarity index 88% rename from chainconsumer/diagnostic.py rename to src/chainconsumer/diagnostic.py index 0316f6a1..8c6451e0 100644 --- a/chainconsumer/diagnostic.py +++ b/src/chainconsumer/diagnostic.py @@ -1,16 +1,16 @@ -# -*- coding: utf-8 -*- -import numpy as np import logging + +import numpy as np from scipy.stats import normaltest -class Diagnostic(object): +class Diagnostic: def __init__(self, parent): self.parent = parent self._logger = logging.getLogger("chainconsumer") def gelman_rubin(self, chain=None, threshold=0.05): - r""" Runs the Gelman Rubin diagnostic on the supplied chains. + r"""Runs the Gelman Rubin diagnostic on the supplied chains. Parameters ---------- @@ -73,11 +73,11 @@ def gelman_rubin(self, chain=None, threshold=0.05): print("Gelman-Rubin Statistic values for chain %s" % name) for p, v, pas in zip(parameters, R, passed): param = "Param %d" % p if isinstance(p, int) else p - print("%s: %7.5f (%s)" % (param, v, "Passed" if pas else "Failed")) + print("{}: {:7.5f} ({})".format(param, v, "Passed" if pas else "Failed")) return np.all(passed) def geweke(self, chain=None, first=0.1, last=0.5, threshold=0.05): - """ Runs the Geweke diagnostic on the supplied chains. + """Runs the Geweke diagnostic on the supplied chains. Parameters ---------- @@ -107,7 +107,9 @@ def geweke(self, chain=None, first=0.1, last=0.5, threshold=0.05): chain = self.parent.chains[index[0]] num_walkers = chain.walkers - assert num_walkers is not None and num_walkers > 0, "You need to specify the number of walkers to use the Geweke diagnostic." + assert ( + num_walkers is not None and num_walkers > 0 + ), "You need to specify the number of walkers to use the Geweke diagnostic." name = chain.name data = chain.chain chains = np.split(data, num_walkers) @@ -115,12 +117,14 @@ def geweke(self, chain=None, first=0.1, last=0.5, threshold=0.05): n_start = int(np.floor(first * n)) n_end = int(np.floor((1 - last) * n)) mean_start = np.array([np.mean(c[:n_start, i]) for c in chains for i in range(c.shape[1])]) - var_start = np.array([self._spec(c[:n_start, i]) / c[:n_start, i].size for c in chains for i in range(c.shape[1])]) + var_start = np.array( + [self._spec(c[:n_start, i]) / c[:n_start, i].size for c in chains for i in range(c.shape[1])] + ) mean_end = np.array([np.mean(c[n_end:, i]) for c in chains for i in range(c.shape[1])]) var_end = np.array([self._spec(c[n_end:, i]) / c[n_end:, i].size for c in chains for i in range(c.shape[1])]) zs = (mean_start - mean_end) / (np.sqrt(var_start + var_end)) _, pvalue = normaltest(zs) - print("Gweke Statistic for chain %s has p-value %e" % (name, pvalue)) + print(f"Gweke Statistic for chain {name} has p-value {pvalue:e}") return pvalue > threshold # Method of estimating spectral density following PyMC. @@ -129,4 +133,4 @@ def _spec(self, x, order=2): from statsmodels.regression.linear_model import yule_walker beta, sigma = yule_walker(x, order) - return sigma ** 2 / (1.0 - np.sum(beta)) ** 2 + return sigma**2 / (1.0 - np.sum(beta)) ** 2 diff --git a/chainconsumer/helpers.py b/src/chainconsumer/helpers.py similarity index 91% rename from chainconsumer/helpers.py rename to src/chainconsumer/helpers.py index 8f515f01..d68d7afa 100644 --- a/chainconsumer/helpers.py +++ b/src/chainconsumer/helpers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import numpy as np @@ -27,7 +26,9 @@ def get_extents(data, weight, plot=False, wide_extents=True, tiny=False, pad=Fal def get_bins(chains): - proposal = [max(35, np.floor(1.0 * np.power(chain.chain.shape[0] / chain.chain.shape[1], 0.25))) for chain in chains] + proposal = [ + max(35, np.floor(1.0 * np.power(chain.chain.shape[0] / chain.chain.shape[1], 0.25))) for chain in chains + ] return proposal diff --git a/chainconsumer/kde.py b/src/chainconsumer/kde.py similarity index 93% rename from chainconsumer/kde.py rename to src/chainconsumer/kde.py index c16dddde..941fa4c6 100644 --- a/chainconsumer/kde.py +++ b/src/chainconsumer/kde.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- -from scipy import spatial import numpy as np +from scipy import spatial + +class MegKDE: + """Matched Elliptical Gaussian Kernel Density Estimator -class MegKDE(object): - """ Matched Elliptical Gaussian Kernel Density Estimator - Adapted from the algorithm specified in the BAMBIS's model specified Wolf 2017 to support weighted samples. """ @@ -52,13 +51,13 @@ def __init__(self, train, weights=None, truncation=3.0, nmin=4, factor=1.0): # self.scaling = np.power(self.norm * self.sigma, -self.num_dim) def evaluate(self, data): - """ Estimate un-normalised probability density at target points - + """Estimate un-normalised probability density at target points + Parameters ---------- data : np.ndarray - A `(num_targets, num_dim)` array of points to investigate. - + A `(num_targets, num_dim)` array of points to investigate. + Returns ------- np.ndarray diff --git a/chainconsumer/plotter.py b/src/chainconsumer/plotter.py similarity index 91% rename from chainconsumer/plotter.py rename to src/chainconsumer/plotter.py index 5be11cdb..c2a67004 100644 --- a/chainconsumer/plotter.py +++ b/src/chainconsumer/plotter.py @@ -1,21 +1,21 @@ -# -*- coding: utf-8 -*- import logging -import numpy as np -import matplotlib.pyplot as plt + import matplotlib +import matplotlib.pyplot as plt +import numpy as np from matplotlib.font_manager import FontProperties -from matplotlib.ticker import MaxNLocator, ScalarFormatter, LogLocator from matplotlib.textpath import TextPath +from matplotlib.ticker import LogLocator, MaxNLocator, ScalarFormatter from numpy import meshgrid from scipy.interpolate import interp1d from scipy.ndimage import gaussian_filter from scipy.stats import norm -from .helpers import get_extents, get_smoothed_bins, get_grid_bins +from .helpers import get_extents, get_grid_bins, get_smoothed_bins from .kde import MegKDE -class Plotter(object): +class Plotter: def __init__(self, parent): self.parent = parent self._logger = logging.getLogger("chainconsumer") @@ -37,7 +37,7 @@ def plot( watermark=None, log_scales=None, ): # pragma: no cover - """ Plot the chain! + """Plot the chain! Parameters ---------- @@ -98,7 +98,9 @@ def plot( legend = legend and len([n for n in names if n]) > 0 # Calculate cmap extents - unique_color_params = list(set([c.config["color_params"] for c in chains if c.config["color_params"] is not None])) + unique_color_params = list( + set([c.config["color_params"] for c in chains if c.config["color_params"] is not None]) + ) num_cax = len(unique_color_params) color_param_extents = {} for u in unique_color_params: @@ -132,7 +134,13 @@ def plot( flip = len(parameters) == 2 and plot_hists and self.parent.config["flip"] fig, axes, params1, params2, extents = self._get_figure( - parameters, chains=chains, figsize=figsize, flip=flip, external_extents=extents, blind=blind, log_scales=log_scales + parameters, + chains=chains, + figsize=figsize, + flip=flip, + external_extents=extents, + blind=blind, + log_scales=log_scales, ) label_font_size = self.parent.config["label_font_size"] @@ -142,7 +150,7 @@ def plot( if summary is None: summary = len(parameters) < 5 and len(self.parent.chains) == 1 if len(chains) == 1: - self._logger.debug("Plotting surfaces for chain of dimension %s" % (chains[0].chain.shape,)) + self._logger.debug(f"Plotting surfaces for chain of dimension {chains[0].chain.shape}") else: self._logger.debug("Plotting surfaces for %d chains" % len(chains)) cbar_done = [] @@ -237,10 +245,7 @@ def plot( legend_location = self.parent.config["legend_location"] if legend_location is None: - if not flip or len(parameters) > 2: - legend_location = (0, -1) - else: - legend_location = (-1, 0) + legend_location = (0, -1) if not flip or len(parameters) > 2 else (-1, 0) outside = legend_location[0] >= legend_location[1] if names is not None and legend: ax = axes[legend_location[0], legend_location[1]] @@ -283,19 +288,16 @@ def plot( fig.canvas.draw() for ax in axes[-1, :]: offset = ax.get_xaxis().get_offset_text() - ax.set_xlabel("{0} {1}".format(ax.get_xlabel(), "[{0}]".format(offset.get_text()) if offset.get_text() else "")) + ax.set_xlabel("{} {}".format(ax.get_xlabel(), f"[{offset.get_text()}]" if offset.get_text() else "")) offset.set_visible(False) for ax in axes[:, 0]: offset = ax.get_yaxis().get_offset_text() - ax.set_ylabel("{0} {1}".format(ax.get_ylabel(), "[{0}]".format(offset.get_text()) if offset.get_text() else "")) + ax.set_ylabel("{} {}".format(ax.get_ylabel(), f"[{offset.get_text()}]" if offset.get_text() else "")) offset.set_visible(False) dpi = 300 if watermark: - if flip and len(parameters) == 2: - ax = axes[-1, 0] - else: - ax = None + ax = axes[-1, 0] if flip and len(parameters) == 2 else None self._add_watermark(fig, ax, figsize, watermark, dpi=dpi) if filename is not None: @@ -331,7 +333,7 @@ def _add_watermark(self, fig, axes, figsize, text, dpi=300, size_scale=1.0): # bb1 = TextPath((0, 0), text, size=51, prop=font_prop, usetex=usetex).get_extents() dw = (bb1.width - bb0.width) * (dpi / 100) dh = (bb1.height - bb0.height) * (dpi / 100) - size = np.sqrt(dy ** 2 + dx ** 2) / (dh * abs(dy / dx) + dw) * 0.6 * scale * size_scale + size = np.sqrt(dy**2 + dx**2) / (dh * abs(dy / dx) + dw) * 0.6 * scale * size_scale if axes is not None: if usetex: size *= 0.7 @@ -359,7 +361,7 @@ def plot_walks( log_weight=None, log_scales=None, ): # pragma: no cover - """ Plots the chain walk; the parameter values as a function of step index. + """Plots the chain walk; the parameter values as a function of step index. This plot is more for a sanity or consistency check than for use with final results. Plotting this before plotting with :func:`plot` allows you to quickly see if the @@ -413,7 +415,9 @@ def plot_walks( """ - chains, parameters, truth, extents, _, log_scales = self._sanitise(chains, parameters, truth, extents, log_scales=log_scales) + chains, parameters, truth, extents, _, log_scales = self._sanitise( + chains, parameters, truth, extents, log_scales=log_scales + ) chains = [c for c in chains if c.mcmc_chain] n = len(parameters) @@ -441,20 +445,40 @@ def plot_walks( if p in chain.parameters: chain_row = chain.get_data(p) log = log_scales.get(p, False) - self._plot_walk(ax, p, chain_row, extents=extents.get(p), convolve=convolve, color=chain.config["color"], log_scale=log) + self._plot_walk( + ax, + p, + chain_row, + extents=extents.get(p), + convolve=convolve, + color=chain.config["color"], + log_scale=log, + ) if truth.get(p) is not None: self._plot_walk_truth(ax, truth.get(p)) else: if i == 0 and plot_posterior: for chain in chains: if chain.posterior is not None: - self._plot_walk(ax, r"$\log(P)$", chain.posterior - chain.posterior.max(), convolve=convolve, color=chain.config["color"]) + self._plot_walk( + ax, + r"$\log(P)$", + chain.posterior - chain.posterior.max(), + convolve=convolve, + color=chain.config["color"], + ) else: if log_weight is None: log_weight = np.any([chain.weights.mean() < 0.1 for chain in chains]) if log_weight: for chain in chains: - self._plot_walk(ax, r"$\log_{10}(w)$", np.log10(chain.weights), convolve=convolve, color=chain.config["color"]) + self._plot_walk( + ax, + r"$\log_{10}(w)$", + np.log10(chain.weights), + convolve=convolve, + color=chain.config["color"], + ) else: for chain in chains: self._plot_walk(ax, "$w$", chain.weights, convolve=convolve, color=chain.config["color"]) @@ -470,9 +494,19 @@ def plot_walks( return fig def plot_distributions( - self, parameters=None, truth=None, extents=None, display=False, filename=None, chains=None, col_wrap=4, figsize=None, blind=None, log_scales=None + self, + parameters=None, + truth=None, + extents=None, + display=False, + filename=None, + chains=None, + col_wrap=4, + figsize=None, + blind=None, + log_scales=None, ): # pragma: no cover - """ Plots the 1D parameter distributions for verification purposes. + """Plots the 1D parameter distributions for verification purposes. This plot is more for a sanity or consistency check than for use with final results. Plotting this before plotting with :func:`plot` allows you to quickly see if the @@ -517,7 +551,9 @@ def plot_distributions( the matplotlib figure created """ - chains, parameters, truth, extents, blind, log_scales = self._sanitise(chains, parameters, truth, extents, blind=blind, log_scales=log_scales) + chains, parameters, truth, extents, blind, log_scales = self._sanitise( + chains, parameters, truth, extents, blind=blind, log_scales=log_scales + ) n = len(parameters) num_cols = min(n, col_wrap) @@ -612,7 +648,7 @@ def plot_summary( show_names=True, log_scales=None, ): # pragma: no cover - """ Plots parameter summaries + """Plots parameter summaries This plot is more for a sanity or consistency check than for use with final results. Plotting this before plotting with :func:`plot` allows you to quickly see if the @@ -680,7 +716,7 @@ def plot_summary( # Check if we're using a chain for truth values if isinstance(truth, str): - assert truth in all_names, "Truth chain %s is not in the list of added chains %s" % (truth, all_names) + assert truth in all_names, f"Truth chain {truth} is not in the list of added chains {all_names}" if not include_truth_chain: chains = [c for c in chains if c.name != truth] truth = self.parent.analysis.get_summary(chains=truth, parameters=parameters) @@ -692,7 +728,10 @@ def plot_summary( if show_names: max_model_name = self._get_size_of_texts([chain.name for chain in chains]) model_width = 0.25 + (max_model_name / fid_dpi) - gridspec_kw = {"width_ratios": [model_width] + [param_width] * len(parameters), "height_ratios": [1] * len(chains)} + gridspec_kw = { + "width_ratios": [model_width] + [param_width] * len(parameters), + "height_ratios": [1] * len(chains), + } ncols = 1 + len(parameters) else: model_width = 0 @@ -708,7 +747,9 @@ def plot_summary( bottom_ratio = bottom_spacing / height figsize = (width * figsize, height * figsize) - fig, axes = plt.subplots(nrows=len(chains), ncols=ncols, figsize=figsize, squeeze=False, gridspec_kw=gridspec_kw) + fig, axes = plt.subplots( + nrows=len(chains), ncols=ncols, figsize=figsize, squeeze=False, gridspec_kw=gridspec_kw + ) fig.subplots_adjust(left=0.05, right=0.95, top=top_ratio, bottom=bottom_ratio, wspace=0.0, hspace=0.0) label_font_size = self.parent.config["label_font_size"] legend_color_text = self.parent.config["legend_color_text"] @@ -717,7 +758,15 @@ def plot_summary( for i, row in enumerate(axes): chain = chains[i] - cs, ws, ps, = chain.chain, chain.weights, chain.parameters + ( + cs, + ws, + ps, + ) = ( + chain.chain, + chain.weights, + chain.parameters, + ) gs, ns = chain.grid, chain.name colour = chain.config["color"] @@ -728,7 +777,14 @@ def plot_summary( ax_first.set_axis_off() text_colour = "k" if not legend_color_text else colour ax_first.text( - 0, 0.5, ns, transform=ax_first.transAxes, fontsize=label_font_size, verticalalignment="center", color=text_colour, weight="medium" + 0, + 0.5, + ns, + transform=ax_first.transAxes, + fontsize=label_font_size, + verticalalignment="center", + color=text_colour, + weight="medium", ) cols = row[1:] else: @@ -754,7 +810,7 @@ def plot_summary( # Add truth values truth_value = truth.get(p) if truth_value is not None: - if isinstance(truth_value, float) or isinstance(truth_value, int): + if isinstance(truth_value, float | int): truth_mean = truth_value truth_min, truth_max = None, None else: @@ -812,8 +868,9 @@ def _get_size_of_texts(self, texts): # pragma: no cover widths = [TextPath((0, 0), text, usetex=usetex, size=size).get_extents().width for text in texts] return max(widths) - def _sanitise(self, chains, parameters, truth, extents, color_p=False, blind=None, wide_extents=True, log_scales=None): # pragma: no cover - + def _sanitise( + self, chains, parameters, truth, extents, color_p=False, blind=None, wide_extents=True, log_scales=None + ): # pragma: no cover chains = self._sanitise_chains(chains) if color_p: @@ -896,7 +953,7 @@ def set_rc_params(self): plt.rc("font", family="sans-serif") def restore_rc_params(self): - """ Restores the matplotlib rc parameters modified by usetex and serif. + """Restores the matplotlib rc parameters modified by usetex and serif. Unfortunately this cannot be automated because you cannot invoke it whilst you have an active figure object or matplotlib will destroy you. So do all your plotting, close @@ -914,7 +971,9 @@ def _get_custom_extents(self, parameters, chains, external_extents, wide_extents extents[p] = self._get_parameter_extents(p, chains, wide_extents=wide_extents) return extents - def _get_figure(self, all_parameters, flip, figsize=(5, 5), external_extents=None, chains=None, blind=None, log_scales=None): # pragma: no cover + def _get_figure( + self, all_parameters, flip, figsize=(5, 5), external_extents=None, chains=None, blind=None, log_scales=None + ): # pragma: no cover n = len(all_parameters) max_ticks = self.parent.config["max_ticks"] spacing = self.parent.config["spacing"] @@ -934,10 +993,7 @@ def _get_figure(self, all_parameters, flip, figsize=(5, 5), external_extents=Non if spacing is None: spacing = 1.0 if n < 6 else 0.0 - if n == 2 and plot_hists and flip: - gridspec_kw = {"width_ratios": [3, 1], "height_ratios": [1, 3]} - else: - gridspec_kw = {} + gridspec_kw = {"width_ratios": [3, 1], "height_ratios": [1, 3]} if n == 2 and plot_hists and flip else {} fig, axes = plt.subplots(n, n, figsize=figsize, squeeze=False, gridspec_kw=gridspec_kw) fig.subplots_adjust(left=0.1, right=0.95, top=0.95, bottom=0.1, wspace=0.05 * spacing, hspace=0.05 * spacing) @@ -1097,7 +1153,6 @@ def _plot_points(self, ax, chains_groups, markers, sizes, alphas, py, px): # pr return h def _sanitise_chains(self, chains): - if not self.parent._configured: self.parent.configure() if not self.parent._configured_truth: @@ -1106,7 +1161,7 @@ def _sanitise_chains(self, chains): if chains is None: chains = list(range(len(self.parent.chains))) else: - if isinstance(chains, str) or isinstance(chains, int): + if isinstance(chains, int | str): chains = [chains] chains = [i for c in chains for i in self.parent._get_chain(c)] @@ -1114,7 +1169,7 @@ def _sanitise_chains(self, chains): return chains def plot_contour(self, ax, parameter_x, parameter_y, chains=None): - """ A lightweight method to plot contours in an external axis given two specified parameters + """A lightweight method to plot contours in an external axis given two specified parameters Parameters ========== @@ -1133,7 +1188,6 @@ def plot_contour(self, ax, parameter_x, parameter_y, chains=None): self._plot_contour(ax, chain, parameter_y, parameter_x) def _plot_contour(self, ax, chain, px, py, color_extents=None): # pragma: no cover - levels = self._get_levels() cloud = chain.config["cloud"] colour = chain.config["color"] @@ -1183,7 +1237,16 @@ def _plot_contour(self, ax, chain, px, py, color_extents=None): # pragma: no co if shade and shade_alpha > 0: ax.contourf(x_centers, y_centers, vals, levels=levels, colors=colours, alpha=shade_alpha, zorder=zorder) - con = ax.contour(x_centers, y_centers, vals, levels=levels, colors=colours2, linestyles=linestyle, linewidths=linewidth, zorder=zorder) + con = ax.contour( + x_centers, + y_centers, + vals, + levels=levels, + colors=colours2, + linestyles=linestyle, + linewidths=linewidth, + zorder=zorder, + ) if contour_labels is not None: lvls = [l for l in con.levels if l != 0.0] @@ -1207,7 +1270,6 @@ def _add_truth(self, ax, truth, px, py=None): # pragma: no cover ax.axvline(truth_value, **self.parent.config_truth) def _plot_bars(self, ax, parameter, chain, flip=False, summary=False): # pragma: no cover - # Get values from config colour = chain.config["color"] linestyle = chain.config["linestyle"] @@ -1227,20 +1289,27 @@ def _plot_bars(self, ax, parameter, chain, flip=False, summary=False): # pragma else: ax.plot(xs, ys, color=colour, ls=linestyle, lw=linewidth, zorder=zorder) else: - if flip: - orientation = "horizontal" - else: - orientation = "vertical" + orientation = "horizontal" if flip else "vertical" if chain.grid: bins = get_grid_bins(chain_row) else: bins, smooth = get_smoothed_bins(smooth, bins, chain_row, weights) hist, edges = np.histogram(chain_row, bins=bins, density=True, weights=weights) if chain.power is not None: - hist = hist ** chain.power + hist = hist**chain.power edge_center = 0.5 * (edges[:-1] + edges[1:]) xs, ys = edge_center, hist - ax.hist(xs, weights=ys, bins=bins, histtype="step", color=colour, orientation=orientation, ls=linestyle, lw=linewidth, zorder=zorder) + ax.hist( + xs, + weights=ys, + bins=bins, + histtype="step", + color=colour, + orientation=orientation, + ls=linestyle, + lw=linewidth, + zorder=zorder, + ) interp_type = "linear" if smooth else "nearest" interpolator = interp1d(xs, ys, kind=interp_type) @@ -1262,7 +1331,7 @@ def _plot_bars(self, ax, parameter, chain, flip=False, summary=False): # pragma if summary: t = self.parent.analysis.get_parameter_text(*fit_values) if isinstance(parameter, str): - ax.set_title(r"$%s = %s$" % (parameter.strip("$"), t), fontsize=title_size) + ax.set_title(r"${} = {}$".format(parameter.strip("$"), t), fontsize=title_size) else: ax.set_title(r"$%s$" % t, fontsize=title_size) return ys.max() @@ -1279,17 +1348,16 @@ def _plot_point_histogram(self, ax, chains_groups, parameter, flip=False): # pr hist, bin_edges = np.histogram(xs, bins=num_bins, density=True) if hist.max() > max_val: max_val = hist.max() - if flip: - orientation = "horizontal" - else: - orientation = "vertical" + orientation = "horizontal" if flip else "vertical" bin_center = 0.5 * (bin_edges[:-1] + bin_edges[1:]) xs, ys = bin_center, hist ax.hist(xs, weights=ys, bins=bin_edges, histtype="step", color=colour, orientation=orientation) return max_val - def _plot_walk(self, ax, parameter, data, truth=None, extents=None, convolve=None, color=None, log_scale=False): # pragma: no cover + def _plot_walk( + self, ax, parameter, data, truth=None, extents=None, convolve=None, color=None, log_scale=False + ): # pragma: no cover if extents is not None: ax.set_ylim(extents) assert convolve is None or isinstance(convolve, int), "Convolve must be an integer pixel window width" @@ -1354,7 +1422,7 @@ def _get_smoothed_histogram2d(self, chain, param1, param2): # pragma: no cover hist, x_bins, y_bins = np.histogram2d(x, y, bins=[binsx, binsy], weights=w) if chain.power is not None: - hist = hist ** chain.power + hist = hist**chain.power x_centers = 0.5 * (x_bins[:-1] + x_bins[1:]) y_centers = 0.5 * (y_bins[:-1] + y_bins[1:]) @@ -1368,7 +1436,7 @@ def _get_smoothed_histogram2d(self, chain, param1, param2): # pragma: no cover data = np.vstack((x, y)).T hist = MegKDE(data, w, kde).evaluate(coords).reshape((nn, nn)) if chain.power is not None: - hist = hist ** chain.power + hist = hist**chain.power elif smooth: hist = gaussian_filter(hist, smooth, mode=self.parent._gauss_mode) diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 33e7bae7..445728e8 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -2,14 +2,14 @@ import tempfile import numpy as np -from scipy.interpolate import interp1d -from scipy.stats import skewnorm, norm import pytest +from scipy.interpolate import interp1d +from scipy.stats import norm, skewnorm from chainconsumer import ChainConsumer -class TestChain(object): +class TestChain: np.random.seed(1) n = 2000000 data = np.random.normal(loc=5.0, scale=1.5, size=n) @@ -23,7 +23,7 @@ def test_summary(self): consumer.add_chain(self.data[::10]) consumer.configure(kde=True) summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -34,7 +34,7 @@ def test_summary_no_smooth(self): consumer.add_chain(self.data) consumer.configure(smooth=0, bins=2.4) summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -75,7 +75,7 @@ def test_summary1(self): consumer.add_chain(self.data) consumer.configure(bins=0.8) summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -86,7 +86,7 @@ def test_summary_specific(self): consumer.add_chain(self.data, name="A") consumer.configure(bins=0.8) summary = consumer.analysis.get_summary(chains="A") - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -111,7 +111,7 @@ def test_summary_power(self): data = np.random.normal(loc=0, scale=np.sqrt(2), size=1000000) consumer.add_chain(data, power=2.0) summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([-1.0, 0.0, 1.0]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -269,7 +269,7 @@ def test_file_loading1(self): consumer = ChainConsumer() consumer.add_chain(filename) summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) assert np.abs(actual[1] - 5.0) < 0.5 def test_file_loading2(self): @@ -281,7 +281,7 @@ def test_file_loading2(self): consumer = ChainConsumer() consumer.add_chain(filename) summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) assert np.abs(actual[1] - 5.0) < 0.5 def test_using_list(self): @@ -289,7 +289,7 @@ def test_using_list(self): c = ChainConsumer() c.add_chain(data) summary = c.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) assert np.abs(actual[1] - 5.0) < 0.1 def test_using_dict(self): @@ -304,7 +304,7 @@ def test_summary_when_no_parameter_names(self): c = ChainConsumer() c.add_chain(self.data) summary = c.analysis.get_summary() - assert list(summary.keys()) == ['0'] + assert list(summary.keys()) == ["0"] def test_squeeze_squeezes(self): sum = ChainConsumer().add_chain(self.data).analysis.get_summary() @@ -327,18 +327,16 @@ def test_dictionary_and_parameters_fail(self): ChainConsumer().add_chain({"x": self.data}, parameters=["$x$"]) def test_convergence_failure(self): - data = np.concatenate((np.random.normal(loc=0.0, size=10000), - np.random.normal(loc=4.0, size=10000))) + data = np.concatenate((np.random.normal(loc=0.0, size=10000), np.random.normal(loc=4.0, size=10000))) consumer = ChainConsumer() consumer.add_chain(data) summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) assert actual[0] is None and actual[2] is None def test_divide_chains_default(self): np.random.seed(0) - data = np.concatenate((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))) + data = np.concatenate((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))) consumer = ChainConsumer() num_walkers = 2 consumer.add_chain(data, walkers=num_walkers) @@ -347,14 +345,13 @@ def test_divide_chains_default(self): c.configure(bins=0.7) means = [0, 1.0] for i in range(num_walkers): - stats = list(c.analysis.get_summary()[i].values())[0] + stats = next(iter(c.analysis.get_summary()[i].values())) assert np.abs(stats[1] - means[i]) < 1e-1 assert np.abs(c.chains[i].chain[:, 0].mean() - means[i]) < 1e-2 def test_divide_chains_index(self): np.random.seed(0) - data = np.concatenate((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))) + data = np.concatenate((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))) consumer = ChainConsumer() num_walkers = 2 consumer.add_chain(data, walkers=num_walkers) @@ -363,14 +360,13 @@ def test_divide_chains_index(self): c.configure(bins=0.7) means = [0, 1.0] for i in range(num_walkers): - stats = list(c.analysis.get_summary()[i].values())[0] + stats = next(iter(c.analysis.get_summary()[i].values())) assert np.abs(stats[1] - means[i]) < 1e-1 assert np.abs(c.chains[i].chain[:, 0].mean() - means[i]) < 1e-2 def test_divide_chains_name(self): np.random.seed(0) - data = np.concatenate((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))) + data = np.concatenate((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))) consumer = ChainConsumer() num_walkers = 2 consumer.add_chain(data, walkers=num_walkers, name="test") @@ -378,14 +374,13 @@ def test_divide_chains_name(self): c.configure(bins=0.7) means = [0, 1.0] for i in range(num_walkers): - stats = list(c.analysis.get_summary()[i].values())[0] + stats = next(iter(c.analysis.get_summary()[i].values())) assert np.abs(stats[1] - means[i]) < 1e-1 assert np.abs(c.chains[i].chain[:, 0].mean() - means[i]) < 1e-2 def test_divide_chains_fail(self): np.random.seed(0) - data = np.concatenate((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))) + data = np.concatenate((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))) consumer = ChainConsumer() consumer.add_chain(data, walkers=2) with pytest.raises(ValueError): @@ -393,12 +388,11 @@ def test_divide_chains_fail(self): def test_divide_chains_name_fail(self): np.random.seed(0) - data = np.concatenate((np.random.normal(loc=0.0, size=200000), - np.random.normal(loc=1.0, size=200000))) + data = np.concatenate((np.random.normal(loc=0.0, size=200000), np.random.normal(loc=1.0, size=200000))) consumer = ChainConsumer() consumer.add_chain(data, walkers=2) with pytest.raises(AssertionError): - c = consumer.divide_chain(chain="notexist") + consumer.divide_chain(chain="notexist") def test_stats_max_normal(self): tolerance = 5e-2 @@ -406,7 +400,7 @@ def test_stats_max_normal(self): consumer.add_chain(self.data) consumer.configure(statistics="max") summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -420,7 +414,7 @@ def test_stats_max_cliff(self): consumer.add_chain(data, weights=weights) consumer.configure(statistics="max", bins=4.0, smooth=1) summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([0.0, 1.0, 2.73]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -431,7 +425,7 @@ def test_stats_mean_normal(self): consumer.add_chain(self.data) consumer.configure(statistics="mean") summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -442,7 +436,7 @@ def test_stats_cum_normal(self): consumer.add_chain(self.data) consumer.configure(statistics="cumulative") summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -459,7 +453,7 @@ def test_stats_max_skew(self): consumer.add_chain(self.data_skew) consumer.configure(statistics="max") summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([1.01, 1.55, 2.72]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -470,7 +464,7 @@ def test_stats_mean_skew(self): consumer.add_chain(self.data_skew) consumer.configure(statistics="mean") summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([1.27, 2.19, 3.11]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -481,7 +475,7 @@ def test_stats_cum_skew(self): consumer.add_chain(self.data_skew) consumer.configure(statistics="cumulative") summary = consumer.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([1.27, 2.01, 3.11]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -493,8 +487,8 @@ def test_stats_list_skew(self): consumer.add_chain(self.data_skew) consumer.configure(statistics=["cumulative", "mean"]) summary = consumer.analysis.get_summary() - actual0 = np.array(list(summary[0].values())[0]) - actual1 = np.array(list(summary[1].values())[0]) + actual0 = np.array(next(iter(summary[0].values()))) + actual1 = np.array(next(iter(summary[1].values()))) expected0 = np.array([1.27, 2.01, 3.11]) expected1 = np.array([1.27, 2.19, 3.11]) diff0 = np.abs(expected0 - actual0) @@ -510,21 +504,21 @@ def test_weights(self): c.add_chain(samples, weights=weights) expected = np.array([-1.0, 0.0, 1.0]) summary = c.analysis.get_summary() - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) diff = np.abs(expected - actual) assert np.all(diff < tolerance) def test_grid_data(self): x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) - xx, yy = np.meshgrid(x, y, indexing='ij') + xx, yy = np.meshgrid(x, y, indexing="ij") xs, ys = xx.flatten(), yy.flatten() chain = np.vstack((xs, ys)).T pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xs * xs + ys * ys / 4)) c = ChainConsumer() - c.add_chain(chain, parameters=['x', 'y'], weights=pdf, grid=True) + c.add_chain(chain, parameters=["x", "y"], weights=pdf, grid=True) summary = c.analysis.get_summary() - x_sum = summary['x'] - y_sum = summary['y'] + x_sum = summary["x"] + y_sum = summary["y"] expected_x = np.array([-1.0, 0.0, 1.0]) expected_y = np.array([-2.0, 0.0, 2.0]) threshold = 0.1 @@ -533,13 +527,13 @@ def test_grid_data(self): def test_grid_list_input(self): x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) - xx, yy = np.meshgrid(x, y, indexing='ij') + xx, yy = np.meshgrid(x, y, indexing="ij") pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xx * xx + yy * yy / 4)) c = ChainConsumer() - c.add_chain([x, y], parameters=['x', 'y'], weights=pdf, grid=True) + c.add_chain([x, y], parameters=["x", "y"], weights=pdf, grid=True) summary = c.analysis.get_summary() - x_sum = summary['x'] - y_sum = summary['y'] + x_sum = summary["x"] + y_sum = summary["y"] expected_x = np.array([-1.0, 0.0, 1.0]) expected_y = np.array([-2.0, 0.0, 2.0]) threshold = 0.05 @@ -548,21 +542,21 @@ def test_grid_list_input(self): def test_grid_dict_input(self): x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) - xx, yy = np.meshgrid(x, y, indexing='ij') + xx, yy = np.meshgrid(x, y, indexing="ij") pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xx * xx + yy * yy / 4)) c = ChainConsumer() with pytest.raises(AssertionError): - c.add_chain({'x': x, 'y': y}, weights=pdf, grid=True) + c.add_chain({"x": x, "y": y}, weights=pdf, grid=True) def test_grid_dict_input2(self): x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) - xx, yy = np.meshgrid(x, y, indexing='ij') + xx, yy = np.meshgrid(x, y, indexing="ij") pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xx * xx + yy * yy / 4)) c = ChainConsumer() - c.add_chain({'x': xx.flatten(), 'y': yy.flatten()}, weights=pdf.flatten(), grid=True) + c.add_chain({"x": xx.flatten(), "y": yy.flatten()}, weights=pdf.flatten(), grid=True) summary = c.analysis.get_summary() - x_sum = summary['x'] - y_sum = summary['y'] + x_sum = summary["x"] + y_sum = summary["y"] expected_x = np.array([-1.0, 0.0, 1.0]) expected_y = np.array([-2.0, 0.0, 2.0]) threshold = 0.05 @@ -572,11 +566,11 @@ def test_grid_dict_input2(self): def test_normal_list_input(self): tolerance = 5e-2 consumer = ChainConsumer() - consumer.add_chain([self.data, self.data2], parameters=['x', 'y']) + consumer.add_chain([self.data, self.data2], parameters=["x", "y"]) # consumer.configure(bins=1.6) summary = consumer.analysis.get_summary() - actual1 = summary['x'] - actual2 = summary['y'] + actual1 = summary["x"] + actual2 = summary["y"] expected1 = np.array([3.5, 5.0, 6.5]) expected2 = np.array([2.0, 3.0, 4.0]) diff1 = np.abs(expected1 - actual1) @@ -586,10 +580,10 @@ def test_normal_list_input(self): def test_grid_3d(self): x, y, z = np.linspace(-3, 3, 30), np.linspace(-3, 3, 30), np.linspace(-3, 3, 30) - xx, yy, zz = np.meshgrid(x, y, z, indexing='ij') + xx, yy, zz = np.meshgrid(x, y, z, indexing="ij") pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xx * xx + yy * yy + zz * zz)) c = ChainConsumer() - c.add_chain([x, y, z], parameters=['x', 'y', 'z'], weights=pdf, grid=True) + c.add_chain([x, y, z], parameters=["x", "y", "z"], weights=pdf, grid=True) summary = c.analysis.get_summary() expected = np.array([-1.0, 0.0, 1.0]) for k in summary: @@ -770,7 +764,7 @@ def test_1d_levels(self): def test_summary_area(self): c = ChainConsumer() c.add_chain(self.data) - summary = c.analysis.get_summary()['0'] + summary = c.analysis.get_summary()["0"] expected = [3.5, 5, 6.5] assert np.all(np.isclose(summary, expected, atol=0.1)) @@ -778,7 +772,7 @@ def test_summary_area_default(self): c = ChainConsumer() c.add_chain(self.data) c.configure(summary_area=0.6827) - summary = c.analysis.get_summary()['0'] + summary = c.analysis.get_summary()["0"] expected = [3.5, 5, 6.5] assert np.all(np.isclose(summary, expected, atol=0.1)) @@ -786,7 +780,7 @@ def test_summary_area_95(self): c = ChainConsumer() c.add_chain(self.data) c.configure(summary_area=0.95) - summary = c.analysis.get_summary()['0'] + summary = c.analysis.get_summary()["0"] expected = [2, 5, 8] assert np.all(np.isclose(summary, expected, atol=0.1)) @@ -794,7 +788,7 @@ def test_summary_max_symmetric_1(self): c = ChainConsumer() c.add_chain(self.data) c.configure(statistics="max_symmetric") - summary = c.analysis.get_summary()['0'] + summary = c.analysis.get_summary()["0"] expected = [3.5, 5, 6.5] assert np.all(np.isclose(summary, expected, atol=0.1)) assert np.isclose(summary[2] - summary[1], summary[1] - summary[0]) @@ -804,7 +798,7 @@ def test_summary_max_symmetric_2(self): c.add_chain(self.data_skew) summary_area = 0.6827 c.configure(statistics="max_symmetric", bins=1.0, summary_area=summary_area) - summary = c.analysis.get_summary()['0'] + summary = c.analysis.get_summary()["0"] xs = np.linspace(0, 2, 1000) pdf = skewnorm.pdf(xs, 5, 1, 1.5) @@ -822,7 +816,7 @@ def test_summary_max_symmetric_3(self): c.add_chain(self.data_skew) summary_area = 0.95 c.configure(statistics="max_symmetric", bins=1.0, summary_area=summary_area) - summary = c.analysis.get_summary()['0'] + summary = c.analysis.get_summary()["0"] xs = np.linspace(0, 2, 1000) pdf = skewnorm.pdf(xs, 5, 1, 1.5) @@ -839,7 +833,7 @@ def test_summary_max_shortest_1(self): c = ChainConsumer() c.add_chain(self.data) c.configure(statistics="max_shortest") - summary = c.analysis.get_summary()['0'] + summary = c.analysis.get_summary()["0"] expected = [3.5, 5, 6.5] assert np.all(np.isclose(summary, expected, atol=0.1)) @@ -848,7 +842,7 @@ def test_summary_max_shortest_2(self): c.add_chain(self.data_skew) summary_area = 0.6827 c.configure(statistics="max_shortest", bins=1.0, summary_area=summary_area) - summary = c.analysis.get_summary()['0'] + summary = c.analysis.get_summary()["0"] xs = np.linspace(-1, 5, 1000) pdf = skewnorm.pdf(xs, 5, 1, 1.5) @@ -869,7 +863,7 @@ def test_summary_max_shortest_3(self): c.add_chain(self.data_skew) summary_area = 0.95 c.configure(statistics="max_shortest", bins=1.0, summary_area=summary_area) - summary = c.analysis.get_summary()['0'] + summary = c.analysis.get_summary()["0"] xs = np.linspace(-1, 5, 1000) pdf = skewnorm.pdf(xs, 5, 1, 1.5) @@ -889,7 +883,7 @@ def test_summary_max_central_1(self): c = ChainConsumer() c.add_chain(self.data) c.configure(statistics="max_central") - summary = c.analysis.get_summary()['0'] + summary = c.analysis.get_summary()["0"] expected = [3.5, 5, 6.5] assert np.all(np.isclose(summary, expected, atol=0.1)) @@ -898,7 +892,7 @@ def test_summary_max_central_2(self): c.add_chain(self.data_skew) summary_area = 0.6827 c.configure(statistics="max_central", bins=1.0, summary_area=summary_area) - summary = c.analysis.get_summary()['0'] + summary = c.analysis.get_summary()["0"] xs = np.linspace(-1, 5, 1000) pdf = skewnorm.pdf(xs, 5, 1, 1.5) @@ -915,7 +909,7 @@ def test_summary_max_central_3(self): c.add_chain(self.data_skew) summary_area = 0.95 c.configure(statistics="max_central", bins=1.0, summary_area=summary_area) - summary = c.analysis.get_summary()['0'] + summary = c.analysis.get_summary()["0"] xs = np.linspace(-1, 5, 1000) pdf = skewnorm.pdf(xs, 5, 1, 1.5) @@ -980,5 +974,4 @@ def test_max_likelihood_5_failure(self): data[:, 1] += 2 c.add_chain(data, parameters=["x", "y"], name="A") result = c.analysis.get_max_posteriors(parameters="x", chains="A") - print(result) assert result is None diff --git a/tests/test_chain.py b/tests/test_chain.py index 227de83d..c2ddf83d 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -1,15 +1,14 @@ import numpy as np import pandas as pd -from scipy.stats import norm -from numpy.random import normal import pytest -import sys -sys.path.append("..") +from numpy.random import normal +from scipy.stats import norm + from chainconsumer.chain import Chain from chainconsumer.chainconsumer import ChainConsumer -class TestChain(object): +class TestChain: d = normal(size=(100, 3)) d2 = normal(size=(1000000, 3)) bad = d.copy() @@ -323,10 +322,3 @@ def test_pass_in_dataframe3(self): assert np.isclose(summary1["a"][0], -1, atol=0.03) assert np.isclose(summary1["a"][1], 0, atol=0.05) assert np.isclose(summary1["a"][2], 1, atol=0.03) - -if __name__ == "__main__": - import sys - sys.path.append("..") - c = TestChain() - - c.test_pass_in_dataframe2() \ No newline at end of file diff --git a/tests/test_chainconsumer.py b/tests/test_chainconsumer.py index d55bc985..ada9edec 100644 --- a/tests/test_chainconsumer.py +++ b/tests/test_chainconsumer.py @@ -5,7 +5,7 @@ from chainconsumer import ChainConsumer -class TestChainConsumer(object): +class TestChainConsumer: np.random.seed(1) n = 2000000 data = np.random.normal(loc=5.0, scale=1.5, size=n) @@ -62,7 +62,7 @@ def test_remove_last_chain(self): consumer.configure() summary = consumer.analysis.get_summary() assert isinstance(summary, dict) - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -76,7 +76,7 @@ def test_remove_first_chain(self): consumer.configure() summary = consumer.analysis.get_summary() assert isinstance(summary, dict) - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -90,7 +90,7 @@ def test_remove_chain_by_name(self): consumer.configure() summary = consumer.analysis.get_summary() assert isinstance(summary, dict) - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -106,7 +106,7 @@ def test_remove_chain_recompute_params(self): assert isinstance(summary, dict) assert "p2" in summary assert "p1" not in summary - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -124,7 +124,7 @@ def test_remove_multiple_chains(self): assert "p2" in summary assert "p1" not in summary assert "p3" not in summary - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -142,7 +142,7 @@ def test_remove_multiple_chains2(self): assert "p2" in summary assert "p1" not in summary assert "p3" not in summary - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -160,14 +160,14 @@ def test_remove_multiple_chains3(self): assert "p2" in summary assert "p1" not in summary assert "p3" not in summary - actual = np.array(list(summary.values())[0]) + actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) def test_remove_multiple_chains_fails(self): with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data).remove_chain(chain=[0,0]) + ChainConsumer().add_chain(self.data).remove_chain(chain=[0, 0]) def test_shade_alpha_algorithm1(self): consumer = ChainConsumer() diff --git a/tests/test_comparisons.py b/tests/test_comparisons.py index 695c34bc..4bcf72e3 100644 --- a/tests/test_comparisons.py +++ b/tests/test_comparisons.py @@ -206,4 +206,4 @@ def test_dic_posterior_dependence(): assert bics[1] == 0 dic1 = 2 * np.mean(-2 * p) + 2 * norm.logpdf(0) dic2 = 2 * np.mean(-2 * p2) + 2 * norm.logpdf(0, scale=2) - assert np.isclose(bics[0], dic1 - dic2, atol=1e-3) \ No newline at end of file + assert np.isclose(bics[0], dic1 - dic2, atol=1e-3) diff --git a/tests/test_diagnostic.py b/tests/test_diagnostic.py index b253a1ad..529a62b7 100644 --- a/tests/test_diagnostic.py +++ b/tests/test_diagnostic.py @@ -5,16 +5,14 @@ def test_gelman_rubin_index(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4) assert consumer.diagnostic.gelman_rubin(chain=0) def test_gelman_rubin_index_not_converged(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T data[80000:, :] *= 2 data[80000:, :] += 1 consumer = ChainConsumer() @@ -24,8 +22,7 @@ def test_gelman_rubin_index_not_converged(): def test_gelman_rubin_index_not_converged(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T data[:, 0] += np.linspace(0, 10, 100000) consumer = ChainConsumer() @@ -34,8 +31,7 @@ def test_gelman_rubin_index_not_converged(): def test_gelman_rubin_index_fails(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4) with pytest.raises(AssertionError): @@ -43,16 +39,14 @@ def test_gelman_rubin_index_fails(): def test_gelman_rubin_name(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4, name="testchain") assert consumer.diagnostic.gelman_rubin(chain="testchain") def test_gelman_rubin_name_fails(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4, name="testchain") with pytest.raises(AssertionError): @@ -60,8 +54,7 @@ def test_gelman_rubin_name_fails(): def test_gelman_rubin_unknown_fails(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4, name="testchain") with pytest.raises(ValueError): @@ -69,8 +62,7 @@ def test_gelman_rubin_unknown_fails(): def test_gelman_rubin_default(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4, name="c1") consumer.add_chain(data, walkers=4, name="c2") @@ -79,8 +71,7 @@ def test_gelman_rubin_default(): def test_gelman_rubin_default_not_converge(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4, name="c1") consumer.add_chain(data, walkers=4, name="c2") @@ -91,16 +82,14 @@ def test_gelman_rubin_default_not_converge(): def test_geweke_index(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=20, name="c1") assert consumer.diagnostic.geweke(chain=0) def test_geweke_index_failed(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() data[98000:, :] += 0.5 consumer.add_chain(data, walkers=20, name="c1") @@ -109,8 +98,7 @@ def test_geweke_index_failed(): def test_geweke_default(): np.random.seed(0) - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=20, name="c1") consumer.add_chain(data, walkers=20, name="c2") @@ -118,11 +106,10 @@ def test_geweke_default(): def test_geweke_default_failed(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), - np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=20, name="c1") data2 = data.copy() data2[98000:, :] += 0.3 consumer.add_chain(data2, walkers=20, name="c2") - assert not consumer.diagnostic.geweke() \ No newline at end of file + assert not consumer.diagnostic.geweke() diff --git a/tests/test_kde.py b/tests/test_kde.py index 3dd219fe..cf5df885 100644 --- a/tests/test_kde.py +++ b/tests/test_kde.py @@ -53,9 +53,9 @@ def test_megkde_1d_changing_weights(): def test_megkde_2d_basic(): # Draw from normal, fit KDE, see if sampling from kde's pdf recovers norm np.random.seed(1) - data = np.random.multivariate_normal([0, 1], [[1.0, 0.], [0., 0.75 ** 2]], size=10000) + data = np.random.multivariate_normal([0, 1], [[1.0, 0.0], [0.0, 0.75**2]], size=10000) xs, ys = np.linspace(-4, 4, 50), np.linspace(-4, 4, 50) - xx, yy = np.meshgrid(xs, ys, indexing='ij') + xx, yy = np.meshgrid(xs, ys, indexing="ij") samps = np.vstack((xx.flatten(), yy.flatten())).T zs = MegKDE(data).evaluate(samps).reshape(xx.shape) zs_x = zs.sum(axis=1) diff --git a/tests/test_plotter.py b/tests/test_plotter.py index eab925f8..4edeb05b 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -4,7 +4,7 @@ from chainconsumer import ChainConsumer -class TestChain(object): +class TestChain: np.random.seed(1) n = 2000000 data = np.random.normal(loc=5.0, scale=1.5, size=n) @@ -47,12 +47,12 @@ def test_plotter_extents4(self): def test_plotter_extents5(self): x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) - xx, yy = np.meshgrid(x, y, indexing='ij') + xx, yy = np.meshgrid(x, y, indexing="ij") xs, ys = xx.flatten(), yy.flatten() chain = np.vstack((xs, ys)).T pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xs * xs + ys * ys / 4)) c = ChainConsumer() - c.add_chain(chain, parameters=['x', 'y'], weights=pdf, grid=True) + c.add_chain(chain, parameters=["x", "y"], weights=pdf, grid=True) c.configure() minv, maxv = c.plotter._get_parameter_extents("x", c.chains) assert np.isclose(minv, -3, atol=0.001) @@ -64,10 +64,9 @@ def test_plotter_extents6(self): data = np.random.normal(loc=0, size=1000) posterior = norm.logpdf(data) data += mid - c.add_chain(data, parameters=['x'], posterior=posterior, plot_point=True, plot_contour=False) + c.add_chain(data, parameters=["x"], posterior=posterior, plot_point=True, plot_contour=False) c.configure() minv, maxv = c.plotter._get_parameter_extents("x", c.chains) assert np.isclose(minv, -1, atol=0.01) assert np.isclose(maxv, 1, atol=0.01) - From c59e4fcbb8d3050efac796597101ac70f2c160f9 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Wed, 4 Oct 2023 21:51:38 +1000 Subject: [PATCH 02/22] Starting pydantic conversion --- poetry.lock | 150 +++++++++- pyproject.toml | 1 + src/chainconsumer/analysis.py | 10 + src/chainconsumer/base.py | 5 + src/chainconsumer/chain.py | 447 ++++++++++------------------- src/chainconsumer/chainconsumer.py | 61 ---- src/chainconsumer/colors.py | 42 +-- 7 files changed, 342 insertions(+), 374 deletions(-) create mode 100644 src/chainconsumer/base.py diff --git a/poetry.lock b/poetry.lock index 8ffa1ee4..b9834240 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +[[package]] +name = "annotated-types" +version = "0.5.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.7" +files = [ + {file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"}, + {file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"}, +] + [[package]] name = "black" version = "23.9.1" @@ -799,6 +810,143 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "pydantic" +version = "2.4.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"}, + {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.10.1" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.10.1" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"}, + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"}, + {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"}, + {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"}, + {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"}, + {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"}, + {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"}, + {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"}, + {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"}, + {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f"}, + {file = "pydantic_core-2.10.1-cp37-none-win32.whl", hash = "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c"}, + {file = "pydantic_core-2.10.1-cp37-none-win_amd64.whl", hash = "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"}, + {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"}, + {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"}, + {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"}, + {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"}, + {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pyparsing" version = "3.1.1" @@ -1127,4 +1275,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "113c079051fa0ea1674fa95d22d095f96129c500bead8b4fad276ef2c4986e20" +content-hash = "a2d3b9079e6395cf540ab0c4efa45b7e6e6022f4f724f95d5ec62ea657b57819" diff --git a/pyproject.toml b/pyproject.toml index c2faca6d..f394ad2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ matplotlib = "^3.8.0" statsmodels = "^0.14.0" pandas = "^2.1.1" pillow = "^10.0.1" +pydantic = "^2.4.2" [tool.poetry.group.test.dependencies] diff --git a/src/chainconsumer/analysis.py b/src/chainconsumer/analysis.py index da880d31..071fff5d 100644 --- a/src/chainconsumer/analysis.py +++ b/src/chainconsumer/analysis.py @@ -1,4 +1,5 @@ import logging +from enum import Enum import numpy as np from scipy.integrate import simps @@ -9,6 +10,15 @@ from .kde import MegKDE +class SummaryStatistic(Enum): + MAX = "max" + MEAN = "mean" + CUMULATIVE = "cumulative" + MAX_SYMMETRIC = "max_symmetric" + MAX_SHORTEST = "max_shortest" + MAX_CENTRAL = "max_central" + + class Analysis: summaries = ["max", "mean", "cumulative", "max_symmetric", "max_shortest", "max_central"] diff --git a/src/chainconsumer/base.py b/src/chainconsumer/base.py new file mode 100644 index 00000000..0f0233ed --- /dev/null +++ b/src/chainconsumer/base.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel, ConfigDict + + +class BetterBase(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/src/chainconsumer/chain.py b/src/chainconsumer/chain.py index 1ca5ad83..2b9b9623 100644 --- a/src/chainconsumer/chain.py +++ b/src/chainconsumer/chain.py @@ -1,302 +1,163 @@ import logging import numpy as np - -from .analysis import Analysis -from .colors import Colors - - -class Chain: - colors = Colors() # Static colors object to do color mapping - - def __init__( - self, - chain, - parameters, - name, - weights=None, - posterior=None, - walkers=None, - grid=False, - num_free_params=None, - num_eff_data_points=None, - power=None, - statistics="max", - color=None, - linestyle=None, - linewidth=None, - cloud=None, - shade=None, - shade_alpha=None, - shade_gradient=None, - bar_shade=None, - bins=None, - kde=None, - smooth=None, - color_params=None, - plot_color_params=None, - cmap=None, - num_cloud=None, - plot_contour=True, - plot_point=False, - show_as_1d_prior=False, - marker_style=None, - marker_size=None, - marker_alpha=None, - zorder=None, - shift_params=None, - ): - self.chain = chain - self.parameters = parameters - self.name = name - self.mcmc_chain = True - - self.posterior_max_index = None - self.posterior_max_params = {} - - if weights is None: - weights = np.ones(chain.shape[0]) - weights = weights.squeeze() - - if posterior is not None: - posterior = posterior.squeeze() - self.posterior_max_index = np.argmax(posterior) - for i, p in enumerate(parameters): - self.posterior_max_params[p] = chain[self.posterior_max_index, i] - - self.shift_params = shift_params - if shift_params is not None: - for key in shift_params: - try: - index = self.parameters.index(key) - avg = np.average(chain[:, index], weights=weights) - chain[:, index] += shift_params[key] - avg - except ValueError: - continue - self.weights = weights - self.posterior = posterior - self.walkers = walkers - self.grid = grid - self.num_free_params = num_free_params - self.num_eff_data_points = num_eff_data_points - self.power = power - - self._logger = logging.getLevelName(self.__class__.__name__) - - # Storing config overrides - self.color = color - self.linewidth = linewidth - self.linestyle = linestyle - self.kde = kde - self.shade_alpha = shade_alpha - - self.summaries = {} - self.config = {} - - self.configure( - statistics=statistics, - color=color, - linestyle=linestyle, - linewidth=linewidth, - cloud=cloud, - shade=shade, - shade_alpha=shade_alpha, - shade_gradient=shade_gradient, - bar_shade=bar_shade, - bins=bins, - kde=kde, - smooth=smooth, - color_params=color_params, - plot_color_params=plot_color_params, - cmap=cmap, - num_cloud=num_cloud, - plot_contour=plot_contour, - plot_point=plot_point, - show_as_1d_prior=show_as_1d_prior, - marker_style=marker_style, - marker_size=marker_size, - marker_alpha=marker_alpha, - zorder=zorder, +import pandas as pd +from pydantic import ConfigDict, Field, field_validator, model_validator + +from .analysis import SummaryStatistic +from .base import BetterBase +from .colors import ColourInput, colors + + +class Chain(BetterBase): + chain: pd.DataFrame = Field( + default=..., + description="The chain data as a pandas DataFrame", + ) + name: str = Field( + default=..., + description="The name of the chain", + ) + column_labels: dict[str, str] = Field( + default={}, description="A dictionary mapping column names to labels. If not set, will use the column names." + ) + weight_column: str = Field( + default="weights", + description="The name of the weight column, if it exists", + ) + posterior_column: str = Field( + default="posterior", + description="The name of the log posterior column, if it exists", + ) + walkers: int = Field( + default=1, + ge=1, + description="The number of walkers in the chain", + ) + grid: bool = Field( + default=False, + description="Whether the chain is a sampled grid or not", + ) + num_free_params: int | None = Field( + default=None, + description="The number of free parameters in the chain", + ge=0, + ) + num_eff_data_points: float | None = Field( + default=None, + description="The number of effective data points", + ge=0, + ) + power: float = Field( + default=1.0, + description="Raise the posterior surface to this. Useful for inflating or deflating uncertainty for debugging.", + ) + + statistics: SummaryStatistic = Field( + default=SummaryStatistic.MAX, + description="The summary statistic to use", + ) + + color: ColourInput | None = Field(default=None, description="The color of the chain") + linestyle: str | None = Field(default=None, description="The line style of the chain") + linewidth: float | None = Field(default=None, description="The line width of the chain") + cloud: bool | None = Field(default=False, description="Whether to show the cloud of the chain") + shade: bool | None = Field(default=True, description="Whether to shade the chain") + shade_alpha: float | None = Field(default=None, description="The alpha of the shading") + shade_gradient: float | None = Field(default=None, description="The contrast between contour levels") + bar_shade: bool | None = Field(default=None, description="Whether to shade marginalised distributions") + bins: int | float | None = Field(default=None, description="The number of bins to use for histograms") + kde: int | float | bool | None = Field(default=False, description="The bandwidth for KDEs") + smooth: int | float | bool | None = Field(default=3, description="The smoothing for histograms.") + color_params: str | None = Field(default=None, description="The parameter (column) to use for coloring") + plot_color_params: bool | None = Field(default=None, description="Whether to plot the color parameter") + cmap: str | None = Field(default=None, description="The colormap to use for shading") + num_cloud: int | float | None = Field(default=None, description="The number of points in the cloud") + plot_contour: bool | None = Field(default=True, description="Whether to plot contours") + plot_point: bool | None = Field(default=False, description="Whether to plot points") + show_as_1d_prior: bool | None = Field(default=False, description="Whether to show as a 1D prior") + marker_style: str | None = Field(default=None, description="The marker style to use") + marker_size: int | float | None = Field(default=None, description="The marker size to use") + marker_alpha: int | float | None = Field(default=None, description="The marker alpha to use") + zorder: int | None = Field(default=None, description="The zorder to use") + + shift_params: bool = Field( + default=False, + description="Whether to shift the parameters by subtracting each parameters mean", + ) + + model_config = ConfigDict(arbitrary_types_allowed=True) + + @property + def max_posterior_row(self) -> pd.Series | None: + if self.posterior_column not in self.chain.columns: + logging.warning("No posterior column found, cannot find max posterior row") + return None + argmax = self.chain[self.posterior_column].argmax() + return self.chain.loc[argmax] + + @property + def labels(self) -> list[str]: + return [self.column_labels.get(col, col) for col in self.chain.columns] + + @property + def weights(self) -> np.ndarray: + return self.chain[self.weight_column].to_numpy() + + @property + def log_posterior(self) -> np.ndarray | None: + if self.posterior_column not in self.chain.columns: + return None + return self.chain[self.posterior_column].to_numpy() + + @property + def color_data(self) -> np.ndarray | None: + if self.color_params is None: + return None + return self.chain[self.color_params].to_numpy() + + @field_validator("color") + @classmethod + def validate_color(cls, v: str | np.ndarray | list[float] | None) -> str | None: + if v is None: + return None + return colors.format(v) + + @model_validator(mode="after") + def validate_model(self) -> "Chain": + assert not self.chain.empty, "Your chain is empty. This is not ideal." + + # If weights aren't set, add them all as one + if self.weight_column not in self.chain: + self.chain[self.weight_column] = 1.0 + else: + assert np.all(self.weights > 0), "Weights must be positive and non-zero" + assert np.all(np.isfinite(self.weights)), "Weights must be finite" + + # Apply the mean shift if it is set to true + if self.shift_params: + for param in self.chain: + self.chain[param] -= np.average(self.chain[param], weights=self.weights) # type: ignore + + # Check the walkers + assert self.chain.shape[0] % self.walkers == 0, ( + f"Chain {self.name} has {self.chain.shape[0]} steps, " + "which is not divisible by {self.walkers} walkers. This is not good." ) - self.validate_chain() - self.validated_params = set() - def configure( - self, - statistics=None, - color=None, - linestyle=None, - linewidth=None, - cloud=None, - shade=None, - shade_alpha=None, - shade_gradient=None, - bar_shade=None, - bins=None, - kde=None, - smooth=None, - color_params=None, - plot_color_params=None, - cmap=None, - num_cloud=None, - marker_style=None, - marker_size=None, - marker_alpha=None, - plot_contour=True, - plot_point=False, - show_as_1d_prior=False, - zorder=None, - ): - if statistics is not None: - assert isinstance(statistics, str), "statistics should be a string" - assert statistics in list(Analysis.summaries), "statistics {} not recognised. Should be in {}".format( - statistics, - Analysis.summaries, - ) - self.config["statistics"] = statistics + # And the log posterior + if self.log_posterior is not None: + assert np.all(np.isfinite(self.log_posterior)), f"Chain {self.name} has NaN or inf in the log-posterior" - if color is not None: - color = self.colors.format(color) - self.config["color"] = color - - # See I wish I didnt have to do this, but I get too many issues raised when people - # pass in the weirdest stuff and expect it to work. - self._validate_config("linestyle", linestyle, str) - self._validate_config("linewidth", linewidth, int, float) - self._validate_config("cloud", cloud, bool) - self._validate_config("shade", shade, bool) - self._validate_config("shade_alpha", shade_alpha, int, float) - self._validate_config("shade_gradient", shade_gradient, int, float) - self._validate_config("bar_shade", bar_shade, bool) - self._validate_config("bins", bins, int, float) - self._validate_config("kde", kde, int, float, bool) - self._validate_config("smooth", smooth, int, float, bool) - self._validate_config("color_params", color_params, str) - self._validate_config("plot_color_params", plot_color_params, bool) - self._validate_config("cmap", cmap, str) - self._validate_config("num_cloud", num_cloud, int, float) - self._validate_config("marker_style", marker_style, str) - self._validate_config("marker_size", marker_size, int, float) - self._validate_config("marker_alpha", marker_alpha, int, float) - self._validate_config("plot_contour", plot_contour, bool) - self._validate_config("plot_point", plot_point, bool) - self._validate_config("show_as_1d_prior", show_as_1d_prior, bool) - self._validate_config("zorder", zorder, int) - - def update_unset_config(self, name, value, override=None): - if (override is not None and name in override) or self.config.get(name) is None: - self.config[name] = value - - def _validate_config(self, name, value, *types): - if value is not None: - assert isinstance(value, tuple(types)), "{}, which is {}, should be type of: {}".format( - name, - value, - " or ".join([t.__name__ for t in types]), - ) - self.config[name] = value - - def validate_chain(self): - # So many people request help when the pass in junk data without realising it. - # Let's try and flag this as quickly as we can. - # Defensive coding; engage! - - assert isinstance(self.name, str), "Chain name needs to be a string. It is %s" % type(self.name) - assert np.all(np.isfinite(self.weights)), "Chain %s has weights which are NaN or inf!" % self.name - assert len(self.weights.shape) == 1, "Weights should be a 1D array, have instead %s" % str(self.weights.shape) - assert self.weights.size == self.chain.shape[0], "Chain %s has %d steps but %d weights" % ( - self.name, - self.weights.size, - self.chain.shape[0], - ) - assert self.chain.shape[0] > 0, "Chain has shape %s, which means it has 0 steps!" % str(self.chain.shape) - assert np.sum(self.weights) > 0, "Chain weights sum to zero, this is not good" - if self.walkers is not None: - assert int(self.walkers) == self.walkers, "Walkers should be an integer!" + # And if the color_params are set, ensure they're in the dataframe + if self.color_params is not None: assert ( - self.chain.shape[0] % self.walkers == 0 - ), "Chain %s has %d walkers and %d steps... which aren't divisible. They need to be!" % ( - self.name, - self.walkers, - self.chain.shape[0], - ) - assert isinstance(self.grid, bool), f"Chain {self.name} has {type(self.grid)} for grid, should be a bool" - assert self.parameters is not None, "Chain %s has parameter list of None. Please give names" % self.name - assert len(self.parameters) == self.chain.shape[1], "Chain %s has %d parameters but data has %d columns" % ( - self.name, - len(self.parameters), - self.chain.shape[1], - ) - for i, p in enumerate(self.parameters): - assert isinstance(p, str), "Param index %d, which is %s, needs to be a string!" % (i, p) - if self.posterior is not None: - assert len(self.posterior.shape) == 1, "posterior should be a 1D array, have instead %s" % str( - self.posterior.shape - ) - assert self.posterior.size == self.chain.shape[0], "Chain %s has %d steps but %d log-posterior values" % ( - self.name, - self.chain.shape[0], - self.posterior.size, - ) - assert np.all(np.isfinite(self.posterior)), "Chain %s has NaN or inf in the log-posterior" % self.name - if self.num_free_params is not None: - assert isinstance( - self.num_free_params, int | float - ), f"Chain {self.name} has num_free_params which is not an integer, its {type(self.num_free_params)}" - assert np.isfinite(self.num_free_params), "num_free_params is either infinite or NaN" - assert self.num_free_params > 0, "num_free_params must be positive" - if self.num_eff_data_points is not None: - assert isinstance( - self.num_eff_data_points, int | float - ), "Chain {} has num_eff_data_points which is not an a number, its {}".format( - self.name, - type(self.num_eff_data_points), - ) - assert np.isfinite(self.num_eff_data_points), "num_eff_data_points is either infinite or NaN" - assert self.num_eff_data_points > 0, "num_eff_data_points must be positive" - - # def reset_config(self): - # self.config = {} - # self.summaries = {} - # self.validated_params = set() - - def get_summary(self, param, callback): - stat = "{} {}".format(self.config["statistics"], self.config["summary_area"]) - if stat in self.summaries and param in self.summaries[stat]: - return self.summaries[stat][param] - result = callback(self, param) - if stat not in self.summaries: - self.summaries[stat] = {} - self.summaries[stat][param] = result - return result - - def get_color_data(self): - color_param = self.config.get("color_params") - color_data = None - if color_param in self.parameters: - color_data = self.get_data(color_param) - elif color_param == "weights": - color_data = self.weights - elif color_param == "log_weights": - color_data = np.log(self.weights) - elif color_param == "posterior": - color_data = self.posterior - return color_data - - def get_data(self, params): - if not isinstance(params, list): - params = [params] + self.color_params in self.chain.columns + ), f"Chain {self.name} does not have color parameter {self.color_params}" - params = [self.parameters[param] if isinstance(param, int) else param for param in params] - for p in params: - self.validate_parameter(p) - indexes = [self.parameters.index(param) for param in params] - return np.squeeze(self.chain[:, indexes]) + return self - def validate_parameter(self, param): - if param not in self.validated_params: - index = self.parameters.index(param) - data = self.chain[:, index] - msg = "Data for chain %s, parameter %s is being used, but has either NaNs or infs in it!" - assert np.all(np.isfinite(data)), msg % (self.name, param) - self.validated_params.add(param) + def get_data(self, columns: list[str] | str): + if isinstance(columns, str): + columns = [columns] + return self.chain[columns] diff --git a/src/chainconsumer/chainconsumer.py b/src/chainconsumer/chainconsumer.py index 443d7d79..eabacc30 100644 --- a/src/chainconsumer/chainconsumer.py +++ b/src/chainconsumer/chainconsumer.py @@ -1075,64 +1075,3 @@ def _get_chain_name(self, index): def _all_names(self): return [c.name for c in self.chains] - - # Deprecated methods - def plot(self, *args, **kwargs): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.plotter.plot instead") - return self.plotter.plot(*args, **kwargs) - - def plot_walks(self, *args, **kwargs): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.plotter.plot_walks instead") - return self.plotter.plot_walks(*args, **kwargs) - - def get_latex_table(self, *args, **kwargs): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.analysis.get_latex_table instead") - return self.analysis.get_latex_table(*args, **kwargs) - - def get_parameter_text(self, *args, **kwargs): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.analysis.get_parameter_text instead") - return self.analysis.get_parameter_text(*args, **kwargs) - - def get_summary(self, *args, **kwargs): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.analysis.get_summary instead") - return self.analysis.get_summary(*args, **kwargs) - - def get_correlations(self, *args, **kwargs): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.analysis.get_correlations instead") - return self.analysis.get_correlations(*args, **kwargs) - - def get_correlation_table(self, *args, **kwargs): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.analysis.get_correlation_table instead") - return self.analysis.get_correlation_table(*args, **kwargs) - - def get_covariance(self, *args, **kwargs): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.analysis.get_covariance instead") - return self.analysis.get_covariance(*args, **kwargs) - - def get_covariance_table(self, *args, **kwargs): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.analysis.get_covariance_table instead") - return self.analysis.get_covariance_table(*args, **kwargs) - - def diagnostic_gelman_rubin(self, *args, **kwargs): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.diagnostic.gelman_rubin instead") - return self.diagnostic.gelman_rubin(*args, **kwargs) - - def diagnostic_geweke(self, *args, **kwargs): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.diagnostic.geweke instead") - return self.diagnostic.geweke(*args, **kwargs) - - def comparison_aic(self): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.comparison.aic instead") - return self.comparison.aic() - - def comparison_bic(self): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.comparison.bic instead") - return self.comparison.bic() - - def comparison_dic(self): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.comparison.dic instead") - return self.comparison.dic() - - def comparison_table(self, *args, **kwargs): # pragma: no cover - print("This method is deprecated. Please use chainConsumer.comparison.comparison_table instead") - return self.comparison.comparison_table(*args, **kwargs) diff --git a/src/chainconsumer/colors.py b/src/chainconsumer/colors.py index d61b7d28..9b2c096a 100644 --- a/src/chainconsumer/colors.py +++ b/src/chainconsumer/colors.py @@ -1,13 +1,17 @@ +from collections.abc import Iterable + import matplotlib.pyplot as plt import numpy as np from matplotlib.colors import rgb2hex # Colours drawn from material designs colour pallet at https://material.io/guidelines/style/color.html +ColourInput = str | np.ndarray | list[float] + class Colors: def __init__(self): - self.color_map = { + self.color_map: dict[str, str] = { "blue": "#1976D2", "lblue": "#4FC3F7", "red": "#E53935", @@ -23,7 +27,7 @@ def __init__(self): "amber": "#FFB300", "brown": "#795548", } - self.aliases = { + self.aliases: dict[str, str] = { "b": "blue", "r": "red", "g": "green", @@ -38,7 +42,7 @@ def __init__(self): "lg": "lgreen", "lb": "lblue", } - self.default_colors = [ + self.default_colors: tuple[str, ...] = ( "blue", "lgreen", "red", @@ -51,11 +55,11 @@ def __init__(self): "brown", "black", "orange", - ] + ) - def format(self, color): - if isinstance(color, np.ndarray): - color = rgb2hex(color) + def format(self, color: ColourInput) -> str: + if isinstance(color, np.ndarray | list): + color = rgb2hex(color) # type: ignore if color[0] == "#": return color elif color in self.color_map: @@ -66,34 +70,34 @@ def format(self, color): else: raise ValueError("Color %s is not mapped. Please give a hex code" % color) - def get_formatted(self, list_colors): + def get_formatted(self, list_colors: Iterable[ColourInput]) -> list[str]: return [self.format(c) for c in list_colors] - def get_default(self): + def get_default(self) -> list[str]: return self.get_formatted(self.default_colors) - def get_colormap(self, num, cmap_name, scale=0.7): # pragma: no cover + def get_colormap(self, num: int, cmap_name: str, scale: float = 0.7) -> list[str]: # pragma: no cover color_list = self.get_formatted(plt.get_cmap(cmap_name)(np.linspace(0.05, 0.9, num))) scales = scale + (1 - scale) * np.abs(1 - np.linspace(0, 2, num)) scaled = [self.scale_colour(c, s) for c, s in zip(color_list, scales)] return scaled - def scale_colour(self, colour, scalefactor): # pragma: no cover - if isinstance(colour, np.ndarray): - r, g, b = colour[:3] * 255.0 - else: - hexx = colour.strip("#") - if scalefactor < 0 or len(hexx) != 6: - return hexx - r, g, b = int(hexx[:2], 16), int(hexx[2:4], 16), int(hexx[4:], 16) + def scale_colour(self, color: ColourInput, scalefactor: float) -> str: # pragma: no cover + hexx = self.format(color).strip("#") + if scalefactor < 0 or len(hexx) != 6: + return hexx + r, g, b = int(hexx[:2], 16), int(hexx[2:4], 16), int(hexx[4:], 16) r = self._clamp(int(r * scalefactor)) g = self._clamp(int(g * scalefactor)) b = self._clamp(int(b * scalefactor)) return f"#{r:02x}{g:02x}{b:02x}" - def _clamp(self, val, minimum=0, maximum=255): + def _clamp(self, val: float, minimum: int = 0, maximum: int = 255): if val < minimum: return minimum if val > maximum: return maximum return val + + +colors = Colors() From 9ae425aeac624f1a9b7685e824dc0de69b763142 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Wed, 4 Oct 2023 23:20:50 +1000 Subject: [PATCH 03/22] Fixing up ruff errors --- examples/Basics/plot_convergence.py | 2 +- examples/Basics/plot_statistics.py | 2 +- examples/Basics/plot_two_disjoint_chains.py | 2 +- examples/customisations/plot_as_prior.py | 6 +- examples/customisations/plot_blinding.py | 5 +- .../customisations/plot_chain_override.py | 9 ++- examples/customisations/plot_cloud_sigma.py | 7 +-- examples/customisations/plot_colorpoints.py | 7 +-- examples/customisations/plot_colours_shade.py | 15 +++-- .../customisations/plot_confidence_levels.py | 5 +- .../customisations/plot_contour_labels.py | 5 +- examples/customisations/plot_dont_flip.py | 2 +- .../customisations/plot_fewer_parameters.py | 6 +- examples/customisations/plot_font_changes.py | 5 +- examples/customisations/plot_kde_extents.py | 2 +- .../customisations/plot_legend_options.py | 5 +- examples/customisations/plot_linestyles.py | 7 +-- examples/customisations/plot_lists.py | 15 +++-- examples/customisations/plot_no_histograms.py | 7 +-- examples/customisations/plot_one_chain.py | 6 +- examples/customisations/plot_preliminary.py | 7 +-- .../customisations/plot_rainbow_serif_bins.py | 12 ++-- .../customisations/plot_selected_chains.py | 9 ++- .../customisations/plot_shade_gradient.py | 7 +-- examples/customisations/plot_shift.py | 9 ++- examples/customisations/plot_spacing.py | 6 +- examples/customisations/plot_three_chains.py | 10 ++-- examples/customisations/plot_zorder.py | 7 +-- examples/more/plot_colorpoints2.py | 11 ++-- examples/more/plot_divide_chain.py | 5 +- examples/more/plot_many.py | 9 ++- examples/plot_correlations.py | 3 +- examples/plot_covariance.py | 3 +- examples/plot_distributions.py | 7 +-- examples/plot_introduction.py | 4 +- examples/plot_summary.py | 8 +-- examples/plot_table.py | 4 +- examples/plot_walk.py | 6 +- pyproject.toml | 2 +- src/chainconsumer/analysis.py | 20 +++---- src/chainconsumer/plotter.py | 10 ++-- tests/test_analysis.py | 57 +++++++++---------- tests/test_chain.py | 12 ++-- tests/test_chainconsumer.py | 6 +- tests/test_diagnostic.py | 57 ++++++++++--------- tests/test_helpers.py | 18 ++++-- tests/test_kde.py | 32 ++++++----- tests/test_plotter.py | 8 +-- 48 files changed, 231 insertions(+), 238 deletions(-) diff --git a/examples/Basics/plot_convergence.py b/examples/Basics/plot_convergence.py index d9806d15..601bc7ba 100644 --- a/examples/Basics/plot_convergence.py +++ b/examples/Basics/plot_convergence.py @@ -17,7 +17,7 @@ from chainconsumer import ChainConsumer -np.random.seed(0) +rng = np.random.default_rng(0) # Here we have some nice data, and then some bad data, # where the last part of the chain has walked off, and the first part # of the chain isn't agreeing with anything else! diff --git a/examples/Basics/plot_statistics.py b/examples/Basics/plot_statistics.py index 5054b1fb..166f23ab 100644 --- a/examples/Basics/plot_statistics.py +++ b/examples/Basics/plot_statistics.py @@ -19,7 +19,7 @@ from chainconsumer import ChainConsumer # Lets create some data here to set things up -np.random.seed(0) +rng = np.random.default_rng(0) data = skewnorm.rvs(5, size=(1000000, 2)) parameters = ["$x$", "$y$"] diff --git a/examples/Basics/plot_two_disjoint_chains.py b/examples/Basics/plot_two_disjoint_chains.py index 34db859e..971c6baa 100644 --- a/examples/Basics/plot_two_disjoint_chains.py +++ b/examples/Basics/plot_two_disjoint_chains.py @@ -12,7 +12,7 @@ from chainconsumer import ChainConsumer -np.random.seed(0) +rng = np.random.default_rng(0) cov = normal(size=(3, 3)) cov2 = normal(size=(4, 4)) data = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) diff --git a/examples/customisations/plot_as_prior.py b/examples/customisations/plot_as_prior.py index 16314b4a..be54c537 100644 --- a/examples/customisations/plot_as_prior.py +++ b/examples/customisations/plot_as_prior.py @@ -7,14 +7,14 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal, random +from numpy.random import normal, random from chainconsumer import ChainConsumer if __name__ == "__main__": - np.random.seed(0) + rng = np.random.default_rng(0) cov = random(size=(2, 2)) + np.identity(2) - data = multivariate_normal(normal(size=2), np.dot(cov, cov.T), size=100000) + data = rng.multivariate_normal(normal(size=2), np.dot(cov, cov.T), size=100000) prior = normal(0, 1, size=100000) diff --git a/examples/customisations/plot_blinding.py b/examples/customisations/plot_blinding.py index 3accf466..2ddcbe73 100644 --- a/examples/customisations/plot_blinding.py +++ b/examples/customisations/plot_blinding.py @@ -11,12 +11,11 @@ """ import numpy as np -from numpy.random import multivariate_normal from chainconsumer import ChainConsumer -np.random.seed(0) -data = multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) +rng = np.random.default_rng(0) +data = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$"]) c.configure(colors=["g"]) diff --git a/examples/customisations/plot_chain_override.py b/examples/customisations/plot_chain_override.py index 3bac18b2..83fb0e14 100644 --- a/examples/customisations/plot_chain_override.py +++ b/examples/customisations/plot_chain_override.py @@ -14,14 +14,13 @@ """ import numpy as np -from numpy.random import multivariate_normal from chainconsumer import ChainConsumer -np.random.seed(0) -data1 = multivariate_normal([-2, 0], [[1, 0], [0, 1]], size=100000) -data2 = multivariate_normal([4, -4], [[1, 0], [0, 1]], size=100000) -data3 = multivariate_normal([-2, -4], [[1, 0.7], [0.7, 1]], size=100000) +rng = np.random.default_rng(0) +data1 = rng.multivariate_normal([-2, 0], [[1, 0], [0, 1]], size=100000) +data2 = rng.multivariate_normal([4, -4], [[1, 0], [0, 1]], size=100000) +data3 = rng.multivariate_normal([-2, -4], [[1, 0.7], [0.7, 1]], size=100000) c = ChainConsumer() c.add_chain(data1, parameters=["x", "y"], color="red", linestyle=":", name="Red dots") diff --git a/examples/customisations/plot_cloud_sigma.py b/examples/customisations/plot_cloud_sigma.py index 06ab3c31..791c25aa 100644 --- a/examples/customisations/plot_cloud_sigma.py +++ b/examples/customisations/plot_cloud_sigma.py @@ -14,13 +14,12 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal from chainconsumer import ChainConsumer -np.random.seed(1) -cov = normal(size=(3, 3)) -data = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) +rng = np.random.default_rng(1) +cov = rng.normal(size=(3, 3)) +data = rng.multivariate_normal(rng.normal(size=3), np.dot(cov, cov.T), size=100000) c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", "$z$"]) c.configure(summary=False, bins=1.4, cloud=True, sigmas=np.linspace(0, 2, 10)) diff --git a/examples/customisations/plot_colorpoints.py b/examples/customisations/plot_colorpoints.py index 85beb6bc..848b0178 100644 --- a/examples/customisations/plot_colorpoints.py +++ b/examples/customisations/plot_colorpoints.py @@ -15,13 +15,12 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal from chainconsumer import ChainConsumer -np.random.seed(1) -cov = normal(size=(3, 3)) -data = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) +rng = np.random.default_rng(1) +cov = rng.normal(size=(3, 3)) +data = rng.multivariate_normal(rng.normal(size=3), np.dot(cov, cov.T), size=100000) c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", "$z$"]) c.configure(color_params="$z$") diff --git a/examples/customisations/plot_colours_shade.py b/examples/customisations/plot_colours_shade.py index bfe4f4c7..76fa51aa 100644 --- a/examples/customisations/plot_colours_shade.py +++ b/examples/customisations/plot_colours_shade.py @@ -16,17 +16,16 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal from chainconsumer import ChainConsumer -np.random.seed(2) -cov = normal(size=(2, 2)) + np.identity(2) -d1 = multivariate_normal(normal(size=2), np.dot(cov, cov.T), size=100000) -cov = normal(size=(2, 2)) + np.identity(2) -d2 = multivariate_normal(normal(size=2), np.dot(cov, cov.T), size=100000) -cov = normal(size=(2, 2)) + np.identity(2) -d3 = multivariate_normal(normal(size=2), np.dot(cov, cov.T), size=100000) +rng = np.random.default_rng(2) +cov = rng.normal(size=(2, 2)) + np.identity(2) +d1 = rng.multivariate_normal(rng.normal(size=2), np.dot(cov, cov.T), size=100000) +cov = rng.normal(size=(2, 2)) + np.identity(2) +d2 = rng.multivariate_normal(rng.normal(size=2), np.dot(cov, cov.T), size=100000) +cov = rng.normal(size=(2, 2)) + np.identity(2) +d3 = rng.multivariate_normal(rng.normal(size=2), np.dot(cov, cov.T), size=100000) c = ChainConsumer().add_chain(d1, parameters=["$x$", "$y$"]).add_chain(d2).add_chain(d3) c.configure(colors=["#B32222", "#D1D10D", "#455A64"], shade=True, shade_alpha=0.2, bar_shade=True) diff --git a/examples/customisations/plot_confidence_levels.py b/examples/customisations/plot_confidence_levels.py index a908ef0a..49a9fcb9 100644 --- a/examples/customisations/plot_confidence_levels.py +++ b/examples/customisations/plot_confidence_levels.py @@ -15,12 +15,11 @@ """ import numpy as np -from numpy.random import multivariate_normal from chainconsumer import ChainConsumer -np.random.seed(0) -data = multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) +rng = np.random.default_rng(0) +data = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$"]) c.configure(flip=False, sigma2d=False, sigmas=[1, 2]) # The default case, so you don't need to specify sigma2d diff --git a/examples/customisations/plot_contour_labels.py b/examples/customisations/plot_contour_labels.py index 14234e77..7cc7a874 100644 --- a/examples/customisations/plot_contour_labels.py +++ b/examples/customisations/plot_contour_labels.py @@ -9,11 +9,12 @@ """ -from numpy.random import multivariate_normal +import numpy as np from chainconsumer import ChainConsumer -data = multivariate_normal([0, 0], [[1, 0.5], [0.5, 1.0]], size=1000000) +rng = np.random.default_rng(0) +data = rng.multivariate_normal([0, 0], [[1, 0.5], [0.5, 1.0]], size=1000000) c = ChainConsumer().add_chain(data).configure(contour_labels="confidence") diff --git a/examples/customisations/plot_dont_flip.py b/examples/customisations/plot_dont_flip.py index 59e646ae..87a3c4f6 100644 --- a/examples/customisations/plot_dont_flip.py +++ b/examples/customisations/plot_dont_flip.py @@ -16,7 +16,7 @@ from chainconsumer import ChainConsumer -np.random.seed(0) +rng = np.random.default_rng(0) data = np.random.multivariate_normal([1.5, 4.0], [[1.0, 0.7], [0.7, 1.5]], size=1000000) data[:, 0] = np.abs(data[:, 0]) diff --git a/examples/customisations/plot_fewer_parameters.py b/examples/customisations/plot_fewer_parameters.py index 2df7f1cb..eff3215d 100644 --- a/examples/customisations/plot_fewer_parameters.py +++ b/examples/customisations/plot_fewer_parameters.py @@ -13,13 +13,13 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal, random +from numpy.random import normal, random from chainconsumer import ChainConsumer -np.random.seed(0) +rng = np.random.default_rng(0) cov = random(size=(6, 6)) -data = multivariate_normal(normal(size=6), np.dot(cov, cov.T), size=200000) +data = rng.multivariate_normal(normal(size=6), np.dot(cov, cov.T), size=200000) parameters = ["$x$", "$y$", "$z$", "$a$", "$b$", "$c$"] c = ChainConsumer().add_chain(data, parameters=parameters).configure(colors="#388E3C") fig = c.plotter.plot(parameters=parameters[:4], figsize="page") diff --git a/examples/customisations/plot_font_changes.py b/examples/customisations/plot_font_changes.py index 0bd234c1..f7ad1922 100644 --- a/examples/customisations/plot_font_changes.py +++ b/examples/customisations/plot_font_changes.py @@ -10,12 +10,11 @@ """ import numpy as np -from numpy.random import multivariate_normal from chainconsumer import ChainConsumer -np.random.seed(0) -data = multivariate_normal([0, 1, 2], np.eye(3) + 0.2, size=100000) +rng = np.random.default_rng(0) +data = rng.multivariate_normal([0, 1, 2], np.eye(3) + 0.2, size=100000) # If you pass in parameter labels and only one chain, you can also get parameter bounds c = ChainConsumer() diff --git a/examples/customisations/plot_kde_extents.py b/examples/customisations/plot_kde_extents.py index effc4575..c9e5778f 100644 --- a/examples/customisations/plot_kde_extents.py +++ b/examples/customisations/plot_kde_extents.py @@ -18,7 +18,7 @@ from chainconsumer import ChainConsumer -np.random.seed(0) +rng = np.random.default_rng(0) data = np.random.multivariate_normal([0.0, 4.0], [[1.0, -0.7], [-0.7, 1.5]], size=3000) c = ChainConsumer() diff --git a/examples/customisations/plot_legend_options.py b/examples/customisations/plot_legend_options.py index a9a4346f..32feaa55 100644 --- a/examples/customisations/plot_legend_options.py +++ b/examples/customisations/plot_legend_options.py @@ -9,12 +9,11 @@ """ import numpy as np -from numpy.random import multivariate_normal from chainconsumer import ChainConsumer -np.random.seed(0) -data1 = multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) +rng = np.random.default_rng(0) +data1 = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) data2 = data1 + 2 c = ChainConsumer() diff --git a/examples/customisations/plot_linestyles.py b/examples/customisations/plot_linestyles.py index bac147c2..2909058a 100644 --- a/examples/customisations/plot_linestyles.py +++ b/examples/customisations/plot_linestyles.py @@ -11,13 +11,12 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal from chainconsumer import ChainConsumer -np.random.seed(1) -cov = normal(size=(3, 3)) -data = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) +rng = np.random.default_rng(1) +cov = rng.normal(size=(3, 3)) +data = rng.multivariate_normal(rng.normal(size=3), np.dot(cov, cov.T), size=100000) data2 = data * 1.1 + 0.5 c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", "$z$"]).add_chain(data2) diff --git a/examples/customisations/plot_lists.py b/examples/customisations/plot_lists.py index 8c7acf6f..399a12b0 100644 --- a/examples/customisations/plot_lists.py +++ b/examples/customisations/plot_lists.py @@ -11,17 +11,16 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal from chainconsumer import ChainConsumer -np.random.seed(2) -cov = normal(size=(2, 2)) + np.identity(2) -d1 = multivariate_normal(normal(size=2), np.dot(cov, cov.T), size=100000) -cov = normal(size=(2, 2)) + np.identity(2) -d2 = multivariate_normal(normal(size=2), np.dot(cov, cov.T), size=100000) -cov = normal(size=(2, 2)) + np.identity(2) -d3 = multivariate_normal(normal(size=2), np.dot(cov, cov.T), size=1000000) +rng = np.random.default_rng(2) +cov = rng.normal(size=(2, 2)) + np.identity(2) +d1 = rng.multivariate_normal(rng.normal(size=2), np.dot(cov, cov.T), size=100000) +cov = rng.normal(size=(2, 2)) + np.identity(2) +d2 = rng.multivariate_normal(rng.normal(size=2), np.dot(cov, cov.T), size=100000) +cov = rng.normal(size=(2, 2)) + np.identity(2) +d3 = rng.multivariate_normal(rng.normal(size=2), np.dot(cov, cov.T), size=1000000) c = ChainConsumer() c.add_chain(d1, parameters=["$x$", "$y$"]) diff --git a/examples/customisations/plot_no_histograms.py b/examples/customisations/plot_no_histograms.py index 265f560d..bb62ba7e 100644 --- a/examples/customisations/plot_no_histograms.py +++ b/examples/customisations/plot_no_histograms.py @@ -9,13 +9,12 @@ import numpy as np -from numpy.random import multivariate_normal, normal, seed from chainconsumer import ChainConsumer -seed(0) -cov = normal(size=(3, 3)) -data = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) +rng = np.random.default_rng(0) +cov = rng.normal(size=(3, 3)) +data = rng.multivariate_normal(rng.normal(size=3), np.dot(cov, cov.T), size=100000) c = ChainConsumer().add_chain(data) c.configure(plot_hists=False) diff --git a/examples/customisations/plot_one_chain.py b/examples/customisations/plot_one_chain.py index 2350a48a..8d1c64d9 100644 --- a/examples/customisations/plot_one_chain.py +++ b/examples/customisations/plot_one_chain.py @@ -11,13 +11,13 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal +from numpy.random import normal from chainconsumer import ChainConsumer -np.random.seed(0) +rng = np.random.default_rng(0) cov = 1e2 * normal(size=(3, 3)) -data = multivariate_normal(1e3 * normal(size=3), np.dot(cov, cov.T), size=100000) +data = rng.multivariate_normal(1e3 * normal(size=3), np.dot(cov, cov.T), size=100000) # If you pass in parameter labels and only one chain, you can also get parameter bounds fig = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", r"$\epsilon$"]).plotter.plot() diff --git a/examples/customisations/plot_preliminary.py b/examples/customisations/plot_preliminary.py index 31575e24..e3b1f296 100644 --- a/examples/customisations/plot_preliminary.py +++ b/examples/customisations/plot_preliminary.py @@ -17,13 +17,12 @@ """ import numpy as np -from numpy.random import multivariate_normal from chainconsumer import ChainConsumer -np.random.seed(0) -data1 = multivariate_normal([3, 5], [[1, 0], [0, 1]], size=1000000) -data2 = multivariate_normal([5, 3], [[1, 0], [0, 1]], size=10000) +rng = np.random.default_rng(0) +data1 = rng.multivariate_normal([3, 5], [[1, 0], [0, 1]], size=1000000) +data2 = rng.multivariate_normal([5, 3], [[1, 0], [0, 1]], size=10000) c = ChainConsumer() diff --git a/examples/customisations/plot_rainbow_serif_bins.py b/examples/customisations/plot_rainbow_serif_bins.py index 19c2e9e6..9935e3a3 100644 --- a/examples/customisations/plot_rainbow_serif_bins.py +++ b/examples/customisations/plot_rainbow_serif_bins.py @@ -18,19 +18,19 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal, random +from numpy.random import normal, random from chainconsumer import ChainConsumer -np.random.seed(0) +rng = np.random.default_rng(0) cov = 0.3 * random(size=(3, 3)) + np.identity(3) -data = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) +data = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) cov = 0.3 * random(size=(3, 3)) + np.identity(3) -data2 = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) +data2 = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) cov = 0.3 * random(size=(3, 3)) + np.identity(3) -data3 = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) +data3 = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) cov = 0.3 * random(size=(3, 3)) + np.identity(3) -data4 = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) +data4 = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) c = ChainConsumer() c.add_chain(data, name="A") diff --git a/examples/customisations/plot_selected_chains.py b/examples/customisations/plot_selected_chains.py index 6c86d008..55b4cda1 100644 --- a/examples/customisations/plot_selected_chains.py +++ b/examples/customisations/plot_selected_chains.py @@ -12,14 +12,13 @@ """ import numpy as np -from numpy.random import multivariate_normal from chainconsumer import ChainConsumer -np.random.seed(0) -data1 = multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) -data2 = multivariate_normal([2, 0], [[1, 0], [0, 1]], size=1000000) -data3 = multivariate_normal([4, 0], [[1, 0], [0, 1]], size=1000000) +rng = np.random.default_rng(0) +data1 = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) +data2 = rng.multivariate_normal([2, 0], [[1, 0], [0, 1]], size=1000000) +data3 = rng.multivariate_normal([4, 0], [[1, 0], [0, 1]], size=1000000) c = ChainConsumer() c.add_chain(data1, parameters=["$x$", "$y$"], name="Chain A") diff --git a/examples/customisations/plot_shade_gradient.py b/examples/customisations/plot_shade_gradient.py index 454084d1..dd956284 100644 --- a/examples/customisations/plot_shade_gradient.py +++ b/examples/customisations/plot_shade_gradient.py @@ -10,13 +10,12 @@ """ import numpy as np -from numpy.random import multivariate_normal from chainconsumer import ChainConsumer -np.random.seed(0) -data1 = multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) -data2 = multivariate_normal([4, -4], [[1, 0], [0, 1]], size=1000000) +rng = np.random.default_rng(0) +data1 = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) +data2 = rng.multivariate_normal([4, -4], [[1, 0], [0, 1]], size=1000000) c = ChainConsumer() c.add_chain(data1, parameters=["$x$", "$y$"]) diff --git a/examples/customisations/plot_shift.py b/examples/customisations/plot_shift.py index a3dfcf6a..d340f809 100644 --- a/examples/customisations/plot_shift.py +++ b/examples/customisations/plot_shift.py @@ -12,14 +12,13 @@ """ import numpy as np -from numpy.random import multivariate_normal from chainconsumer import ChainConsumer -np.random.seed(0) -data1 = multivariate_normal([1, 0], [[3, 2], [2, 3]], size=300000) -data2 = multivariate_normal([0, 0.5], [[1, -0.7], [-0.7, 1]], size=300000) -data3 = multivariate_normal([2, -1], [[0.5, 0], [0, 0.5]], size=300000) +rng = np.random.default_rng(0) +data1 = rng.multivariate_normal([1, 0], [[3, 2], [2, 3]], size=300000) +data2 = rng.multivariate_normal([0, 0.5], [[1, -0.7], [-0.7, 1]], size=300000) +data3 = rng.multivariate_normal([2, -1], [[0.5, 0], [0, 0.5]], size=300000) ############################################################################### # And this is how easy it is to shift them. Note the different means for each dataset! diff --git a/examples/customisations/plot_spacing.py b/examples/customisations/plot_spacing.py index d9462376..189ec812 100644 --- a/examples/customisations/plot_spacing.py +++ b/examples/customisations/plot_spacing.py @@ -8,13 +8,13 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal, random +from numpy.random import normal, random from chainconsumer import ChainConsumer -np.random.seed(0) +rng = np.random.default_rng(0) cov = random(size=(3, 3)) -data = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=200000) +data = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=200000) c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", "$z$"]) c.configure(spacing=0.0) diff --git a/examples/customisations/plot_three_chains.py b/examples/customisations/plot_three_chains.py index 59dc783c..af0a170e 100644 --- a/examples/customisations/plot_three_chains.py +++ b/examples/customisations/plot_three_chains.py @@ -8,18 +8,18 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal, random +from numpy.random import normal, random from chainconsumer import ChainConsumer if __name__ == "__main__": - np.random.seed(0) + rng = np.random.default_rng(0) cov = random(size=(3, 3)) + np.identity(3) - data = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) + data = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) cov = random(size=(3, 3)) + np.identity(3) - data2 = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) + data2 = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) cov = random(size=(3, 3)) + np.identity(3) - data3 = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) + data3 = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) # If the parameters are the same between chains, you can just pass it the # first time, and they will become the default parameters. diff --git a/examples/customisations/plot_zorder.py b/examples/customisations/plot_zorder.py index 1c4a1dc1..a4c0fb99 100644 --- a/examples/customisations/plot_zorder.py +++ b/examples/customisations/plot_zorder.py @@ -11,13 +11,12 @@ """ import numpy as np -from numpy.random import multivariate_normal from chainconsumer import ChainConsumer -np.random.seed(0) -data1 = multivariate_normal([3, 5], [[1, 0], [0, 1]], size=100000) -data2 = multivariate_normal([3, 5], [[0.2, 0.1], [0.1, 0.3]], size=100000) +rng = np.random.default_rng(0) +data1 = rng.multivariate_normal([3, 5], [[1, 0], [0, 1]], size=100000) +data2 = rng.multivariate_normal([3, 5], [[0.2, 0.1], [0.1, 0.3]], size=100000) c = ChainConsumer() diff --git a/examples/more/plot_colorpoints2.py b/examples/more/plot_colorpoints2.py index bc781e84..dc8cd1df 100644 --- a/examples/more/plot_colorpoints2.py +++ b/examples/more/plot_colorpoints2.py @@ -15,15 +15,14 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal from chainconsumer import ChainConsumer -np.random.seed(1) -cov = normal(size=(4, 4)) -data = multivariate_normal(normal(size=4), np.dot(cov, cov.T), size=100000) -cov = 1 + 0.5 * normal(size=(4, 4)) -data2 = multivariate_normal(4 + normal(size=4), np.dot(cov, cov.T), size=100000) +rng = np.random.default_rng(1) +cov = rng.normal(size=(4, 4)) +data = rng.multivariate_normal(rng.normal(size=4), np.dot(cov, cov.T), size=100000) +cov = 1 + 0.5 * rng.normal(size=(4, 4)) +data2 = rng.multivariate_normal(4 + rng.normal(size=4), np.dot(cov, cov.T), size=100000) c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", "$z$", "$g$"], name="a") c.add_chain(data2, parameters=["$x$", "$y$", "$z$", "$t$"], name="b") diff --git a/examples/more/plot_divide_chain.py b/examples/more/plot_divide_chain.py index 073fbc25..5c45a6e8 100644 --- a/examples/more/plot_divide_chain.py +++ b/examples/more/plot_divide_chain.py @@ -18,12 +18,11 @@ """ import numpy as np -from numpy.random import multivariate_normal from chainconsumer import ChainConsumer -np.random.seed(0) -data = multivariate_normal([0.0, 4.0], [[1.0, 0.7], [0.7, 1.5]], size=1000000) +rng = np.random.default_rng(0) +data = rng.multivariate_normal([0.0, 4.0], [[1.0, 0.7], [0.7, 1.5]], size=1000000) data[:, 0] += np.linspace(0, 1, data.shape[0]) c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$"], walkers=5) diff --git a/examples/more/plot_many.py b/examples/more/plot_many.py index 8e0a668f..2e98c41c 100644 --- a/examples/more/plot_many.py +++ b/examples/more/plot_many.py @@ -12,17 +12,16 @@ """ import numpy as np -from numpy.random import multivariate_normal, normal, uniform from chainconsumer import ChainConsumer -np.random.seed(1) +rng = np.random.default_rng(1) n = 1000000 -data = multivariate_normal([0.4, 1], [[0.01, -0.003], [-0.003, 0.001]], size=n) +data = rng.multivariate_normal([0.4, 1], [[0.01, -0.003], [-0.003, 0.001]], size=n) data = np.hstack((data, (67 + 10 * data[:, 0] - data[:, 1] ** 2)[:, None])) -data2 = np.vstack((uniform(-0.1, 1.1, n), normal(1.2, 0.1, n))).T +data2 = np.vstack((rng.uniform(-0.1, 1.1, n), rng.normal(1.2, 0.1, n))).T data2[:, 1] -= data2[:, 0] ** 2 -data3 = multivariate_normal([0.3, 0.7], [[0.02, 0.05], [0.05, 0.1]], size=n) +data3 = rng.multivariate_normal([0.3, 0.7], [[0.02, 0.05], [0.05, 0.1]], size=n) c = ChainConsumer() c.add_chain(data2, parameters=[r"$\Omega_m$", "$-w$"], name="B") diff --git a/examples/plot_correlations.py b/examples/plot_correlations.py index 862dd136..d80a47b2 100644 --- a/examples/plot_correlations.py +++ b/examples/plot_correlations.py @@ -21,8 +21,9 @@ from chainconsumer import ChainConsumer +rng = np.random.default_rng(0) cov = [[1, 0.5, 0.2], [0.5, 1, 0.3], [0.2, 0.3, 1.0]] -data = np.random.multivariate_normal([0, 0, 1], cov, size=100000) +data = rng.multivariate_normal([0, 0, 1], cov, size=100000) parameters = ["x", "y", "z"] c = ChainConsumer() c.add_chain(data, parameters=parameters) diff --git a/examples/plot_covariance.py b/examples/plot_covariance.py index af956ad4..8c05e179 100644 --- a/examples/plot_covariance.py +++ b/examples/plot_covariance.py @@ -21,8 +21,9 @@ from chainconsumer import ChainConsumer +rng = np.random.default_rng(0) cov = [[1.0, 0.5, 0.2], [0.5, 2.0, 0.3], [0.2, 0.3, 3.0]] -data = np.random.multivariate_normal([0, 0, 1], cov, size=1000000) +data = rng.multivariate_normal([0, 0, 1], cov, size=1000000) parameters = ["x", "y", "z"] c = ChainConsumer() c.add_chain(data, parameters=parameters) diff --git a/examples/plot_distributions.py b/examples/plot_distributions.py index 6c8b1267..f26fd456 100644 --- a/examples/plot_distributions.py +++ b/examples/plot_distributions.py @@ -11,13 +11,12 @@ """ import numpy as np -from numpy.random import multivariate_normal, random from chainconsumer import ChainConsumer -np.random.seed(0) -means, cov = np.arange(8), random(size=(8, 8)) -data = multivariate_normal(means, np.dot(cov, cov.T), size=1000000) +rng = np.random.default_rng(0) +means, cov = np.arange(8), rng.random(size=(8, 8)) +data = rng.multivariate_normal(means, np.dot(cov, cov.T), size=1000000) params = ["$x$", "$y$", "$z$", "a", "b", "c", "d", "e"] c = ChainConsumer().add_chain(data, parameters=params) diff --git a/examples/plot_introduction.py b/examples/plot_introduction.py index 9a6dee41..1e9c7ee7 100644 --- a/examples/plot_introduction.py +++ b/examples/plot_introduction.py @@ -23,8 +23,8 @@ from chainconsumer import ChainConsumer -np.random.seed(0) -data = np.random.multivariate_normal([0.0, 4.0], [[1.0, 0.7], [0.7, 1.5]], size=1000000) +rng = np.random.default_rng(0) +data = rng.multivariate_normal([0.0, 4.0], [[1.0, 0.7], [0.7, 1.5]], size=1000000) c = ChainConsumer() c.add_chain(data, parameters=["$x_1$", "$x_2$"]) diff --git a/examples/plot_summary.py b/examples/plot_summary.py index 98161a81..89493231 100644 --- a/examples/plot_summary.py +++ b/examples/plot_summary.py @@ -17,14 +17,14 @@ def get_instance(): - np.random.seed(0) + rng = np.random.default_rng(0) c = ChainConsumer() parameters = ["$x$", r"$\Omega_\epsilon$", "$r^2(x_0)$"] for name in ["Ref. model", "Test A", "Test B", "Test C"]: # Add some random data - mean = np.random.normal(loc=0, scale=3, size=3) - sigma = np.random.uniform(low=1, high=3, size=3) - data = np.random.multivariate_normal(mean=mean, cov=np.diag(sigma**2), size=100000) + mean = rng.normal(loc=0, scale=3, size=3) + sigma = rng.uniform(low=1, high=3, size=3) + data = rng.multivariate_normal(mean=mean, cov=np.diag(sigma**2), size=100000) c.add_chain(data, parameters=parameters, name=name) return c diff --git a/examples/plot_table.py b/examples/plot_table.py index a08ff35e..16ab668a 100644 --- a/examples/plot_table.py +++ b/examples/plot_table.py @@ -21,9 +21,9 @@ from chainconsumer import ChainConsumer ndim, nsamples = 4, 200000 -np.random.seed(0) +rng = np.random.default_rng(0) -data = np.random.randn(nsamples, ndim) +data = rng.random((nsamples, ndim)) data[:, 2] += data[:, 1] * data[:, 2] data[:, 1] = data[:, 1] * 3 + 5 data[:, 3] /= np.abs(data[:, 1]) + 1 diff --git a/examples/plot_walk.py b/examples/plot_walk.py index f0c92e85..df9603c9 100644 --- a/examples/plot_walk.py +++ b/examples/plot_walk.py @@ -24,9 +24,9 @@ from chainconsumer import ChainConsumer -np.random.seed(0) -data1 = np.random.randn(100000, 2) -data2 = np.random.randn(100000, 2) - 2 +rng = np.random.default_rng(0) +data1 = rng.random((100000, 2)) +data2 = rng.random((100000, 2)) - 2 data1[:, 1] += 1 c = ChainConsumer() diff --git a/pyproject.toml b/pyproject.toml index f394ad2e..a32e82a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ target-version = "py310" [tool.ruff.extend-per-file-ignores] "test/***" = ["INP001"] "__init__.py" = ["E402", "F401"] - +"examples/***" = ["T201"] [tool.mypy] check_untyped_defs = true diff --git a/src/chainconsumer/analysis.py b/src/chainconsumer/analysis.py index 071fff5d..aa642518 100644 --- a/src/chainconsumer/analysis.py +++ b/src/chainconsumer/analysis.py @@ -480,35 +480,35 @@ def get_parameter_summary_max(self, chain, parameter): ys = np.concatenate((y_start, ys, y_end)) cs = ys.cumsum() cs = cs / cs.max() - startIndex = ys.argmax() - maxVal = ys[startIndex] - minVal = 0 + start_index = ys.argmax() + max_val = ys[start_index] + min_val = 0 threshold = 0.003 x1 = None x2 = None count = 0 while x1 is None: - mid = (maxVal + minVal) / 2.0 + mid = (max_val + min_val) / 2.0 count += 1 try: if count > 50: raise ValueError("Failed to converge") - i1 = startIndex - np.where(ys[:startIndex][::-1] < mid)[0][0] - i2 = startIndex + np.where(ys[startIndex:] < mid)[0][0] + i1 = start_index - np.where(ys[:start_index][::-1] < mid)[0][0] + i2 = start_index + np.where(ys[start_index:] < mid)[0][0] area = cs[i2] - cs[i1] deviation = np.abs(area - desired_area) if deviation < threshold: x1 = xs[i1] x2 = xs[i2] elif area < desired_area: - maxVal = mid + max_val = mid elif area > desired_area: - minVal = mid + min_val = mid except ValueError: self._logger.warning(f"Parameter {parameter} in chain {chain.name} is not constrained") - return [None, xs[startIndex], None] + return [None, xs[start_index], None] - return [x1, xs[startIndex], x2] + return [x1, xs[start_index], x2] def get_paramater_summary_max_symmetric(self, chain, parameter): xs, ys, cs = self._get_smoothed_histogram(chain, parameter) diff --git a/src/chainconsumer/plotter.py b/src/chainconsumer/plotter.py index c2a67004..fec93364 100644 --- a/src/chainconsumer/plotter.py +++ b/src/chainconsumer/plotter.py @@ -1086,7 +1086,7 @@ def _get_figure( return fig, axes, params1, params2, extents - def _get_parameter_extents(self, parameter, chains, wide_extents=True): + def _get_parameter_extents(self, parameter, chains, wide_extents=True) -> tuple[float, float]: min_val, max_val = None, None for chain in chains: if parameter not in chain.parameters: @@ -1249,12 +1249,14 @@ def _plot_contour(self, ax, chain, px, py, color_extents=None): # pragma: no co ) if contour_labels is not None: - lvls = [l for l in con.levels if l != 0.0] + lvls = [lvl for lvl in con.levels if lvl != 0.0] if contour_labels == "sigma": sigmas = self.parent.config["sigmas"] - fmt = dict([(l, ("$%.1f \\sigma$" % s).replace(".0", "")) for l, s in zip(lvls, sigmas[1:])]) + fmt = dict( + [(lvl, ("$%.1f \\sigma$" % sigma).replace(".0", "")) for lvl, sigma in zip(lvls, sigmas[1:])] + ) else: - fmt = dict([(l, "%d\\%%" % (100 * l)) for l in lvls]) + fmt = dict([(lvl, "%d\\%%" % (100 * lvl)) for lvl in lvls]) ax.clabel(con, lvls, inline=True, fmt=fmt, fontsize=self.parent.config["contour_label_font_size"]) return h diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 445728e8..7ff4bd2d 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -10,10 +10,10 @@ class TestChain: - np.random.seed(1) + rng = np.random.default_rng(1) n = 2000000 - data = np.random.normal(loc=5.0, scale=1.5, size=n) - data2 = np.random.normal(loc=3, scale=1.0, size=n) + data = rng.normal(loc=5.0, scale=1.5, size=n) + data2 = rng.normal(loc=3, scale=1.0, size=n) data_combined = np.vstack((data, data2)).T data_skew = skewnorm.rvs(5, loc=1, scale=1.5, size=n) @@ -108,7 +108,7 @@ def test_summary_disjoint(self): def test_summary_power(self): tolerance = 5e-2 consumer = ChainConsumer() - data = np.random.normal(loc=0, scale=np.sqrt(2), size=1000000) + data = self.rng.normal(loc=0, scale=np.sqrt(2), size=1000000) consumer.add_chain(data, power=2.0) summary = consumer.analysis.get_summary() actual = np.array(next(iter(summary.values()))) @@ -327,7 +327,7 @@ def test_dictionary_and_parameters_fail(self): ChainConsumer().add_chain({"x": self.data}, parameters=["$x$"]) def test_convergence_failure(self): - data = np.concatenate((np.random.normal(loc=0.0, size=10000), np.random.normal(loc=4.0, size=10000))) + data = np.concatenate((self.rng.normal(loc=0.0, size=10000), self.rng.normal(loc=4.0, size=10000))) consumer = ChainConsumer() consumer.add_chain(data) summary = consumer.analysis.get_summary() @@ -335,8 +335,7 @@ def test_convergence_failure(self): assert actual[0] is None and actual[2] is None def test_divide_chains_default(self): - np.random.seed(0) - data = np.concatenate((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))) + data = np.concatenate((self.rng.normal(loc=0.0, size=100000), self.rng.normal(loc=1.0, size=100000))) consumer = ChainConsumer() num_walkers = 2 consumer.add_chain(data, walkers=num_walkers) @@ -350,8 +349,7 @@ def test_divide_chains_default(self): assert np.abs(c.chains[i].chain[:, 0].mean() - means[i]) < 1e-2 def test_divide_chains_index(self): - np.random.seed(0) - data = np.concatenate((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))) + data = np.concatenate((self.rng.normal(loc=0.0, size=100000), self.rng.normal(loc=1.0, size=100000))) consumer = ChainConsumer() num_walkers = 2 consumer.add_chain(data, walkers=num_walkers) @@ -365,8 +363,7 @@ def test_divide_chains_index(self): assert np.abs(c.chains[i].chain[:, 0].mean() - means[i]) < 1e-2 def test_divide_chains_name(self): - np.random.seed(0) - data = np.concatenate((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))) + data = np.concatenate((self.rng.normal(loc=0.0, size=100000), self.rng.normal(loc=1.0, size=100000))) consumer = ChainConsumer() num_walkers = 2 consumer.add_chain(data, walkers=num_walkers, name="test") @@ -379,16 +376,14 @@ def test_divide_chains_name(self): assert np.abs(c.chains[i].chain[:, 0].mean() - means[i]) < 1e-2 def test_divide_chains_fail(self): - np.random.seed(0) - data = np.concatenate((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))) + data = np.concatenate((self.rng.normal(loc=0.0, size=100000), self.rng.normal(loc=1.0, size=100000))) consumer = ChainConsumer() consumer.add_chain(data, walkers=2) with pytest.raises(ValueError): consumer.divide_chain(chain=2.0) def test_divide_chains_name_fail(self): - np.random.seed(0) - data = np.concatenate((np.random.normal(loc=0.0, size=200000), np.random.normal(loc=1.0, size=200000))) + data = np.concatenate((self.rng.normal(loc=0.0, size=200000), self.rng.normal(loc=1.0, size=200000))) consumer = ChainConsumer() consumer.add_chain(data, walkers=2) with pytest.raises(AssertionError): @@ -590,7 +585,7 @@ def test_grid_3d(self): assert np.all(np.abs(summary[k] - expected) < 0.2) def test_correlations_1d(self): - data = np.random.normal(0, 1, size=100000) + data = self.rng.normal(0, 1, size=100000) parameters = ["x"] c = ChainConsumer() c.add_chain(data, parameters=parameters) @@ -600,7 +595,7 @@ def test_correlations_1d(self): assert cor.shape == (1, 1) def test_correlations_2d(self): - data = np.random.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=100000) + data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=100000) parameters = ["x", "y"] c = ChainConsumer() c.add_chain(data, parameters=parameters) @@ -613,7 +608,7 @@ def test_correlations_2d(self): assert cor.shape == (2, 2) def test_correlations_3d(self): - data = np.random.multivariate_normal([0, 0, 1], [[1, 0.5, 0.2], [0.5, 1, 0.3], [0.2, 0.3, 1.0]], size=100000) + data = self.rng.multivariate_normal([0, 0, 1], [[1, 0.5, 0.2], [0.5, 1, 0.3], [0.2, 0.3, 1.0]], size=100000) parameters = ["x", "y", "z"] c = ChainConsumer() c.add_chain(data, parameters=parameters, name="chain1") @@ -630,7 +625,7 @@ def test_correlations_3d(self): assert np.abs(cor[1, 2] - 0.2) < 0.01 def test_correlations_2d_non_unitary(self): - data = np.random.multivariate_normal([0, 0], [[4, 0], [0, 4]], size=100000) + data = self.rng.multivariate_normal([0, 0], [[4, 0], [0, 4]], size=100000) parameters = ["x", "y"] c = ChainConsumer() c.add_chain(data, parameters=parameters) @@ -643,7 +638,7 @@ def test_correlations_2d_non_unitary(self): assert cor.shape == (2, 2) def test_correlation_latex_table(self): - data = np.random.multivariate_normal([0, 0, 1], [[1, 0.5, 0.2], [0.5, 1, 0.3], [0.2, 0.3, 1.0]], size=1000000) + data = self.rng.multivariate_normal([0, 0, 1], [[1, 0.5, 0.2], [0.5, 1, 0.3], [0.2, 0.3, 1.0]], size=1000000) parameters = ["x", "y", "z"] c = ChainConsumer() c.add_chain(data, parameters=parameters) @@ -665,7 +660,7 @@ def test_correlation_latex_table(self): assert latex_table.replace(" ", "") == actual.replace(" ", "") def test_covariance_1d(self): - data = np.random.normal(0, 2, size=2000000) + data = self.rng.normal(0, 2, size=2000000) parameters = ["x"] c = ChainConsumer() c.add_chain(data, parameters=parameters) @@ -675,7 +670,7 @@ def test_covariance_1d(self): assert cor.shape == (1, 1) def test_covariance_2d(self): - data = np.random.multivariate_normal([0, 0], [[3, 0], [0, 9]], size=2000000) + data = self.rng.multivariate_normal([0, 0], [[3, 0], [0, 9]], size=2000000) parameters = ["x", "y"] c = ChainConsumer() c.add_chain(data, parameters=parameters) @@ -689,7 +684,7 @@ def test_covariance_2d(self): def test_covariance_3d(self): cov = [[3, 0.5, 0.2], [0.5, 4, 0.3], [0.2, 0.3, 5]] - data = np.random.multivariate_normal([0, 0, 1], cov, size=2000000) + data = self.rng.multivariate_normal([0, 0, 1], cov, size=2000000) parameters = ["x", "y", "z"] c = ChainConsumer() c.add_chain(data, parameters=parameters, name="chain1") @@ -707,7 +702,7 @@ def test_covariance_3d(self): def test_covariance_latex_table(self): cov = [[2, 0.5, 0.2], [0.5, 3, 0.3], [0.2, 0.3, 4.0]] - data = np.random.multivariate_normal([0, 0, 1], cov, size=20000000) + data = self.rng.multivariate_normal([0, 0, 1], cov, size=20000000) parameters = ["x", "y", "z"] c = ChainConsumer() c.add_chain(data, parameters=parameters) @@ -733,8 +728,8 @@ def test_fail_if_more_parameters_than_data(self): ChainConsumer().add_chain(self.data_combined, parameters=["x", "y", "z"]) def test_covariant_covariance_calc(self): - data1 = np.random.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) - data2 = np.random.multivariate_normal([0, 0], [[2, 1], [1, 2]], size=10000) + data1 = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) + data2 = self.rng.multivariate_normal([0, 0], [[2, 1], [1, 2]], size=10000) weights = np.concatenate((np.ones(10000), np.zeros(10000))) data = np.concatenate((data1, data2)) c = ChainConsumer() @@ -923,7 +918,7 @@ def test_summary_max_central_3(self): def test_max_likelihood_1(self): c = ChainConsumer() - data = np.random.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) + data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) posterior = norm.logpdf(data).sum(axis=1) data[:, 1] += 1 c.add_chain(data, parameters=["x", "y"], posterior=posterior, name="A") @@ -934,7 +929,7 @@ def test_max_likelihood_1(self): def test_max_likelihood_2(self): c = ChainConsumer() - data = np.random.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) + data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) posterior = norm.logpdf(data).sum(axis=1) data[:, 1] += 2 c.add_chain(data, parameters=["x", "y"], posterior=posterior, name="A") @@ -946,7 +941,7 @@ def test_max_likelihood_2(self): def test_max_likelihood_3(self): c = ChainConsumer() - data = np.random.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) + data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) posterior = norm.logpdf(data).sum(axis=1) data[:, 1] += 3 c.add_chain(data, parameters=["x", "y"], posterior=posterior, name="A") @@ -958,7 +953,7 @@ def test_max_likelihood_3(self): def test_max_likelihood_4(self): c = ChainConsumer() - data = np.random.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) + data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) posterior = norm.logpdf(data).sum(axis=1) data[:, 1] += 2 c.add_chain(data, parameters=["x", "y"], posterior=posterior, name="A") @@ -970,7 +965,7 @@ def test_max_likelihood_4(self): def test_max_likelihood_5_failure(self): c = ChainConsumer() - data = np.random.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) + data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) data[:, 1] += 2 c.add_chain(data, parameters=["x", "y"], name="A") result = c.analysis.get_max_posteriors(parameters="x", chains="A") diff --git a/tests/test_chain.py b/tests/test_chain.py index c2ddf83d..ba005101 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -1,7 +1,6 @@ import numpy as np import pandas as pd import pytest -from numpy.random import normal from scipy.stats import norm from chainconsumer.chain import Chain @@ -9,11 +8,12 @@ class TestChain: - d = normal(size=(100, 3)) - d2 = normal(size=(1000000, 3)) + rng = np.random.default_rng(0) + d = rng.normal(size=(100, 3)) + d2 = rng.normal(size=(1000000, 3)) bad = d.copy() bad[0, 0] = np.nan - p = ["a", "b", "c"] + p = ("a", "b", "c") n = "A" w = np.ones(100) w2 = np.ones(1000000) @@ -278,7 +278,7 @@ def test_override_kde_grid(self): def test_cache_invalidation(self): c = ChainConsumer() - c.add_chain(normal(size=(1000000, 1)), parameters=["a"]) + c.add_chain(self.rng.normal(size=(1000000, 1)), parameters=["a"]) c.configure(summary_area=0.68) summary1 = c.analysis.get_summary() c.configure(summary_area=0.95) @@ -312,7 +312,7 @@ def test_pass_in_dataframe2(self): assert np.isclose(summary1["c"][0], -1, atol=0.03) def test_pass_in_dataframe3(self): - data = np.random.uniform(-4, 6, size=(1000000, 1)) + data = self.rng.uniform(-4, 6, size=(1000000, 1)) weight = norm.pdf(data) df = pd.DataFrame(data, columns=["a"]) df["weight"] = weight diff --git a/tests/test_chainconsumer.py b/tests/test_chainconsumer.py index ada9edec..f58446d1 100644 --- a/tests/test_chainconsumer.py +++ b/tests/test_chainconsumer.py @@ -6,10 +6,10 @@ class TestChainConsumer: - np.random.seed(1) + rng = np.random.default_rng(1) n = 2000000 - data = np.random.normal(loc=5.0, scale=1.5, size=n) - data2 = np.random.normal(loc=3, scale=1.0, size=n) + data = rng.normal(loc=5.0, scale=1.5, size=n) + data2 = rng.normal(loc=3, scale=1.0, size=n) data_combined = np.vstack((data, data2)).T data_skew = skewnorm.rvs(5, loc=1, scale=1.5, size=n) diff --git a/tests/test_diagnostic.py b/tests/test_diagnostic.py index 529a62b7..a113d062 100644 --- a/tests/test_diagnostic.py +++ b/tests/test_diagnostic.py @@ -4,15 +4,20 @@ from chainconsumer import ChainConsumer -def test_gelman_rubin_index(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T +@pytest.fixture(scope="session") +def rng(): + return np.random.default_rng() + + +def test_gelman_rubin_index(rng): + data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4) assert consumer.diagnostic.gelman_rubin(chain=0) -def test_gelman_rubin_index_not_converged(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T +def test_gelman_rubin_index_not_converged(rng): + data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T data[80000:, :] *= 2 data[80000:, :] += 1 consumer = ChainConsumer() @@ -21,8 +26,8 @@ def test_gelman_rubin_index_not_converged(): assert not consumer.diagnostic.gelman_rubin(chain=0) -def test_gelman_rubin_index_not_converged(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T +def test_gelman_rubin_index_not_converged2(rng): + data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T data[:, 0] += np.linspace(0, 10, 100000) consumer = ChainConsumer() @@ -30,39 +35,39 @@ def test_gelman_rubin_index_not_converged(): assert not consumer.diagnostic.gelman_rubin(chain=0) -def test_gelman_rubin_index_fails(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T +def test_gelman_rubin_index_fails(rng): + data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4) with pytest.raises(AssertionError): consumer.diagnostic.gelman_rubin(chain=10) -def test_gelman_rubin_name(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T +def test_gelman_rubin_name(rng): + data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4, name="testchain") assert consumer.diagnostic.gelman_rubin(chain="testchain") -def test_gelman_rubin_name_fails(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T +def test_gelman_rubin_name_fails(rng): + data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4, name="testchain") with pytest.raises(AssertionError): consumer.diagnostic.gelman_rubin(chain="testchain2") -def test_gelman_rubin_unknown_fails(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T +def test_gelman_rubin_unknown_fails(rng): + data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4, name="testchain") with pytest.raises(ValueError): consumer.diagnostic.gelman_rubin(chain=np.pi) -def test_gelman_rubin_default(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T +def test_gelman_rubin_default(rng): + data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4, name="c1") consumer.add_chain(data, walkers=4, name="c2") @@ -70,8 +75,8 @@ def test_gelman_rubin_default(): assert consumer.diagnostic.gelman_rubin() -def test_gelman_rubin_default_not_converge(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T +def test_gelman_rubin_default_not_converge(rng): + data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=4, name="c1") consumer.add_chain(data, walkers=4, name="c2") @@ -81,24 +86,24 @@ def test_gelman_rubin_default_not_converge(): assert not consumer.diagnostic.gelman_rubin() -def test_geweke_index(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T +def test_geweke_index(rng): + data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=20, name="c1") assert consumer.diagnostic.geweke(chain=0) -def test_geweke_index_failed(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T +def test_geweke_index_failed(rng): + data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() data[98000:, :] += 0.5 consumer.add_chain(data, walkers=20, name="c1") assert not consumer.diagnostic.geweke(chain=0) -def test_geweke_default(): - np.random.seed(0) - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T +def test_geweke_default(rng): + generator = np.random.default_rng(0) + data = np.vstack((generator.normal(loc=0.0, size=100000), generator.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=20, name="c1") consumer.add_chain(data, walkers=20, name="c2") @@ -106,7 +111,7 @@ def test_geweke_default(): def test_geweke_default_failed(): - data = np.vstack((np.random.normal(loc=0.0, size=100000), np.random.normal(loc=1.0, size=100000))).T + data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() consumer.add_chain(data, walkers=20, name="c1") data2 = data.copy() diff --git a/tests/test_helpers.py b/tests/test_helpers.py index b6a316cd..d967e61f 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,11 +1,17 @@ import numpy as np +import pytest from scipy.stats import norm from chainconsumer.helpers import get_extents -def test_extents(): - xs = np.random.normal(size=1000000) +@pytest.fixture +def rng(): + return np.random.default_rng(0) + + +def test_extents(rng): + xs = rng.normal(size=1000000) weights = np.ones(xs.shape) low, high = get_extents(xs, weights) threshold = 0.5 @@ -13,8 +19,8 @@ def test_extents(): assert np.abs(high - 4) < threshold -def test_extents_weighted(): - xs = np.random.uniform(low=-4, high=4, size=1000000) +def test_extents_weighted(rng): + xs = rng.uniform(low=-4, high=4, size=1000000) weights = norm.pdf(xs) low, high = get_extents(xs, weights) threshold = 0.5 @@ -22,8 +28,8 @@ def test_extents_weighted(): assert np.abs(high - 4) < threshold -def test_extents_summary(): - xs = np.random.normal(size=1000000) +def test_extents_summary(rng): + xs = rng.normal(size=1000000) low, high = get_extents(xs, np.ones(xs.shape), plot=True, wide_extents=False) threshold = 0.1 assert np.abs(low + 1.644855) < threshold diff --git a/tests/test_kde.py b/tests/test_kde.py index cf5df885..74716811 100644 --- a/tests/test_kde.py +++ b/tests/test_kde.py @@ -1,50 +1,53 @@ import numpy as np +import pytest from scipy.interpolate import interp1d from scipy.stats import norm from chainconsumer.kde import MegKDE -def test_megkde_1d_basic(): +@pytest.fixture +def rng(): + return np.random.default_rng(0) + + +def test_megkde_1d_basic(rng): # Draw from normal, fit KDE, see if sampling from kde's pdf recovers norm - np.random.seed(0) - data = np.random.normal(loc=0, scale=1.0, size=2000) + data = rng.normal(loc=0, scale=1.0, size=2000) xs = np.linspace(-3, 3, 100) ys = MegKDE(data).evaluate(xs) cs = ys.cumsum() cs /= cs[-1] cs[0] = 0 - samps = interp1d(cs, xs)(np.random.uniform(size=10000)) + samps = interp1d(cs, xs)(rng.uniform(size=10000)) mu, std = norm.fit(samps) assert np.isclose(mu, 0, atol=0.1) assert np.isclose(std, 1.0, atol=0.1) -def test_megkde_1d_uniform_weight(): +def test_megkde_1d_uniform_weight(rng): # Draw from normal, fit KDE, see if sampling from kde's pdf recovers norm - np.random.seed(0) - data = np.random.normal(loc=0, scale=1.0, size=2000) + data = rng.normal(loc=0, scale=1.0, size=2000) xs = np.linspace(-3, 3, 100) ys = MegKDE(data, weights=np.ones(2000)).evaluate(xs) cs = ys.cumsum() cs /= cs[-1] cs[0] = 0 - samps = interp1d(cs, xs)(np.random.uniform(size=10000)) + samps = interp1d(cs, xs)(rng.uniform(size=10000)) mu, std = norm.fit(samps) assert np.isclose(mu, 0, atol=0.1) assert np.isclose(std, 1.0, atol=0.1) -def test_megkde_1d_changing_weights(): +def test_megkde_1d_changing_weights(rng): # Draw from normal, fit KDE, see if sampling from kde's pdf recovers norm - np.random.seed(0) xs = np.linspace(-3, 3, 1000) weights = norm.pdf(xs) ys = MegKDE(xs, weights=weights).evaluate(xs) cs = ys.cumsum() cs /= cs[-1] cs[0] = 0 - samps = interp1d(cs, xs)(np.random.uniform(size=10000)) + samps = interp1d(cs, xs)(rng.uniform(size=10000)) mu, std = norm.fit(samps) assert np.isclose(mu, 0, atol=0.1) assert np.isclose(std, 1.0, atol=0.1) @@ -52,8 +55,7 @@ def test_megkde_1d_changing_weights(): def test_megkde_2d_basic(): # Draw from normal, fit KDE, see if sampling from kde's pdf recovers norm - np.random.seed(1) - data = np.random.multivariate_normal([0, 1], [[1.0, 0.0], [0.0, 0.75**2]], size=10000) + data = rng.multivariate_normal([0, 1], [[1.0, 0.0], [0.0, 0.75**2]], size=10000) xs, ys = np.linspace(-4, 4, 50), np.linspace(-4, 4, 50) xx, yy = np.meshgrid(xs, ys, indexing="ij") samps = np.vstack((xx.flatten(), yy.flatten())).T @@ -66,8 +68,8 @@ def test_megkde_2d_basic(): cs_y = zs_y.cumsum() cs_y /= cs_y[-1] cs_y[0] = 0 - samps_x = interp1d(cs_x, xs)(np.random.uniform(size=10000)) - samps_y = interp1d(cs_y, ys)(np.random.uniform(size=10000)) + samps_x = interp1d(cs_x, xs)(rng.uniform(size=10000)) + samps_y = interp1d(cs_y, ys)(rng.uniform(size=10000)) mu_x, std_x = norm.fit(samps_x) mu_y, std_y = norm.fit(samps_y) assert np.isclose(mu_x, 0, atol=0.1) diff --git a/tests/test_plotter.py b/tests/test_plotter.py index 4edeb05b..5e7f6e32 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -5,10 +5,10 @@ class TestChain: - np.random.seed(1) + rng = np.random.default_rng(1) n = 2000000 - data = np.random.normal(loc=5.0, scale=1.5, size=n) - data2 = np.random.normal(loc=3, scale=1.0, size=n) + data = rng.normal(loc=5.0, scale=1.5, size=n) + data2 = rng.normal(loc=3, scale=1.0, size=n) def test_plotter_extents1(self): c = ChainConsumer() @@ -61,7 +61,7 @@ def test_plotter_extents5(self): def test_plotter_extents6(self): c = ChainConsumer() for mid in np.linspace(-1, 1, 3): - data = np.random.normal(loc=0, size=1000) + data = self.rng.normal(loc=0, size=1000) posterior = norm.logpdf(data) data += mid c.add_chain(data, parameters=["x"], posterior=posterior, plot_point=True, plot_contour=False) From 6bb563a99890f1486fcccb0250e654f8c4535835 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Fri, 6 Oct 2023 20:53:44 +1000 Subject: [PATCH 04/22] Oh boy what fun this is --- examples/Basics/plot_covariance_and_marker.py | 2 +- examples/Basics/plot_grid.py | 2 +- examples/Basics/plot_hundreds_of_chains.py | 4 +- examples/Basics/plot_statistics.py | 2 +- .../customisations/plot_contour_labels.py | 4 +- examples/customisations/plot_font_changes.py | 2 +- examples/customisations/plot_kde_extents.py | 2 +- .../customisations/plot_legend_options.py | 6 +- examples/customisations/plot_lists.py | 4 +- examples/customisations/plot_no_histograms.py | 2 +- examples/customisations/plot_no_smooth.py | 2 +- examples/customisations/plot_preliminary.py | 2 +- .../customisations/plot_rainbow_serif_bins.py | 2 +- .../customisations/plot_shade_gradient.py | 2 +- examples/customisations/plot_zorder.py | 4 +- examples/more/plot_many.py | 2 +- examples/plot_summary.py | 6 +- poetry.lock | 28 +- pyproject.toml | 10 +- src/chainconsumer/TODO.md | 16 + src/chainconsumer/__init__.py | 4 +- src/chainconsumer/analysis.py | 392 ++-- src/chainconsumer/chain.py | 228 ++- src/chainconsumer/chainconsumer.py | 1679 +++++++---------- src/chainconsumer/colors.py | 96 +- src/chainconsumer/comparisons.py | 230 +-- src/chainconsumer/diagnostic.py | 17 +- src/chainconsumer/helpers.py | 22 +- src/chainconsumer/kde.py | 47 +- src/chainconsumer/log.py | 3 + src/chainconsumer/plotter.py | 8 +- src/chainconsumer/truth.py | 25 + tests/test_analysis.py | 66 +- tests/test_chain.py | 26 +- tests/test_chainconsumer.py | 38 +- tests/test_plotter.py | 12 +- 36 files changed, 1358 insertions(+), 1639 deletions(-) create mode 100644 src/chainconsumer/TODO.md create mode 100644 src/chainconsumer/log.py create mode 100644 src/chainconsumer/truth.py diff --git a/examples/Basics/plot_covariance_and_marker.py b/examples/Basics/plot_covariance_and_marker.py index 1fb9eaf9..b8682fe6 100644 --- a/examples/Basics/plot_covariance_and_marker.py +++ b/examples/Basics/plot_covariance_and_marker.py @@ -21,7 +21,7 @@ c = ChainConsumer() c.add_covariance(mean, cov, parameters=parameters, name="Cov") c.add_marker(mean, parameters=parameters, name="Marker!", marker_style="*", marker_size=100, color="r") -c.configure(usetex=False, serif=False) +c.configure_overrides(usetex=False, serif=False) fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/Basics/plot_grid.py b/examples/Basics/plot_grid.py index ee2a0b67..de1c4a2c 100644 --- a/examples/Basics/plot_grid.py +++ b/examples/Basics/plot_grid.py @@ -39,7 +39,7 @@ pdf_flat = multivariate_normal.pdf(coords, mean=[0.0, 0.0], cov=[[1.0, 0.7], [0.7, 3.5]]) c = ChainConsumer() c.add_chain([xs, ys], parameters=["$x$", "$y$"], weights=pdf_flat, grid=True) -c.configure(smooth=1) # Notice how smoothing changes the results! +c.configure_overrides(smooth=1) # Notice how smoothing changes the results! fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/Basics/plot_hundreds_of_chains.py b/examples/Basics/plot_hundreds_of_chains.py index 2424664d..4f201115 100644 --- a/examples/Basics/plot_hundreds_of_chains.py +++ b/examples/Basics/plot_hundreds_of_chains.py @@ -88,7 +88,7 @@ bar_shade=True, ) -c.configure(legend_artists=True) +c.configure_overrides(legend_artists=True) fig = c.plotter.plot() fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. @@ -109,7 +109,7 @@ # by the posterior values, the maximum point of each 2D slice, we can specify # to `configure` that `global_point=False`. -c.configure(legend_artists=True, global_point=False) +c.configure_overrides(legend_artists=True, global_point=False) fig = c.plotter.plot(chains="Sim1") fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/Basics/plot_statistics.py b/examples/Basics/plot_statistics.py index 166f23ab..80bc24aa 100644 --- a/examples/Basics/plot_statistics.py +++ b/examples/Basics/plot_statistics.py @@ -74,6 +74,6 @@ stats = list(c.analysis._summaries.keys()) for stat in stats: c.add_chain(data, parameters=parameters, name=stat.replace("_", " ").title()) -c.configure(statistics=stats, bar_shade=True) +c.configure_overrides(statistics=stats, bar_shade=True) fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_contour_labels.py b/examples/customisations/plot_contour_labels.py index 7cc7a874..4c46eb67 100644 --- a/examples/customisations/plot_contour_labels.py +++ b/examples/customisations/plot_contour_labels.py @@ -17,7 +17,7 @@ data = rng.multivariate_normal([0, 0], [[1, 0.5], [0.5, 1.0]], size=1000000) -c = ChainConsumer().add_chain(data).configure(contour_labels="confidence") +c = ChainConsumer().add_chain(data).configure_overrides(contour_labels="confidence") fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. @@ -27,6 +27,6 @@ # the confidence levels, because of the ambiguity over sigma levels introduced # by the `sigma2d` keyword. -c = ChainConsumer().add_chain(data).configure(contour_labels="sigma") +c = ChainConsumer().add_chain(data).configure_overrides(contour_labels="sigma") fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_font_changes.py b/examples/customisations/plot_font_changes.py index f7ad1922..a385108d 100644 --- a/examples/customisations/plot_font_changes.py +++ b/examples/customisations/plot_font_changes.py @@ -19,7 +19,7 @@ # If you pass in parameter labels and only one chain, you can also get parameter bounds c = ChainConsumer() c.add_chain(data, parameters=["$x$", "$y^2$", r"$\Omega_\beta$"], name="Example") -c.configure(diagonal_tick_labels=False, tick_font_size=8, label_font_size=25, max_ticks=8) +c.configure_overrides(diagonal_tick_labels=False, tick_font_size=8, label_font_size=25, max_ticks=8) fig = c.plotter.plot(figsize="column", legend=True) fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_kde_extents.py b/examples/customisations/plot_kde_extents.py index c9e5778f..d7fa8bb9 100644 --- a/examples/customisations/plot_kde_extents.py +++ b/examples/customisations/plot_kde_extents.py @@ -25,7 +25,7 @@ c.add_chain(data, name="KDE on") c.add_chain(data + 1, name="KDE off") c.add_chain(data + 2, name="KDE x2!") -c.configure(kde=[True, False, 2.0], shade_alpha=0.1, flip=False) +c.configure_overrides(kde=[True, False, 2.0], shade_alpha=0.1, flip=False) fig = c.plotter.plot(extents=[(-2, 4), (0, 9)]) fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_legend_options.py b/examples/customisations/plot_legend_options.py index 32feaa55..216b7262 100644 --- a/examples/customisations/plot_legend_options.py +++ b/examples/customisations/plot_legend_options.py @@ -19,7 +19,7 @@ c = ChainConsumer() c.add_chain(data1, parameters=["$x$", "$y$"], name="Chain 1") c.add_chain(data2, parameters=["$x$", "$y$"], name="Chain 2") -c.configure(colors=["lb", "g"]) +c.configure_overrides(colors=["lb", "g"]) fig = c.plotter.plot() fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. @@ -30,7 +30,7 @@ c = ChainConsumer() c.add_chain(data1, parameters=["$x$", "$y$"], name="Chain 1") c.add_chain(data2, parameters=["$x$", "$y$"], name="Chain 2") -c.configure(colors=["lb", "lb"], linestyles=["-", "--"]) +c.configure_overrides(colors=["lb", "lb"], linestyles=["-", "--"]) fig = c.plotter.plot() fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. @@ -41,7 +41,7 @@ c = ChainConsumer() c.add_chain(data1, parameters=["$x$", "$y$"], name="Chain 1") c.add_chain(data2, parameters=["$x$", "$y$"], name="Chain 2") -c.configure( +c.configure_overrides( linestyles=["-", "--"], sigmas=[0, 1, 2, 3], legend_kwargs={"loc": "upper left", "fontsize": 10}, diff --git a/examples/customisations/plot_lists.py b/examples/customisations/plot_lists.py index 399a12b0..d4347740 100644 --- a/examples/customisations/plot_lists.py +++ b/examples/customisations/plot_lists.py @@ -27,7 +27,7 @@ c.add_chain(d2) c.add_chain(d3) -c.configure( +c.configure_overrides( linestyles=["-", "--", "-"], linewidths=[1.0, 3.0, 1.0], bins=[3.0, 1.0, 1.0], @@ -54,7 +54,7 @@ c = ChainConsumer() c.add_chain(d1, parameters=["$x$", "$y$"]).add_chain(d2).add_chain(d3, linestyle="-", linewidth=5) -c.configure(linestyles=":", linewidths=2) +c.configure_overrides(linestyles=":", linewidths=2) fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_no_histograms.py b/examples/customisations/plot_no_histograms.py index bb62ba7e..14ea2601 100644 --- a/examples/customisations/plot_no_histograms.py +++ b/examples/customisations/plot_no_histograms.py @@ -17,7 +17,7 @@ data = rng.multivariate_normal(rng.normal(size=3), np.dot(cov, cov.T), size=100000) c = ChainConsumer().add_chain(data) -c.configure(plot_hists=False) +c.configure_overrides(plot_hists=False) fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_no_smooth.py b/examples/customisations/plot_no_smooth.py index bcc10094..e31565c2 100644 --- a/examples/customisations/plot_no_smooth.py +++ b/examples/customisations/plot_no_smooth.py @@ -21,7 +21,7 @@ c = ChainConsumer() c.add_chain(data, parameters=["$x_1$", "$x_2$"]) -c.configure(smooth=0, linewidths=2, colors="#673AB7") +c.configure_overrides(smooth=0, linewidths=2, colors="#673AB7") fig = c.plotter.plot(figsize="column", truth=[0.0, 4.0]) # If we wanted to save to file, we would instead have written diff --git a/examples/customisations/plot_preliminary.py b/examples/customisations/plot_preliminary.py index e3b1f296..bf7355cb 100644 --- a/examples/customisations/plot_preliminary.py +++ b/examples/customisations/plot_preliminary.py @@ -38,5 +38,5 @@ c.add_chain(data1, parameters=["$x$", "$y$"], name="Good results") c.add_chain(data2, name="Unfinished results") kwargs = {"color": "purple", "alpha": 1.0, "family": "sanserif", "usetex": False, "weight": "bold"} -c.configure(watermark_text_kwargs=kwargs, flip=True) +c.configure_overrides(watermark_text_kwargs=kwargs, flip=True) fig = c.plotter.plot(watermark="SECRET RESULTS", figsize=2.0) diff --git a/examples/customisations/plot_rainbow_serif_bins.py b/examples/customisations/plot_rainbow_serif_bins.py index 9935e3a3..cb0e7f79 100644 --- a/examples/customisations/plot_rainbow_serif_bins.py +++ b/examples/customisations/plot_rainbow_serif_bins.py @@ -37,7 +37,7 @@ c.add_chain(data2, name="B") c.add_chain(data3, name="C") c.add_chain(data4, name="D") -c.configure(bins=50, cmap="plasma") +c.configure_overrides(bins=50, cmap="plasma") fig = c.plotter.plot(figsize=0.75) # Also making the figure 75% of its original size, for fun fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_shade_gradient.py b/examples/customisations/plot_shade_gradient.py index dd956284..96dbeb5b 100644 --- a/examples/customisations/plot_shade_gradient.py +++ b/examples/customisations/plot_shade_gradient.py @@ -20,7 +20,7 @@ c = ChainConsumer() c.add_chain(data1, parameters=["$x$", "$y$"]) c.add_chain(data2, parameters=["$x$", "$y$"]) -c.configure(shade_gradient=[0.1, 3.0], colors=["o", "k"], sigmas=[0, 1, 2, 3], shade_alpha=1.0) +c.configure_overrides(shade_gradient=[0.1, 3.0], colors=["o", "k"], sigmas=[0, 1, 2, 3], shade_alpha=1.0) fig = c.plotter.plot() fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_zorder.py b/examples/customisations/plot_zorder.py index a4c0fb99..491d8630 100644 --- a/examples/customisations/plot_zorder.py +++ b/examples/customisations/plot_zorder.py @@ -22,7 +22,7 @@ c = ChainConsumer() c.add_chain(data1, parameters=["$x$", "$y$"], color="k", shade_alpha=0.7, zorder=1) c.add_chain(data2, color="o", shade_alpha=0.7, zorder=2) -c.configure(spacing=0) +c.configure_overrides(spacing=0) c.plotter.plot(display=True, figsize=2.0) ############################################################################### @@ -31,5 +31,5 @@ c = ChainConsumer() c.add_chain(data1, parameters=["$x$", "$y$"], color="k", shade_alpha=0.7, zorder=2) c.add_chain(data2, color="o", shade_alpha=0.7, zorder=1) -c.configure(spacing=0) +c.configure_overrides(spacing=0) c.plotter.plot(display=True, figsize=2.0) diff --git a/examples/more/plot_many.py b/examples/more/plot_many.py index 2e98c41c..1edf2c68 100644 --- a/examples/more/plot_many.py +++ b/examples/more/plot_many.py @@ -28,7 +28,7 @@ c.add_chain(data3, name="S") c.add_chain(data, parameters=[r"$\Omega_m$", "$-w$", "$H_0$"], name="P") -c.configure( +c.configure_overrides( color_params="$H_0$", shade=[True, True, False], shade_alpha=0.2, bar_shade=True, linestyles=["-", "--", "-"] ) fig = c.plotter.plot(figsize=2.0, extents=[[0, 1], [0, 1.5]]) diff --git a/examples/plot_summary.py b/examples/plot_summary.py index 89493231..43f92e79 100644 --- a/examples/plot_summary.py +++ b/examples/plot_summary.py @@ -33,7 +33,7 @@ def get_instance(): # If we want the full shape of the distributions, well, thats the default # behaviour! c = get_instance() -c.configure(bar_shade=True) +c.configure_overrides(bar_shade=True) c.plotter.plot_summary() ############################################################################### @@ -41,7 +41,7 @@ def get_instance(): # want errorbars, not distributions. And some fun truth values. c = get_instance() -c.configure(legend_color_text=False) +c.configure_overrides(legend_color_text=False) c.configure_truth(ls=":", color="#FB8C00") c.plotter.plot_summary(errorbar=True, truth=[[0], [-1, 1], [-2, 0, 2]]) @@ -50,6 +50,6 @@ def get_instance(): # it with the others c = get_instance() -c.configure(legend_color_text=False) +c.configure_overrides(legend_color_text=False) c.configure_truth(ls="-", color="#555555") c.plotter.plot_summary(errorbar=True, truth="Ref. model", include_truth_chain=False, extra_parameter_spacing=1.5) diff --git a/poetry.lock b/poetry.lock index b9834240..16696c16 100644 --- a/poetry.lock +++ b/poetry.lock @@ -666,6 +666,21 @@ sql-other = ["SQLAlchemy (>=1.4.36)"] test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.8.0)"] +[[package]] +name = "pandas-stubs" +version = "2.1.1.230928" +description = "Type annotations for pandas" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas_stubs-2.1.1.230928-py3-none-any.whl", hash = "sha256:992d97159e054ca3175ebe8321ac5616cf6502dd8218b03bb0eaf3c4f6939037"}, + {file = "pandas_stubs-2.1.1.230928.tar.gz", hash = "sha256:ce1691c71c5d67b8f332da87763f7f54650f46895d99964d588c3a5d79e2cacc"}, +] + +[package.dependencies] +numpy = {version = ">=1.26.0", markers = "python_version < \"3.13\""} +types-pytz = ">=2022.1.1" + [[package]] name = "pathspec" version = "0.11.2" @@ -1230,6 +1245,17 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "types-pytz" +version = "2023.3.1.1" +description = "Typing stubs for pytz" +optional = false +python-versions = "*" +files = [ + {file = "types-pytz-2023.3.1.1.tar.gz", hash = "sha256:cc23d0192cd49c8f6bba44ee0c81e4586a8f30204970fc0894d209a6b08dab9a"}, + {file = "types_pytz-2023.3.1.1-py3-none-any.whl", hash = "sha256:1999a123a3dc0e39a2ef6d19f3f8584211de9e6a77fe7a0259f04a524e90a5cf"}, +] + [[package]] name = "typing-extensions" version = "4.8.0" @@ -1275,4 +1301,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "a2d3b9079e6395cf540ab0c4efa45b7e6e6022f4f724f95d5ec62ea657b57819" +content-hash = "1fecd7dfb911894538c627c8dbeb3bdffbf5d2250f950eb2e9e806d33ebe3940" diff --git a/pyproject.toml b/pyproject.toml index a32e82a6..5765eb94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ black = ">=23.3.0" pre-commit = ">=3.3.3" ruff = ">=0.0.276, <1" mypy = "^1.4.1" +pandas-stubs = "^2.1.1.230928" [tool.black] line-length = 120 @@ -48,17 +49,18 @@ target-version = "py310" "examples/***" = ["T201"] [tool.mypy] +plugins = ["numpy.typing.mypy_plugin", "pydantic.mypy"] check_untyped_defs = true -disallow_any_unimported = true -disallow_any_generics = true +disallow_any_unimported = false +disallow_any_generics = false disallow_incomplete_defs = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = false -disallow_untyped_defs = true +disallow_untyped_defs = false implicit_reexport = false no_implicit_optional = false -python_version = "3.11" +python_version = "3.10" strict_equality = true show_error_codes = true warn_redundant_casts = true diff --git a/src/chainconsumer/TODO.md b/src/chainconsumer/TODO.md new file mode 100644 index 00000000..4c3ec7d2 --- /dev/null +++ b/src/chainconsumer/TODO.md @@ -0,0 +1,16 @@ +- [ ] Add max_ticks to plotter +- [ ] plot_hists +- [ ] flip +- [ ] serif +- [ ] sigma2d +- [ ] usetex +- [ ] diagonal_tick_labels +- [ ] label_font_size +- [ ] tick_font_size +- [ ] spacing +- [ ] contour_label_font_size +- [ ] legend_kwargs +- [ ] legend_location +- [ ] legend_artists +- [ ] legend_color_text +- [ ] watermark_text_kwargs \ No newline at end of file diff --git a/src/chainconsumer/__init__.py b/src/chainconsumer/__init__.py index fa11af91..7d415be3 100644 --- a/src/chainconsumer/__init__.py +++ b/src/chainconsumer/__init__.py @@ -1,3 +1,5 @@ +from .chain import Chain, Config from .chainconsumer import ChainConsumer +from .truth import Truth -__all__ = ["ChainConsumer"] +__all__ = ["ChainConsumer", "Chain", "Config", "Truth"] diff --git a/src/chainconsumer/analysis.py b/src/chainconsumer/analysis.py index aa642518..bec4b1ab 100644 --- a/src/chainconsumer/analysis.py +++ b/src/chainconsumer/analysis.py @@ -1,11 +1,15 @@ import logging from enum import Enum +from pathlib import Path import numpy as np +from pydantic import Field from scipy.integrate import simps from scipy.interpolate import interp1d from scipy.ndimage.filters import gaussian_filter +from .base import BetterBase +from .chain import Chain, ChainName, ColumnName, MaxPosterior, Named2DMatrix from .helpers import get_grid_bins, get_latex_table_frame, get_smoothed_bins from .kde import MegKDE @@ -19,71 +23,73 @@ class SummaryStatistic(Enum): MAX_CENTRAL = "max_central" -class Analysis: - summaries = ["max", "mean", "cumulative", "max_symmetric", "max_shortest", "max_central"] +class Bound(BetterBase): + lower: float | None = Field(default=None) + center: float | None = Field(default=None) + upper: float | None = Field(default=None) + - def __init__(self, parent): +class Analysis: + def __init__(self, parent: "ChainConsumer"): self.parent = parent self._logger = logging.getLogger("chainconsumer") self._summaries = { - "max": self.get_parameter_summary_max, - "mean": self.get_parameter_summary_mean, - "cumulative": self.get_parameter_summary_cumulative, - "max_symmetric": self.get_paramater_summary_max_symmetric, - "max_shortest": self.get_parameter_summary_max_shortest, - "max_central": self.get_parameter_summary_max_central, + SummaryStatistic.MAX: self.get_parameter_summary_max, + SummaryStatistic.MEAN: self.get_parameter_summary_mean, + SummaryStatistic.CUMULATIVE: self.get_parameter_summary_cumulative, + SummaryStatistic.MAX_SYMMETRIC: self.get_paramater_summary_max_symmetric, + SummaryStatistic.MAX_SHORTEST: self.get_parameter_summary_max_shortest, + SummaryStatistic.MAX_CENTRAL: self.get_parameter_summary_max_central, } def get_latex_table( self, - parameters=None, - transpose=False, - caption=None, - label="tab:model_params", - hlines=True, - blank_fill="--", - filename=None, - ): # pragma: no cover + columns: list[str] | int | None = None, + transpose: bool = False, + caption: str | None = None, + label: str = "tab:model_params", + hlines: bool = True, + blank_fill: str = "--", + filename: str | Path | None = None, + ) -> str: # pragma: no cover """Generates a LaTeX table from parameter summaries. - Parameters - ---------- - parameters : list[str], int optional - A list of what parameters to include in the table. By default, includes all parameters - transpose : bool, optional - Defaults to False, which gives each column as a parameter, each chain (framework) - as a row. You can swap it so that you have a parameter each row and a framework - each column by setting this to True - caption : str, optional - If you want to generate a caption for the table through Python, use this. - Defaults to an empty string - label : str, optional - If you want to generate a label for the table through Python, use this. - Defaults to an empty string - hlines : bool, optional - Inserts ``\\hline`` before and after the header, and at the end of table. - blank_fill : str, optional - If a framework does not have a particular parameter, will fill that cell of - the table with this string. - filename : str, optional - The file to save the output string to - - Returns - ------- - str - the LaTeX table. + Args: + columns : list[str], int optional + A list of what parameters to include in the table. By default, includes all columns. + If an integer is passed, will include the first N columns. + transpose : bool, optional + Defaults to False, which gives each column as a parameter, each chain (framework) + as a row. You can swap it so that you have a parameter each row and a framework + each column by setting this to True + caption : str, optional + If you want to generate a caption for the table through Python, use this. + Defaults to an empty string + label : str, optional + If you want to generate a label for the table through Python, use this. + Defaults to an empty string + hlines : bool, optional + Inserts ``\\hline`` before and after the header, and at the end of table. + blank_fill : str, optional + If a framework does not have a particular parameter, will fill that cell of + the table with this string. + filename : str | Path, optional + The file to save the output string to + + Returns: + str: the LaTeX table. """ - if parameters is None: - parameters = self.parent._all_parameters - elif isinstance(parameters, int): - parameters = self.parent._all_parameters[:parameters] - for p in parameters: - assert isinstance(p, str), "Generating a LaTeX table requires all parameters have labels" - num_parameters = len(parameters) - chains = self.parent.get_mcmc_chains() + if columns is None: + columns = self.parent.all_columns + elif isinstance(columns, int): + columns = self.parent.all_columns[:columns] + # TODO: ensure labels are a thin we can add + num_parameters = len(columns) + + chains = self.parent.chains num_chains = len(chains) - fit_values = self.get_summary(squeeze=False, chains=chains) + fit_values = self.get_summary(chains=chains) if label is None: label = "" if caption is None: @@ -97,26 +103,26 @@ def get_latex_table( if hlines: center_text += hline_text + "\t\t" if transpose: - center_text += " & ".join(["Parameter"] + [c.name for c in chains]) + end_text + center_text += " & ".join(["Parameter"] + [c.name for c in chains.values()]) + end_text if hlines: center_text += "\t\t" + hline_text - for p in parameters: - arr = ["\t\t" + p] - for chain_res in fit_values: - if p in chain_res: - arr.append(self.get_parameter_text(*chain_res[p], wrap=True)) + for p in columns: + arr = ["\t\t" + self.parent.get_label(p)] + for _, column_results in fit_values.items(): + if p in column_results: + arr.append(self.get_parameter_text(column_results[p], wrap=True)) else: arr.append(blank_fill) center_text += " & ".join(arr) + end_text else: - center_text += " & ".join(["Model", *parameters]) + end_text + center_text += " & ".join(["Model", *[self.parent.get_label(c) for c in columns]]) + end_text if hlines: center_text += "\t\t" + hline_text - for name, chain_res in zip([c.name for c in chains], fit_values): + for name, chain_res in fit_values.items(): arr = ["\t\t" + name] - for p in parameters: + for p in columns: if p in chain_res: - arr.append(self.get_parameter_text(*chain_res[p], wrap=True)) + arr.append(self.get_parameter_text(chain_res[p], wrap=True)) else: arr.append(blank_fill) center_text += " & ".join(arr) + end_text @@ -125,211 +131,135 @@ def get_latex_table( final_text = get_latex_table_frame(caption, label) % (column_text, center_text) if filename is not None: - with open(filename, "w") as f: + if isinstance(filename, str): + filename = Path(filename) + with Path.open(filename, "w") as f: f.write(final_text) return final_text - def get_summary(self, squeeze=True, parameters=None, chains=None): + def get_summary( + self, + columns: list[str] | None = None, + chains: dict[str, Chain] | list[str] | None = None, + ) -> dict[ChainName, dict[ColumnName, Bound]]: """Gets a summary of the marginalised parameter distributions. - Parameters - ---------- - squeeze : bool, optional - Squeeze the summaries. If you only have one chain, squeeze will not return - a length one list, just the single summary. If this is false, you will - get a length one list. - parameters : list[str], optional - A list of parameters which to generate summaries for. - chains : list[int|str], optional - A list of the chains to get a summary of. + Args: + parameters (list[str], optional): A list of parameters which to generate summaries for. + chains (dict[str, Chain] | list[str], optional): A list of chains to generate summaries for. - Returns - ------- - list of dictionaries - One entry per chain, parameter bounds stored in dictionary with parameter as key + Returns: + dict[ChainName, dict[ColumnName, Bound]]: A map from chain name to column name to bound. """ - results = [] + results = {} if chains is None: - chains = self.parent.get_mcmc_chains() - else: - if isinstance(chains, int | str): - chains = [chains] - if isinstance(chains[0], int | str): - chains = [self.parent.chains[i] for c in chains for i in self.parent._get_chain(c)] + chains = self.parent.chains + if isinstance(chains, list): + chains = {c: self.parent.chains[c] for c in chains} - for chain in chains: + for name, chain in chains.items(): res = {} - params_to_find = parameters if parameters is not None else chain.parameters + params_to_find = columns if columns is not None else chain.samples.columns for p in params_to_find: - if p not in chain.parameters: + if p not in chain.samples: continue summary = self.get_parameter_summary(chain, p) res[p] = summary - results.append(res) - if squeeze and len(results) == 1: - return results[0] + results[name] = res + return results - def get_max_posteriors(self, parameters=None, squeeze=True, chains=None): + def get_max_posteriors(self, chains: dict[str, Chain] | list[str] | None = None) -> dict[ChainName, MaxPosterior]: """Gets the maximum posterior point in parameter space from the passed parameters. Requires the chains to have set `posterior` values. - Parameters - ---------- - parameters : str|list[str] - The parameters to find - squeeze : bool, optional - Squeeze the summaries. If you only have one chain, squeeze will not return - a length one list, just the single summary. If this is false, you will - get a length one list. - chains : list[int|str], optional - A list of the chains to get a summary of. + Args: + chains (dict[str, Chain] | list[str], optional): A list of chains to generate summaries for. - Returns - ------- - list of two-tuples - One entry per chain, two-tuple represents the max-likelihood coordinate + Returns: + dict[ChainName, MaxPosterior]: A map from chain name to max posterior point. """ - results = [] + results = {} if chains is None: chains = self.parent.chains - else: - if isinstance(chains, int | str): - chains = [chains] - chains = [self.parent.chains[i] for c in chains for i in self.parent._get_chain(c)] + if isinstance(chains, list): + chains = {c: self.parent.chains[c] for c in chains} - if isinstance(parameters, str): - parameters = [parameters] - - for chain in chains: - if chain.posterior_max_index is None: - results.append(None) + for chain_name, chain in chains.items(): + max_posterior = chain.get_max_posterior_point() + if max_posterior is None: continue - res = {} - params_to_find = parameters if parameters is not None else chain.parameters - for p in params_to_find: - if p in chain.parameters: - res[p] = chain.posterior_max_params[p] - results.append(res) + results[chain_name] = max_posterior - if squeeze and len(results) == 1: - return results[0] return results def get_parameter_summary(self, chain, parameter): # Ensure config has been called so we get the statistics set in config if not self.parent._configured: - self.parent.configure() + self.parent.configure_overrides() callback = self._summaries[chain.config["statistics"]] return chain.get_summary(parameter, callback) - def get_correlations(self, chain=0, parameters=None): - """ - Takes a chain and returns the correlation between chain parameters. - - Parameters - ---------- - chain : int|str, optional - The chain index or name. Defaults to first chain. - parameters : list[str], optional - The list of parameters to compute correlations. Defaults to all parameters - for the given chain. - - Returns - ------- - tuple - The first index giving a list of parameter names, the second index being the - 2D correlation matrix. - """ - parameters, cov = self.get_covariance(chain=chain, parameters=parameters) - diag = np.sqrt(np.diag(cov)) - divisor = diag[None, :] * diag[:, None] - correlations = cov / divisor - return parameters, correlations - - def get_covariance(self, chain=0, parameters=None): - """ - Takes a chain and returns the covariance between chain parameters. - - Parameters - ---------- - chain : int|str, optional - The chain index or name. Defaults to first chain. - parameters : list[str], optional - The list of parameters to compute correlations. Defaults to all parameters - for the given chain. - - Returns - ------- - tuple - The first index giving a list of parameter names, the second index being the - 2D covariance matrix. - """ - index = self.parent._get_chain(chain) - assert len(index) == 1, "Please specify only one chain, have %d chains" % len(index) - chain = self.parent.chains[index[0]] - if parameters is None: - parameters = chain.parameters - - data = chain.get_data(parameters) - cov = np.atleast_2d(np.cov(data, aweights=chain.weights, rowvar=False)) - - return parameters, cov - def get_correlation_table( - self, chain=0, parameters=None, caption="Parameter Correlations", label="tab:parameter_correlations" - ): + self, + chain: str | Chain, + columns: list[str] | None = None, + caption: str = "Parameter Correlations", + label: str = "tab:parameter_correlations", + ) -> str: """ Gets a LaTeX table of parameter correlations. - Parameters - ---------- - chain : int|str, optional - The chain index or name. Defaults to first chain. - parameters : list[str], optional - The list of parameters to compute correlations. Defaults to all parameters - for the given chain. - caption : str, optional - The LaTeX table caption. - label : str, optional - The LaTeX table label. + Args: + chain (str|Chain, optional_: The chain index or name. Defaults to first chain. + columns (list[str], optional): The list of parameters to compute correlations. Defaults to all columns + caption (str, optional): The LaTeX table caption. + label (str, optional): The LaTeX table label. - Returns - ------- - str - The LaTeX table ready to go! + Returns: + str: The LaTeX table ready to go! """ - parameters, cor = self.get_correlations(chain=chain, parameters=parameters) - return self._get_2d_latex_table(parameters, cor, caption, label) + if isinstance(chain, str): + assert chain in self.parent.chains, f"Chain {chain} not found!" + chain = self.parent.chains[chain] + if chain is None: + assert len(self.parent.chains) == 1, "You must specify a chain if there are multiple chains" + chain = next(iter(self.parent.chains.values())) + + correlations = chain.get_correlation(columns=columns) + return self._get_2d_latex_table(correlations, caption, label) def get_covariance_table( - self, chain=0, parameters=None, caption="Parameter Covariance", label="tab:parameter_covariance" - ): + self, + chain: str | Chain, + columns: list[str] | None = None, + caption: str = "Parameter Covariance", + label: str = "tab:parameter_covariance", + ) -> str: """ - Gets a LaTeX table of parameter covariance. + Gets a LaTeX table of parameter covariances. - Parameters - ---------- - chain : int|str, optional - The chain index or name. Defaults to first chain. - parameters : list[str], optional - The list of parameters to compute correlations. Defaults to all parameters - for the given chain. - caption : str, optional - The LaTeX table caption. - label : str, optional - The LaTeX table label. + Args: + chain (str|Chain, optional_: The chain index or name. Defaults to first chain. + columns (list[str], optional): The list of parameters to compute covariances on. Defaults to all columns + caption (str, optional): The LaTeX table caption. + label (str, optional): The LaTeX table label. - Returns - ------- - str - The LaTeX table ready to go! + Returns: + str: The LaTeX table ready to go! """ - parameters, cov = self.get_covariance(chain=chain, parameters=parameters) - return self._get_2d_latex_table(parameters, cov, caption, label) + if isinstance(chain, str): + assert chain in self.parent.chains, f"Chain {chain} not found!" + chain = self.parent.chains[chain] + if chain is None: + assert len(self.parent.chains) == 1, "You must specify a chain if there are multiple chains" + chain = next(iter(self.parent.chains.values())) + + covariance = chain.get_covariance(columns=columns) + return self._get_2d_latex_table(covariance, caption, label) def _get_smoothed_histogram(self, chain, parameter, pad=False): data = chain.get_data(parameter) @@ -347,7 +277,7 @@ def _get_smoothed_histogram(self, chain, parameter, pad=False): xs = np.linspace(edge_centers[0], edge_centers[-1], 10000) if smooth: - hist = gaussian_filter(hist, smooth, mode=self.parent._gauss_mode) + hist = gaussian_filter(hist, smooth, mode="reflect") kde = chain.config["kde"] if kde: kde_xs = np.linspace(edge_centers[0], edge_centers[-1], max(200, int(bins.max()))) @@ -361,7 +291,9 @@ def _get_smoothed_histogram(self, chain, parameter, pad=False): cs /= cs.max() return xs, ys, cs - def _get_2d_latex_table(self, parameters, matrix, caption, label): + def _get_2d_latex_table(self, named_matrix: Named2DMatrix, caption: str, label: str): + parameters = [self.parent.get_label(c) for c in named_matrix.columns] + matrix = named_matrix.matrix latex_table = get_latex_table_frame(caption=caption, label=label) column_def = "c|%s" % ("c" * len(parameters)) hline_text = " \\hline\n" @@ -379,7 +311,7 @@ def _get_2d_latex_table(self, parameters, matrix, caption, label): table += hline_text return latex_table % (column_def, table) - def get_parameter_text(self, lower, maximum, upper, wrap=False): + def get_parameter_text(self, bound: Bound, wrap: bool = False): """Generates LaTeX appropriate text from marginalised parameter bounds. Parameters @@ -398,10 +330,10 @@ def get_parameter_text(self, lower, maximum, upper, wrap=False): str The formatted text given the parameter bounds """ - if lower is None or upper is None: + if bound.lower is None or bound.upper is None or bound.center is None: return "" - upper_error = upper - maximum - lower_error = maximum - lower + upper_error = bound.upper - bound.center + lower_error = bound.center - bound.lower if upper_error != 0 and lower_error != 0: resolution = min(np.floor(np.log10(np.abs(upper_error))), np.floor(np.log10(np.abs(lower_error)))) elif upper_error == 0 and lower_error != 0: @@ -409,7 +341,7 @@ def get_parameter_text(self, lower, maximum, upper, wrap=False): elif upper_error != 0 and lower_error == 0: resolution = np.floor(np.log10(np.abs(upper_error))) else: - resolution = np.floor(np.log10(np.abs(maximum))) + resolution = np.floor(np.log10(np.abs(bound.center))) factor = 0 fmt = "%0.1f" r = 1 @@ -429,7 +361,7 @@ def get_parameter_text(self, lower, maximum, upper, wrap=False): r = 3 upper_error *= 10**factor lower_error *= 10**factor - maximum *= 10**factor + maximum = bound.center * 10**factor upper_error = round(upper_error, r) lower_error = round(lower_error, r) maximum = round(maximum, r) @@ -514,7 +446,7 @@ def get_paramater_summary_max_symmetric(self, chain, parameter): xs, ys, cs = self._get_smoothed_histogram(chain, parameter) desired_area = chain.config["summary_area"] - x_to_c = interp1d(xs, cs, bounds_error=False, fill_value=(0, 1)) + x_to_c = interp1d(xs, cs, bounds_error=False, fill_value=(0, 1)) # type: ignore # Get max likelihood x max_index = ys.argmax() @@ -537,7 +469,7 @@ def get_parameter_summary_max_shortest(self, chain, parameter): xs, ys, cs = self._get_smoothed_histogram(chain, parameter) desired_area = chain.config["summary_area"] - c_to_x = interp1d(cs, xs, bounds_error=False, fill_value=(-np.inf, np.inf)) + c_to_x = interp1d(cs, xs, bounds_error=False, fill_value=(-np.inf, np.inf)) # type: ignore # Get max likelihood x max_index = ys.argmax() @@ -565,3 +497,7 @@ def get_parameter_summary_max_central(self, chain, parameter): xvals = c_to_x(vals) return [xvals[0], x, xvals[1]] + + +if __name__ == "__main__": + from .chainconsumer import ChainConsumer diff --git a/src/chainconsumer/chain.py b/src/chainconsumer/chain.py index 2b9b9623..3e08a4f6 100644 --- a/src/chainconsumer/chain.py +++ b/src/chainconsumer/chain.py @@ -1,31 +1,93 @@ import logging +from typing import Any, TypeAlias import numpy as np import pandas as pd -from pydantic import ConfigDict, Field, field_validator, model_validator +from pydantic import Field, field_validator, model_validator from .analysis import SummaryStatistic from .base import BetterBase -from .colors import ColourInput, colors +from .colors import ColorInput, colors +from .helpers import get_bins +ChainName: TypeAlias = str +ColumnName: TypeAlias = str -class Chain(BetterBase): - chain: pd.DataFrame = Field( + +class MaxPosterior(BetterBase): + log_posterior: float + coordinate: dict[ColumnName, float] + + @property + def vec_coordinate(self) -> np.ndarray: + return np.array(list(self.coordinate.values())) + + +class Named2DMatrix(BetterBase): + columns: list[str] + matrix: np.ndarray # type: ignore + + +class Config(BetterBase): + # Note that a None default means that this will be inferred + # automatically when you go to plot. + statistics: SummaryStatistic = Field(default=SummaryStatistic.MAX, description="The summary statistic to use") + summary_area: float | None = Field(default=0.6827, description="The area to use for summary statistics") + sigmas: list[float] | None = Field(default=None, description="The sigmas to use for summary statistics") + color: ColorInput | None = Field(default=None, description="The color of the chain") + linestyle: str | None = Field(default="-", description="The line style of the chain") + linewidth: float | None = Field(default=1.0, description="The line width of the chain") + cloud: bool | None = Field(default=False, description="Whether to show the cloud of the chain") + show_contour_labels: bool | None = Field(default=False, description="Whether to show contour labels") + shade: bool | None = Field(default=None, description="Whether to shade the chain") + shade_alpha: float | None = Field(default=None, description="The alpha of the shading") + shade_gradient: float | None = Field(default=1.0, description="The contrast between contour levels") + bar_shade: bool | None = Field(default=None, description="Whether to shade marginalised distributions") + bins: int | float | None = Field(default=None, description="The number of bins to use for histograms") + kde: int | float | bool | None = Field(default=False, description="The bandwidth for KDEs") + smooth: int | float | bool | None = Field(default=3, description="The smoothing for histograms.") + color_params: str | None = Field(default=None, description="The parameter (column) to use for coloring") + plot_color_params: bool | None = Field(default=None, description="Whether to plot the color parameter") + cmap: str | None = Field(default="viridis", description="The colormap to use for shading cloud points") + num_cloud: int | float | None = Field(default=10000, description="The number of points in the cloud") + plot_cloud: bool | None = Field(default=False, description="Whether to plot the cloud") + plot_contour: bool | None = Field(default=True, description="Whether to plot contours") + plot_point: bool | None = Field(default=False, description="Whether to plot points") + show_as_1d_prior: bool | None = Field(default=False, description="Whether to show as a 1D prior") + marker_style: str | None = Field(default=None, description="The marker style to use") + marker_size: int | float | None = Field(default=None, description="The marker size to use") + marker_alpha: int | float | None = Field(default=None, description="The marker alpha to use") + zorder: int | None = Field(default=None, description="The zorder to use") + shift_params: bool = Field( + default=False, + description="Whether to shift the parameters by subtracting each parameters mean", + ) + + def apply_if_none(self, **kwargs: dict[str, Any]) -> None: + for key, value in kwargs.items(): + if getattr(self, key) is None: + setattr(self, key, value) + + def apply(self, **kwargs: dict[str, Any]) -> None: + for key, value in kwargs.items(): + setattr(self, key, value) + + +class Chain(Config): + samples: pd.DataFrame = Field( default=..., description="The chain data as a pandas DataFrame", ) - name: str = Field( + name: ChainName = Field( default=..., description="The name of the chain", ) - column_labels: dict[str, str] = Field( - default={}, description="A dictionary mapping column names to labels. If not set, will use the column names." - ) - weight_column: str = Field( + + weight_column: ColumnName = Field( default="weights", description="The name of the weight column, if it exists", ) - posterior_column: str = Field( + posterior_column: ColumnName = Field( default="posterior", description="The name of the log posterior column, if it exists", ) @@ -53,68 +115,29 @@ class Chain(BetterBase): description="Raise the posterior surface to this. Useful for inflating or deflating uncertainty for debugging.", ) - statistics: SummaryStatistic = Field( - default=SummaryStatistic.MAX, - description="The summary statistic to use", - ) - - color: ColourInput | None = Field(default=None, description="The color of the chain") - linestyle: str | None = Field(default=None, description="The line style of the chain") - linewidth: float | None = Field(default=None, description="The line width of the chain") - cloud: bool | None = Field(default=False, description="Whether to show the cloud of the chain") - shade: bool | None = Field(default=True, description="Whether to shade the chain") - shade_alpha: float | None = Field(default=None, description="The alpha of the shading") - shade_gradient: float | None = Field(default=None, description="The contrast between contour levels") - bar_shade: bool | None = Field(default=None, description="Whether to shade marginalised distributions") - bins: int | float | None = Field(default=None, description="The number of bins to use for histograms") - kde: int | float | bool | None = Field(default=False, description="The bandwidth for KDEs") - smooth: int | float | bool | None = Field(default=3, description="The smoothing for histograms.") - color_params: str | None = Field(default=None, description="The parameter (column) to use for coloring") - plot_color_params: bool | None = Field(default=None, description="Whether to plot the color parameter") - cmap: str | None = Field(default=None, description="The colormap to use for shading") - num_cloud: int | float | None = Field(default=None, description="The number of points in the cloud") - plot_contour: bool | None = Field(default=True, description="Whether to plot contours") - plot_point: bool | None = Field(default=False, description="Whether to plot points") - show_as_1d_prior: bool | None = Field(default=False, description="Whether to show as a 1D prior") - marker_style: str | None = Field(default=None, description="The marker style to use") - marker_size: int | float | None = Field(default=None, description="The marker size to use") - marker_alpha: int | float | None = Field(default=None, description="The marker alpha to use") - zorder: int | None = Field(default=None, description="The zorder to use") - - shift_params: bool = Field( - default=False, - description="Whether to shift the parameters by subtracting each parameters mean", - ) - - model_config = ConfigDict(arbitrary_types_allowed=True) - @property def max_posterior_row(self) -> pd.Series | None: - if self.posterior_column not in self.chain.columns: + if self.posterior_column not in self.samples.columns: logging.warning("No posterior column found, cannot find max posterior row") return None - argmax = self.chain[self.posterior_column].argmax() - return self.chain.loc[argmax] - - @property - def labels(self) -> list[str]: - return [self.column_labels.get(col, col) for col in self.chain.columns] + argmax = self.samples[self.posterior_column].argmax() + return self.samples.loc[argmax] @property def weights(self) -> np.ndarray: - return self.chain[self.weight_column].to_numpy() + return self.samples[self.weight_column].to_numpy() @property def log_posterior(self) -> np.ndarray | None: - if self.posterior_column not in self.chain.columns: + if self.posterior_column not in self.samples.columns: return None - return self.chain[self.posterior_column].to_numpy() + return self.samples[self.posterior_column].to_numpy() @property def color_data(self) -> np.ndarray | None: if self.color_params is None: return None - return self.chain[self.color_params].to_numpy() + return self.samples[self.color_params].to_numpy() @field_validator("color") @classmethod @@ -125,23 +148,23 @@ def validate_color(cls, v: str | np.ndarray | list[float] | None) -> str | None: @model_validator(mode="after") def validate_model(self) -> "Chain": - assert not self.chain.empty, "Your chain is empty. This is not ideal." + assert not self.samples.empty, "Your chain is empty. This is not ideal." # If weights aren't set, add them all as one - if self.weight_column not in self.chain: - self.chain[self.weight_column] = 1.0 + if self.weight_column not in self.samples: + self.samples[self.weight_column] = 1.0 else: assert np.all(self.weights > 0), "Weights must be positive and non-zero" assert np.all(np.isfinite(self.weights)), "Weights must be finite" # Apply the mean shift if it is set to true if self.shift_params: - for param in self.chain: - self.chain[param] -= np.average(self.chain[param], weights=self.weights) # type: ignore + for param in self.samples: + self.samples[param] -= np.average(self.samples[param], weights=self.weights) # type: ignore # Check the walkers - assert self.chain.shape[0] % self.walkers == 0, ( - f"Chain {self.name} has {self.chain.shape[0]} steps, " + assert self.samples.shape[0] % self.walkers == 0, ( + f"Chain {self.name} has {self.samples.shape[0]} steps, " "which is not divisible by {self.walkers} walkers. This is not good." ) @@ -152,12 +175,85 @@ def validate_model(self) -> "Chain": # And if the color_params are set, ensure they're in the dataframe if self.color_params is not None: assert ( - self.color_params in self.chain.columns + self.color_params in self.samples.columns ), f"Chain {self.name} does not have color parameter {self.color_params}" return self - def get_data(self, columns: list[str] | str): + def get_data(self, columns: list[str] | str) -> pd.DataFrame: if isinstance(columns, str): columns = [columns] - return self.chain[columns] + return self.samples[columns] + + @classmethod + def from_covariance( + cls, + mean: np.ndarray, + covariance: np.ndarray, + columns: list[str], + name: str, + **kwargs: dict[str, Any], + ) -> "Chain": + """Generate samples as per mean and covariance supplied. Useful for Fisher matrix forecasts. + + Args: + mean (np.ndarray): The an array of mean values. + covariance (np.ndarray): The 2D array describing the covariance. + Dimensions should agree with the `mean` input. + columns (list[str]): A list of parameter names, one for each column (dimension) in the mean array. + name (str): The name of the chain. Defaults to None. + kwargs: Any other arguments to pass to the Chain constructor. + + Returns: + Chain: The generated chain. + """ + rng = np.random.default_rng() + samples = rng.multivariate_normal(mean, covariance, size=1000000) + df = pd.DataFrame(samples, columns=columns) + return cls(samples=df, name=name, **kwargs) # type: ignore + + def divide(self) -> list["Chain"]: + """Returns a ChainConsumer instance containing all the walks of a given chain + as individual chains themselves. + + This method might be useful if, for example, your chain was made using + MCMC with 4 walkers. To check the sampling of all 4 walkers agree, you could + call this to get a ChainConsumer instance with one chain for ech of the + four walks. If you then plot, hopefully all four contours + you would see agree. + + Returns: + list[Chain]: One chain per walker, split evenly + """ + assert self.walkers > 1, "Cannot divide a chain with only one walker" + assert not self.grid, "Cannot divide a grid chain" + + splits = np.split(self.samples, self.walkers) + chains = [] + for i, split in enumerate(splits): + df = pd.DataFrame(split, columns=self.samples.columns) + options = self.model_dump(exclude={"samples", "name"}) + chain = Chain(samples=df, name=f"{self.name} Walker {i}", **options) + chains.append(chain) + + return chains + + def get_max_posterior_point(self) -> MaxPosterior | None: + if self.max_posterior_row is None: + return None + row = self.max_posterior_row.to_dict() + log_posterior = row.pop(self.posterior_column) + return MaxPosterior(log_posterior=log_posterior, coordinate=row) + + def get_covariance(self, columns: list[str] | None) -> Named2DMatrix: + if columns is None: + columns = list(self.samples.columns) + cov = np.cov(self.samples[columns], rowvar=False, aweights=self.weights) + return Named2DMatrix(columns=columns, matrix=cov) + + def get_correlation(self, columns: list[str] | None) -> Named2DMatrix: + cov = self.get_covariance(columns) + diag = np.sqrt(np.diag(cov.matrix)) + divisor = diag[None, :] * diag[:, None] + correlations = cov.matrix / divisor + return Named2DMatrix(columns=cov.columns, matrix=correlations) diff --git a/src/chainconsumer/chainconsumer.py b/src/chainconsumer/chainconsumer.py index eabacc30..1a5fbb67 100644 --- a/src/chainconsumer/chainconsumer.py +++ b/src/chainconsumer/chainconsumer.py @@ -1,15 +1,14 @@ -import logging - import numpy as np import pandas as pd from .analysis import Analysis -from .chain import Chain -from .colors import Colors +from .chain import Chain, ChainName, ColumnName, Config +from .colors import ColorInput, colors from .comparisons import Comparison from .diagnostic import Diagnostic from .helpers import get_bins from .plotter import Plotter +from .truth import Truth __all__ = ["ChainConsumer"] @@ -19,1059 +18,699 @@ class ChainConsumer: figures, tables, diagnostics, you name it.""" def __init__(self): - logging.basicConfig(level=logging.INFO) - self._logger = logging.getLogger("chainconsumer") - self.color_finder = Colors() - self._all_colours = self.color_finder.get_default() - self._cmaps = ["viridis", "inferno", "hot", "Blues", "Greens", "Greys"] - self._linestyles = ["-", "--", ":"] - self.chains = [] - self._all_parameters = [] - self._default_parameters = None - self._init_params() - self._gauss_mode = "reflect" - self._configured = False - self._num_configure_calls = 0 + self.chains: dict[ChainName, Chain] = {} + self.truths: list[Truth] = [] + self.labels = {} + self.override: Config | None = None self.plotter = Plotter(self) self.diagnostic = Diagnostic(self) self.comparison = Comparison(self) self.analysis = Analysis(self) - def _init_params(self): - self.config = {} - self.config_truth = {} - self._configured = False - self._configured_truth = False + @property + def all_columns(self) -> list[str]: + return list(set([c for chain in self.chains.values() for c in chain.samples.columns])) - def get_mcmc_chains(self): - return [c for c in self.chains if c.mcmc_chain] + def get_label(self, str: ColumnName) -> str: + return self.labels.get(str, str) - def add_chain( - self, - chain, - parameters=None, - name=None, - weights=None, - posterior=None, - walkers=None, - grid=False, - num_eff_data_points=None, - num_free_params=None, - color=None, - linewidth=None, - linestyle=None, - kde=None, - shade=None, - shade_alpha=None, - power=None, - marker_style=None, - marker_size=None, - marker_alpha=None, - plot_contour=None, - plot_point=None, - show_as_1d_prior=None, - statistics=None, - cloud=None, - shade_gradient=None, - bar_shade=None, - bins=None, - smooth=None, - color_params=None, - plot_color_params=None, - cmap=None, - num_cloud=None, - zorder=None, - shift_params=None, - ): - r"""Add a chain to the consumer. - - Parameters - ---------- - chain : str|ndarray|dict|pandas.DataFrame - The chain to load. Normally a ``numpy.ndarray``. If a string is found, it - interprets the string as a filename and attempts to load it in using pandas.read_csv. If a ``dict`` - is passed in, it assumes the dict has keys of parameter names and values of - an array of samples. Notice that using a dictionary puts the order of - parameters in the output under the control of the python ``dict.keys()`` function. - If you passed ``grid`` is set, you can pass in the parameter ranges in list form. If you pass - a DataFrame, I will look for a "weight" and "posterior" column by default. If they are - called something different, extract them and pass them directly into weights and posterior. - parameters : list[str], optional - A list of parameter names, one for each column (dimension) in the chain. This parameter - should remain ``None`` if a dictionary is given as ``chain``, as the parameter names - are taken from the dictionary keys. - name : str, optional - The name of the chain. Used when plotting multiple chains at once. - weights : ndarray, optional - If given, uses this array to weight the samples in chain - posterior : ndarray, optional - If given, records the log posterior for each sample in the chain - walkers : int, optional - How many walkers went into creating the chain. Each walker should - contribute the same number of steps, and should appear in contiguous - blocks in the final chain. - grid : boolean, optional - Whether the input is a flattened chain from a grid search instead of a Monte-Carlo - chains. Note that when this is set, `walkers` should not be set, and `weights` should - be set to the posterior evaluation for the grid point. **Be careful** when using - a coarse grid of setting a high smoothing value, as this may oversmooth the posterior - surface and give unreasonably large parameter bounds. - num_eff_data_points : int|float, optional - The number of effective (independent) data points used in the model fitting. Not required - for plotting, but required if loading in multiple chains to perform model comparison. - num_free_params : int, optional - The number of degrees of freedom in your model. Not required for plotting, but required if - loading in multiple chains to perform model comparison. - color : str(hex), optional - Provide a colour for the chain. Can be used instead of calling `configure` for convenience. - linewidth : float, optional - Provide a line width to plot the contours. Can be used instead of calling `configure` for convenience. - linestyle : str, optional - Provide a line style to plot the contour. Can be used instead of calling `configure` for convenience. - kde : bool|float, optional - Set the `kde` value for this specific chain. Can be used instead of calling `configure` for convenience. - shade : booloptional - If set, overrides the default behaviour and plots filled contours or not. If a list of - bools is passed, you can turn shading on or off for specific chains. - shade_alpha : float, optional - Filled contour alpha value. Can be used instead of calling `configure` for convenience. - power : float, optional - The power to raise the posterior surface to. Useful for inflating or deflating uncertainty for debugging. - marker_style : str|, optional - The marker style to use when plotting points. Defaults to `'.'` - marker_size : numeric|, optional - Size of markers, if plotted. Defaults to `20`. - marker_alpha : numeric, optional - The alpha values when plotting markers. - plot_contour : bool, optional - Whether to plot the whole contour (as opposed to a point). Defaults to true for less than - 25 concurrent chains. - plot_point : bool, optional - Whether to plot a maximum likelihood point. Defaults to true for more then 24 chains. - show_as_1d_prior : bool, optional - Showing as a 1D prior will show the 1D histograms, but won't plot the 2D contours. - statistics : string, optional - Which sort of statistics to use. Defaults to `"max"` for maximum likelihood - statistics. Other available options are `"mean"`, `"cumulative"`, `"max_symmetric"`, - `"max_closest"` and `"max_central"`. In the - very, very rare case you want to enable different statistics for different - chains, you can pass in a list of strings. - cloud : bool, optional - If set, overrides the default behaviour and plots the cloud or not shade_gradient : - bar_shade : bool, optional - If set to true, shades in confidence regions in under histogram. By default - this happens if you less than 3 chains, but is disabled if you are comparing - more chains. You can pass a list if you wish to shade some chains but not others. - bins : int|float, optional - The number of bins to use. By default uses :math:`\frac{\sqrt{n}}{10}`, where - :math:`n` are the number of data points. Giving an integer will set the number - of bins to the given value. Giving a float will scale the number of bins, such - that giving ``bins=1.5`` will result in using :math:`\frac{1.5\sqrt{n}}{10}` bins. - Note this parameter is most useful if `kde=False` is also passed, so you - can actually see the bins and not a KDE. smooth : - color_params : str, optional - The name of the parameter to use for the colour scatter. Defaults to none, for no colour. If set - to 'weights', 'log_weights', or 'posterior' (without the quotes), and that is not a parameter in the chain, - it will respectively use the weights, log weights, or posterior, to colour the points. - plot_color_params : bool, optional - Whether or not the colour parameter should also be plotted as a posterior surface. - cmaps : str, optional - The matplotlib colourmap to use in the `colour_param`. If you have multiple `color_param`s, you can - specific a different cmap for each variable. By default ChainConsumer will cycle between several - cmaps. - num_cloud : int, optional - The number of scatter points to show when enabling `cloud` or setting one of the parameters - to colour scatter. Defaults to 15k per chain. - zorder : int, optional - The zorder to pass to `matplotlib` when plotting to determine visual order in the plot. - shift_params : dict|list, optional - Shifts the parameters specify to the numeric values. Useful to shift contours to the same location to perform blinded - uncertainty comparisons. - Returns - ------- - ChainConsumer - Itself, to allow chaining calls. - """ - is_dict = False - assert chain is not None, "You cannot have a chain of None" - if isinstance(chain, str): - chain = np.load(chain) if chain.lower().endswith(".npy") else pd.read_csv(chain) - elif isinstance(chain, dict): - assert parameters is None, "You cannot pass a dictionary and specify parameter names" - is_dict = True - parameters = list(chain.keys()) - chain = np.array([chain[p] for p in parameters]).T - elif isinstance(chain, list): - chain = np.array(chain).T - - if isinstance(chain, pd.DataFrame): - assert ( - parameters is None - ), "You cannot pass a DataFrame and use parameter names, we're using the columns names" - parameters = list(chain.columns) - if "weight" in parameters: - weights = chain["weight"] - if "posterior" in parameters: - posterior = chain["posterior"] - parameters = [p for p in parameters if p not in ["weight", "posterior"]] - chain = chain[parameters].to_numpy() - - if grid: - assert walkers is None, "If grid is set, walkers should not be" - assert weights is not None, "If grid is set, you need to supply weights" - if len(weights.shape) > 1: - assert not is_dict, ( - "We cannot construct a meshgrid from a dictionary, as the parameters" - "are no longer ordered. Please pass in a flattened array instead." - ) - self._logger.info("Constructing meshgrid for grid results") - meshes = np.meshgrid(*[u for u in chain.T], indexing="ij") - chain = np.vstack([m.flatten() for m in meshes]).T - weights = weights.flatten() - assert ( - weights.size == chain[:, 0].size - ), "Error, given weight array size disagrees with parameter sampling" - - if len(chain.shape) == 1: - chain = chain[None].T - - if name is None: - name = "Chain %d" % len(self.chains) - - if power is not None: - assert isinstance(power, float | int), "Power should be numeric, but is %s" % type(power) - - if self._default_parameters is None and parameters is not None: - self._default_parameters = parameters - - if parameters is None: - if self._default_parameters is not None: - assert chain.shape[1] == len( - self._default_parameters - ), "Chain has %d dimensions, but default parameters have %d dimensions" % ( - chain.shape[1], - len(self._default_parameters), - ) - parameters = self._default_parameters - self._logger.debug("Adding chain using default parameters") - else: - self._logger.debug("Adding chain with no parameter names") - parameters = ["%d" % x for x in range(chain.shape[1])] - else: - self._logger.debug("Adding chain with defined parameters") - assert len(parameters) <= chain.shape[1], ( - "Have only %d columns in chain, but have been given %d parameters names! " - "Please double check this." - % ( - chain.shape[1], - len(parameters), - ) - ) - for p in parameters: - if p not in self._all_parameters: - self._all_parameters.append(p) - - if shift_params is not None: - if isinstance(shift_params, list): - shift_params = dict([(p, s) for p, s in zip(parameters, shift_params)]) - for key in shift_params: - if key not in parameters: - self._logger.warning(f"Warning, shift parameter {key} is not in list of parameters {parameters}") - - # Sorry, no KDE for you on a grid. - if grid: - kde = None - if color is not None: - color = self.color_finder.get_formatted([color])[0] - - c = Chain( - chain, - parameters, - name, - weights=weights, - posterior=posterior, - walkers=walkers, - grid=grid, - num_free_params=num_free_params, - num_eff_data_points=num_eff_data_points, - color=color, - linewidth=linewidth, - linestyle=linestyle, - kde=kde, - shade_alpha=shade_alpha, - power=power, - marker_style=marker_style, - marker_size=marker_size, - marker_alpha=marker_alpha, - plot_contour=plot_contour, - plot_point=plot_point, - show_as_1d_prior=show_as_1d_prior, - statistics=statistics, - cloud=cloud, - shade=shade, - shade_gradient=shade_gradient, - bar_shade=bar_shade, - bins=bins, - smooth=smooth, - color_params=color_params, - plot_color_params=plot_color_params, - cmap=cmap, - num_cloud=num_cloud, - zorder=zorder, - shift_params=shift_params, - ) - self.chains.append(c) - self._init_params() - return self + def add_chain(self, chain: Chain): + """Add a chain to ChainConsumer. - def add_covariance(self, mean, covariance, parameters=None, name=None, **kwargs): - r"""Generate samples as per mean and covariance supplied. Useful for Fisher matrix forecasts. - - Parameters - ---------- - mean : list|np.ndarray - The an array of mean values. - covariance : list|np.ndarray - The 2D array describing the covariance. Dimensions should agree with the `mean` input. - parameters : list[str], optional - A list of parameter names, one for each column (dimension) in the mean array. - name : str, optional - The name of the chain. Used when plotting multiple chains at once. - kwargs : - Extra arguments about formatting - identical to what you would find in `add_chain`. `linewidth`, `color`, - etc. - - Returns - ------- - ChainConsumer - Itself, to allow chaining calls. + Args: + chain (Chain): The chain to add. + + Returns: + ChainConsumer: Itself, to allow chaining calls. """ - chain = np.random.multivariate_normal(mean, covariance, size=1000000) - self.add_chain(chain, parameters=parameters, name=name, **kwargs) - self.chains[-1].mcmc_chain = False # So we dont plot this when looking at walks, etc + key = chain.name + assert key not in self.chains, f"Chain with name {key} already exists!" + self.chains[key] = chain return self def add_marker( self, - location, - parameters=None, - name=None, - color=None, - marker_size=None, - marker_style=None, - marker_alpha=None, + location: np.ndarray, + columns: list[str], + name: str, + color: ColorInput | None = None, + marker_size: float = 20.0, + marker_style: str = ".", + marker_alpha: float = 1.0, ): r"""Add a marker to the plot at the given location. - Parameters - ---------- - location : list|np.ndarray - The coordinates to place the marker - parameters : list[str], optional - A list of parameter names, one for each column (dimension) in the mean array. - name : str, optional - The name of the chain. Used when plotting multiple chains at once. - color : str(hex), optional - Provide a colour for the chain. Can be used instead of calling `configure` for convenience. - marker_style : str|, optional - The marker style to use when plotting points. Defaults to `'.'` - marker_size : numeric|, optional - Size of markers, if plotted. Defaults to `20`. - marker_alpha : numeric, optional - The alpha values when plotting markers. - - Returns - ------- - ChainConsumer - Itself, to allow chaining calls. + Args: + location (np.ndarray): The location of the marker. + columns (list[str]): The names of the columns in the chain that correspond to the location. + name (str): The name of the marker. + color (ColourInput, optional): The colour of the marker. Defaults to None. + marker_size (float, optional): The size of the marker. Defaults to 20.0. + marker_style (str, optional): The style of the marker. Defaults to ".". + marker_alpha (float, optional): The alpha of the marker. Defaults to 1.0. + + + Returns: + ChainConsumer: Itself, to allow chaining calls. """ - chain = np.vstack((location, location)) - posterior = np.array([0, 1]) - self.add_chain( - chain, - parameters=parameters, - posterior=posterior, + assert len(location.shape) == 1, "Location should be a 1D array" + assert len(location) == len(columns), "Location and columns should be the same length" + + samples = pd.DataFrame(np.atleast_2d(location), columns=columns) + samples["weight"] = 1.0 + samples["posterior"] = 1.0 + chain = Chain( + samples=samples, name=name, color=color, marker_size=marker_size, marker_style=marker_style, marker_alpha=marker_alpha, - plot_point=True, plot_contour=False, + plot_point=True, ) - self.chains[-1].mcmc_chain = False # So we dont plot this when looking at walks, etc + self.add_chain(chain) return self - def remove_chain(self, chain=-1): + def remove_chain(self, remove: str | Chain) -> "ChainConsumer": r"""Removes a chain from ChainConsumer. - Calling this will require any configurations set to be redone! - - Parameters - ---------- - chain : int|str, list[str|int] - The chain(s) to remove. You can pass in either the chain index, or the chain name, to remove it. - By default removes the last chain added. + Args: + remove (str|Chain): The name of the chain to remove, or the chain itself. - Returns - ------- - ChainConsumer - Itself, to allow chaining calls. + Returns: + ChainConsumer: Itself, to allow chaining calls. """ - if isinstance(chain, int | str): - chain = [chain] - - chain = sorted([i for c in chain for i in self._get_chain(c)])[::-1] - assert len(chain) == len(list(set(chain))), "Error, you are trying to remove a chain more than once." - - for index in chain: - del self.chains[index] - - seen = set() - self._all_parameters = [p for c in self.chains for p in c.parameters if not (p in seen or seen.add(p))] - - # Need to reconfigure - self._init_params() + if isinstance(remove, Chain): + remove = remove.name + assert remove in self.chains, f"Chain with name {remove} does not exist!" + self.chains.pop(remove) return self - def configure( + def add_override( self, - statistics="max", - max_ticks=5, - plot_hists=True, - flip=True, - serif=False, - sigma2d=False, - sigmas=None, - summary=None, - bins=None, - cmap=None, - colors=None, - linestyles=None, - linewidths=None, - kde=False, - smooth=None, - cloud=None, - shade=None, - shade_alpha=None, - shade_gradient=None, - bar_shade=None, - num_cloud=None, - color_params=None, - plot_color_params=False, - cmaps=None, - plot_contour=None, - plot_point=None, - show_as_1d_prior=None, - global_point=True, - marker_style=None, - marker_size=None, - marker_alpha=None, - usetex=False, - diagonal_tick_labels=True, - label_font_size=12, - tick_font_size=10, - spacing=None, - contour_labels=None, - contour_label_font_size=10, - legend_kwargs=None, - legend_location=None, - legend_artists=None, - legend_color_text=True, - watermark_text_kwargs=None, - summary_area=0.6827, - zorder=None, - stack=False, - ): # pragma: no cover - r"""Configure the general plotting parameters common across the bar - and contour plots. - - If you do not call this explicitly, the :func:`plot` - method will invoke this method automatically. - - Please ensure that you call this method *after* adding all the relevant data to the - chain consumer, as the consume changes configuration values depending on - the supplied data. - - Parameters - ---------- - statistics : string|list[str], optional - Which sort of statistics to use. Defaults to `"max"` for maximum likelihood - statistics. Other available options are `"mean"`, `"cumulative"`, `"max_symmetric"`, - `"max_closest"` and `"max_central"`. In the - very, very rare case you want to enable different statistics for different - chains, you can pass in a list of strings. - max_ticks : int, optional - The maximum number of ticks to use on the plots - plot_hists : bool, optional - Whether to plot marginalised distributions or not - flip : bool, optional - Set to false if, when plotting only two parameters, you do not want it to - rotate the histogram so that it is horizontal. - sigma2d: bool, optional - Defaults to `False`. When `False`, uses :math:`\sigma` levels for 1D Gaussians - ie confidence - levels of 68% and 95%. When `True`, uses the confidence levels for 2D Gaussians, where 1 and 2 - :math:`\sigma` represents 39% and 86% confidence levels respectively. - sigmas : np.array, optional - The :math:`\sigma` contour levels to plot. Defaults to [0, 1, 2, 3] for a single chain - and [0, 1, 2] for multiple chains. - serif : bool, optional - Whether to display ticks and labels with serif font. - summary : bool, optional - If overridden, sets whether parameter summaries should be set as axis titles. - Will not work if you have multiple chains - bins : int|float,list[int|float], optional - The number of bins to use. By default uses :math:`\frac{\sqrt{n}}{10}`, where - :math:`n` are the number of data points. Giving an integer will set the number - of bins to the given value. Giving a float will scale the number of bins, such - that giving ``bins=1.5`` will result in using :math:`\frac{1.5\sqrt{n}}{10}` bins. - Note this parameter is most useful if `kde=False` is also passed, so you - can actually see the bins and not a KDE. - cmap : str, optional - Set to the matplotlib colour map you want to use to overwrite the default colours. - Note that this parameter overwrites colours. The `cmaps` parameters is different, - and used when you ask for an extra dimension to be used to colour scatter points. - See the online examples to see the difference. - colors : str(hex)|list[str(hex)], optional - Provide a list of colours to use for each chain. If you provide more chains - than colours, you *will* get the rainbow colour spectrum. If you only pass - one colour, all chains are set to this colour. This probably won't look good. - linestyles : str|list[str], optional - Provide a list of line styles to plot the contours and marginalised - distributions with. By default, this will become a list of solid lines. If a - string is passed instead of a list, this style is used for all chains. - linewidths : float|list[float], optional - Provide a list of line widths to plot the contours and marginalised - distributions with. By default, this is a width of 1. If a float - is passed instead of a list, this width is used for all chains. - kde : bool|float|list[bool|float], optional - Whether to use a Gaussian KDE to smooth marginalised posteriors. If false, uses - bins and linear interpolation, so ensure you have plenty of samples if your - distribution is highly non-gaussian. Due to the slowness of performing a - KDE on all data, it is often useful to disable this before producing final - plots. If float, scales the width of the KDE bandpass manually. - smooth : int|list[int], optional - Defaults to 3. How much to smooth the marginalised distributions using a gaussian filter. - If ``kde`` is set to true, this parameter is ignored. Setting it to either - ``0``, ``False`` disables smoothing. For grid data, smoothing - is set to 0 by default, not 3. - cloud : bool|list[bool], optional - If set, overrides the default behaviour and plots the cloud or not - shade : bool|list[bool] optional - If set, overrides the default behaviour and plots filled contours or not. If a list of - bools is passed, you can turn shading on or off for specific chains. - shade_alpha : float|list[float], optional - Filled contour alpha value override. Default is 1.0. If a list is passed, you can set the - shade opacity for specific chains. - shade_gradient : float|list[float], optional - How much to vary colours in different contour levels. - bar_shade : bool|list[bool], optional - If set to true, shades in confidence regions in under histogram. By default - this happens if you less than 3 chains, but is disabled if you are comparing - more chains. You can pass a list if you wish to shade some chains but not others. - num_cloud : int|list[int], optional - The number of scatter points to show when enabling `cloud` or setting one of the parameters - to colour scatter. Defaults to 15k per chain. - color_params : str|list[str], optional - The name of the parameter to use for the colour scatter. Defaults to none, for no colour. If set - to 'weights', 'log_weights', or 'posterior' (without the quotes), and that is not a parameter in the chain, - it will respectively use the weights, log weights, or posterior, to colour the points. - plot_color_params : bool|list[bool], optional - Whether or not the colour parameter should also be plotted as a posterior surface. - cmaps : str|list[str], optional - The matplotlib colourmap to use in the `colour_param`. If you have multiple `color_param`s, you can - specific a different cmap for each variable. By default ChainConsumer will cycle between several - cmaps. - plot_contour : bool|list[bool], optional - Whether to plot the whole contour (as opposed to a point). Defaults to true for less than - 25 concurrent chains. - plot_point : bool|list[bool], optional - Whether to plot a maximum likelihood point. Defaults to true for more then 24 chains. - show_as_1d_prior : bool|list[bool], optional - Showing as a 1D prior will show the 1D histograms, but won't plot the 2D contours. - global_point : bool, optional - Whether the point which gets plotted is the global posterior maximum, or the marginalised 2D - posterior maximum. Note that when you use marginalised 2D maximums for the points, you do not - get the 1D histograms. Defaults to `True`, for a global maximum value. - marker_style : str|list[str], optional - The marker style to use when plotting points. Defaults to `'.'` - marker_size : numeric|list[numeric], optional - Size of markers, if plotted. Defaults to `20`. - marker_alpha : numeric|list[numeric], optional - The alpha values when plotting markers. - usetex : bool, optional - Whether or not to parse text as LaTeX in plots. - diagonal_tick_labels : bool, optional - Whether to display tick labels on a 45 degree angle. - label_font_size : int|float, optional - The font size for plot axis labels and axis titles if summaries are configured to display. - tick_font_size : int|float, optional - The font size for the tick labels in the plots. - spacing : float, optional - The amount of spacing to add between plots. Defaults to `None`, which equates to 1.0 for less - than 6 dimensions and 0.0 for higher dimensions. - contour_labels : string, optional - If unset do not plot contour labels. If set to "confidence", label the using confidence - intervals. If set to "sigma", labels using sigma. - contour_label_font_size : int|float, optional - The font size for contour labels, if they are enabled. - legend_kwargs : dict, optional - Extra arguments to pass to the legend api. - legend_location : tuple(int,int), optional - Specifies the subplot in which to locate the legend. By default, this will be (0, -1), - corresponding to the top right subplot if there are more than two parameters, - and the bottom left plot for only two parameters with flip on. - For having the legend in the primary subplot - in the bottom left, set to (-1,0). - legend_artists : bool, optional - Whether to include hide artists in the legend. If all linestyles and line widths are identical, - this will default to false (as only the colours change). Otherwise it will be true. - legend_color_text : bool, optional - Whether to colour the legend text. - watermark_text_kwargs : dict, optional - Options to pass to the fontdict property when generating text for the watermark. - summary_area : float, optional - The confidence interval used when generating parameter summaries. Defaults to 1 sigma, aka 0.6827 - zorder : int, optional - The zorder to pass to `matplotlib` to determine visual ordering when plotting. - - Returns - ------- - ChainConsumer - Itself, to allow chaining calls. - """ - # Warn the user if configure has been invoked multiple times - self._num_configure_calls += 1 - if self._num_configure_calls > 1: - self._logger.warning( - "Configure has been called %d times - this is not good - it should be once!" % self._num_configure_calls - ) - self._logger.warning("To avoid this, load your chains in first, then call analysis/plotting methods") - - # Dirty way of ensuring overrides happen when requested - l = locals() - explicit = [] - for k in l: - if l[k] is not None: - explicit.append(k) - if k.endswith("s"): - explicit.append(k[:-1]) - self._init_params() - - num_chains = len(self.chains) - - assert cmap is None or colors is None, "You cannot both ask for cmap colours and then give explicit colours" - - # Determine statistics - assert statistics is not None, "statistics should be a string or list of strings!" - if isinstance(statistics, str): - assert statistics in list(Analysis.summaries), "statistics {} not recognised. Should be in {}".format( - statistics, - Analysis.summaries, - ) - statistics = [statistics.lower()] * len(self.chains) - elif isinstance(statistics, list): - for i, l in enumerate(statistics): - statistics[i] = l.lower() - else: - raise ValueError("statistics is not a string or a list!") - - # Determine KDEs - if isinstance(kde, bool | float): - kde = [False if c.grid else kde for c in self.chains] - - kde_override = [c.kde for c in self.chains] - kde = [c2 if c2 is not None else c1 for c1, c2 in zip(kde, kde_override)] - - # Determine bins - if bins is None: - bins = get_bins(self.chains) - elif isinstance(bins, list): - bins = [b2 if isinstance(b2, int) else np.floor(b2 * b1) for b1, b2 in zip(get_bins(self.chains), bins)] - elif isinstance(bins, float): - bins = [np.floor(b * bins) for b in get_bins(self.chains)] - elif isinstance(bins, int): - bins = [bins] * len(self.chains) - else: - raise ValueError("bins value is not a recognised class (float or int)") - - # Determine smoothing - if smooth is None: - smooth = [0 if c.grid or k else 3 for c, k in zip(self.chains, kde)] - else: - if smooth is not None and not smooth: - smooth = 0 - if isinstance(smooth, list): - smooth = [0 if k else s for s, k in zip(smooth, kde)] - else: - smooth = [0 if k else smooth for k in kde] - - # Determine color parameters - if color_params is None: - color_params = [None] * num_chains - else: - if isinstance(color_params, str): - color_params = [ - color_params if color_params in [*cs.parameters, "log_weights", "weights", "posterior"] else None - for cs in self.chains - ] - color_params = [ - None if c == "posterior" and self.chains[i].posterior is None else c - for i, c in enumerate(color_params) - ] - elif isinstance(color_params, list | tuple): - for c, chain in zip(color_params, self.chains): - p = chain.parameters - if c is not None: - assert c in p, f"Color parameter {c} not in parameters {p}" - # Determine if we should plot color parameters - if isinstance(plot_color_params, bool): - plot_color_params = [plot_color_params] * len(color_params) - - # Determine cmaps - if cmaps is None: - param_cmaps = {} - cmaps = [] - i = 0 - for cp in color_params: - if cp is None: - cmaps.append(None) - elif cp in param_cmaps: - cmaps.append(param_cmaps[cp]) - else: - param_cmaps[cp] = self._cmaps[i] - cmaps.append(self._cmaps[i]) - i = (i + 1) % len(self._cmaps) - - # Determine colours - if colors is None: - if cmap: - colors = self.color_finder.get_colormap(num_chains, cmap) - else: - if num_chains > len(self._all_colours): - num_needed_colours = np.sum([c is None for c in color_params]) - colour_list = self.color_finder.get_colormap(num_needed_colours, "inferno") - else: - colour_list = self._all_colours - colors = [] - ci = 0 - for c in color_params: - if c: - colors.append("#000000") - else: - colors.append(colour_list[ci]) - ci += 1 - elif isinstance(colors, str): - colors = [colors] * len(self.chains) - colors = self.color_finder.get_formatted(colors) - - # Determine linestyles - if linestyles is None: - i = 0 - linestyles = [] - for c in color_params: - if c is None: - linestyles.append(self._linestyles[0]) - else: - linestyles.append(self._linestyles[i]) - i = (i + 1) % len(self._linestyles) - elif isinstance(linestyles, str): - linestyles = [linestyles] * len(self.chains) - - # Determine linewidths - if linewidths is None: - linewidths = [1.0] * len(self.chains) - elif isinstance(linewidths, float | int): - linewidths = [linewidths] * len(self.chains) - - # Determine clouds - if cloud is None: - cloud = False - cloud = [cloud or c is not None for c in color_params] - - # Determine cloud points - if num_cloud is None: - num_cloud = 30000 - if isinstance(num_cloud, float | int): - num_cloud = [int(num_cloud)] * num_chains - - # Should we shade the contours - if shade is None: - shade = num_chains <= 3 if shade_alpha is None else True - if isinstance(shade, bool): - # If not overridden, do not shade chains with colour scatter points - shade = [shade and c is None for c in color_params] - - # Modify shade alpha based on how many chains we have - if shade_alpha is None: - if num_chains == 1: - shade_alpha = 0.75 if contour_labels is not None else 1.0 - else: - shade_alpha = 1.0 / np.sqrt(num_chains) - # Decrease the shading amount if there are colour scatter points - if isinstance(shade_alpha, float | int): - shade_alpha = [shade_alpha if c is None else 0.25 * shade_alpha for c in color_params] - - if shade_gradient is None: - shade_gradient = 1.0 - if isinstance(shade_gradient, float): - shade_gradient = [shade_gradient] * num_chains - elif isinstance(shade_gradient, list): - assert len(shade_gradient) == num_chains, "Have %d shade_gradient but % chains" % ( - len(shade_gradient), - num_chains, - ) - - contour_over_points = num_chains < 20 - - if plot_contour is None: - plot_contour = [contour_over_points if chain.posterior is not None else True for chain in self.chains] - elif isinstance(plot_contour, bool): - plot_contour = [plot_contour] * num_chains - - if plot_point is None: - plot_point = [not contour_over_points] * num_chains - elif isinstance(plot_point, bool): - plot_point = [plot_point] * num_chains - - if show_as_1d_prior is None: - show_as_1d_prior = [not contour_over_points] * num_chains - elif isinstance(show_as_1d_prior, bool): - show_as_1d_prior = [show_as_1d_prior] * num_chains - - if marker_style is None: - marker_style = ["."] * num_chains - elif isinstance(marker_style, str): - marker_style = [marker_style] * num_chains - - if marker_size is None: - marker_size = [20] * num_chains - elif isinstance(marker_style, int | float): - marker_size = [marker_size] * num_chains - - if marker_alpha is None: - marker_alpha = [1.0] * num_chains - elif isinstance(marker_alpha, int | float): - marker_alpha = [marker_alpha] * num_chains - - # Figure out if we should display parameter summaries - if summary is not None: - summary = summary and num_chains == 1 - - # Figure out bar shading - if bar_shade is None: - bar_shade = num_chains <= 3 - if isinstance(bar_shade, bool): - bar_shade = [bar_shade] * num_chains - - if zorder is None: - zorder = [1] * num_chains - - # Figure out how many sigmas to plot - if sigmas is None: - sigmas = np.array([0, 1, 2]) if num_chains == 1 else np.array([0, 1, 2]) - if sigmas[0] != 0: - sigmas = np.concatenate(([0], sigmas)) - sigmas = np.sort(sigmas) - - if contour_labels is not None: - assert isinstance(contour_labels, str), "contour_labels parameter should be a string" - contour_labels = contour_labels.lower() - assert contour_labels in [ - "sigma", - "confidence", - ], "contour_labels should be either sigma or confidence" - assert isinstance(contour_label_font_size, float | int), "contour_label_font_size needs to be numeric" - - if legend_artists is None: - legend_artists = len(set(linestyles)) > 1 or len(set(linewidths)) > 1 - - if legend_kwargs is not None: - assert isinstance(legend_kwargs, dict), "legend_kwargs should be a dict" - else: - legend_kwargs = {} - - if num_chains < 3: - labelspacing = 0.5 - elif num_chains == 3: - labelspacing = 0.2 - else: - labelspacing = 0.15 - legend_kwargs_default = { - "labelspacing": labelspacing, - "loc": "upper right", - "frameon": False, - "fontsize": label_font_size, - "handlelength": 1, - "handletextpad": 0.2, - "borderaxespad": 0.0, - } - legend_kwargs_default.update(legend_kwargs) - - watermark_text_kwargs_default = { - "color": "#333333", - "alpha": 0.7, - "verticalalignment": "center", - "horizontalalignment": "center", - } - if watermark_text_kwargs is None: - watermark_text_kwargs = {} - watermark_text_kwargs_default.update(watermark_text_kwargs) - - assert isinstance(summary_area, float), "summary_area needs to be a float, not %s!" % type(summary_area) - assert summary_area > 0, "summary_area should be a positive number, instead is %s!" % summary_area - assert summary_area < 1, "summary_area must be less than unity, instead is %s!" % summary_area - assert isinstance(global_point, bool), "global_point should be a bool" - - # List options - for i, c in enumerate(self.chains): - try: - c.update_unset_config("statistics", statistics[i], override=explicit) - c.update_unset_config("color", colors[i], override=explicit) - c.update_unset_config("linestyle", linestyles[i], override=explicit) - c.update_unset_config("linewidth", linewidths[i], override=explicit) - c.update_unset_config("cloud", cloud[i], override=explicit) - c.update_unset_config("shade", shade[i], override=explicit) - c.update_unset_config("shade_alpha", shade_alpha[i], override=explicit) - c.update_unset_config("shade_gradient", shade_gradient[i], override=explicit) - c.update_unset_config("bar_shade", bar_shade[i], override=explicit) - c.update_unset_config("bins", bins[i], override=explicit) - c.update_unset_config("kde", kde[i], override=explicit) - c.update_unset_config("smooth", smooth[i], override=explicit) - c.update_unset_config("color_params", color_params[i], override=explicit) - c.update_unset_config("plot_color_params", plot_color_params[i], override=explicit) - c.update_unset_config("cmap", cmaps[i], override=explicit) - c.update_unset_config("num_cloud", num_cloud[i], override=explicit) - c.update_unset_config("marker_style", marker_style[i], override=explicit) - c.update_unset_config("marker_size", marker_size[i], override=explicit) - c.update_unset_config("marker_alpha", marker_alpha[i], override=explicit) - c.update_unset_config("plot_contour", plot_contour[i], override=explicit) - c.update_unset_config("plot_point", plot_point[i], override=explicit) - c.update_unset_config("show_as_1d_prior", show_as_1d_prior[i], override=explicit) - c.update_unset_config("zorder", zorder[i], override=explicit) - c.config["summary_area"] = summary_area - - except IndentationError: - print( - "Index error when assigning chain properties, make sure you " - "have enough properties set for the number of chains you have loaded! " - "See the stack trace for which config item has the wrong number of entries." - ) - raise - - # Non list options - self.config["sigma2d"] = sigma2d - self.config["sigmas"] = sigmas - self.config["summary"] = summary - self.config["flip"] = flip - self.config["serif"] = serif - self.config["plot_hists"] = plot_hists - self.config["max_ticks"] = max_ticks - self.config["usetex"] = usetex - self.config["diagonal_tick_labels"] = diagonal_tick_labels - self.config["label_font_size"] = label_font_size - self.config["tick_font_size"] = tick_font_size - self.config["spacing"] = spacing - self.config["contour_labels"] = contour_labels - self.config["contour_label_font_size"] = contour_label_font_size - self.config["legend_location"] = legend_location - self.config["legend_kwargs"] = legend_kwargs_default - self.config["legend_artists"] = legend_artists - self.config["legend_color_text"] = legend_color_text - self.config["watermark_text_kwargs"] = watermark_text_kwargs_default - self.config["global_point"] = global_point - - self._configured = True - return self - - def configure_truth(self, **kwargs): # pragma: no cover - r"""Configure the arguments passed to the ``axvline`` and ``axhline`` - methods when plotting truth values. - - If you do not call this explicitly, the :func:`plot` method will - invoke this method automatically. - - Recommended to set the parameters ``linestyle``, ``color`` and/or ``alpha`` - if you want some basic control. + override: Config, + ) -> "ChainConsumer": + """Apply a custom override config - Default is to use an opaque black dashed line. + Args: + override (Config, optional): The override config. Defaults to None. - Parameters - ---------- - kwargs : dict - The keyword arguments to unwrap when calling ``axvline`` and ``axhline``. - - Returns - ------- - ChainConsumer - Itself, to allow chaining calls. + Returns: + ChainConsumer: Itself, to allow chaining calls. """ - if kwargs.get("ls") is None and kwargs.get("linestyle") is None: - kwargs["ls"] = "--" - # kwargs["dashes"] = (3, 3) - if kwargs.get("lw") is None and kwargs.get("linewidth") is None: - kwargs["linewidth"] = 1 - if kwargs.get("color") is None: - kwargs["color"] = "#000000" - if kwargs.get("zorder") is None: - kwargs["zorder"] = 100 - self.config_truth = kwargs - self._configured_truth = True + self.override = override return self - def divide_chain(self, chain=0): - r""" - Returns a ChainConsumer instance containing all the walks of a given chain - as individual chains themselves. - - This method might be useful if, for example, your chain was made using - MCMC with 4 walkers. To check the sampling of all 4 walkers agree, you could - call this to get a ChainConsumer instance with one chain for ech of the - four walks. If you then plot, hopefully all four contours - you would see agree. - - Parameters - ---------- - chain : int|str, optional - The index or name of the chain you want divided - - Returns - ------- - ChainConsumer - A new ChainConsumer instance with the same settings as the parent instance, containing - ``num_walker`` chains. - """ - indexes = self._get_chain(chain) - con = ChainConsumer() - - for index in indexes: - chain = self.chains[index] - assert chain.walkers is not None, "The chain you have selected was not added with any walkers!" - num_walkers = chain.walkers - data = np.split(chain.chain, num_walkers) - ws = np.split(chain.weights, num_walkers) - for j, (c, w) in enumerate(zip(data, ws)): - con.add_chain(c, weights=w, name="Chain %d" % j, parameters=chain.parameters) - return con - - def _get_chain(self, chain): - if isinstance(chain, Chain): - return [self.chains.index(chain)] - if isinstance(chain, str): - names = [c.name for c in self.chains] - assert chain in names, "Chain %s not found!" % chain - index = [i for i, n in enumerate(names) if chain == n] - elif isinstance(chain, int): - assert chain < len(self.chains), "Chain index %d not found!" % chain - index = [chain] - else: - raise ValueError("Type %s not recognised for chain" % type(chain)) - return index - - def _get_chain_name(self, index): - return self.chains[index].name - - def _all_names(self): - return [c.name for c in self.chains] + def _get_final_chains(self) -> dict[ChainName, Chain]: + # Copy the original chain list + final_chains = {k: v.model_copy() for k, v in self.chains.items()} + chain_list = list(final_chains.values()) + num_chains = len(self.chains) + + # Note we only have to override things without a default + # and things which should change as the number of chains change + global_config = {} + global_config["bar_shade"] = num_chains < 5 + global_config["sigmas"] = [0, 1, 2] + global_config["shade"] = num_chains < 5 + global_config["bins"] = get_bins(chain_list) + global_config["shade_alpha"] = 1.0 / np.sqrt(num_chains) + + for _, chain in final_chains.items(): + # copy global config into local config + local_config = global_config.copy() + + # Reduce shade alpha if we're showing contour labels + if chain.show_contour_labels: + local_config["shade_alpha"] *= 0.5 + + # Check to see if the color is set + if chain.color is None: + local_config["color"] = next(colors.next_colour()) + + chain.apply_if_none(**local_config) + + # Apply user overrides + if self.override is not None: + chain.apply(**self.override.model_dump()) + + return final_chains + + # def configure_overrides( + # self, + # statistics="max", + # max_ticks=5, + # plot_hists=True, + # flip=True, + # serif=False, + # sigma2d=False, + # sigmas=None, + # summary=None, + # bins=None, + # cmap=None, + # colors=None, + # linestyles=None, + # linewidths=None, + # kde=False, + # smooth=None, + # cloud=None, + # shade=None, + # shade_alpha=None, + # shade_gradient=None, + # bar_shade=None, + # num_cloud=None, + # color_params=None, + # plot_color_params=False, + # cmaps=None, + # plot_contour=None, + # plot_point=None, + # show_as_1d_prior=None, + # global_point=True, + # marker_style=None, + # marker_size=None, + # marker_alpha=None, + # usetex=False, + # diagonal_tick_labels=True, + # label_font_size=12, + # tick_font_size=10, + # spacing=None, + # contour_labels=None, + # contour_label_font_size=10, + # legend_kwargs=None, + # legend_location=None, + # legend_artists=None, + # legend_color_text=True, + # watermark_text_kwargs=None, + # summary_area=0.6827, + # zorder=None, + # ): # pragma: no cover + # r"""Configure the general plotting parameters common across the bar + # and contour plots. + + # If you do not call this explicitly, the :func:`plot` + # method will invoke this method automatically. + + # Please ensure that you call this method *after* adding all the relevant data to the + # chain consumer, as the consume changes configuration values depending on + # the supplied data. + + # Parameters + # ---------- + # statistics : string|list[str], optional + # Which sort of statistics to use. Defaults to `"max"` for maximum likelihood + # statistics. Other available options are `"mean"`, `"cumulative"`, `"max_symmetric"`, + # `"max_closest"` and `"max_central"`. In the + # very, very rare case you want to enable different statistics for different + # chains, you can pass in a list of strings. + # max_ticks : int, optional + # The maximum number of ticks to use on the plots + # plot_hists : bool, optional + # Whether to plot marginalised distributions or not + # flip : bool, optional + # Set to false if, when plotting only two parameters, you do not want it to + # rotate the histogram so that it is horizontal. + # sigma2d: bool, optional + # Defaults to `False`. When `False`, uses :math:`\sigma` levels for 1D Gaussians - ie confidence + # levels of 68% and 95%. When `True`, uses the confidence levels for 2D Gaussians, where 1 and 2 + # :math:`\sigma` represents 39% and 86% confidence levels respectively. + # sigmas : np.array, optional + # The :math:`\sigma` contour levels to plot. Defaults to [0, 1, 2, 3] for a single chain + # and [0, 1, 2] for multiple chains. + # serif : bool, optional + # Whether to display ticks and labels with serif font. + # summary : bool, optional + # If overridden, sets whether parameter summaries should be set as axis titles. + # Will not work if you have multiple chains + # bins : int|float,list[int|float], optional + # The number of bins to use. By default uses :math:`\frac{\sqrt{n}}{10}`, where + # :math:`n` are the number of data points. Giving an integer will set the number + # of bins to the given value. Giving a float will scale the number of bins, such + # that giving ``bins=1.5`` will result in using :math:`\frac{1.5\sqrt{n}}{10}` bins. + # Note this parameter is most useful if `kde=False` is also passed, so you + # can actually see the bins and not a KDE. + # cmap : str, optional + # Set to the matplotlib colour map you want to use to overwrite the default colours. + # Note that this parameter overwrites colours. The `cmaps` parameters is different, + # and used when you ask for an extra dimension to be used to colour scatter points. + # See the online examples to see the difference. + # colors : str(hex)|list[str(hex)], optional + # Provide a list of colours to use for each chain. If you provide more chains + # than colours, you *will* get the rainbow colour spectrum. If you only pass + # one colour, all chains are set to this colour. This probably won't look good. + # linestyles : str|list[str], optional + # Provide a list of line styles to plot the contours and marginalised + # distributions with. By default, this will become a list of solid lines. If a + # string is passed instead of a list, this style is used for all chains. + # linewidths : float|list[float], optional + # Provide a list of line widths to plot the contours and marginalised + # distributions with. By default, this is a width of 1. If a float + # is passed instead of a list, this width is used for all chains. + # kde : bool|float|list[bool|float], optional + # Whether to use a Gaussian KDE to smooth marginalised posteriors. If false, uses + # bins and linear interpolation, so ensure you have plenty of samples if your + # distribution is highly non-gaussian. Due to the slowness of performing a + # KDE on all data, it is often useful to disable this before producing final + # plots. If float, scales the width of the KDE bandpass manually. + # smooth : int|list[int], optional + # Defaults to 3. How much to smooth the marginalised distributions using a gaussian filter. + # If ``kde`` is set to true, this parameter is ignored. Setting it to either + # ``0``, ``False`` disables smoothing. For grid data, smoothing + # is set to 0 by default, not 3. + # cloud : bool|list[bool], optional + # If set, overrides the default behaviour and plots the cloud or not + # shade : bool|list[bool] optional + # If set, overrides the default behaviour and plots filled contours or not. If a list of + # bools is passed, you can turn shading on or off for specific chains. + # shade_alpha : float|list[float], optional + # Filled contour alpha value override. Default is 1.0. If a list is passed, you can set the + # shade opacity for specific chains. + # shade_gradient : float|list[float], optional + # How much to vary colours in different contour levels. + # bar_shade : bool|list[bool], optional + # If set to true, shades in confidence regions in under histogram. By default + # this happens if you less than 3 chains, but is disabled if you are comparing + # more chains. You can pass a list if you wish to shade some chains but not others. + # num_cloud : int|list[int], optional + # The number of scatter points to show when enabling `cloud` or setting one of the parameters + # to colour scatter. Defaults to 15k per chain. + # color_params : str|list[str], optional + # The name of the parameter to use for the colour scatter. Defaults to none, for no colour. If set + # to 'weights', 'log_weights', or 'posterior' (without the quotes), and that is not a parameter in the chain, + # it will respectively use the weights, log weights, or posterior, to colour the points. + # plot_color_params : bool|list[bool], optional + # Whether or not the colour parameter should also be plotted as a posterior surface. + # cmaps : str|list[str], optional + # The matplotlib colourmap to use in the `colour_param`. If you have multiple `color_param`s, you can + # specific a different cmap for each variable. By default ChainConsumer will cycle between several + # cmaps. + # plot_contour : bool|list[bool], optional + # Whether to plot the whole contour (as opposed to a point). Defaults to true for less than + # 25 concurrent chains. + # plot_point : bool|list[bool], optional + # Whether to plot a maximum likelihood point. Defaults to true for more then 24 chains. + # show_as_1d_prior : bool|list[bool], optional + # Showing as a 1D prior will show the 1D histograms, but won't plot the 2D contours. + # global_point : bool, optional + # Whether the point which gets plotted is the global posterior maximum, or the marginalised 2D + # posterior maximum. Note that when you use marginalised 2D maximums for the points, you do not + # get the 1D histograms. Defaults to `True`, for a global maximum value. + # marker_style : str|list[str], optional + # The marker style to use when plotting points. Defaults to `'.'` + # marker_size : numeric|list[numeric], optional + # Size of markers, if plotted. Defaults to `20`. + # marker_alpha : numeric|list[numeric], optional + # The alpha values when plotting markers. + # usetex : bool, optional + # Whether or not to parse text as LaTeX in plots. + # diagonal_tick_labels : bool, optional + # Whether to display tick labels on a 45 degree angle. + # label_font_size : int|float, optional + # The font size for plot axis labels and axis titles if summaries are configured to display. + # tick_font_size : int|float, optional + # The font size for the tick labels in the plots. + # spacing : float, optional + # The amount of spacing to add between plots. Defaults to `None`, which equates to 1.0 for less + # than 6 dimensions and 0.0 for higher dimensions. + # contour_labels : string, optional + # If unset do not plot contour labels. If set to "confidence", label the using confidence + # intervals. If set to "sigma", labels using sigma. + # contour_label_font_size : int|float, optional + # The font size for contour labels, if they are enabled. + # legend_kwargs : dict, optional + # Extra arguments to pass to the legend api. + # legend_location : tuple(int,int), optional + # Specifies the subplot in which to locate the legend. By default, this will be (0, -1), + # corresponding to the top right subplot if there are more than two parameters, + # and the bottom left plot for only two parameters with flip on. + # For having the legend in the primary subplot + # in the bottom left, set to (-1,0). + # legend_artists : bool, optional + # Whether to include hide artists in the legend. If all linestyles and line widths are identical, + # this will default to false (as only the colours change). Otherwise it will be true. + # legend_color_text : bool, optional + # Whether to colour the legend text. + # watermark_text_kwargs : dict, optional + # Options to pass to the fontdict property when generating text for the watermark. + # summary_area : float, optional + # The confidence interval used when generating parameter summaries. Defaults to 1 sigma, aka 0.6827 + # zorder : int, optional + # The zorder to pass to `matplotlib` to determine visual ordering when plotting. + + # Returns + # ------- + # ChainConsumer + # Itself, to allow chaining calls. + # """ + # # Warn the user if configure has been invoked multiple times + # self._num_configure_calls += 1 + # if self._num_configure_calls > 1: + # logger.warning( + # "Configure has been called %d times - this is not good - it should be once!" % self._num_configure_calls + # ) + # logger.warning("To avoid this, load your chains in first, then call analysis/plotting methods") + + # # Dirty way of ensuring overrides happen when requested + # l = locals() + # explicit = [] + # for k in l: + # if l[k] is not None: + # explicit.append(k) + # if k.endswith("s"): + # explicit.append(k[:-1]) + # self._init_params() + + # num_chains = len(self.chains) + + # assert cmap is None or colors is None, "You cannot both ask for cmap colours and then give explicit colours" + + # # Determine statistics + # assert statistics is not None, "statistics should be a string or list of strings!" + # if isinstance(statistics, str): + # assert statistics in list(Analysis.summaries), "statistics {} not recognised. Should be in {}".format( + # statistics, + # Analysis.summaries, + # ) + # statistics = [statistics.lower()] * len(self.chains) + # elif isinstance(statistics, list): + # for i, l in enumerate(statistics): + # statistics[i] = l.lower() + # else: + # raise ValueError("statistics is not a string or a list!") + + # # Determine KDEs + # if isinstance(kde, bool | float): + # kde = [False if c.grid else kde for c in self.chains] + + # kde_override = [c.kde for c in self.chains] + # kde = [c2 if c2 is not None else c1 for c1, c2 in zip(kde, kde_override)] + + # # Determine bins + # if bins is None: + # bins = get_bins(self.chains) + # elif isinstance(bins, list): + # bins = [b2 if isinstance(b2, int) else np.floor(b2 * b1) for b1, b2 in zip(get_bins(self.chains), bins)] + # elif isinstance(bins, float): + # bins = [np.floor(b * bins) for b in get_bins(self.chains)] + # elif isinstance(bins, int): + # bins = [bins] * len(self.chains) + # else: + # raise ValueError("bins value is not a recognised class (float or int)") + + # # Determine smoothing + # if smooth is None: + # smooth = [0 if c.grid or k else 3 for c, k in zip(self.chains, kde)] + # else: + # if smooth is not None and not smooth: + # smooth = 0 + # if isinstance(smooth, list): + # smooth = [0 if k else s for s, k in zip(smooth, kde)] + # else: + # smooth = [0 if k else smooth for k in kde] + + # # Determine color parameters + # if color_params is None: + # color_params = [None] * num_chains + # else: + # if isinstance(color_params, str): + # color_params = [ + # color_params if color_params in [*cs.parameters, "log_weights", "weights", "posterior"] else None + # for cs in self.chains + # ] + # color_params = [ + # None if c == "posterior" and self.chains[i].posterior is None else c + # for i, c in enumerate(color_params) + # ] + # elif isinstance(color_params, list | tuple): + # for c, chain in zip(color_params, self.chains): + # p = chain.parameters + # if c is not None: + # assert c in p, f"Color parameter {c} not in parameters {p}" + # # Determine if we should plot color parameters + # if isinstance(plot_color_params, bool): + # plot_color_params = [plot_color_params] * len(color_params) + + # # Determine cmaps + # if cmaps is None: + # param_cmaps = {} + # cmaps = [] + # i = 0 + # for cp in color_params: + # if cp is None: + # cmaps.append(None) + # elif cp in param_cmaps: + # cmaps.append(param_cmaps[cp]) + # else: + # param_cmaps[cp] = self._cmaps[i] + # cmaps.append(self._cmaps[i]) + # i = (i + 1) % len(self._cmaps) + + # # Determine colours + # if colors is None: + # if cmap: + # colors = colors.get_colormap(num_chains, cmap) + # else: + # if num_chains > len(self._all_colours): + # num_needed_colours = np.sum([c is None for c in color_params]) + # colour_list = colors.get_colormap(num_needed_colours, "inferno") + # else: + # colour_list = self._all_colours + # colors = [] + # ci = 0 + # for c in color_params: + # if c: + # colors.append("#000000") + # else: + # colors.append(colour_list[ci]) + # ci += 1 + # elif isinstance(colors, str): + # colors = [colors] * len(self.chains) + # colors = colors.get_formatted(colors) + + # # Determine linestyles + # if linestyles is None: + # i = 0 + # linestyles = [] + # for c in color_params: + # if c is None: + # linestyles.append(self._linestyles[0]) + # else: + # linestyles.append(self._linestyles[i]) + # i = (i + 1) % len(self._linestyles) + # elif isinstance(linestyles, str): + # linestyles = [linestyles] * len(self.chains) + + # # Determine linewidths + # if linewidths is None: + # linewidths = [1.0] * len(self.chains) + # elif isinstance(linewidths, float | int): + # linewidths = [linewidths] * len(self.chains) + + # # Determine clouds + # if cloud is None: + # cloud = False + # cloud = [cloud or c is not None for c in color_params] + + # # Determine cloud points + # if num_cloud is None: + # num_cloud = 30000 + # if isinstance(num_cloud, float | int): + # num_cloud = [int(num_cloud)] * num_chains + + # # Should we shade the contours + # if shade is None: + # shade = num_chains <= 3 if shade_alpha is None else True + # if isinstance(shade, bool): + # # If not overridden, do not shade chains with colour scatter points + # shade = [shade and c is None for c in color_params] + + # # Modify shade alpha based on how many chains we have + # if shade_alpha is None: + # if num_chains == 1: + # shade_alpha = 0.75 if contour_labels is not None else 1.0 + # else: + # shade_alpha = 1.0 / np.sqrt(num_chains) + # # Decrease the shading amount if there are colour scatter points + # if isinstance(shade_alpha, float | int): + # shade_alpha = [shade_alpha if c is None else 0.25 * shade_alpha for c in color_params] + + # if shade_gradient is None: + # shade_gradient = 1.0 + # if isinstance(shade_gradient, float): + # shade_gradient = [shade_gradient] * num_chains + # elif isinstance(shade_gradient, list): + # assert len(shade_gradient) == num_chains, "Have %d shade_gradient but % chains" % ( + # len(shade_gradient), + # num_chains, + # ) + + # contour_over_points = num_chains < 20 + + # if plot_contour is None: + # plot_contour = [contour_over_points if chain.log_posterior is not None else True for chain in self.chains] + # elif isinstance(plot_contour, bool): + # plot_contour = [plot_contour] * num_chains + + # if plot_point is None: + # plot_point = [not contour_over_points] * num_chains + # elif isinstance(plot_point, bool): + # plot_point = [plot_point] * num_chains + + # if show_as_1d_prior is None: + # show_as_1d_prior = [not contour_over_points] * num_chains + # elif isinstance(show_as_1d_prior, bool): + # show_as_1d_prior = [show_as_1d_prior] * num_chains + + # if marker_style is None: + # marker_style = ["."] * num_chains + # elif isinstance(marker_style, str): + # marker_style = [marker_style] * num_chains + + # if marker_size is None: + # marker_size = [20] * num_chains + # elif isinstance(marker_style, int | float): + # marker_size = [marker_size] * num_chains + + # if marker_alpha is None: + # marker_alpha = [1.0] * num_chains + # elif isinstance(marker_alpha, int | float): + # marker_alpha = [marker_alpha] * num_chains + + # # Figure out if we should display parameter summaries + # if summary is not None: + # summary = summary and num_chains == 1 + + # # Figure out bar shading + # if bar_shade is None: + # bar_shade = num_chains <= 3 + # if isinstance(bar_shade, bool): + # bar_shade = [bar_shade] * num_chains + + # if zorder is None: + # zorder = [1] * num_chains + + # # Figure out how many sigmas to plot + # if sigmas is None: + # sigmas = np.array([0, 1, 2]) if num_chains == 1 else np.array([0, 1, 2]) + # if sigmas[0] != 0: + # sigmas = np.concatenate(([0], sigmas)) + # sigmas = np.sort(sigmas) + + # if contour_labels is not None: + # assert isinstance(contour_labels, str), "contour_labels parameter should be a string" + # contour_labels = contour_labels.lower() + # assert contour_labels in [ + # "sigma", + # "confidence", + # ], "contour_labels should be either sigma or confidence" + # assert isinstance(contour_label_font_size, float | int), "contour_label_font_size needs to be numeric" + + # if legend_artists is None: + # legend_artists = len(set(linestyles)) > 1 or len(set(linewidths)) > 1 + + # if legend_kwargs is not None: + # assert isinstance(legend_kwargs, dict), "legend_kwargs should be a dict" + # else: + # legend_kwargs = {} + + # if num_chains < 3: + # labelspacing = 0.5 + # elif num_chains == 3: + # labelspacing = 0.2 + # else: + # labelspacing = 0.15 + # legend_kwargs_default = { + # "labelspacing": labelspacing, + # "loc": "upper right", + # "frameon": False, + # "fontsize": label_font_size, + # "handlelength": 1, + # "handletextpad": 0.2, + # "borderaxespad": 0.0, + # } + # legend_kwargs_default.update(legend_kwargs) + + # watermark_text_kwargs_default = { + # "color": "#333333", + # "alpha": 0.7, + # "verticalalignment": "center", + # "horizontalalignment": "center", + # } + # if watermark_text_kwargs is None: + # watermark_text_kwargs = {} + # watermark_text_kwargs_default.update(watermark_text_kwargs) + + # assert isinstance(summary_area, float), "summary_area needs to be a float, not %s!" % type(summary_area) + # assert summary_area > 0, "summary_area should be a positive number, instead is %s!" % summary_area + # assert summary_area < 1, "summary_area must be less than unity, instead is %s!" % summary_area + # assert isinstance(global_point, bool), "global_point should be a bool" + + # # List options + # for i, c in enumerate(self.chains): + # try: + # c.update_unset_config("statistics", statistics[i], override=explicit) + # c.update_unset_config("color", colors[i], override=explicit) + # c.update_unset_config("linestyle", linestyles[i], override=explicit) + # c.update_unset_config("linewidth", linewidths[i], override=explicit) + # c.update_unset_config("cloud", cloud[i], override=explicit) + # c.update_unset_config("shade", shade[i], override=explicit) + # c.update_unset_config("shade_alpha", shade_alpha[i], override=explicit) + # c.update_unset_config("shade_gradient", shade_gradient[i], override=explicit) + # c.update_unset_config("bar_shade", bar_shade[i], override=explicit) + # c.update_unset_config("bins", bins[i], override=explicit) + # c.update_unset_config("kde", kde[i], override=explicit) + # c.update_unset_config("smooth", smooth[i], override=explicit) + # c.update_unset_config("color_params", color_params[i], override=explicit) + # c.update_unset_config("plot_color_params", plot_color_params[i], override=explicit) + # c.update_unset_config("cmap", cmaps[i], override=explicit) + # c.update_unset_config("num_cloud", num_cloud[i], override=explicit) + # c.update_unset_config("marker_style", marker_style[i], override=explicit) + # c.update_unset_config("marker_size", marker_size[i], override=explicit) + # c.update_unset_config("marker_alpha", marker_alpha[i], override=explicit) + # c.update_unset_config("plot_contour", plot_contour[i], override=explicit) + # c.update_unset_config("plot_point", plot_point[i], override=explicit) + # c.update_unset_config("show_as_1d_prior", show_as_1d_prior[i], override=explicit) + # c.update_unset_config("zorder", zorder[i], override=explicit) + # c.config["summary_area"] = summary_area + + # except IndentationError: + # print( + # "Index error when assigning chain properties, make sure you " + # "have enough properties set for the number of chains you have loaded! " + # "See the stack trace for which config item has the wrong number of entries." + # ) + # raise + + # # Non list options + # self.config["sigma2d"] = sigma2d + # self.config["sigmas"] = sigmas + # self.config["summary"] = summary + # self.config["flip"] = flip + # self.config["serif"] = serif + # self.config["plot_hists"] = plot_hists + # self.config["max_ticks"] = max_ticks + # self.config["usetex"] = usetex + # self.config["diagonal_tick_labels"] = diagonal_tick_labels + # self.config["label_font_size"] = label_font_size + # self.config["tick_font_size"] = tick_font_size + # self.config["spacing"] = spacing + # self.config["contour_labels"] = contour_labels + # self.config["contour_label_font_size"] = contour_label_font_size + # self.config["legend_location"] = legend_location + # self.config["legend_kwargs"] = legend_kwargs_default + # self.config["legend_artists"] = legend_artists + # self.config["legend_color_text"] = legend_color_text + # self.config["watermark_text_kwargs"] = watermark_text_kwargs_default + # self.config["global_point"] = global_point + + # self._configured = True + # return self + + def get_chain(self, name: str) -> Chain: + assert name in self.chains, f"Chain with name {name} does not exist!" + return self.chains[name] + + def get_names(self) -> list[str]: + return list(self.chains.keys()) diff --git a/src/chainconsumer/colors.py b/src/chainconsumer/colors.py index 9b2c096a..627ddd09 100644 --- a/src/chainconsumer/colors.py +++ b/src/chainconsumer/colors.py @@ -1,76 +1,98 @@ from collections.abc import Iterable +from typing import Generator import matplotlib.pyplot as plt import numpy as np from matplotlib.colors import rgb2hex -# Colours drawn from material designs colour pallet at https://material.io/guidelines/style/color.html +# Colours drawn from tailwind: https://tailwindcss.com/docs/customizing-colors -ColourInput = str | np.ndarray | list[float] +ColorInput = str | np.ndarray | list[float] + +ALL_COLOURS = { + "rose": ["#ffe4e6", "#fecdd3", "#fda4af", "#fb7185", "#f43f5e", "#e11d48", "#be123c", "#9f1239", "#881337"], + "pink": ["#fce7f3", "#fbcfe8", "#f9a8d4", "#f472b6", "#ec4899", "#db2777", "#be185d", "#9d174d", "#831843"], + "fuchsia": ["#fae8ff", "#f5d0fe", "#f0abfc", "#e879f9", "#d946ef", "#c026d3", "#a21caf", "#86198f", "#701a75"], + "purple": ["#f3e8ff", "#e9d5ff", "#d8b4fe", "#c084fc", "#a855f7", "#9333ea", "#7e22ce", "#6b21a8", "#581c87"], + "violet": ["#ede9fe", "#ddd6fe", "#c4b5fd", "#a78bfa", "#8b5cf6", "#7c3aed", "#6d28d9", "#5b21b6", "#4c1d95"], + "indigo": ["#e0e7ff", "#c7d2fe", "#a5b4fc", "#818cf8", "#6366f1", "#4f46e5", "#4338ca", "#3730a3", "#312e81"], + "blue": ["#dbeafe", "#bfdbfe", "#93c5fd", "#60a5fa", "#3b82f6", "#2563eb", "#1d4ed8", "#1e40af", "#1e3a8a"], + "sky": ["#e0f2fe", "#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#0284c7", "#0369a1", "#075985", "#0c4a6e"], + "cyan": ["#cffafe", "#a5f3fc", "#67e8f9", "#22d3ee", "#06b6d4", "#0891b2", "#0e7490", "#155e75", "#164e63"], + "teal": ["#ccfbf1", "#99f6e4", "#5eead4", "#2dd4bf", "#14b8a6", "#0d9488", "#0f766e", "#115e59", "#134e4a"], + "emerald": ["#d1fae5", "#a7f3d0", "#6ee7b7", "#34d399", "#10b981", "#059669", "#047857", "#065f46", "#064e3b"], + "green": ["#dcfce7", "#bbf7d0", "#86efac", "#4ade80", "#22c55e", "#16a34a", "#15803d", "#166534", "#14532d"], + "lime": ["#ecfccb", "#d9f99d", "#bef264", "#a3e635", "#84cc16", "#65a30d", "#4d7c0f", "#3f6212", "#365314"], + "yellow": ["#fef9c3", "#fef08a", "#fde047", "#facc15", "#eab308", "#ca8a04", "#a16207", "#854d0e", "#713f12"], + "amber": ["#fef3c7", "#fde68a", "#fcd34d", "#fbbf24", "#f59e0b", "#d97706", "#b45309", "#92400e", "#78350f"], + "orange": ["#ffedd5", "#fed7aa", "#fdba74", "#fb923c", "#f97316", "#ea580c", "#c2410c", "#9a3412", "#7c2d12"], + "red": ["#fee2e2", "#fecaca", "#fca5a5", "#f87171", "#ef4444", "#dc2626", "#b91c1c", "#991b1b", "#7f1d1d"], + "warmGray": ["#f5f5f4", "#e7e5e4", "#d6d3d1", "#a8a29e", "#78716c", "#57534e", "#44403c", "#292524", "#1c1917"], + "trueGray": ["#f5f5f5", "#e5e5e5", "#d4d4d4", "#a3a3a3", "#737373", "#525252", "#404040", "#262626", "#171717"], + "gray": ["#f4f4f5", "#e4e4e7", "#d4d4d8", "#a1a1aa", "#71717a", "#52525b", "#3f3f46", "#27272a", "#18181b"], + "coolGray": ["#f3f4f6", "#e5e7eb", "#d1d5db", "#9ca3af", "#6b7280", "#4b5563", "#374151", "#1f2937", "#111827"], + "blueGray": ["#f1f5f9", "#e2e8f0", "#cbd5e1", "#94a3b8", "#64748b", "#475569", "#334155", "#1e293b", "#0f172a"], +} class Colors: - def __init__(self): - self.color_map: dict[str, str] = { - "blue": "#1976D2", - "lblue": "#4FC3F7", - "red": "#E53935", - "green": "#43A047", - "lgreen": "#8BC34A", - "purple": "#673AB7", - "cyan": "#4DD0E1", - "magenta": "#E91E63", - "yellow": "#F2D026", - "black": "#333333", - "grey": "#9E9E9E", - "orange": "#FB8C00", - "amber": "#FFB300", - "brown": "#795548", - } + def __init__(self) -> None: self.aliases: dict[str, str] = { "b": "blue", "r": "red", "g": "green", - "k": "black", - "m": "magenta", + "k": "gray", + "m": "rose", "c": "cyan", "o": "orange", "y": "yellow", "a": "amber", "p": "purple", "e": "grey", - "lg": "lgreen", - "lb": "lblue", + "lg": "lime", + "lb": "sky", + "black": "gray", + "white": "gray", } self.default_colors: tuple[str, ...] = ( "blue", - "lgreen", + "emerald", "red", "purple", - "yellow", + "amber", "grey", - "lblue", - "magenta", + "cyan", + "teal", "green", - "brown", - "black", "orange", + "indigo", ) - def format(self, color: ColourInput) -> str: + def next_colour(self) -> Generator[str, None, None]: + """A generator to return a sequence of colors""" + for index in [4, 7, 2]: + for color in self.default_colors: + yield ALL_COLOURS[color][index] + + def format(self, color: ColorInput) -> str: if isinstance(color, np.ndarray | list): color = rgb2hex(color) # type: ignore if color[0] == "#": return color - elif color in self.color_map: - return self.color_map[color] + elif color in ALL_COLOURS: + return ALL_COLOURS[color][4] elif color in self.aliases: alias = self.aliases[color] - return self.color_map[alias] + index = 4 + if color.lower() == "black": + index = -1 + elif color.lower() == "white": + index = 0 + return ALL_COLOURS[alias][index] else: - raise ValueError("Color %s is not mapped. Please give a hex code" % color) + raise ValueError(f"Color {color} is not mapped. Please give a hex code") - def get_formatted(self, list_colors: Iterable[ColourInput]) -> list[str]: + def get_formatted(self, list_colors: Iterable[ColorInput]) -> list[str]: return [self.format(c) for c in list_colors] def get_default(self) -> list[str]: @@ -82,7 +104,7 @@ def get_colormap(self, num: int, cmap_name: str, scale: float = 0.7) -> list[str scaled = [self.scale_colour(c, s) for c, s in zip(color_list, scales)] return scaled - def scale_colour(self, color: ColourInput, scalefactor: float) -> str: # pragma: no cover + def scale_colour(self, color: ColorInput, scalefactor: float) -> str: # pragma: no cover hexx = self.format(color).strip("#") if scalefactor < 0 or len(hexx) != 6: return hexx @@ -92,7 +114,7 @@ def scale_colour(self, color: ColourInput, scalefactor: float) -> str: # pragma b = self._clamp(int(b * scalefactor)) return f"#{r:02x}{g:02x}{b:02x}" - def _clamp(self, val: float, minimum: int = 0, maximum: int = 255): + def _clamp(self, val: int, minimum: int = 0, maximum: int = 255) -> int: if val < minimum: return minimum if val > maximum: diff --git a/src/chainconsumer/comparisons.py b/src/chainconsumer/comparisons.py index df1dd6f9..7f07919b 100644 --- a/src/chainconsumer/comparisons.py +++ b/src/chainconsumer/comparisons.py @@ -1,17 +1,18 @@ -import logging +from typing import Literal import numpy as np +import pandas as pd from scipy.interpolate import griddata from .helpers import get_latex_table_frame +from .log import logger class Comparison: - def __init__(self, parent): + def __init__(self, parent: "ChainConsumer"): self.parent = parent - self._logger = logging.getLogger("chainconsumer") - def dic(self): + def dic(self) -> dict[str, float]: r"""Returns the corrected Deviance Information Criterion (DIC) for all chains loaded into ChainConsumer. If a chain does not have a posterior, this method will return `None` for that chain. **Note that @@ -27,45 +28,34 @@ def dic(self): .. math:: DIC \equiv D(\bar{\theta}) + 2p_D = \bar{D}(\theta) + p_D. - Returns - ------- - list[float] - A list of all the DIC values - one per chain, in the order in which the chains were added. + Returns: + dict[str, float]: A dict of chain name to DIC value. References ---------- [1] Andrew R. Liddle, "Information criteria for astrophysical model selection", MNRAS (2007) """ - dics = [] - dics_bool = [] - for i, chain in enumerate(self.parent.chains): - p = chain.posterior + dics = {} + for name, chain in self.parent.chains.items(): + p = chain.log_posterior if p is None: - dics_bool.append(False) - self._logger.warning("You need to set the posterior for chain %s to get the DIC" % chain.name) + logger.warning("You need to set the posterior for chain %s to get the DIC" % chain.name) else: - dics_bool.append(True) - num_params = chain.chain.shape[1] - means = np.array([np.average(chain.chain[:, ii], weights=chain.weights) for ii in range(num_params)]) + num_params = chain.samples.shape[1] + means = np.array([np.average(chain.samples[:, ii], weights=chain.weights) for ii in range(num_params)]) d = -2 * p - d_of_mean = griddata(chain.chain, d, means, method="nearest")[0] + d_of_mean = griddata(chain.samples, d, means, method="nearest")[0] mean_d = np.average(d, weights=chain.weights) p_d = mean_d - d_of_mean dic = mean_d + p_d - dics.append(dic) - if len(dics) > 0: - dics -= np.min(dics) - dics_fin = [] - i = 0 - for b in dics_bool: - if not b: - dics_fin.append(None) - else: - dics_fin.append(dics[i]) - i += 1 - return dics_fin - - def bic(self): + dics[name] = dic + if dics: + min_dic = np.min(list(dics.values())) + for name in dics: + dics[name] -= min_dic + return dics + + def bic(self) -> dict[str, float]: r"""Returns the corrected Bayesian Information Criterion (BIC) for all chains loaded into ChainConsumer. If a chain does not have a posterior, number of data points, and number of free parameters @@ -77,17 +67,14 @@ def bic(self): where :math:`P` represents the posterior, :math:`k` the number of model parameters and :math:`N` the number of independent data points used in the model fitting. - Returns - ------- - list[float] - A list of all the BIC values - one per chain, in the order in which the chains were added. + Returns: + dict[str, float]: A dict of chain name to BIC value. + """ - bics = [] - bics_bool = [] - for i, chain in enumerate(self.parent.chains): - p, n_data, n_free = chain.posterior, chain.num_eff_data_points, chain.num_free_params + bics = {} + for name, chain in self.parent.chains.items(): + p, n_data, n_free = chain.log_posterior, chain.num_eff_data_points, chain.num_free_params if p is None or n_data is None or n_free is None: - bics_bool.append(False) missing = "" if p is None: missing += "posterior, " @@ -96,23 +83,17 @@ def bic(self): if n_free is None: missing += "num_free_params, " - self._logger.warning(f"You need to set {missing[:-2]} for chain {chain.name} to get the BIC") + logger.warning(f"You need to set {missing[:-2]} for chain {name} to get the BIC") else: - bics_bool.append(True) - bics.append(n_free * np.log(n_data) - 2 * np.max(p)) - if len(bics) > 0: - bics -= np.min(bics) - bics_fin = [] - i = 0 - for b in bics_bool: - if not b: - bics_fin.append(None) - else: - bics_fin.append(bics[i]) - i += 1 - return bics_fin + bics[name] = n_free * np.log(n_data) - 2 * np.max(p) + if bics: + min_bic = np.min(list(bics.values())) + for name in bics: + bics[name] -= min_bic + + return bics - def aic(self): + def aic(self) -> dict[str, float]: r"""Returns the corrected Akaike Information Criterion (AICc) for all chains loaded into ChainConsumer. If a chain does not have a posterior, number of data points, and number of free parameters @@ -130,17 +111,14 @@ def aic(self): where :math:`N` represents the number of independent data points used in the model fitting. The AICc is a correction for the AIC to take into account finite chain sizes. - Returns - ------- - list[float] - A list of all the AICc values - one per chain, in the order in which the chains were added. + Returns: + dict[str, float]: A dict of chain name to AIC value. + """ - aics = [] - aics_bool = [] - for i, chain in enumerate(self.parent.chains): - p, n_data, n_free = chain.posterior, chain.num_eff_data_points, chain.num_free_params + aics = {} + for name, chain in self.parent.chains.items(): + p, n_data, n_free = chain.log_posterior, chain.num_eff_data_points, chain.num_free_params if p is None or n_data is None or n_free is None: - aics_bool.append(False) missing = "" if p is None: missing += "posterior, " @@ -149,60 +127,42 @@ def aic(self): if n_free is None: missing += "num_free_params, " - self._logger.warning(f"You need to set {missing[:-2]} for chain {chain.name} to get the AIC") + logger.warning(f"You need to set {missing[:-2]} for chain {chain.name} to get the AIC") else: - aics_bool.append(True) c_cor = 1.0 * n_free * (n_free + 1) / (n_data - n_free - 1) - aics.append(2.0 * (n_free + c_cor - np.max(p))) - if len(aics) > 0: - aics -= np.min(aics) - aics_fin = [] - i = 0 - for b in aics_bool: - if not b: - aics_fin.append(None) - else: - aics_fin.append(aics[i]) - i += 1 - return aics_fin + aics[name] = 2.0 * (n_free + c_cor - np.max(p)) + if aics: + min_aic = np.min(list(aics.values())) + for name in aics: + aics[name] -= min_aic + return aics def comparison_table( self, - caption=None, - label="tab:model_comp", - hlines=True, - aic=True, - bic=True, - dic=True, - sort="bic", - descending=True, - ): # pragma: no cover + caption: str | None = None, + label: str = "tab:model_comp", + hlines: bool = True, + aic: bool = True, + bic: bool = True, + dic: bool = True, + sort: Literal["bic", "aic", "dic"] = "bic", + descending: bool = True, + ) -> str: # pragma: no cover """ Return a LaTeX ready table of model comparisons. - Parameters - ---------- - caption : str, optional - The table caption to insert. - label : str, optional - The table label to insert. - hlines : bool, optional - Whether to insert hlines in the table or not. - aic : bool, optional - Whether to include a column for AICc or not. - bic : bool, optional - Whether to include a column for BIC or not. - dic : bool, optional - Whether to include a column for DIC or not. - sort : str, optional - How to sort the models. Should be one of "bic", "aic" or "dic". - descending : bool, optional - The sort order. - - Returns - ------- - str - A LaTeX table to be copied into your document. + Args: + caption (str, optional): The table caption to insert. Defaults to None. + label (str, optional): The table label to insert. Defaults to "tab:model_comp". + hlines (bool, optional): Whether to insert hlines in the table or not. Defaults to True. + aic (bool, optional): Whether to include a column for AICc or not. Defaults to True. + bic (bool, optional): Whether to include a column for BIC or not. Defaults to True. + dic (bool, optional): Whether to include a column for DIC or not. Defaults to True. + sort (str, optional): How to sort the models. Should be one of "bic", "aic" or "dic". Defaults to "bic". + descending (bool, optional): The sort order. Defaults to True. + + Returns: + str: A LaTeX table to be copied into your document. """ if sort == "bic": @@ -211,6 +171,7 @@ def comparison_table( assert aic, "You cannot sort by AIC if you turn it off" if sort == "dic": assert dic, "You cannot sort by DIC if you turn it off" + assert sort in ["bic", "aic", "dic"], f"sort {sort} not recognised, must be dic, aic or dic" if caption is None: caption = "" @@ -230,41 +191,32 @@ def comparison_table( ) if hlines: center_text += "\t" + hline_text - aics = self.aic() if aic else np.zeros(len(self.parent.chains)) - bics = self.bic() if bic else np.zeros(len(self.parent.chains)) - dics = self.dic() if dic else np.zeros(len(self.parent.chains)) - if sort == "bic": - to_sort = bics - elif sort == "aic": - to_sort = aics - elif sort == "dic": - to_sort = dics - else: - raise ValueError("sort %s not recognised, must be dic, aic or dic" % sort) - - good = [i for i, t in enumerate(to_sort) if t is not None] - names = [self.parent.chains[g].name for g in good] - aics = [aics[g] for g in good] - bics = [bics[g] for g in good] - to_sort = bics if sort == "bic" else aics - - indexes = np.argsort(to_sort) - - if descending: - indexes = indexes[::-1] - - for i in indexes: - line = "\t" + names[i] + series = {} + if aic: + series["aic"] = self.aic() + if bic: + series["bic"] = self.bic() + if dic: + series["dic"] = self.dic() + + df = pd.DataFrame(series).sort_values(by=sort, ascending=not descending) + for name, row in df.iterrows(): + chain_name: str = str(name) + line = "\t" + chain_name if aic: - line += " & %5.1f " % aics[i] + line += f" & {row['aic']:5.1f} " if bic: - line += " & %5.1f " % bics[i] + line += f" & {row['bic']:5.1f} " if dic: - line += " & %5.1f " % dics[i] + line += f" & {row['dic']:5.1f} " line += end_text center_text += line if hlines: center_text += "\t" + hline_text return base_string % (column_text, center_text) + + +if __name__ == "__main__": + from .chainconsumer import ChainConsumer diff --git a/src/chainconsumer/diagnostic.py b/src/chainconsumer/diagnostic.py index 8c6451e0..26f002ee 100644 --- a/src/chainconsumer/diagnostic.py +++ b/src/chainconsumer/diagnostic.py @@ -3,11 +3,12 @@ import numpy as np from scipy.stats import normaltest +from .log import logger + class Diagnostic: def __init__(self, parent): self.parent = parent - self._logger = logging.getLogger("chainconsumer") def gelman_rubin(self, chain=None, threshold=0.05): r"""Runs the Gelman Rubin diagnostic on the supplied chains. @@ -67,13 +68,13 @@ def gelman_rubin(self, chain=None, threshold=0.05): w = (1 / m) * chain_var.sum(axis=0) var = (n - 1) * w / n + b / n v = var + b / (n * m) - R = np.sqrt(v / w) + r = np.sqrt(v / w) - passed = np.abs(R - 1) < threshold - print("Gelman-Rubin Statistic values for chain %s" % name) - for p, v, pas in zip(parameters, R, passed): + passed = np.abs(r - 1) < threshold + logger.info("Gelman-Rubin Statistic values for chain %s" % name) + for p, v, pas in zip(parameters, r, passed): param = "Param %d" % p if isinstance(p, int) else p - print("{}: {:7.5f} ({})".format(param, v, "Passed" if pas else "Failed")) + logger.info(f"{param}: {v:7.5f} ({'Passed' if pas else 'Failed'})") return np.all(passed) def geweke(self, chain=None, first=0.1, last=0.5, threshold=0.05): @@ -124,7 +125,7 @@ def geweke(self, chain=None, first=0.1, last=0.5, threshold=0.05): var_end = np.array([self._spec(c[n_end:, i]) / c[n_end:, i].size for c in chains for i in range(c.shape[1])]) zs = (mean_start - mean_end) / (np.sqrt(var_start + var_end)) _, pvalue = normaltest(zs) - print(f"Gweke Statistic for chain {name} has p-value {pvalue:e}") + logger.info(f"Gweke Statistic for chain {name} has p-value {pvalue:e}") return pvalue > threshold # Method of estimating spectral density following PyMC. @@ -132,5 +133,5 @@ def geweke(self, chain=None, first=0.1, last=0.5, threshold=0.05): def _spec(self, x, order=2): from statsmodels.regression.linear_model import yule_walker - beta, sigma = yule_walker(x, order) + beta, sigma = yule_walker(x, order) # type: ignore return sigma**2 / (1.0 - np.sum(beta)) ** 2 diff --git a/src/chainconsumer/helpers.py b/src/chainconsumer/helpers.py index d68d7afa..33bc5001 100644 --- a/src/chainconsumer/helpers.py +++ b/src/chainconsumer/helpers.py @@ -1,5 +1,7 @@ import numpy as np +from .chain import Chain + def get_extents(data, weight, plot=False, wide_extents=True, tiny=False, pad=False): hist, be = np.histogram(data, weights=weight, bins=2000) @@ -25,9 +27,9 @@ def get_extents(data, weight, plot=False, wide_extents=True, tiny=False, pad=Fal return lower, upper -def get_bins(chains): +def get_bins(chains: list[Chain]): proposal = [ - max(35, np.floor(1.0 * np.power(chain.chain.shape[0] / chain.chain.shape[1], 0.25))) for chain in chains + max(35, np.floor(1.0 * np.power(chain.samples.shape[0] / chain.samples.shape[1], 0.25))) for chain in chains ] return proposal @@ -47,12 +49,12 @@ def get_grid_bins(data): return bins -def get_latex_table_frame(caption, label): # pragma: no cover - base_string = r"""\begin{table} +def get_latex_table_frame(caption: str, label: str) -> str: # pragma: no cover + base_string = rf"""\begin{{table}} \centering - \caption{%s} - \label{%s} - \begin{tabular}{%s} - %s \end{tabular} -\end{table}""" - return base_string % (caption, label, "%s", "%s") + \caption{{{caption}}} + \label{{{label}}} + \begin{{tabular}}{{%s}} + %s \end{{tabular}} +\end{{table}}""" + return base_string diff --git a/src/chainconsumer/kde.py b/src/chainconsumer/kde.py index 941fa4c6..b4eebcb3 100644 --- a/src/chainconsumer/kde.py +++ b/src/chainconsumer/kde.py @@ -9,20 +9,22 @@ class MegKDE: to support weighted samples. """ - def __init__(self, train, weights=None, truncation=3.0, nmin=4, factor=1.0): + def __init__( + self, + train: np.ndarray, + weights: np.ndarray | None = None, + truncation: float = 3.0, + nmin: int = 4, + factor: float = 1.0, + ): """ - Parameters - ---------- - train : np.ndarray - The training data set. Should be a 1D array of samples or a 2D array of shape (n_samples, n_dim). - weights : np.ndarray, optional - An array of weights. If not specified, equal weights are assumed. - truncation : float, optional - The maximum deviation (in sigma) to use points in the KDE - nmin : int, optional - The minimum number of points required to estimate the density - factor : float, optional - Send bandwidth to this factor of the data estimate + Args: + train (np.ndarray): The training data set. Should be a 1D array of samples or a 2D array + of shape (n_samples, n_dim). + weights (np.ndarray, optional): An array of weights. If not specified, equal weights are assumed. + truncation (float, optional): The maximum deviation (in sigma) to use points in the KDE + nmin (int, optional): The minimum number of points required to estimate the density + factor (float, optional): Send bandwidth to this factor of the data estimate """ self.truncation = truncation @@ -50,20 +52,15 @@ def __init__(self, train, weights=None, truncation=3.0, nmin=4, factor=1.0): # self.norm = np.product(np.diagonal(self.A)) * (2 * np.pi) ** (-0.5 * self.num_dim) # prob norm # self.scaling = np.power(self.norm * self.sigma, -self.num_dim) - def evaluate(self, data): + def evaluate(self, data: np.ndarray) -> np.ndarray: """Estimate un-normalised probability density at target points - Parameters - ---------- - data : np.ndarray - A `(num_targets, num_dim)` array of points to investigate. + Args: + data (np.ndarray): 2D array of shape (n_samples, n_dim). - Returns - ------- - np.ndarray - A `(num_targets)` length array of estimates + Returns: + np.ndarray: A `(n_samples)` length array of estimates - Returns array of probability densities """ if len(data.shape) == 1 and self.num_dim == 1: data = np.atleast_2d(data).T @@ -71,7 +68,7 @@ def evaluate(self, data): _d = np.dot(data - self.mean, self.A) # Get all points within range of kernels - neighbors = self.tree.query_ball_point(_d, self.sigma * self.truncation) + neighbors = self.tree.query_ball_point(_d, self.sigma * self.truncation) # type: ignore out = [] for i, n in enumerate(neighbors): if len(n) >= self.nmin: @@ -79,7 +76,7 @@ def evaluate(self, data): distsq = np.sum(diff * diff, axis=1) else: # If too few points get nmin closest - dist, n = self.tree.query(_d[i], k=self.nmin) + dist, n = self.tree.query(_d[i], k=self.nmin) # noqa: PLW2901 # TODO: I assume this isnt an error distsq = dist * dist out.append(np.sum(self.weights[n] * np.exp(self.sigma_fact * distsq))) return np.array(out) # * self.scaling diff --git a/src/chainconsumer/log.py b/src/chainconsumer/log.py new file mode 100644 index 00000000..18340627 --- /dev/null +++ b/src/chainconsumer/log.py @@ -0,0 +1,3 @@ +import logging + +logger = logging.getLogger("chainconsumer") diff --git a/src/chainconsumer/plotter.py b/src/chainconsumer/plotter.py index fec93364..81dea217 100644 --- a/src/chainconsumer/plotter.py +++ b/src/chainconsumer/plotter.py @@ -459,11 +459,11 @@ def plot_walks( else: if i == 0 and plot_posterior: for chain in chains: - if chain.posterior is not None: + if chain.log_posterior is not None: self._plot_walk( ax, r"$\log(P)$", - chain.posterior - chain.posterior.max(), + chain.log_posterior - chain.log_posterior.max(), convolve=convolve, color=chain.config["color"], ) @@ -1097,7 +1097,7 @@ def _get_parameter_extents(self, parameter, chains, wide_extents=True) -> tuple[ min_prop, max_prop = np.min(data), np.max(data) else: if self.parent.config["global_point"]: - min_prop = chain.posterior_max_params.get(parameter) + min_prop = chain.log_posterior_max_params.get(parameter) max_prop = min_prop else: data = chain.get_data(parameter) @@ -1440,6 +1440,6 @@ def _get_smoothed_histogram2d(self, chain, param1, param2): # pragma: no cover if chain.power is not None: hist = hist**chain.power elif smooth: - hist = gaussian_filter(hist, smooth, mode=self.parent._gauss_mode) + hist = gaussian_filter(hist, smooth, mode="reflect") return hist, x_centers, y_centers diff --git a/src/chainconsumer/truth.py b/src/chainconsumer/truth.py new file mode 100644 index 00000000..37098250 --- /dev/null +++ b/src/chainconsumer/truth.py @@ -0,0 +1,25 @@ +import pandas as pd +from pydantic import Field, ValidationError, field_validator + +from .base import BetterBase + + +class Truth(BetterBase): + truth_value: dict[str, float] = Field( + default=..., description="The truth value, either as dictionary or pandas series" + ) + truth_name: str | None = Field(default=None, description="The name of the truth line") + line_color: str = Field(default="black", description="The color of the truth line") + line_width: float = Field(default=1.0, description="The width of the truth line") + line_alpha: float = Field(default=1.0, description="The alpha of the truth line") + line_style: str = Field(default="--", description="The style of the truth line") + line_zorder: int = Field(default=100, description="The zorder of the truth line") + + @field_validator("truth_value") + @classmethod + def ensure_dict(cls, v): + if isinstance(v, dict): + return v + elif isinstance(v, pd.Series): + return v.to_dict() + raise ValidationError("Truth must be a dict or a pandas Series") diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 7ff4bd2d..f63fe0e3 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -21,7 +21,7 @@ def test_summary(self): tolerance = 4e-2 consumer = ChainConsumer() consumer.add_chain(self.data[::10]) - consumer.configure(kde=True) + consumer.configure_overrides(kde=True) summary = consumer.analysis.get_summary() actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) @@ -32,7 +32,7 @@ def test_summary_no_smooth(self): tolerance = 5e-2 consumer = ChainConsumer() consumer.add_chain(self.data) - consumer.configure(smooth=0, bins=2.4) + consumer.configure_overrides(smooth=0, bins=2.4) summary = consumer.analysis.get_summary() actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) @@ -63,7 +63,7 @@ def test_summary2(self): def test_summary_some_params(self): consumer = ChainConsumer() consumer.add_chain(self.data_combined, parameters=["a", "b"], name="chain1") - summary = consumer.analysis.get_summary(parameters=["a"], squeeze=False) + summary = consumer.analysis.get_summary(columns=["a"], squeeze=False) k1 = list(summary[0].keys()) assert len(k1) == 1 assert "a" in k1 @@ -73,7 +73,7 @@ def test_summary1(self): tolerance = 5e-2 consumer = ChainConsumer() consumer.add_chain(self.data) - consumer.configure(bins=0.8) + consumer.configure_overrides(bins=0.8) summary = consumer.analysis.get_summary() actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) @@ -84,7 +84,7 @@ def test_summary_specific(self): tolerance = 5e-2 consumer = ChainConsumer() consumer.add_chain(self.data, name="A") - consumer.configure(bins=0.8) + consumer.configure_overrides(bins=0.8) summary = consumer.analysis.get_summary(chains="A") actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) @@ -96,8 +96,8 @@ def test_summary_disjoint(self): consumer = ChainConsumer() consumer.add_chain(self.data, parameters="A") consumer.add_chain(self.data, parameters="B") - consumer.configure(bins=0.8) - summary = consumer.analysis.get_summary(parameters="A") + consumer.configure_overrides(bins=0.8) + summary = consumer.analysis.get_summary(columns="A") assert len(summary) == 2 # Two chains assert summary[1] == {} # Second chain doesnt have param A actual = summary[0]["A"] @@ -119,7 +119,7 @@ def test_summary_power(self): def test_output_text(self): consumer = ChainConsumer() consumer.add_chain(self.data, parameters=["a"]) - consumer.configure(bins=0.8) + consumer.configure_overrides(bins=0.8) vals = consumer.analysis.get_summary()["a"] text = consumer.analysis.get_parameter_text(*vals) assert text == r"5.0\pm 1.5" @@ -341,7 +341,7 @@ def test_divide_chains_default(self): consumer.add_chain(data, walkers=num_walkers) c = consumer.divide_chain() - c.configure(bins=0.7) + c.configure_overrides(bins=0.7) means = [0, 1.0] for i in range(num_walkers): stats = next(iter(c.analysis.get_summary()[i].values())) @@ -355,7 +355,7 @@ def test_divide_chains_index(self): consumer.add_chain(data, walkers=num_walkers) c = consumer.divide_chain(chain=0) - c.configure(bins=0.7) + c.configure_overrides(bins=0.7) means = [0, 1.0] for i in range(num_walkers): stats = next(iter(c.analysis.get_summary()[i].values())) @@ -368,7 +368,7 @@ def test_divide_chains_name(self): num_walkers = 2 consumer.add_chain(data, walkers=num_walkers, name="test") c = consumer.divide_chain(chain="test") - c.configure(bins=0.7) + c.configure_overrides(bins=0.7) means = [0, 1.0] for i in range(num_walkers): stats = next(iter(c.analysis.get_summary()[i].values())) @@ -393,7 +393,7 @@ def test_stats_max_normal(self): tolerance = 5e-2 consumer = ChainConsumer() consumer.add_chain(self.data) - consumer.configure(statistics="max") + consumer.configure_overrides(statistics="max") summary = consumer.analysis.get_summary() actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) @@ -407,7 +407,7 @@ def test_stats_max_cliff(self): weights = norm.pdf(data, 1, 2) consumer = ChainConsumer() consumer.add_chain(data, weights=weights) - consumer.configure(statistics="max", bins=4.0, smooth=1) + consumer.configure_overrides(statistics="max", bins=4.0, smooth=1) summary = consumer.analysis.get_summary() actual = np.array(next(iter(summary.values()))) expected = np.array([0.0, 1.0, 2.73]) @@ -418,7 +418,7 @@ def test_stats_mean_normal(self): tolerance = 5e-2 consumer = ChainConsumer() consumer.add_chain(self.data) - consumer.configure(statistics="mean") + consumer.configure_overrides(statistics="mean") summary = consumer.analysis.get_summary() actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) @@ -429,7 +429,7 @@ def test_stats_cum_normal(self): tolerance = 5e-2 consumer = ChainConsumer() consumer.add_chain(self.data) - consumer.configure(statistics="cumulative") + consumer.configure_overrides(statistics="cumulative") summary = consumer.analysis.get_summary() actual = np.array(next(iter(summary.values()))) expected = np.array([3.5, 5.0, 6.5]) @@ -440,13 +440,13 @@ def test_reject_bad_satst(self): consumer = ChainConsumer() consumer.add_chain(self.data) with pytest.raises(AssertionError): - consumer.configure(statistics="monkey") + consumer.configure_overrides(statistics="monkey") def test_stats_max_skew(self): tolerance = 3e-2 consumer = ChainConsumer() consumer.add_chain(self.data_skew) - consumer.configure(statistics="max") + consumer.configure_overrides(statistics="max") summary = consumer.analysis.get_summary() actual = np.array(next(iter(summary.values()))) expected = np.array([1.01, 1.55, 2.72]) @@ -457,7 +457,7 @@ def test_stats_mean_skew(self): tolerance = 3e-2 consumer = ChainConsumer() consumer.add_chain(self.data_skew) - consumer.configure(statistics="mean") + consumer.configure_overrides(statistics="mean") summary = consumer.analysis.get_summary() actual = np.array(next(iter(summary.values()))) expected = np.array([1.27, 2.19, 3.11]) @@ -468,7 +468,7 @@ def test_stats_cum_skew(self): tolerance = 3e-2 consumer = ChainConsumer() consumer.add_chain(self.data_skew) - consumer.configure(statistics="cumulative") + consumer.configure_overrides(statistics="cumulative") summary = consumer.analysis.get_summary() actual = np.array(next(iter(summary.values()))) expected = np.array([1.27, 2.01, 3.11]) @@ -480,7 +480,7 @@ def test_stats_list_skew(self): consumer = ChainConsumer() consumer.add_chain(self.data_skew) consumer.add_chain(self.data_skew) - consumer.configure(statistics=["cumulative", "mean"]) + consumer.configure_overrides(statistics=["cumulative", "mean"]) summary = consumer.analysis.get_summary() actual0 = np.array(next(iter(summary[0].values()))) actual1 = np.array(next(iter(summary[1].values()))) @@ -745,14 +745,14 @@ def test_covariant_covariance_calc(self): def test_2d_levels(self): c = ChainConsumer() c.add_chain(self.data) - c.configure(sigmas=[0, 1, 2], sigma2d=True) + c.configure_overrides(sigmas=[0, 1, 2], sigma2d=True) levels = c.plotter._get_levels() assert np.allclose(levels, [0, 0.39, 0.86], atol=0.01) def test_1d_levels(self): c = ChainConsumer() c.add_chain(self.data) - c.configure(sigmas=[0, 1, 2], sigma2d=False) + c.configure_overrides(sigmas=[0, 1, 2], sigma2d=False) levels = c.plotter._get_levels() assert np.allclose(levels, [0, 0.68, 0.95], atol=0.01) @@ -766,7 +766,7 @@ def test_summary_area(self): def test_summary_area_default(self): c = ChainConsumer() c.add_chain(self.data) - c.configure(summary_area=0.6827) + c.configure_overrides(summary_area=0.6827) summary = c.analysis.get_summary()["0"] expected = [3.5, 5, 6.5] assert np.all(np.isclose(summary, expected, atol=0.1)) @@ -774,7 +774,7 @@ def test_summary_area_default(self): def test_summary_area_95(self): c = ChainConsumer() c.add_chain(self.data) - c.configure(summary_area=0.95) + c.configure_overrides(summary_area=0.95) summary = c.analysis.get_summary()["0"] expected = [2, 5, 8] assert np.all(np.isclose(summary, expected, atol=0.1)) @@ -782,7 +782,7 @@ def test_summary_area_95(self): def test_summary_max_symmetric_1(self): c = ChainConsumer() c.add_chain(self.data) - c.configure(statistics="max_symmetric") + c.configure_overrides(statistics="max_symmetric") summary = c.analysis.get_summary()["0"] expected = [3.5, 5, 6.5] assert np.all(np.isclose(summary, expected, atol=0.1)) @@ -792,7 +792,7 @@ def test_summary_max_symmetric_2(self): c = ChainConsumer() c.add_chain(self.data_skew) summary_area = 0.6827 - c.configure(statistics="max_symmetric", bins=1.0, summary_area=summary_area) + c.configure_overrides(statistics="max_symmetric", bins=1.0, summary_area=summary_area) summary = c.analysis.get_summary()["0"] xs = np.linspace(0, 2, 1000) @@ -810,7 +810,7 @@ def test_summary_max_symmetric_3(self): c = ChainConsumer() c.add_chain(self.data_skew) summary_area = 0.95 - c.configure(statistics="max_symmetric", bins=1.0, summary_area=summary_area) + c.configure_overrides(statistics="max_symmetric", bins=1.0, summary_area=summary_area) summary = c.analysis.get_summary()["0"] xs = np.linspace(0, 2, 1000) @@ -827,7 +827,7 @@ def test_summary_max_symmetric_3(self): def test_summary_max_shortest_1(self): c = ChainConsumer() c.add_chain(self.data) - c.configure(statistics="max_shortest") + c.configure_overrides(statistics="max_shortest") summary = c.analysis.get_summary()["0"] expected = [3.5, 5, 6.5] assert np.all(np.isclose(summary, expected, atol=0.1)) @@ -836,7 +836,7 @@ def test_summary_max_shortest_2(self): c = ChainConsumer() c.add_chain(self.data_skew) summary_area = 0.6827 - c.configure(statistics="max_shortest", bins=1.0, summary_area=summary_area) + c.configure_overrides(statistics="max_shortest", bins=1.0, summary_area=summary_area) summary = c.analysis.get_summary()["0"] xs = np.linspace(-1, 5, 1000) @@ -857,7 +857,7 @@ def test_summary_max_shortest_3(self): c = ChainConsumer() c.add_chain(self.data_skew) summary_area = 0.95 - c.configure(statistics="max_shortest", bins=1.0, summary_area=summary_area) + c.configure_overrides(statistics="max_shortest", bins=1.0, summary_area=summary_area) summary = c.analysis.get_summary()["0"] xs = np.linspace(-1, 5, 1000) @@ -877,7 +877,7 @@ def test_summary_max_shortest_3(self): def test_summary_max_central_1(self): c = ChainConsumer() c.add_chain(self.data) - c.configure(statistics="max_central") + c.configure_overrides(statistics="max_central") summary = c.analysis.get_summary()["0"] expected = [3.5, 5, 6.5] assert np.all(np.isclose(summary, expected, atol=0.1)) @@ -886,7 +886,7 @@ def test_summary_max_central_2(self): c = ChainConsumer() c.add_chain(self.data_skew) summary_area = 0.6827 - c.configure(statistics="max_central", bins=1.0, summary_area=summary_area) + c.configure_overrides(statistics="max_central", bins=1.0, summary_area=summary_area) summary = c.analysis.get_summary()["0"] xs = np.linspace(-1, 5, 1000) @@ -903,7 +903,7 @@ def test_summary_max_central_3(self): c = ChainConsumer() c.add_chain(self.data_skew) summary_area = 0.95 - c.configure(statistics="max_central", bins=1.0, summary_area=summary_area) + c.configure_overrides(statistics="max_central", bins=1.0, summary_area=summary_area) summary = c.analysis.get_summary()["0"] xs = np.linspace(-1, 5, 1000) diff --git a/tests/test_chain.py b/tests/test_chain.py index ba005101..2966f4b3 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -206,66 +206,66 @@ def test_bad_num_eff_data_points4(self): def test_color_data_none(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) - c.configure(color_params=None) + c.configure_overrides(color_params=None) chain = c.chains[0] assert chain.get_color_data() is None def test_color_data_p1(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) - c.configure(color_params=self.p[0]) + c.configure_overrides(color_params=self.p[0]) chain = c.chains[0] assert np.all(chain.get_color_data() == self.d[:, 0]) def test_color_data_w(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) - c.configure(color_params="weights") + c.configure_overrides(color_params="weights") chain = c.chains[0] assert np.all(chain.get_color_data() == self.w) def test_color_data_logw(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) - c.configure(color_params="log_weights") + c.configure_overrides(color_params="log_weights") chain = c.chains[0] assert np.all(chain.get_color_data() == np.log(self.w)) def test_color_data_posterior(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) - c.configure(color_params="posterior") + c.configure_overrides(color_params="posterior") chain = c.chains[0] assert np.all(chain.get_color_data() == np.ones(100)) def test_override_color(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, color="#4286f4") - c.configure() + c.configure_overrides() assert c.chains[0].config["color"] == "#4286f4" def test_override_linewidth(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, linewidth=2.0) - c.configure(linewidths=[100]) + c.configure_overrides(linewidths=[100]) assert c.chains[0].config["linewidth"] == 100 def test_override_linestyle(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, linestyle="--") - c.configure() + c.configure_overrides() assert c.chains[0].config["linestyle"] == "--" def test_override_shade_alpha(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, shade_alpha=0.8) - c.configure() + c.configure_overrides() assert c.chains[0].config["shade_alpha"] == 0.8 def test_override_kde(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, kde=2.0) - c.configure() + c.configure_overrides() assert c.chains[0].config["kde"] == 2.0 def test_override_kde_grid(self): @@ -273,15 +273,15 @@ def test_override_kde_grid(self): x, y = np.linspace(0, 10, 10), np.linspace(0, 10, 10) z = np.ones((10, 10)) c.add_chain([x, y], weights=z, grid=True, kde=2.0) - c.configure() + c.configure_overrides() assert not c.chains[0].config["kde"] def test_cache_invalidation(self): c = ChainConsumer() c.add_chain(self.rng.normal(size=(1000000, 1)), parameters=["a"]) - c.configure(summary_area=0.68) + c.configure_overrides(summary_area=0.68) summary1 = c.analysis.get_summary() - c.configure(summary_area=0.95) + c.configure_overrides(summary_area=0.95) summary2 = c.analysis.get_summary() assert np.isclose(summary1["a"][0], -1, atol=0.03) assert np.isclose(summary2["a"][0], -2, atol=0.03) diff --git a/tests/test_chainconsumer.py b/tests/test_chainconsumer.py index f58446d1..3b6fa599 100644 --- a/tests/test_chainconsumer.py +++ b/tests/test_chainconsumer.py @@ -28,30 +28,30 @@ def test_get_chain_via_object(self): c = ChainConsumer() c.add_chain(self.data, name="A") c.add_chain(self.data, name="B") - assert c._get_chain(c.chains[0])[0] == 0 - assert c._get_chain(c.chains[1])[0] == 1 - assert len(c._get_chain(c.chains[0])) == 1 - assert len(c._get_chain(c.chains[1])) == 1 + assert c.get_chain(c.chains[0])[0] == 0 + assert c.get_chain(c.chains[1])[0] == 1 + assert len(c.get_chain(c.chains[0])) == 1 + assert len(c.get_chain(c.chains[1])) == 1 def test_summary_bad_input1(self): with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data).configure(summary_area=None) + ChainConsumer().add_chain(self.data).configure_overrides(summary_area=None) def test_summary_bad_input2(self): with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data).configure(summary_area="Nope") + ChainConsumer().add_chain(self.data).configure_overrides(summary_area="Nope") def test_summary_bad_input3(self): with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data).configure(summary_area=0) + ChainConsumer().add_chain(self.data).configure_overrides(summary_area=0) def test_summary_bad_input4(self): with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data).configure(summary_area=1) + ChainConsumer().add_chain(self.data).configure_overrides(summary_area=1) def test_summary_bad_input5(self): with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data).configure(summary_area=-0.2) + ChainConsumer().add_chain(self.data).configure_overrides(summary_area=-0.2) def test_remove_last_chain(self): tolerance = 5e-2 @@ -59,7 +59,7 @@ def test_remove_last_chain(self): consumer.add_chain(self.data) consumer.add_chain(self.data * 2) consumer.remove_chain() - consumer.configure() + consumer.configure_overrides() summary = consumer.analysis.get_summary() assert isinstance(summary, dict) actual = np.array(next(iter(summary.values()))) @@ -73,7 +73,7 @@ def test_remove_first_chain(self): consumer.add_chain(self.data * 2) consumer.add_chain(self.data) consumer.remove_chain(chain=0) - consumer.configure() + consumer.configure_overrides() summary = consumer.analysis.get_summary() assert isinstance(summary, dict) actual = np.array(next(iter(summary.values()))) @@ -87,7 +87,7 @@ def test_remove_chain_by_name(self): consumer.add_chain(self.data * 2, name="a") consumer.add_chain(self.data, name="b") consumer.remove_chain(chain="a") - consumer.configure() + consumer.configure_overrides() summary = consumer.analysis.get_summary() assert isinstance(summary, dict) actual = np.array(next(iter(summary.values()))) @@ -101,7 +101,7 @@ def test_remove_chain_recompute_params(self): consumer.add_chain(self.data * 2, parameters=["p1"], name="a") consumer.add_chain(self.data, parameters=["p2"], name="b") consumer.remove_chain(chain="a") - consumer.configure() + consumer.configure_overrides() summary = consumer.analysis.get_summary() assert isinstance(summary, dict) assert "p2" in summary @@ -118,7 +118,7 @@ def test_remove_multiple_chains(self): consumer.add_chain(self.data, parameters=["p2"], name="b") consumer.add_chain(self.data * 3, parameters=["p3"], name="c") consumer.remove_chain(chain=["a", "c"]) - consumer.configure() + consumer.configure_overrides() summary = consumer.analysis.get_summary() assert isinstance(summary, dict) assert "p2" in summary @@ -136,7 +136,7 @@ def test_remove_multiple_chains2(self): consumer.add_chain(self.data, parameters=["p2"], name="b") consumer.add_chain(self.data * 3, parameters=["p3"], name="c") consumer.remove_chain(chain=[0, 2]) - consumer.configure() + consumer.configure_overrides() summary = consumer.analysis.get_summary() assert isinstance(summary, dict) assert "p2" in summary @@ -154,7 +154,7 @@ def test_remove_multiple_chains3(self): consumer.add_chain(self.data, parameters=["p2"], name="b") consumer.add_chain(self.data * 3, parameters=["p3"], name="c") consumer.remove_chain(chain=["a", 2]) - consumer.configure() + consumer.configure_overrides() summary = consumer.analysis.get_summary() assert isinstance(summary, dict) assert "p2" in summary @@ -172,7 +172,7 @@ def test_remove_multiple_chains_fails(self): def test_shade_alpha_algorithm1(self): consumer = ChainConsumer() consumer.add_chain(self.data) - consumer.configure() + consumer.configure_overrides() alpha = consumer.chains[0].config["shade_alpha"] assert alpha == 1.0 @@ -180,7 +180,7 @@ def test_shade_alpha_algorithm2(self): consumer = ChainConsumer() consumer.add_chain(self.data) consumer.add_chain(self.data) - consumer.configure() + consumer.configure_overrides() alpha0 = consumer.chains[0].config["shade_alpha"] alpha1 = consumer.chains[0].config["shade_alpha"] assert alpha0 == 1.0 / np.sqrt(2.0) @@ -191,7 +191,7 @@ def test_shade_alpha_algorithm3(self): consumer.add_chain(self.data) consumer.add_chain(self.data) consumer.add_chain(self.data) - consumer.configure() + consumer.configure_overrides() alphas = [c.config["shade_alpha"] for c in consumer.chains] assert len(alphas) == 3 assert alphas[0] == 1.0 / np.sqrt(3.0) diff --git a/tests/test_plotter.py b/tests/test_plotter.py index 5e7f6e32..180c7b85 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -13,7 +13,7 @@ class TestChain: def test_plotter_extents1(self): c = ChainConsumer() c.add_chain(self.data, parameters=["x"]) - c.configure() + c.configure_overrides() minv, maxv = c.plotter._get_parameter_extents("x", c.chains) assert np.isclose(minv, (5.0 - 1.5 * 3.7), atol=0.2) assert np.isclose(maxv, (5.0 + 1.5 * 3.7), atol=0.2) @@ -22,7 +22,7 @@ def test_plotter_extents2(self): c = ChainConsumer() c.add_chain(self.data, parameters=["x"]) c.add_chain(self.data + 5, parameters=["y"]) - c.configure() + c.configure_overrides() minv, maxv = c.plotter._get_parameter_extents("x", c.chains) assert np.isclose(minv, (5.0 - 1.5 * 3.7), atol=0.2) assert np.isclose(maxv, (5.0 + 1.5 * 3.7), atol=0.2) @@ -31,7 +31,7 @@ def test_plotter_extents3(self): c = ChainConsumer() c.add_chain(self.data, parameters=["x"]) c.add_chain(self.data + 5, parameters=["x"]) - c.configure() + c.configure_overrides() minv, maxv = c.plotter._get_parameter_extents("x", c.chains) assert np.isclose(minv, (5.0 - 1.5 * 3.7), atol=0.2) assert np.isclose(maxv, (10.0 + 1.5 * 3.7), atol=0.2) @@ -40,7 +40,7 @@ def test_plotter_extents4(self): c = ChainConsumer() c.add_chain(self.data, parameters=["x"]) c.add_chain(self.data + 5, parameters=["y"]) - c.configure() + c.configure_overrides() minv, maxv = c.plotter._get_parameter_extents("x", c.chains[:1]) assert np.isclose(minv, (5.0 - 1.5 * 3.7), atol=0.2) assert np.isclose(maxv, (5.0 + 1.5 * 3.7), atol=0.2) @@ -53,7 +53,7 @@ def test_plotter_extents5(self): pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xs * xs + ys * ys / 4)) c = ChainConsumer() c.add_chain(chain, parameters=["x", "y"], weights=pdf, grid=True) - c.configure() + c.configure_overrides() minv, maxv = c.plotter._get_parameter_extents("x", c.chains) assert np.isclose(minv, -3, atol=0.001) assert np.isclose(maxv, 3, atol=0.001) @@ -66,7 +66,7 @@ def test_plotter_extents6(self): data += mid c.add_chain(data, parameters=["x"], posterior=posterior, plot_point=True, plot_contour=False) - c.configure() + c.configure_overrides() minv, maxv = c.plotter._get_parameter_extents("x", c.chains) assert np.isclose(minv, -1, atol=0.01) assert np.isclose(maxv, 1, atol=0.01) From 5319bd478d66b6287ee23a8b099b2edd143dc87a Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Sat, 7 Oct 2023 16:29:49 +1000 Subject: [PATCH 05/22] Thats right theres more --- examples/customisations/plot_font_changes.py | 2 +- src/chainconsumer/__init__.py | 4 +- src/chainconsumer/analysis.py | 109 +- src/chainconsumer/chain.py | 102 +- src/chainconsumer/chainconsumer.py | 44 +- src/chainconsumer/colors.py | 3 +- src/chainconsumer/diagnostic.py | 2 +- src/chainconsumer/helpers.py | 48 +- src/chainconsumer/plotter.py | 2081 +++++++++--------- src/chainconsumer/truth.py | 11 + 10 files changed, 1216 insertions(+), 1190 deletions(-) diff --git a/examples/customisations/plot_font_changes.py b/examples/customisations/plot_font_changes.py index a385108d..43fa3a67 100644 --- a/examples/customisations/plot_font_changes.py +++ b/examples/customisations/plot_font_changes.py @@ -20,6 +20,6 @@ c = ChainConsumer() c.add_chain(data, parameters=["$x$", "$y^2$", r"$\Omega_\beta$"], name="Example") c.configure_overrides(diagonal_tick_labels=False, tick_font_size=8, label_font_size=25, max_ticks=8) -fig = c.plotter.plot(figsize="column", legend=True) +fig = c.plotter.plot(figsize="column", show_legend=True) fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/src/chainconsumer/__init__.py b/src/chainconsumer/__init__.py index 7d415be3..951b5b39 100644 --- a/src/chainconsumer/__init__.py +++ b/src/chainconsumer/__init__.py @@ -1,5 +1,5 @@ -from .chain import Chain, Config +from .chain import Chain, ChainConfig from .chainconsumer import ChainConsumer from .truth import Truth -__all__ = ["ChainConsumer", "Chain", "Config", "Truth"] +__all__ = ["ChainConsumer", "Chain", "ChainConfig", "Truth"] diff --git a/src/chainconsumer/analysis.py b/src/chainconsumer/analysis.py index bec4b1ab..8fc055cd 100644 --- a/src/chainconsumer/analysis.py +++ b/src/chainconsumer/analysis.py @@ -10,7 +10,7 @@ from .base import BetterBase from .chain import Chain, ChainName, ColumnName, MaxPosterior, Named2DMatrix -from .helpers import get_grid_bins, get_latex_table_frame, get_smoothed_bins +from .helpers import get_bins, get_grid_bins, get_latex_table_frame, get_smoothed_bins from .kde import MegKDE @@ -38,7 +38,6 @@ def __init__(self, parent: "ChainConsumer"): SummaryStatistic.MAX: self.get_parameter_summary_max, SummaryStatistic.MEAN: self.get_parameter_summary_mean, SummaryStatistic.CUMULATIVE: self.get_parameter_summary_cumulative, - SummaryStatistic.MAX_SYMMETRIC: self.get_paramater_summary_max_symmetric, SummaryStatistic.MAX_SHORTEST: self.get_parameter_summary_max_shortest, SummaryStatistic.MAX_CENTRAL: self.get_parameter_summary_max_central, } @@ -196,12 +195,9 @@ def get_max_posteriors(self, chains: dict[str, Chain] | list[str] | None = None) return results - def get_parameter_summary(self, chain, parameter): - # Ensure config has been called so we get the statistics set in config - if not self.parent._configured: - self.parent.configure_overrides() - callback = self._summaries[chain.config["statistics"]] - return chain.get_summary(parameter, callback) + def get_parameter_summary(self, chain: Chain, column: ColumnName): + callback = self._summaries[chain.statistics] + return callback(chain, column) def get_correlation_table( self, @@ -261,14 +257,14 @@ def get_covariance_table( covariance = chain.get_covariance(columns=columns) return self._get_2d_latex_table(covariance, caption, label) - def _get_smoothed_histogram(self, chain, parameter, pad=False): - data = chain.get_data(parameter) - smooth = chain.config["smooth"] + def _get_smoothed_histogram( + self, chain: Chain, column: ColumnName, pad: bool = False + ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + data = chain.get_data(column) if chain.grid: bins = get_grid_bins(data) else: - bins = chain.config["bins"] - bins, smooth = get_smoothed_bins(smooth, bins, data, chain.weights, pad=pad) + bins, _ = get_smoothed_bins(chain.smooth, get_bins(chain), data, chain.weights, pad=pad) hist, edges = np.histogram(data, bins=bins, density=True, weights=chain.weights) if chain.power is not None: @@ -276,12 +272,12 @@ def _get_smoothed_histogram(self, chain, parameter, pad=False): edge_centers = 0.5 * (edges[1:] + edges[:-1]) xs = np.linspace(edge_centers[0], edge_centers[-1], 10000) - if smooth: - hist = gaussian_filter(hist, smooth, mode="reflect") - kde = chain.config["kde"] - if kde: + if chain.smooth: + hist = gaussian_filter(hist, chain.smooth, mode="reflect") + if chain.kde: kde_xs = np.linspace(edge_centers[0], edge_centers[-1], max(200, int(bins.max()))) - ys = MegKDE(data, chain.weights, factor=kde).evaluate(kde_xs) + factor = chain.kde if isinstance(chain.kde, int | float) else 1.0 + ys = MegKDE(data.to_numpy(), chain.weights, factor=factor).evaluate(kde_xs) area = simps(ys, x=kde_xs) ys = ys / area ys = interp1d(kde_xs, ys, kind="linear")(xs) @@ -385,24 +381,21 @@ def get_parameter_text(self, bound: Bound, wrap: bool = False): text = "$%s$" % text return text - def get_parameter_summary_mean(self, chain, parameter): - desired_area = chain.config["summary_area"] - xs, _, cs = self._get_smoothed_histogram(chain, parameter) - vals = [0.5 - desired_area / 2, 0.5, 0.5 + desired_area / 2] + def get_parameter_summary_mean(self, chain: Chain, column: ColumnName) -> Bound | None: + xs, _, cs = self._get_smoothed_histogram(chain, column) + vals = [0.5 - chain.summary_area / 2, 0.5, 0.5 + chain.summary_area / 2] bounds = interp1d(cs, xs)(vals) bounds[1] = 0.5 * (bounds[0] + bounds[2]) - return bounds + return Bound(lower=bounds[0], center=bounds[1], upper=bounds[2]) - def get_parameter_summary_cumulative(self, chain, parameter): - xs, _, cs = self._get_smoothed_histogram(chain, parameter) - desired_area = chain.config["summary_area"] - vals = [0.5 - desired_area / 2, 0.5, 0.5 + desired_area / 2] + def get_parameter_summary_cumulative(self, chain: Chain, column: ColumnName) -> Bound | None: + xs, _, cs = self._get_smoothed_histogram(chain, column) + vals = [0.5 - chain.summary_area / 2, 0.5, 0.5 + chain.summary_area / 2] bounds = interp1d(cs, xs)(vals) - return bounds + return Bound(lower=bounds[0], center=bounds[1], upper=bounds[2]) - def get_parameter_summary_max(self, chain, parameter): - xs, ys, cs = self._get_smoothed_histogram(chain, parameter) - desired_area = chain.config["summary_area"] + def get_parameter_summary_max(self, chain: Chain, column: ColumnName) -> Bound | None: + xs, ys, cs = self._get_smoothed_histogram(chain, column) n_pad = 1000 x_start = xs[0] * np.ones(n_pad) x_end = xs[-1] * np.ones(n_pad) @@ -424,79 +417,53 @@ def get_parameter_summary_max(self, chain, parameter): count += 1 try: if count > 50: - raise ValueError("Failed to converge") + raise ValueError("Failed to converge") # noqa: TRY301 i1 = start_index - np.where(ys[:start_index][::-1] < mid)[0][0] i2 = start_index + np.where(ys[start_index:] < mid)[0][0] area = cs[i2] - cs[i1] - deviation = np.abs(area - desired_area) + deviation = np.abs(area - chain.summary_area) if deviation < threshold: - x1 = xs[i1] - x2 = xs[i2] - elif area < desired_area: + x1 = float(xs[i1]) + x2 = float(xs[i2]) + elif area < chain.summary_area: max_val = mid - elif area > desired_area: + elif area > chain.summary_area: min_val = mid except ValueError: - self._logger.warning(f"Parameter {parameter} in chain {chain.name} is not constrained") - return [None, xs[start_index], None] - - return [x1, xs[start_index], x2] - - def get_paramater_summary_max_symmetric(self, chain, parameter): - xs, ys, cs = self._get_smoothed_histogram(chain, parameter) - desired_area = chain.config["summary_area"] + self._logger.warning(f"Parameter {column} in chain {chain.name} is not constrained") + return Bound(lower=None, center=float(xs[start_index]), upper=None) - x_to_c = interp1d(xs, cs, bounds_error=False, fill_value=(0, 1)) # type: ignore - - # Get max likelihood x - max_index = ys.argmax() - x = xs[max_index] - - # Estimate width - h = 0.5 * (xs[-1] - xs[0]) - prev_h = 0 - - # Hone in on right answer - while True: - current_area = x_to_c(x + h) - x_to_c(x - h) - if np.abs(current_area - desired_area) < 0.0001: - return [x - h, x, x + h] - temp = h - h += 0.5 * np.abs(prev_h - h) * (1 if current_area < desired_area else -1) - prev_h = temp + return Bound(lower=x1, center=float(xs[start_index]), upper=x2) def get_parameter_summary_max_shortest(self, chain, parameter): xs, ys, cs = self._get_smoothed_histogram(chain, parameter) - desired_area = chain.config["summary_area"] - c_to_x = interp1d(cs, xs, bounds_error=False, fill_value=(-np.inf, np.inf)) # type: ignore + c_to_x = interp1d(cs, xs, bounds_error=False, fill_value=(-np.inf, np.inf)) # type: ignore # Get max likelihood x max_index = ys.argmax() x = xs[max_index] # Pair each lower bound with an upper to get the right area - x2 = c_to_x(cs + desired_area) + x2 = c_to_x(cs + chain.summary_area) dists = x2 - xs mask = (xs > x) | (x2 < x) # Ensure max point is inside the area dists[mask] = np.inf ind = dists.argmin() - return [xs[ind], x, x2[ind]] + return Bound(lower=xs[ind], center=x, upper=x2[ind]) def get_parameter_summary_max_central(self, chain, parameter): xs, ys, cs = self._get_smoothed_histogram(chain, parameter) - desired_area = chain.config["summary_area"] c_to_x = interp1d(cs, xs) - # Get max likelihood x max_index = ys.argmax() x = xs[max_index] - vals = [0.5 - 0.5 * desired_area, 0.5 + 0.5 * desired_area] + vals = [0.5 - 0.5 * chain.summary_area, 0.5 + 0.5 * chain.summary_area] xvals = c_to_x(vals) - return [xvals[0], x, xvals[1]] + return Bound(lower=xvals[0], center=x, upper=xvals[1]) if __name__ == "__main__": diff --git a/src/chainconsumer/chain.py b/src/chainconsumer/chain.py index 3e08a4f6..c418145d 100644 --- a/src/chainconsumer/chain.py +++ b/src/chainconsumer/chain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from typing import Any, TypeAlias @@ -8,7 +10,6 @@ from .analysis import SummaryStatistic from .base import BetterBase from .colors import ColorInput, colors -from .helpers import get_bins ChainName: TypeAlias = str ColumnName: TypeAlias = str @@ -28,41 +29,56 @@ class Named2DMatrix(BetterBase): matrix: np.ndarray # type: ignore -class Config(BetterBase): - # Note that a None default means that this will be inferred - # automatically when you go to plot. +class ChainConfig(BetterBase): + """The configuration for a chain. This is used to set the default values for + plotting chains, and is also used to store the configuration of a chain. + + Note that some attributes are defaulted to None instead of their type hint. + Like color. This indicates that this parameter should be inferred if not explicitly + set, and that this inference requires knowledge of the other chains. For example, + if you have two chains, you probably want them to be different colors. + """ + statistics: SummaryStatistic = Field(default=SummaryStatistic.MAX, description="The summary statistic to use") - summary_area: float | None = Field(default=0.6827, description="The area to use for summary statistics") - sigmas: list[float] | None = Field(default=None, description="The sigmas to use for summary statistics") - color: ColorInput | None = Field(default=None, description="The color of the chain") - linestyle: str | None = Field(default="-", description="The line style of the chain") - linewidth: float | None = Field(default=1.0, description="The line width of the chain") - cloud: bool | None = Field(default=False, description="Whether to show the cloud of the chain") - show_contour_labels: bool | None = Field(default=False, description="Whether to show contour labels") - shade: bool | None = Field(default=None, description="Whether to shade the chain") - shade_alpha: float | None = Field(default=None, description="The alpha of the shading") - shade_gradient: float | None = Field(default=1.0, description="The contrast between contour levels") - bar_shade: bool | None = Field(default=None, description="Whether to shade marginalised distributions") - bins: int | float | None = Field(default=None, description="The number of bins to use for histograms") - kde: int | float | bool | None = Field(default=False, description="The bandwidth for KDEs") - smooth: int | float | bool | None = Field(default=3, description="The smoothing for histograms.") - color_params: str | None = Field(default=None, description="The parameter (column) to use for coloring") - plot_color_params: bool | None = Field(default=None, description="Whether to plot the color parameter") - cmap: str | None = Field(default="viridis", description="The colormap to use for shading cloud points") - num_cloud: int | float | None = Field(default=10000, description="The number of points in the cloud") - plot_cloud: bool | None = Field(default=False, description="Whether to plot the cloud") - plot_contour: bool | None = Field(default=True, description="Whether to plot contours") - plot_point: bool | None = Field(default=False, description="Whether to plot points") - show_as_1d_prior: bool | None = Field(default=False, description="Whether to show as a 1D prior") - marker_style: str | None = Field(default=None, description="The marker style to use") - marker_size: int | float | None = Field(default=None, description="The marker size to use") - marker_alpha: int | float | None = Field(default=None, description="The marker alpha to use") - zorder: int | None = Field(default=None, description="The zorder to use") + summary_area: float = Field(default=0.6827, description="The area to use for summary statistics") + sigmas: list[float] = Field(default=None, description="The sigmas to use for summary statistics") + color: ColorInput = Field(default=None, description="The color of the chain") + linestyle: str = Field(default="-", description="The line style of the chain") + linewidth: float = Field(default=1.0, description="The line width of the chain") + cloud: bool = Field(default=False, description="Whether to show the cloud of the chain") + show_contour_labels: bool = Field(default=False, description="Whether to show contour labels") + shade: bool = Field(default=None, description="Whether to shade the chain") + shade_alpha: float = Field(default=None, description="The alpha of the shading") + shade_gradient: float = Field(default=1.0, description="The contrast between contour levels") + bar_shade: bool = Field(default=None, description="Whether to shade marginalised distributions") + bins: int | float = Field( + default=1.0, description="The number of bins to use for histograms. If a float, used to scale the default bins" + ) + kde: int | float | bool = Field(default=False, description="The bandwidth for KDEs") + smooth: int = Field(default=3, description="The smoothing for histograms. Set to 0 for no smoothing") + color_param: str | None = Field(default=None, description="The parameter (column) to use for coloring") + cmap: str = Field(default="viridis", description="The colormap to use for shading cloud points") + num_cloud: int | float = Field(default=10000, description="The number of points in the cloud") + plot_cloud: bool = Field(default=False, description="Whether to plot the cloud") + plot_contour: bool = Field(default=True, description="Whether to plot contours") + plot_point: bool = Field(default=False, description="Whether to plot points") + show_as_1d_prior: bool = Field(default=False, description="Whether to show as a 1D prior") + marker_style: str = Field(default=None, description="The marker style to use") + marker_size: int | float = Field(default=None, description="The marker size to use") + marker_alpha: int | float = Field(default=None, description="The marker alpha to use") + zorder: int = Field(default=None, description="The zorder to use") shift_params: bool = Field( default=False, description="Whether to shift the parameters by subtracting each parameters mean", ) + @field_validator("color") + @classmethod + def convert_color(cls, v: ColorInput | None) -> str | None: + if v is None: + return None + return colors.format(v) + def apply_if_none(self, **kwargs: dict[str, Any]) -> None: for key, value in kwargs.items(): if getattr(self, key) is None: @@ -73,7 +89,7 @@ def apply(self, **kwargs: dict[str, Any]) -> None: setattr(self, key, value) -class Chain(Config): +class Chain(ChainConfig): samples: pd.DataFrame = Field( default=..., description="The chain data as a pandas DataFrame", @@ -115,6 +131,10 @@ class Chain(Config): description="Raise the posterior surface to this. Useful for inflating or deflating uncertainty for debugging.", ) + @property + def skip(self) -> bool: + return self.samples.empty or not (self.plot_contour or self.plot_cloud or self.plot_point) + @property def max_posterior_row(self) -> pd.Series | None: if self.posterior_column not in self.samples.columns: @@ -135,9 +155,9 @@ def log_posterior(self) -> np.ndarray | None: @property def color_data(self) -> np.ndarray | None: - if self.color_params is None: + if self.color_param is None: return None - return self.samples[self.color_params].to_numpy() + return self.samples[self.color_param].to_numpy() @field_validator("color") @classmethod @@ -147,7 +167,7 @@ def validate_color(cls, v: str | np.ndarray | list[float] | None) -> str | None: return colors.format(v) @model_validator(mode="after") - def validate_model(self) -> "Chain": + def validate_model(self) -> Chain: assert not self.samples.empty, "Your chain is empty. This is not ideal." # If weights aren't set, add them all as one @@ -173,16 +193,14 @@ def validate_model(self) -> "Chain": assert np.all(np.isfinite(self.log_posterior)), f"Chain {self.name} has NaN or inf in the log-posterior" # And if the color_params are set, ensure they're in the dataframe - if self.color_params is not None: + if self.color_param is not None: assert ( - self.color_params in self.samples.columns - ), f"Chain {self.name} does not have color parameter {self.color_params}" + self.color_param in self.samples.columns + ), f"Chain {self.name} does not have color parameter {self.color_param}" return self - def get_data(self, columns: list[str] | str) -> pd.DataFrame: - if isinstance(columns, str): - columns = [columns] + def get_data(self, columns: str) -> pd.Series[float]: return self.samples[columns] @classmethod @@ -193,7 +211,7 @@ def from_covariance( columns: list[str], name: str, **kwargs: dict[str, Any], - ) -> "Chain": + ) -> Chain: """Generate samples as per mean and covariance supplied. Useful for Fisher matrix forecasts. Args: @@ -212,7 +230,7 @@ def from_covariance( df = pd.DataFrame(samples, columns=columns) return cls(samples=df, name=name, **kwargs) # type: ignore - def divide(self) -> list["Chain"]: + def divide(self) -> list[Chain]: """Returns a ChainConsumer instance containing all the walks of a given chain as individual chains themselves. diff --git a/src/chainconsumer/chainconsumer.py b/src/chainconsumer/chainconsumer.py index 1a5fbb67..fe970ddc 100644 --- a/src/chainconsumer/chainconsumer.py +++ b/src/chainconsumer/chainconsumer.py @@ -2,12 +2,12 @@ import pandas as pd from .analysis import Analysis -from .chain import Chain, ChainName, ColumnName, Config +from .chain import Chain, ChainConfig, ChainName, ColumnName from .colors import ColorInput, colors from .comparisons import Comparison from .diagnostic import Diagnostic from .helpers import get_bins -from .plotter import Plotter +from .plotter import PlotConfig, Plotter from .truth import Truth __all__ = ["ChainConsumer"] @@ -21,7 +21,7 @@ def __init__(self): self.chains: dict[ChainName, Chain] = {} self.truths: list[Truth] = [] self.labels = {} - self.override: Config | None = None + self.global_chain_override: ChainConfig | None = None self.plotter = Plotter(self) self.diagnostic = Diagnostic(self) @@ -35,6 +35,18 @@ def all_columns(self) -> list[str]: def get_label(self, str: ColumnName) -> str: return self.labels.get(str, str) + def set_labels(self, labels: dict[str, str]) -> "ChainConsumer": + """Set the labels for the chains. + + Args: + labels (dict[str, str]): A dictionary mapping column names to labels. + + Returns: + ChainConsumer: Itself, to allow chaining calls. + """ + self.labels = labels + return self + def add_chain(self, chain: Chain): """Add a chain to ChainConsumer. @@ -49,6 +61,18 @@ def add_chain(self, chain: Chain): self.chains[key] = chain return self + def set_plot_config(self, plot_config: PlotConfig) -> "ChainConsumer": + """Set the plot config for ChainConsumer. + + Args: + plot_config (PlotConfig): The plot config to use. + + Returns: + ChainConsumer: Itself, to allow chaining calls. + """ + self.plotter.set_config(plot_config) + return self + def add_marker( self, location: np.ndarray, @@ -83,7 +107,7 @@ def add_marker( chain = Chain( samples=samples, name=name, - color=color, + color=color, # type: ignore # ignoring the None override as this means we figure out colour later marker_size=marker_size, marker_style=marker_style, marker_alpha=marker_alpha, @@ -111,7 +135,7 @@ def remove_chain(self, remove: str | Chain) -> "ChainConsumer": def add_override( self, - override: Config, + override: ChainConfig, ) -> "ChainConsumer": """Apply a custom override config @@ -121,7 +145,7 @@ def add_override( Returns: ChainConsumer: Itself, to allow chaining calls. """ - self.override = override + self.global_chain_override = override return self def _get_final_chains(self) -> dict[ChainName, Chain]: @@ -136,13 +160,15 @@ def _get_final_chains(self) -> dict[ChainName, Chain]: global_config["bar_shade"] = num_chains < 5 global_config["sigmas"] = [0, 1, 2] global_config["shade"] = num_chains < 5 - global_config["bins"] = get_bins(chain_list) global_config["shade_alpha"] = 1.0 / np.sqrt(num_chains) for _, chain in final_chains.items(): # copy global config into local config local_config = global_config.copy() + if isinstance(chain.bins, float): + chain.bins = int(chain.bins * get_bins(chain)) + # Reduce shade alpha if we're showing contour labels if chain.show_contour_labels: local_config["shade_alpha"] *= 0.5 @@ -154,8 +180,8 @@ def _get_final_chains(self) -> dict[ChainName, Chain]: chain.apply_if_none(**local_config) # Apply user overrides - if self.override is not None: - chain.apply(**self.override.model_dump()) + if self.global_chain_override is not None: + chain.apply(**self.global_chain_override.model_dump()) return final_chains diff --git a/src/chainconsumer/colors.py b/src/chainconsumer/colors.py index 627ddd09..f83a4adf 100644 --- a/src/chainconsumer/colors.py +++ b/src/chainconsumer/colors.py @@ -1,5 +1,4 @@ -from collections.abc import Iterable -from typing import Generator +from collections.abc import Generator, Iterable import matplotlib.pyplot as plt import numpy as np diff --git a/src/chainconsumer/diagnostic.py b/src/chainconsumer/diagnostic.py index 26f002ee..ee4dec33 100644 --- a/src/chainconsumer/diagnostic.py +++ b/src/chainconsumer/diagnostic.py @@ -45,7 +45,7 @@ def gelman_rubin(self, chain=None, threshold=0.05): :math:`\hat{V} = \frac{n_1}{n}W + \frac{1}{n}B`, and have our convergence ratio :math:`\hat{R} = \sqrt{\frac{\hat{V}}{W}}`. We check that for all parameters, this ratio deviates from unity by less than the supplied threshold. - """ + """ # noqa: E501 if chain is None: return np.all([self.gelman_rubin(k, threshold=threshold) for k in range(len(self.parent.chains))]) diff --git a/src/chainconsumer/helpers.py b/src/chainconsumer/helpers.py index 33bc5001..2188a611 100644 --- a/src/chainconsumer/helpers.py +++ b/src/chainconsumer/helpers.py @@ -1,9 +1,17 @@ import numpy as np +import pandas as pd from .chain import Chain -def get_extents(data, weight, plot=False, wide_extents=True, tiny=False, pad=False): +def get_extents( + data: pd.Series, + weight: np.ndarray, + plot: bool = False, + wide_extents: bool = True, + tiny: bool = False, + pad: bool = False, +) -> tuple[float, float]: hist, be = np.histogram(data, weights=weight, bins=2000) bc = 0.5 * (be[1:] + be[:-1]) cdf = hist.cumsum() @@ -18,8 +26,8 @@ def get_extents(data, weight, plot=False, wide_extents=True, tiny=False, pad=Fal threshold = 0.3 i1 = np.where(cdf > threshold)[0][0] i2 = np.where(icdf > threshold)[0][0] - lower = bc[i1] - upper = bc[-i2] + lower = float(bc[i1]) + upper = float(bc[-i2]) if pad: width = upper - lower lower -= 0.2 * width @@ -27,22 +35,38 @@ def get_extents(data, weight, plot=False, wide_extents=True, tiny=False, pad=Fal return lower, upper -def get_bins(chains: list[Chain]): - proposal = [ - max(35, np.floor(1.0 * np.power(chain.samples.shape[0] / chain.samples.shape[1], 0.25))) for chain in chains - ] - return proposal +def get_bins(chain: Chain) -> int: + return max((35, np.floor(1.0 * np.power(chain.samples.shape[0] / chain.samples.shape[1], 0.25)))) -def get_smoothed_bins(smooth, bins, data, weight, marginalised=True, plot=False, pad=False): +def get_smoothed_bins( + smooth: int, + bins: int, + data: pd.Series, + weight: np.ndarray, + plot: bool = False, + pad: bool = False, +) -> tuple[np.ndarray, int]: + """Get the bins for a histogram, with smoothing. + + Args: + smooth (int): The smoothing factor + bins (int): The number of bins + data (pd.Series): The data + weight (np.ndarray): The weights + plot (bool, optional): Whether this is used in plotting. Determines how conservative to be on extents + Defaults to False. + pad (bool, optional): Whether to pad the histogram. Determines how conservative to be on extents + Defaults to False. + """ minv, maxv = get_extents(data, weight, plot=plot, pad=pad) - if smooth is None or not smooth or smooth == 0: + if smooth == 0: return np.linspace(minv, maxv, int(bins)), 0 else: - return np.linspace(minv, maxv, int((2 if marginalised else 2) * smooth * bins)), smooth + return np.linspace(minv, maxv, 2 * smooth * bins), smooth -def get_grid_bins(data): +def get_grid_bins(data: pd.Series[float]) -> np.ndarray: bin_c = np.sort(np.unique(data)) delta = 0.5 * (bin_c[1] - bin_c[0]) bins = np.concatenate((bin_c - delta, [bin_c[-1] + delta])) diff --git a/src/chainconsumer/plotter.py b/src/chainconsumer/plotter.py index 81dea217..db028b2d 100644 --- a/src/chainconsumer/plotter.py +++ b/src/chainconsumer/plotter.py @@ -1,41 +1,187 @@ -import logging +from enum import Enum +from pathlib import Path +from typing import Any import matplotlib import matplotlib.pyplot as plt import numpy as np +import pandas as pd +from matplotlib.axes import Axes +from matplotlib.collections import PathCollection +from matplotlib.figure import Figure from matplotlib.font_manager import FontProperties +from matplotlib.lines import Line2D from matplotlib.textpath import TextPath from matplotlib.ticker import LogLocator, MaxNLocator, ScalarFormatter from numpy import meshgrid +from pydantic import Field from scipy.interpolate import interp1d from scipy.ndimage import gaussian_filter from scipy.stats import norm +from chainconsumer.truth import Truth + +from .base import BetterBase +from .chain import Chain, ChainName, ColumnName +from .colors import ColorInput, colors from .helpers import get_extents, get_grid_bins, get_smoothed_bins from .kde import MegKDE +from .log import logger + + +class PlottingBase(BetterBase): + chains: list[Chain] + columns: list[ColumnName] + extents: dict[ColumnName, tuple[float, float]] + blind: list[ColumnName] + log_scales: list[ColumnName] + + +class PlotConfig(BetterBase): + labels: dict[ColumnName, str] = Field(default={}, description="Labels for parameters") + sigma2d: bool | None = Field( + default=None, + description=( + "Whether to use 2D sigmas for summary statistics. Ie in 2D a 1sigma contour" + r" does *not* encapsulate 68% of the volume, it covers 39.3% of the volume." + ), + ) + max_ticks: int = Field(default=5, ge=0, description="Maximum number of ticks to use on axes") + plot_hists: bool = Field(default=True, description="Whether to plot the 1D histograms") + flip: bool = Field(default=False, description="Whether to flip the 1D histograms") + serif: bool = Field(default=False, description="Whether to use a serif font") + usetex: bool = Field(default=False, description="Whether to use LaTeX for text rendering") + diagonal_tick_labels: bool = Field(default=True, description="Whether to show tick labels on the diagonal") + label_font_size: int = Field(default=12, ge=0, description="Font size for axis labels") + tick_font_size: int = Field(default=10, ge=0, description="Font size for axis ticks") + spacing: float = Field(default=None, ge=0, description="Spacing between subplots") + contour_label_font_size: int = Field(default=10, ge=0, description="Font size for contour labels") + legend_kwargs: dict[str, Any] = Field(default={}, description="Kwargs to pass to the legend") + legend_location: tuple[int, int] | None = Field(default=None, description="Which subplot to put the legend in") + legend_artists: bool | None = Field(default=None, description="Whether to show artists in the legend") + legend_color_text: bool = Field(default=True, description="Whether to color the legend text") + watermark_text_kwargs: dict[str, Any] = Field(default={}, description="Kwargs to pass to the watermark text") + title_size: int = Field(default=14, ge=0, description="Font size for titles") + summarise: bool = Field(default=True, description="Whether to annotate the plot with summary statistics") + + @property + def legend_kwargs_final(self) -> dict[str, Any]: + default = { + "labelspacing": 0.3, + "loc": "upper right", + "frameon": False, + "fontsize": self.label_font_size, + "handlelength": 1, + "handletextpad": 0.2, + "borderaxespad": 0.0, + } + return default | self.legend_kwargs + + @property + def watermark_text_kwargs_final(self) -> dict[str, Any]: + default = { + "color": "#333333", + "alpha": 0.7, + "verticalalignment": "center", + "horizontalalignment": "center", + } + return default | self.watermark_text_kwargs + + def get_label(self, column: ColumnName) -> str: + return self.labels.get(column, column) + + +class FigSize(Enum): + """Enum for figure size options""" + + COLUMN = "COLUMN" + PAGE = "PAGE" + GROW = "GROW" + + @classmethod + def get_size( + cls, input: "FigSize | float | int | tuple[float, float]", num_columns: int, has_cax: bool + ) -> tuple[float, float]: + if input == FigSize.PAGE: + return 10, 10 + if input == FigSize.COLUMN: + return 5 + (1 if has_cax else 0), 5 + grow_factor = 1.0 + if isinstance(input, float): + grow_factor = input + elif isinstance(input, int): + return float(input), float(input) + elif isinstance(input, tuple): + return input + + # Otherwise it must be grow, which is the default + return grow_factor * 1.5 * num_columns + (1 if has_cax else 0), grow_factor * 1.5 * num_columns + + +def get_artists_from_chains(chains: list[Chain]): + artists = [] + for chain in chains: + if chain.plot_contour and not chain.plot_point: + artists.append( + Line2D((0, 1), (0, 0), color=colors.format(chain.color), ls=chain.linestyle, lw=chain.linewidth) + ) + elif not chain.plot_contour and chain.plot_point: + artists.append( + Line2D( + (0, 1), + (0, 0), + color=colors.format(chain.color), + ls=chain.linestyle, + lw=0, + marker=chain.marker_style, + markersize=chain.marker_size, + ) + ) + else: + artists.append( + Line2D( + (0, 1), + (0, 0), + color=colors.format(chain.color), + ls=chain.linestyle, + lw=chain.linewidth, + marker=chain.marker_style, + markersize=chain.marker_size, + ) + ) + return artists class Plotter: def __init__(self, parent): - self.parent = parent - self._logger = logging.getLogger("chainconsumer") + self.parent: "ChainConsumer" = parent + self._config: PlotConfig | None = None + self._default_config = PlotConfig() self.usetex_old = matplotlib.rcParams["text.usetex"] self.serif_old = matplotlib.rcParams["font.family"] + def set_config(self, config: PlotConfig): + self._config = config + + @property + def config(self) -> PlotConfig: + if self._config is None: + return self._default_config + return self._config + def plot( self, - figsize="GROW", - parameters=None, - chains=None, - extents=None, - filename=None, - display=False, - truth=None, - legend=None, - blind=None, - watermark=None, - log_scales=None, + figsize: FigSize | float | int | tuple[float, float] = FigSize.GROW, + columns: list[ColumnName] | None = None, + chains: list[ChainName | Chain] | None = None, + extents: dict[ColumnName, tuple[float, float]] | None = None, + filename: list[str | Path] | str | Path | None = None, + display: bool = False, + show_legend: bool | None = None, + blind: bool | list[str] | None = None, + watermark: str | None = None, + log_scales: list[ColumnName] | None = None, ): # pragma: no cover """Plot the chain! @@ -49,7 +195,7 @@ def plot( scales with parameters (1.5 inches per parameter). String arguments are not case sensitive. If you pass a float, it will scale the default ``GROW`` by that amount, so ``2.0`` would result in a plot 3 inches per parameter. - parameters : list[str]|int, optional + columns : list[str], optional If set, only creates a plot for those specific parameters (if list). If an integer is given, only plots the fist so many parameters. chains : int|str, list[str|int], optional @@ -74,7 +220,7 @@ def plot( or can pass in a string (or list of strings) which specify the parameters to blind. watermark : str, optional A watermark to add to the figure - log_scales : bool, list[bool] or dict[bool], optional + log_scales : bool, list[ColumnName], optional Whether or not to use a log scale on any given axis. Can be a list of True/False, a list of param names to set to true, a dictionary of param names with true/false or just a bool (just `True` would set everything to log scales). @@ -86,115 +232,47 @@ def plot( """ - chains, parameters, truth, extents, blind, log_scales = self._sanitise( - chains, parameters, truth, extents, color_p=True, blind=blind, log_scales=log_scales - ) - names = [chain.name for chain in chains] + base = self._sanitise(chains, columns, extents, blind=blind, log_scales=log_scales) - if legend is None: - legend = len(chains) > 1 + if show_legend is None: + show_legend = len(base.chains) > 1 - # If no chains have names, don't plot the legend - legend = legend and len([n for n in names if n]) > 0 - - # Calculate cmap extents - unique_color_params = list( - set([c.config["color_params"] for c in chains if c.config["color_params"] is not None]) - ) - num_cax = len(unique_color_params) - color_param_extents = {} - for u in unique_color_params: - umin, umax = np.inf, -np.inf - for chain in chains: - if chain.config["color_params"] == u: - data = chain.get_color_data() - if data is not None: - umin = min(umin, data.min()) - umax = max(umax, data.max()) - color_param_extents[u] = (umin, umax) - - grow_size = 1.5 - if isinstance(figsize, float): - grow_size *= figsize - figsize = "GROW" - - if isinstance(figsize, str): - if figsize.upper() == "COLUMN": - figsize = (5 + (1 if num_cax > 0 else 0), 5) - elif figsize.upper() == "PAGE": - figsize = (10, 10) - elif figsize.upper() == "GROW": - figsize = (grow_size * len(parameters) + num_cax * 1.0, grow_size * len(parameters)) - else: - raise ValueError("Unknown figure size %s" % figsize) - elif isinstance(figsize, float): - figsize = (figsize * grow_size * len(parameters), figsize * grow_size * len(parameters)) - - plot_hists = self.parent.config["plot_hists"] - flip = len(parameters) == 2 and plot_hists and self.parent.config["flip"] - - fig, axes, params1, params2, extents = self._get_figure( - parameters, - chains=chains, - figsize=figsize, - flip=flip, - external_extents=extents, - blind=blind, - log_scales=log_scales, - ) - label_font_size = self.parent.config["label_font_size"] + num_cax = len(set([chain.color_param for chain in base.chains if chain.color_param is not None])) + fig_size = FigSize.get_size(figsize, len(base.columns), num_cax > 0) + plot_hists = self.config.plot_hists + flip = len(base.columns) == 2 and plot_hists and self.config.flip + fig, axes, params_x, params_y = self._get_figure(base, figsize=fig_size) axl = axes.ravel().tolist() - summary = self.parent.config["summary"] + summarise = self.config.summarise and len(base.chains) == 1 - if summary is None: - summary = len(parameters) < 5 and len(self.parent.chains) == 1 - if len(chains) == 1: - self._logger.debug(f"Plotting surfaces for chain of dimension {chains[0].chain.shape}") - else: - self._logger.debug("Plotting surfaces for %d chains" % len(chains)) cbar_done = [] - - chain_points = [c for c in chains if c.config["plot_point"]] - num_chain_points = len(chain_points) - if num_chain_points: - subgroup_names = list(set([c.name for c in chain_points])) - subgroups = [[c for c in chain_points if c.name == n] for n in subgroup_names] - markers = [group[0].config["marker_style"] for group in subgroups] # Only one marker per group - marker_sizes = [[g.config["marker_size"] for g in group] for group in subgroups] # But size can diff - marker_alphas = [group[0].config["marker_alpha"] for group in subgroups] # Only one marker per group - for i, p1 in enumerate(params1): - for j, p2 in enumerate(params2): + for i, p1 in enumerate(params_x): + for j, p2 in enumerate(params_y): if i < j: continue - ax = axes[i, j] - do_flip = flip and i == len(params1) - 1 + ax: Axes = axes[i, j] + do_flip = flip and i == len(params_x) - 1 # Plot the histograms if plot_hists and i == j: - if do_flip: - self._add_truth(ax, truth, p1) - else: - self._add_truth(ax, truth, None, py=p2) + for truth in self.parent.truths: + if do_flip: + self._add_truth(ax, truth, px=p1) + else: + self._add_truth(ax, truth, py=p2) max_val = None # Plot each chain - for chain in chains: - if p1 not in chain.parameters: - continue - if not chain.config["plot_contour"]: + for chain in base.chains: + if not chain.plot_contour or p1 not in chain.samples: continue - param_summary = summary and p1 not in blind - m = self._plot_bars(ax, p1, chain, flip=do_flip, summary=param_summary) - - if max_val is None or m > max_val: - max_val = m + do_summary = summarise and p1 not in base.blind + max_hist_val = self._plot_bars(ax, p1, chain, flip=do_flip, summary=do_summary) - if num_chain_points and self.parent.config["global_point"]: - m = self._plot_point_histogram(ax, subgroups, p1, flip=do_flip) - if max_val is None or m > max_val: - max_val = m + if max_val is None or max_hist_val > max_val: + max_val = max_hist_val if max_val is not None: if do_flip: @@ -203,87 +281,57 @@ def plot( ax.set_ylim(0, 1.1 * max_val) else: - for chain in chains: - if p1 not in chain.parameters or p2 not in chain.parameters: + for chain in base.chains: + if p1 not in chain.samples or p2 not in chain.samples: continue - if not chain.config["plot_contour"] or chain.config["show_as_1d_prior"]: + if not chain.plot_contour or chain.show_as_1d_prior: continue - h = None - if p1 in chain.parameters and p2 in chain.parameters: - h = self._plot_contour(ax, chain, p1, p2, color_extents=color_param_extents) - cp = chain.config["color_params"] + + h = self._plot_contour(ax, chain, p1, p2) + cp = chain.color_param if h is not None and cp is not None and cp not in cbar_done: cbar_done.append(cp) - aspect = figsize[1] / 0.15 - fraction = 0.85 / figsize[0] + aspect = fig_size[1] / 0.15 + fraction = 0.85 / fig_size[0] cbar = fig.colorbar(h, ax=axl, aspect=aspect, pad=0.03, fraction=fraction, drawedges=False) - label = cp + label = self.config.get_label(cp) if label == "weights": label = "Weights" elif label == "log_weights": label = "log(Weights)" elif label == "posterior": label = "log(Posterior)" - cbar.set_label(label, fontsize=label_font_size) - cbar.solids.set(alpha=1) - - if num_chain_points: - self._plot_points(ax, subgroups, markers, marker_sizes, marker_alphas, p1, p2) - - self._add_truth(ax, truth, p1, py=p2) - - colors = [c.config["color"] for c in chains] - plot_points = [c.config["plot_point"] for c in chains] - plot_contours = [c.config["plot_contour"] for c in chains] - linestyles = [c.config["linestyle"] for c in chains] - linewidths = [c.config["linewidth"] for c in chains] - marker_styles = [c.config["marker_style"] for c in chains] - marker_sizes = [c.config["marker_size"] for c in chains] - legend_kwargs = self.parent.config["legend_kwargs"] - legend_artists = self.parent.config["legend_artists"] - legend_color_text = self.parent.config["legend_color_text"] - legend_location = self.parent.config["legend_location"] + cbar.set_label(label, fontsize=self.config.label_font_size) + if cbar.solids is not None: + cbar.solids.set(alpha=1) + + for truth in self.parent.truths: + self._add_truth(ax, truth, px=p1, py=p2) + legend_location = self.config.legend_location if legend_location is None: - legend_location = (0, -1) if not flip or len(parameters) > 2 else (-1, 0) - outside = legend_location[0] >= legend_location[1] - if names is not None and legend: + legend_location = (0, -1) if not flip or len(base.columns) > 2 else (-1, 0) + legend_outside = legend_location[0] >= legend_location[1] + + if show_legend: ax = axes[legend_location[0], legend_location[1]] + legend_kwargs = self.config.legend_kwargs_final.copy() if "markerfirst" not in legend_kwargs: - # If we have legend inside a used subplot, switch marker order - legend_kwargs["markerfirst"] = outside or not legend_artists - linewidths2 = linewidths if legend_artists else [0] * len(linewidths) - linestyles2 = linestyles if legend_artists else ["-"] * len(linestyles) - marker_sizes2 = marker_sizes if legend_artists else [0] * len(linestyles) - - artists = [] - done_names = [] - final_colors = [] - for i, (n, c, ls, lw, marker, size, pp, pc) in enumerate( - zip(names, colors, linestyles2, linewidths2, marker_styles, marker_sizes2, plot_points, plot_contours) - ): - if n is None or n in done_names: - continue - done_names.append(n) - final_colors.append(c) - size = np.sqrt(size) # plot vs scatter use size differently, hence the sqrt - if pc and not pp: - artists.append(plt.Line2D((0, 1), (0, 0), color=c, ls=ls, lw=lw)) - elif not pc and pp: - artists.append(plt.Line2D((0, 1), (0, 0), color=c, ls=ls, lw=0, marker=marker, markersize=size)) - else: - artists.append(plt.Line2D((0, 1), (0, 0), color=c, ls=ls, lw=lw, marker=marker, markersize=size)) - - leg = ax.legend(artists, done_names, **legend_kwargs) - if legend_color_text: - for text, c in zip(leg.get_texts(), final_colors): - text.set_weight("medium") - text.set_color(c) - if not outside: - loc = legend_kwargs.get("loc") or "" - if isinstance(loc, str) and "right" in loc.lower(): - vp = leg._legend_box._children[-1]._children[0] - vp.align = "right" + legend_kwargs["markerfirst"] = legend_outside or not self.config.legend_artists + + artists = get_artists_from_chains(base.chains) + leg = ax.legend(artists, **legend_kwargs) + if self.config.legend_color_text: + for text, chain in zip(leg.get_texts(), base.chains): + text.set_fontweight("medium") + text.set_color(colors.format(chain.color)) + + # TODO: This seems like behaviour which no longer works + # if not legend_outside: + # loc = legend_kwargs.get("loc") or "" + # if isinstance(loc, str) and "right" in loc.lower(): + # vp = leg._legend_box._children[-1]._children[0] + # vp.align = "right" fig.canvas.draw() for ax in axes[-1, :]: @@ -297,11 +345,11 @@ def plot( dpi = 300 if watermark: - ax = axes[-1, 0] if flip and len(parameters) == 2 else None - self._add_watermark(fig, ax, figsize, watermark, dpi=dpi) + ax_watermark = axes[-1, 0] if flip and len(base.columns) == 2 else None + self._add_watermark(fig, ax_watermark, fig_size, watermark, dpi=dpi) if filename is not None: - if isinstance(filename, str): + if not isinstance(filename, list): filename = [filename] for f in filename: self._save_fig(fig, f, dpi) @@ -310,20 +358,22 @@ def plot( return fig - def _save_fig(self, fig, filename, dpi): # pragma: no cover + def _save_fig(self, fig: Figure, filename: str | Path, dpi: int) -> None: # pragma: no cover fig.savefig(filename, bbox_inches="tight", dpi=dpi, transparent=True, pad_inches=0.05) - def _add_watermark(self, fig, axes, figsize, text, dpi=300, size_scale=1.0): # pragma: no cover + def _add_watermark( + self, fig: Figure, axes: Axes | None, fig_size: tuple[float, float], text: str, dpi=300, size_scale: float = 1.0 + ) -> None: # pragma: no cover # Code based off github repository https://github.com/cpadavis/preliminize - dx, dy = figsize + dx, dy = fig_size dy, dx = dy * dpi, dx * dpi rotation = 180 / np.pi * np.arctan2(-dy, dx) - property_dict = self.parent.config["watermark_text_kwargs"] + property_dict = self.config.watermark_text_kwargs keys_in_font_dict = ["family", "style", "variant", "weight", "stretch", "size"] fontdict = {k: property_dict[k] for k in keys_in_font_dict if k in property_dict} font_prop = FontProperties(**fontdict) - usetex = property_dict.get("usetex", self.parent.config["usetex"]) + usetex = property_dict.get("usetex", self.config.usetex) if usetex: px, py, scale = 0.5, 0.5, 1.0 else: @@ -340,614 +390,578 @@ def _add_watermark(self, fig, axes, figsize, text, dpi=300, size_scale=1.0): # else: size *= 0.8 size = int(size) - print(f"Font size is {size}") if axes is None: fig.text(px, py, text, fontdict=property_dict, rotation=rotation, fontsize=size) else: axes.text(px, py, text, transform=axes.transAxes, fontdict=property_dict, rotation=rotation, fontsize=size) - def plot_walks( - self, - parameters=None, - truth=None, - extents=None, - display=False, - filename=None, - chains=None, - convolve=None, - figsize=None, - plot_weights=True, - plot_posterior=True, - log_weight=None, - log_scales=None, - ): # pragma: no cover - """Plots the chain walk; the parameter values as a function of step index. - - This plot is more for a sanity or consistency check than for use with final results. - Plotting this before plotting with :func:`plot` allows you to quickly see if the - chains are well behaved, or if certain parameters are suspect - or require a greater burn in period. - - The desired outcome is to see an unchanging distribution along the x-axis of the plot. - If there are obvious tails or features in the parameters, you probably want - to investigate. - - Parameters - ---------- - parameters : list[str]|int, optional - Specify a subset of parameters to plot. If not set, all parameters are plotted. - If an integer is given, only the first so many parameters are plotted. - truth : list[float]|dict[str], optional - A list of truth values corresponding to parameters, or a dictionary of - truth values keyed by the parameter. - extents : list[tuple]|dict[str], optional - A list of two-tuples for plot extents per parameter, or a dictionary of - extents keyed by the parameter. - display : bool, optional - If set, shows the plot using ``plt.show()`` - filename : str, optional - If set, saves the figure to the filename - chains : int|str, list[str|int], optional - Used to specify which chain to show if more than one chain is loaded in. - Can be an integer, specifying the - chain index, or a str, specifying the chain name. - convolve : int, optional - If set, overplots a smoothed version of the steps using ``convolve`` as - the width of the smoothing filter. - figsize : tuple, optional - If set, sets the created figure size. - plot_weights : bool, optional - If true, plots the weight if they are available - plot_posterior : bool, optional - If true, plots the log posterior if they are available - log_weight : bool, optional - Whether to display weights in log space or not. If None, the value is - inferred by the mean weights of the plotted chains. - log_scales : bool, list[bool] or dict[bool], optional - Whether or not to use a log scale on any given axis. Can be a list of True/False, a list of param - names to set to true, a dictionary of param names with true/false - or just a bool (just `True` would set everything to log scales). - - Returns - ------- - figure - the matplotlib figure created - - """ - - chains, parameters, truth, extents, _, log_scales = self._sanitise( - chains, parameters, truth, extents, log_scales=log_scales - ) - - chains = [c for c in chains if c.mcmc_chain] - n = len(parameters) - extra = 0 - if plot_weights: - plot_weights = plot_weights and np.any([np.any(c.weights != 1.0) for c in chains]) - - plot_posterior = plot_posterior and np.any([c.posterior is not None for c in chains]) - - if plot_weights: - extra += 1 - if plot_posterior: - extra += 1 - - if figsize is None: - figsize = (8, 0.75 + (n + extra)) - - fig, axes = plt.subplots(figsize=figsize, nrows=n + extra, squeeze=False, sharex=True) - - for i, axes_row in enumerate(axes): - ax = axes_row[0] - if i >= extra: - p = parameters[i - n] - for chain in chains: - if p in chain.parameters: - chain_row = chain.get_data(p) - log = log_scales.get(p, False) - self._plot_walk( - ax, - p, - chain_row, - extents=extents.get(p), - convolve=convolve, - color=chain.config["color"], - log_scale=log, - ) - if truth.get(p) is not None: - self._plot_walk_truth(ax, truth.get(p)) - else: - if i == 0 and plot_posterior: - for chain in chains: - if chain.log_posterior is not None: - self._plot_walk( - ax, - r"$\log(P)$", - chain.log_posterior - chain.log_posterior.max(), - convolve=convolve, - color=chain.config["color"], - ) - else: - if log_weight is None: - log_weight = np.any([chain.weights.mean() < 0.1 for chain in chains]) - if log_weight: - for chain in chains: - self._plot_walk( - ax, - r"$\log_{10}(w)$", - np.log10(chain.weights), - convolve=convolve, - color=chain.config["color"], - ) - else: - for chain in chains: - self._plot_walk(ax, "$w$", chain.weights, convolve=convolve, color=chain.config["color"]) - - if filename is not None: - if isinstance(filename, str): - filename = [filename] - for f in filename: - self._save_fig(fig, f, 300) - if display: - plt.show() - - return fig - - def plot_distributions( - self, - parameters=None, - truth=None, - extents=None, - display=False, - filename=None, - chains=None, - col_wrap=4, - figsize=None, - blind=None, - log_scales=None, - ): # pragma: no cover - """Plots the 1D parameter distributions for verification purposes. - - This plot is more for a sanity or consistency check than for use with final results. - Plotting this before plotting with :func:`plot` allows you to quickly see if the - chains give well behaved distributions, or if certain parameters are suspect - or require a greater burn in period. - - - Parameters - ---------- - parameters : list[str]|int, optional - Specify a subset of parameters to plot. If not set, all parameters are plotted. - If an integer is given, only the first so many parameters are plotted. - truth : list[float]|dict[str], optional - A list of truth values corresponding to parameters, or a dictionary of - truth values keyed by the parameter. - extents : list[tuple]|dict[str], optional - A list of two-tuples for plot extents per parameter, or a dictionary of - extents keyed by the parameter. - display : bool, optional - If set, shows the plot using ``plt.show()`` - filename : str, optional - If set, saves the figure to the filename - chains : int|str, list[str|int], optional - Used to specify which chain to show if more than one chain is loaded in. - Can be an integer, specifying the - chain index, or a str, specifying the chain name. - col_wrap : int, optional - How many columns to plot before wrapping. - figsize : tuple(float)|float, optional - Either a tuple specifying the figure size or a float scaling factor. - blind : bool|string|list[string], optional - Whether to blind axes values. Can be set to `True` to blind all parameters, - or can pass in a string (or list of strings) which specify the parameters to blind. - log_scales : bool, list[bool] or dict[bool], optional - Whether or not to use a log scale on any given axis. Can be a list of True/False, a list of param - names to set to true, a dictionary of param names with true/false - or just a bool (just `True` would set everything to log scales). - - Returns - ------- - figure - the matplotlib figure created - - """ - chains, parameters, truth, extents, blind, log_scales = self._sanitise( - chains, parameters, truth, extents, blind=blind, log_scales=log_scales - ) - - n = len(parameters) - num_cols = min(n, col_wrap) - num_rows = int(np.ceil(1.0 * n / col_wrap)) - - if figsize is None: - figsize = 1.0 - if isinstance(figsize, float): - figsize_float = figsize - figsize = (num_cols * 2 * figsize, num_rows * 2 * figsize) - else: - figsize_float = 1.0 - - summary = self.parent.config["summary"] - label_font_size = self.parent.config["label_font_size"] - tick_font_size = self.parent.config["tick_font_size"] - max_ticks = self.parent.config["max_ticks"] - diagonal_tick_labels = self.parent.config["diagonal_tick_labels"] - - if summary is None: - summary = len(self.parent.chains) == 1 - - hspace = (0.8 if summary else 0.5) / figsize_float - fig, axes = plt.subplots(nrows=num_rows, ncols=num_cols, figsize=figsize, squeeze=False) - fig.subplots_adjust(left=0.1, right=0.95, top=0.95, bottom=0.1, wspace=0.05, hspace=hspace) - - formatter = ScalarFormatter(useOffset=False) - formatter.set_powerlimits((-3, 4)) - - for i, ax in enumerate(axes.flatten()): - if i >= len(parameters): - ax.set_axis_off() - continue - p = parameters[i] - - ax.set_yticks([]) - if log_scales.get(p, False): - ax.set_xscale("log") - if p in blind: - ax.set_xticks([]) - else: - if diagonal_tick_labels: - _ = [l.set_rotation(45) for l in ax.get_xticklabels()] - _ = [l.set_fontsize(tick_font_size) for l in ax.get_xticklabels()] - - if log_scales.get(p, False): - ax.xaxis.set_major_locator(LogLocator(numticks=max_ticks)) - else: - ax.xaxis.set_major_locator(MaxNLocator(max_ticks, prune="lower")) - ax.xaxis.set_major_formatter(formatter) - ax.set_xlim(extents.get(p) or self._get_parameter_extents(p, chains)) - - max_val = None - for chain in chains: - if not chain.config["plot_contour"]: - continue - if p in chain.parameters: - param_summary = summary and p not in blind - m = self._plot_bars(ax, p, chain, summary=param_summary) - if max_val is None or m > max_val: - max_val = m - - self._add_truth(ax, truth, None, py=p) - ax.set_ylim(0, 1.1 * max_val) - ax.set_xlabel(p, fontsize=label_font_size) - - if filename is not None: - if isinstance(filename, str): - filename = [filename] - for f in filename: - self._save_fig(fig, f, 300) - if display: - plt.show() - - return fig - - def plot_summary( - self, - parameters=None, - truth=None, - extents=None, - display=False, - filename=None, - chains=None, - figsize=1.0, - errorbar=False, - include_truth_chain=True, - blind=None, - watermark=None, - extra_parameter_spacing=0.5, - vertical_spacing_ratio=1.0, - show_names=True, - log_scales=None, - ): # pragma: no cover - """Plots parameter summaries - - This plot is more for a sanity or consistency check than for use with final results. - Plotting this before plotting with :func:`plot` allows you to quickly see if the - chains give well behaved distributions, or if certain parameters are suspect - or require a greater burn in period. - - - Parameters - ---------- - parameters : list[str]|int, optional - Specify a subset of parameters to plot. If not set, all parameters are plotted. - If an integer is given, only the first so many parameters are plotted. - truth : list[float]|list|list[float]|dict[str]|str, optional - A list of truth values corresponding to parameters, or a dictionary of - truth values keyed by the parameter. Each "truth value" can be either a float (will - draw a vertical line), two floats (a shaded interval) or three floats (min, mean, max), - which renders as a shaded interval with a line for the mean. Or, supply a string - which matches a chain name, and the results for that chain will be used as the 'truth' - extents : list[tuple]|dict[str], optional - A list of two-tuples for plot extents per parameter, or a dictionary of - extents keyed by the parameter. - display : bool, optional - If set, shows the plot using ``plt.show()`` - filename : str, optional - If set, saves the figure to the filename - chains : int|str, list[str|int], optional - Used to specify which chain to show if more than one chain is loaded in. - Can be an integer, specifying the - chain index, or a str, specifying the chain name. - figsize : float, optional - Scale horizontal and vertical figure size. - errorbar : bool, optional - Whether to onle plot an error bar, instead of the marginalised distribution. - include_truth_chain : bool, optional - If you specify another chain as the truth chain, determine if it should still - be plotted. - blind : bool|string|list[string], optional - Whether to blind axes values. Can be set to `True` to blind all parameters, - or can pass in a string (or list of strings) which specify the parameters to blind. - watermark : str, optional - A watermark to add to the figure - extra_parameter_spacing : float, optional - Increase horizontal space for parameter values - vertical_spacing_ratio : float, optional - Increase vertical space for each model - show_names : bool, optional - Whether to show chain names or not. Defaults to `True`. - log_scales : bool, list[bool] or dict[bool], optional - Whether or not to use a log scale on any given axis. Can be a list of True/False, a list of param - names to set to true, a dictionary of param names with true/false - or just a bool (just `True` would set everything to log scales). - - Returns - ------- - figure - the matplotlib figure created - - """ - wide_extents = not errorbar - chains, parameters, truth, extents, blind, log_scales = self._sanitise( - chains, parameters, truth, extents, blind=blind, wide_extents=wide_extents, log_scales=log_scales - ) - - all_names = [c.name for c in self.parent.chains] - - # Check if we're using a chain for truth values - if isinstance(truth, str): - assert truth in all_names, f"Truth chain {truth} is not in the list of added chains {all_names}" - if not include_truth_chain: - chains = [c for c in chains if c.name != truth] - truth = self.parent.analysis.get_summary(chains=truth, parameters=parameters) - - max_param = self._get_size_of_texts(parameters) - fid_dpi = 65 # Seriously I have no idea what value this should be - param_width = extra_parameter_spacing + max(0.5, max_param / fid_dpi) - - if show_names: - max_model_name = self._get_size_of_texts([chain.name for chain in chains]) - model_width = 0.25 + (max_model_name / fid_dpi) - gridspec_kw = { - "width_ratios": [model_width] + [param_width] * len(parameters), - "height_ratios": [1] * len(chains), - } - ncols = 1 + len(parameters) - else: - model_width = 0 - gridspec_kw = {"width_ratios": [param_width] * len(parameters), "height_ratios": [1] * len(chains)} - ncols = len(parameters) - - top_spacing = 0.3 - bottom_spacing = 0.2 - row_height = (0.5 if not errorbar else 0.3) * vertical_spacing_ratio - width = param_width * len(parameters) + model_width - height = top_spacing + bottom_spacing + row_height * len(chains) - top_ratio = 1 - (top_spacing / height) - bottom_ratio = bottom_spacing / height - - figsize = (width * figsize, height * figsize) - fig, axes = plt.subplots( - nrows=len(chains), ncols=ncols, figsize=figsize, squeeze=False, gridspec_kw=gridspec_kw - ) - fig.subplots_adjust(left=0.05, right=0.95, top=top_ratio, bottom=bottom_ratio, wspace=0.0, hspace=0.0) - label_font_size = self.parent.config["label_font_size"] - legend_color_text = self.parent.config["legend_color_text"] - - max_vals = {} - for i, row in enumerate(axes): - chain = chains[i] - - ( - cs, - ws, - ps, - ) = ( - chain.chain, - chain.weights, - chain.parameters, - ) - gs, ns = chain.grid, chain.name - - colour = chain.config["color"] - - # First one put name of model - if show_names: - ax_first = row[0] - ax_first.set_axis_off() - text_colour = "k" if not legend_color_text else colour - ax_first.text( - 0, - 0.5, - ns, - transform=ax_first.transAxes, - fontsize=label_font_size, - verticalalignment="center", - color=text_colour, - weight="medium", - ) - cols = row[1:] - else: - cols = row - - for ax, p in zip(cols, parameters): - # Set up the frames - if i > 0: - ax.spines["top"].set_visible(False) - if i < (len(chains) - 1): - ax.spines["bottom"].set_visible(False) - if i < (len(chains) - 1) or p in blind: - ax.set_xticks([]) - ax.set_yticks([]) - ax.set_xlim(extents[p]) - if log_scales.get(p): - ax.set_xscale("log") - - # Put title in - if i == 0: - ax.set_title(r"$%s$" % p, fontsize=label_font_size) - - # Add truth values - truth_value = truth.get(p) - if truth_value is not None: - if isinstance(truth_value, float | int): - truth_mean = truth_value - truth_min, truth_max = None, None - else: - if len(truth_value) == 1: - truth_mean = truth_value - truth_min, truth_max = None, None - elif len(truth_value) == 2: - truth_min, truth_max = truth_value - truth_mean = None - else: - truth_min, truth_mean, truth_max = truth_value - if truth_mean is not None: - ax.axvline(truth_mean, **self.parent.config_truth) - if truth_min is not None and truth_max is not None: - ax.axvspan(truth_min, truth_max, color=self.parent.config_truth["color"], alpha=0.15, lw=0) - # Skip if this chain doesnt have the parameter - if p not in ps: - continue - - # Plot the good stuff - if errorbar: - fv = self.parent.analysis.get_parameter_summary(chain, p) - if fv[0] is not None and fv[2] is not None: - diff = np.abs(np.diff(fv)) - ax.errorbar([fv[1]], 0, xerr=[[diff[0]], [diff[1]]], fmt="o", color=colour) - else: - m = self._plot_bars(ax, p, chain) - if max_vals.get(p) is None or m > max_vals.get(p): - max_vals[p] = m - - for i, row in enumerate(axes): - index = 1 if show_names else 0 - for ax, p in zip(row[index:], parameters): - if not errorbar: - ax.set_ylim(0, 1.1 * max_vals[p]) - - dpi = 300 - if watermark: - ax = None - self._add_watermark(fig, ax, figsize, watermark, dpi=dpi, size_scale=0.8) - - if filename is not None: - if isinstance(filename, str): - filename = [filename] - for f in filename: - self._save_fig(fig, f, dpi) - if display: - plt.show() - - return fig - - def _get_size_of_texts(self, texts): # pragma: no cover - usetex = self.parent.config["usetex"] - size = self.parent.config["label_font_size"] + # def plot_walks( + # self, + # parameters=None, + # truth=None, + # extents=None, + # display=False, + # filename=None, + # chains=None, + # convolve=None, + # figsize=None, + # plot_weights=True, + # plot_posterior=True, + # log_weight=None, + # log_scales=None, + # ): # pragma: no cover + # """Plots the chain walk; the parameter values as a function of step index. + + # This plot is more for a sanity or consistency check than for use with final results. + # Plotting this before plotting with :func:`plot` allows you to quickly see if the + # chains are well behaved, or if certain parameters are suspect + # or require a greater burn in period. + + # The desired outcome is to see an unchanging distribution along the x-axis of the plot. + # If there are obvious tails or features in the parameters, you probably want + # to investigate. + + # Parameters + # ---------- + # parameters : list[str]|int, optional + # Specify a subset of parameters to plot. If not set, all parameters are plotted. + # If an integer is given, only the first so many parameters are plotted. + # truth : list[float]|dict[str], optional + # A list of truth values corresponding to parameters, or a dictionary of + # truth values keyed by the parameter. + # extents : list[tuple]|dict[str], optional + # A list of two-tuples for plot extents per parameter, or a dictionary of + # extents keyed by the parameter. + # display : bool, optional + # If set, shows the plot using ``plt.show()`` + # filename : str, optional + # If set, saves the figure to the filename + # chains : int|str, list[str|int], optional + # Used to specify which chain to show if more than one chain is loaded in. + # Can be an integer, specifying the + # chain index, or a str, specifying the chain name. + # convolve : int, optional + # If set, overplots a smoothed version of the steps using ``convolve`` as + # the width of the smoothing filter. + # figsize : tuple, optional + # If set, sets the created figure size. + # plot_weights : bool, optional + # If true, plots the weight if they are available + # plot_posterior : bool, optional + # If true, plots the log posterior if they are available + # log_weight : bool, optional + # Whether to display weights in log space or not. If None, the value is + # inferred by the mean weights of the plotted chains. + # log_scales : bool, list[bool] or dict[bool], optional + # Whether or not to use a log scale on any given axis. Can be a list of True/False, a list of param + # names to set to true, a dictionary of param names with true/false + # or just a bool (just `True` would set everything to log scales). + + # Returns + # ------- + # figure + # the matplotlib figure created + + # """ + + # chains, parameters, truth, extents, _, log_scales = self._sanitise( + # chains, parameters, truth, extents, log_scales=log_scales + # ) + + # chains = [c for c in chains if c.mcmc_chain] + # n = len(parameters) + # extra = 0 + # if plot_weights: + # plot_weights = plot_weights and np.any([np.any(c.weights != 1.0) for c in chains]) + + # plot_posterior = plot_posterior and np.any([c.posterior is not None for c in chains]) + + # if plot_weights: + # extra += 1 + # if plot_posterior: + # extra += 1 + + # if figsize is None: + # figsize = (8, 0.75 + (n + extra)) + + # fig, axes = plt.subplots(figsize=figsize, nrows=n + extra, squeeze=False, sharex=True) + + # for i, axes_row in enumerate(axes): + # ax = axes_row[0] + # if i >= extra: + # p = parameters[i - n] + # for chain in chains: + # if p in chain.parameters: + # chain_row = chain.get_data(p) + # log = log_scales.get(p, False) + # self._plot_walk( + # ax, + # p, + # chain_row, + # extents=extents.get(p), + # convolve=convolve, + # color=chain.config["color"], + # log_scale=log, + # ) + # if truth.get(p) is not None: + # self._plot_walk_truth(ax, truth.get(p)) + # else: + # if i == 0 and plot_posterior: + # for chain in chains: + # if chain.log_posterior is not None: + # self._plot_walk( + # ax, + # r"$\log(P)$", + # chain.log_posterior - chain.log_posterior.max(), + # convolve=convolve, + # color=chain.config["color"], + # ) + # else: + # if log_weight is None: + # log_weight = np.any([chain.weights.mean() < 0.1 for chain in chains]) + # if log_weight: + # for chain in chains: + # self._plot_walk( + # ax, + # r"$\log_{10}(w)$", + # np.log10(chain.weights), + # convolve=convolve, + # color=chain.config["color"], + # ) + # else: + # for chain in chains: + # self._plot_walk(ax, "$w$", chain.weights, convolve=convolve, color=chain.config["color"]) + + # if filename is not None: + # if isinstance(filename, str): + # filename = [filename] + # for f in filename: + # self._save_fig(fig, f, 300) + # if display: + # plt.show() + + # return fig + + # def plot_distributions( + # self, + # parameters=None, + # truth=None, + # extents=None, + # display=False, + # filename=None, + # chains=None, + # col_wrap=4, + # figsize=None, + # blind=None, + # log_scales=None, + # ): # pragma: no cover + # """Plots the 1D parameter distributions for verification purposes. + + # This plot is more for a sanity or consistency check than for use with final results. + # Plotting this before plotting with :func:`plot` allows you to quickly see if the + # chains give well behaved distributions, or if certain parameters are suspect + # or require a greater burn in period. + + # Parameters + # ---------- + # parameters : list[str]|int, optional + # Specify a subset of parameters to plot. If not set, all parameters are plotted. + # If an integer is given, only the first so many parameters are plotted. + # truth : list[float]|dict[str], optional + # A list of truth values corresponding to parameters, or a dictionary of + # truth values keyed by the parameter. + # extents : list[tuple]|dict[str], optional + # A list of two-tuples for plot extents per parameter, or a dictionary of + # extents keyed by the parameter. + # display : bool, optional + # If set, shows the plot using ``plt.show()`` + # filename : str, optional + # If set, saves the figure to the filename + # chains : int|str, list[str|int], optional + # Used to specify which chain to show if more than one chain is loaded in. + # Can be an integer, specifying the + # chain index, or a str, specifying the chain name. + # col_wrap : int, optional + # How many columns to plot before wrapping. + # figsize : tuple(float)|float, optional + # Either a tuple specifying the figure size or a float scaling factor. + # blind : bool|string|list[string], optional + # Whether to blind axes values. Can be set to `True` to blind all parameters, + # or can pass in a string (or list of strings) which specify the parameters to blind. + # log_scales : bool, list[bool] or dict[bool], optional + # Whether or not to use a log scale on any given axis. Can be a list of True/False, a list of param + # names to set to true, a dictionary of param names with true/false + # or just a bool (just `True` would set everything to log scales). + + # Returns + # ------- + # figure + # the matplotlib figure created + + # """ + # chains, parameters, truth, extents, blind, log_scales = self._sanitise( + # chains, parameters, truth, extents, blind=blind, log_scales=log_scales + # ) + + # n = len(parameters) + # num_cols = min(n, col_wrap) + # num_rows = int(np.ceil(1.0 * n / col_wrap)) + + # if figsize is None: + # figsize = 1.0 + # if isinstance(figsize, float): + # figsize_float = figsize + # figsize = (num_cols * 2 * figsize, num_rows * 2 * figsize) + # else: + # figsize_float = 1.0 + + # summary = self.parent.config["summary"] + # label_font_size = self.parent.config["label_font_size"] + # tick_font_size = self.parent.config["tick_font_size"] + # max_ticks = self.parent.config["max_ticks"] + # diagonal_tick_labels = self.parent.config["diagonal_tick_labels"] + + # if summary is None: + # summary = len(self.parent.chains) == 1 + + # hspace = (0.8 if summary else 0.5) / figsize_float + # fig, axes = plt.subplots(nrows=num_rows, ncols=num_cols, figsize=figsize, squeeze=False) + # fig.subplots_adjust(left=0.1, right=0.95, top=0.95, bottom=0.1, wspace=0.05, hspace=hspace) + + # formatter = ScalarFormatter(useOffset=False) + # formatter.set_powerlimits((-3, 4)) + + # for i, ax in enumerate(axes.flatten()): + # if i >= len(parameters): + # ax.set_axis_off() + # continue + # p = parameters[i] + + # ax.set_yticks([]) + # if log_scales.get(p, False): + # ax.set_xscale("log") + # if p in blind: + # ax.set_xticks([]) + # else: + # if diagonal_tick_labels: + # _ = [l.set_rotation(45) for l in ax.get_xticklabels()] + # _ = [l.set_fontsize(tick_font_size) for l in ax.get_xticklabels()] + + # if log_scales.get(p, False): + # ax.xaxis.set_major_locator(LogLocator(numticks=max_ticks)) + # else: + # ax.xaxis.set_major_locator(MaxNLocator(max_ticks, prune="lower")) + # ax.xaxis.set_major_formatter(formatter) + # ax.set_xlim(extents.get(p) or self._get_parameter_extents(p, chains)) + + # max_val = -np.inf + # for chain in chains: + # if not chain.config["plot_contour"]: + # continue + # if p in chain.parameters: + # param_summary = summary and p not in blind + # m = self._plot_bars(ax, p, chain, summary=param_summary) + # if max_val is None or m > max_val: + # max_val = m + + # self._add_truth(ax, truth, None, py=p) + # ax.set_ylim(0, 1.1 * max_val) + # ax.set_xlabel(p, fontsize=label_font_size) + + # if filename is not None: + # if isinstance(filename, str): + # filename = [filename] + # for f in filename: + # self._save_fig(fig, f, 300) + # if display: + # plt.show() + + # return fig + + # def plot_summary( + # self, + # parameters=None, + # truth=None, + # extents=None, + # display=False, + # filename=None, + # chains=None, + # figsize=1.0, + # errorbar=False, + # include_truth_chain=True, + # blind=None, + # watermark=None, + # extra_parameter_spacing=0.5, + # vertical_spacing_ratio=1.0, + # show_names=True, + # log_scales=None, + # ): # pragma: no cover + # """Plots parameter summaries + + # This plot is more for a sanity or consistency check than for use with final results. + # Plotting this before plotting with :func:`plot` allows you to quickly see if the + # chains give well behaved distributions, or if certain parameters are suspect + # or require a greater burn in period. + + # Parameters + # ---------- + # parameters : list[str]|int, optional + # Specify a subset of parameters to plot. If not set, all parameters are plotted. + # If an integer is given, only the first so many parameters are plotted. + # truth : list[float]|list|list[float]|dict[str]|str, optional + # A list of truth values corresponding to parameters, or a dictionary of + # truth values keyed by the parameter. Each "truth value" can be either a float (will + # draw a vertical line), two floats (a shaded interval) or three floats (min, mean, max), + # which renders as a shaded interval with a line for the mean. Or, supply a string + # which matches a chain name, and the results for that chain will be used as the 'truth' + # extents : list[tuple]|dict[str], optional + # A list of two-tuples for plot extents per parameter, or a dictionary of + # extents keyed by the parameter. + # display : bool, optional + # If set, shows the plot using ``plt.show()`` + # filename : str, optional + # If set, saves the figure to the filename + # chains : int|str, list[str|int], optional + # Used to specify which chain to show if more than one chain is loaded in. + # Can be an integer, specifying the + # chain index, or a str, specifying the chain name. + # figsize : float, optional + # Scale horizontal and vertical figure size. + # errorbar : bool, optional + # Whether to onle plot an error bar, instead of the marginalised distribution. + # include_truth_chain : bool, optional + # If you specify another chain as the truth chain, determine if it should still + # be plotted. + # blind : bool|string|list[string], optional + # Whether to blind axes values. Can be set to `True` to blind all parameters, + # or can pass in a string (or list of strings) which specify the parameters to blind. + # watermark : str, optional + # A watermark to add to the figure + # extra_parameter_spacing : float, optional + # Increase horizontal space for parameter values + # vertical_spacing_ratio : float, optional + # Increase vertical space for each model + # show_names : bool, optional + # Whether to show chain names or not. Defaults to `True`. + # log_scales : bool, list[bool] or dict[bool], optional + # Whether or not to use a log scale on any given axis. Can be a list of True/False, a list of param + # names to set to true, a dictionary of param names with true/false + # or just a bool (just `True` would set everything to log scales). + + # Returns + # ------- + # figure + # the matplotlib figure created + + # """ + # wide_extents = not errorbar + # chains, parameters, truth, extents, blind, log_scales = self._sanitise( + # chains, parameters, truth, extents, blind=blind, wide_extents=wide_extents, log_scales=log_scales + # ) + + # all_names = [c.name for c in self.parent.chains] + + # # Check if we're using a chain for truth values + # if isinstance(truth, str): + # assert truth in all_names, f"Truth chain {truth} is not in the list of added chains {all_names}" + # if not include_truth_chain: + # chains = [c for c in chains if c.name != truth] + # truth = self.parent.analysis.get_summary(chains=truth, parameters=parameters) + + # max_param = self._get_size_of_texts(parameters) + # fid_dpi = 65 # Seriously I have no idea what value this should be + # param_width = extra_parameter_spacing + max(0.5, max_param / fid_dpi) + + # if show_names: + # max_model_name = self._get_size_of_texts([chain.name for chain in chains]) + # model_width = 0.25 + (max_model_name / fid_dpi) + # gridspec_kw = { + # "width_ratios": [model_width] + [param_width] * len(parameters), + # "height_ratios": [1] * len(chains), + # } + # ncols = 1 + len(parameters) + # else: + # model_width = 0 + # gridspec_kw = {"width_ratios": [param_width] * len(parameters), "height_ratios": [1] * len(chains)} + # ncols = len(parameters) + + # top_spacing = 0.3 + # bottom_spacing = 0.2 + # row_height = (0.5 if not errorbar else 0.3) * vertical_spacing_ratio + # width = param_width * len(parameters) + model_width + # height = top_spacing + bottom_spacing + row_height * len(chains) + # top_ratio = 1 - (top_spacing / height) + # bottom_ratio = bottom_spacing / height + + # figsize = (width * figsize, height * figsize) + # fig, axes = plt.subplots( + # nrows=len(chains), ncols=ncols, figsize=figsize, squeeze=False, gridspec_kw=gridspec_kw + # ) + # fig.subplots_adjust(left=0.05, right=0.95, top=top_ratio, bottom=bottom_ratio, wspace=0.0, hspace=0.0) + # label_font_size = self.parent.config["label_font_size"] + # legend_color_text = self.parent.config["legend_color_text"] + + # max_vals = {} + # for i, row in enumerate(axes): + # chain = chains[i] + + # ( + # cs, + # ws, + # ps, + # ) = ( + # chain.chain, + # chain.weights, + # chain.parameters, + # ) + # gs, ns = chain.grid, chain.name + + # colour = chain.config["color"] + + # # First one put name of model + # if show_names: + # ax_first = row[0] + # ax_first.set_axis_off() + # text_colour = "k" if not legend_color_text else colour + # ax_first.text( + # 0, + # 0.5, + # ns, + # transform=ax_first.transAxes, + # fontsize=label_font_size, + # verticalalignment="center", + # color=text_colour, + # weight="medium", + # ) + # cols = row[1:] + # else: + # cols = row + + # for ax, p in zip(cols, parameters): + # # Set up the frames + # if i > 0: + # ax.spines["top"].set_visible(False) + # if i < (len(chains) - 1): + # ax.spines["bottom"].set_visible(False) + # if i < (len(chains) - 1) or p in blind: + # ax.set_xticks([]) + # ax.set_yticks([]) + # ax.set_xlim(extents[p]) + # if log_scales.get(p): + # ax.set_xscale("log") + + # # Put title in + # if i == 0: + # ax.set_title(r"$%s$" % p, fontsize=label_font_size) + + # # Add truth values + # truth_value = truth.get(p) + # if truth_value is not None: + # if isinstance(truth_value, float | int): + # truth_mean = truth_value + # truth_min, truth_max = None, None + # else: + # if len(truth_value) == 1: + # truth_mean = truth_value + # truth_min, truth_max = None, None + # elif len(truth_value) == 2: + # truth_min, truth_max = truth_value + # truth_mean = None + # else: + # truth_min, truth_mean, truth_max = truth_value + # if truth_mean is not None: + # ax.axvline(truth_mean, **self.parent.config_truth) + # if truth_min is not None and truth_max is not None: + # ax.axvspan(truth_min, truth_max, color=self.parent.config_truth["color"], alpha=0.15, lw=0) + # # Skip if this chain doesnt have the parameter + # if p not in ps: + # continue + + # # Plot the good stuff + # if errorbar: + # fv = self.parent.analysis.get_parameter_summary(chain, p) + # if fv[0] is not None and fv[2] is not None: + # diff = np.abs(np.diff(fv)) + # ax.errorbar([fv[1]], 0, xerr=[[diff[0]], [diff[1]]], fmt="o", color=colour) + # else: + # m = self._plot_bars(ax, p, chain) + # if max_vals.get(p) is None or m > max_vals.get(p): + # max_vals[p] = m + + # for i, row in enumerate(axes): + # index = 1 if show_names else 0 + # for ax, p in zip(row[index:], parameters): + # if not errorbar: + # ax.set_ylim(0, 1.1 * max_vals[p]) + + # dpi = 300 + # if watermark: + # ax = None + # self._add_watermark(fig, ax, figsize, watermark, dpi=dpi, size_scale=0.8) + + # if filename is not None: + # if isinstance(filename, str): + # filename = [filename] + # for f in filename: + # self._save_fig(fig, f, dpi) + # if display: + # plt.show() + + # return fig + + def _get_size_of_texts(self, texts: list[str]) -> float: # pragma: no cover + usetex = self.config.usetex + size = self.config.label_font_size widths = [TextPath((0, 0), text, usetex=usetex, size=size).get_extents().width for text in texts] return max(widths) - def _sanitise( - self, chains, parameters, truth, extents, color_p=False, blind=None, wide_extents=True, log_scales=None - ): # pragma: no cover - chains = self._sanitise_chains(chains) - - if color_p: - # Get all parameters to plot, taking into account some of them - # might be excluded colour parameters - all_parameters = [] - for chain in chains: - pc = chain.config["plot_color_params"] - cp = chain.config["color_params"] - ps = chain.parameters - for p in ps: - if (p != cp or pc) and p not in all_parameters: - all_parameters.append(p) - else: - all_parameters = [] - for chain in chains: - for p in chain.parameters: - if p not in all_parameters: - all_parameters.append(p) - - if parameters is None: - parameters = all_parameters - elif isinstance(parameters, int): - parameters = self.parent._all_parameters[:parameters] - - if truth is not None and isinstance(truth, np.ndarray): - truth = truth.tolist() - if truth is None: - truth = {} - else: - if isinstance(truth, np.ndarray): - truth = truth.tolist() - if isinstance(truth, list): - truth = dict((p, t) for p, t in zip(parameters, truth)) - - if extents is None: - extents = {} - elif isinstance(extents, list): - extents = dict((p, e) for p, e in zip(parameters, extents)) - - extents = self._get_custom_extents(parameters, chains, extents, wide_extents=wide_extents) + def _sanitise_columns(self, columns: list[ColumnName] | None, chains: list[Chain]) -> list[ColumnName]: + if columns is None: + return list(set([c for chain in chains for c in chain.samples.columns])) + return columns + def _sanitise_logscale(self, log_scales: list[ColumnName] | None) -> list[ColumnName]: + # We could at some point determine if something should be a log scale by analyising + # its distribution, but for now assume its all linear if log_scales is None: - log_scales = {} - elif isinstance(log_scales, str): - log_scales = {log_scales: True} - elif isinstance(log_scales, list): - old = log_scales - log_scales = {} - for i, item in enumerate(old): - if isinstance(item, bool): - log_scales[parameters[i]] = item - elif isinstance(item, int): - log_scales[parameters[item]] = True - elif isinstance(item, str): - log_scales[item] = True - - elif isinstance(log_scales, bool): - log_scales = dict([(p, log_scales) for p in parameters]) - - if blind is None: - blind = [] - elif isinstance(blind, str): - blind = [blind] - elif isinstance(blind, bool) and blind: - blind = parameters + return [] + return log_scales + def _sanitise_blinds(self, blind: bool | list[ColumnName] | None, columns: list[ColumnName]) -> list[ColumnName]: + if blind is None or blind is False: + return [] + elif blind is True: + return columns + return blind + + def _sanitise( + self, + chains: list[ChainName | Chain] | None, + columns: list[ColumnName] | None, + extents: dict[str, tuple[float, float]] | None, + blind: bool | list[ColumnName] | None = None, + log_scales: list[ColumnName] | None = None, + wide_extents: bool = True, + ) -> PlottingBase: + final_chains = self._sanitise_chains(chains) + final_columns = self._sanitise_columns(columns, final_chains) + extents = self._get_custom_extents(final_columns, final_chains, extents, wide_extents=wide_extents) self.set_rc_params() - return chains, parameters, truth, extents, blind, log_scales + return PlottingBase( + chains=final_chains, + columns=final_columns, + extents=extents, + log_scales=self._sanitise_logscale(log_scales), + blind=self._sanitise_blinds(blind, final_columns), + ) def set_rc_params(self): - if self.parent.config["usetex"]: + if self.config.usetex: plt.rc("text", usetex=True) else: plt.rc("text", usetex=False) - if self.parent.config["serif"]: + if self.config.serif: plt.rc("font", family="serif") else: plt.rc("font", family="sans-serif") @@ -962,52 +976,46 @@ def restore_rc_params(self): plt.rc("text", usetex=self.usetex_old) plt.rc("font", family=self.serif_old) - def _get_custom_extents(self, parameters, chains, external_extents, wide_extents=True): # pragma: no cover - extents = {} - for p in parameters: - if external_extents is not None and p in external_extents: - extents[p] = external_extents[p] - else: + def _get_custom_extents( + self, + columns: list[ColumnName], + chains: list[Chain], + extents: dict[ColumnName, tuple[float, float]] | None, + wide_extents: bool = True, + ) -> dict[ColumnName, tuple[float, float]]: # pragma: no cover + if extents is None: + extents = {} + for p in columns: + if p not in extents: extents[p] = self._get_parameter_extents(p, chains, wide_extents=wide_extents) return extents def _get_figure( - self, all_parameters, flip, figsize=(5, 5), external_extents=None, chains=None, blind=None, log_scales=None - ): # pragma: no cover - n = len(all_parameters) - max_ticks = self.parent.config["max_ticks"] - spacing = self.parent.config["spacing"] - plot_hists = self.parent.config["plot_hists"] - label_font_size = self.parent.config["label_font_size"] - tick_font_size = self.parent.config["tick_font_size"] - diagonal_tick_labels = self.parent.config["diagonal_tick_labels"] - if blind is None: - blind = [] - - if chains is None: - chains = self.parent.chains - - if not plot_hists: + self, base: PlottingBase, figsize: tuple[float, float] + ) -> tuple[Figure, np.ndarray, list[ColumnName], list[ColumnName]]: + n = len(base.columns) + if not self.config.plot_hists: n -= 1 + spacing = self.config.spacing if spacing is None: spacing = 1.0 if n < 6 else 0.0 - gridspec_kw = {"width_ratios": [3, 1], "height_ratios": [1, 3]} if n == 2 and plot_hists and flip else {} + gridspec_kw = {} + if n == 2 and self.config.plot_hists and self.config.flip: + gridspec_kw = {"width_ratios": [3, 1], "height_ratios": [1, 3]} fig, axes = plt.subplots(n, n, figsize=figsize, squeeze=False, gridspec_kw=gridspec_kw) fig.subplots_adjust(left=0.1, right=0.95, top=0.95, bottom=0.1, wspace=0.05 * spacing, hspace=0.05 * spacing) - extents = self._get_custom_extents(all_parameters, chains, external_extents) - - if plot_hists: - params1 = all_parameters - params2 = all_parameters + if self.config.plot_hists: + params_x = base.columns + params_y = base.columns else: - params1 = all_parameters[1:] - params2 = all_parameters[:-1] - for i, p1 in enumerate(params1): - for j, p2 in enumerate(params2): + params_x = base.columns[1:] + params_y = base.columns[:-1] + for i, p1 in enumerate(params_x): + for j, p2 in enumerate(params_y): ax = axes[i, j] formatter_x = ScalarFormatter(useOffset=True) formatter_x.set_powerlimits((-3, 4)) @@ -1024,279 +1032,243 @@ def _get_figure( logx = False logy = False if p1 == p2: - if log_scales.get(p1): - if flip and j == n - 1: + if p1 in base.log_scales: + if self.config.flip and j == n - 1: ax.set_yscale("log") logy = True else: ax.set_xscale("log") logx = True else: - if log_scales.get(p1): + if p1 in base.log_scales: ax.set_yscale("log") logy = True - if log_scales.get(p2): + if p2 in base.log_scales: ax.set_xscale("log") logx = True - if i != n - 1 or (flip and j == n - 1): + if i != n - 1 or (self.config.flip and j == n - 1): ax.set_xticks([]) else: - if p2 in blind: + if p2 in base.blind: ax.set_xticks([]) else: display_x_ticks = True if isinstance(p2, str): - ax.set_xlabel(p2, fontsize=label_font_size) - if j != 0 or (plot_hists and i == 0): + ax.set_xlabel(p2, fontsize=self.config.label_font_size) + if j != 0 or (self.config.plot_hists and i == 0): ax.set_yticks([]) else: - if p1 in blind: + if p1 in base.blind: ax.set_yticks([]) else: display_y_ticks = True if isinstance(p1, str): - ax.set_ylabel(p1, fontsize=label_font_size) + ax.set_ylabel(p1, fontsize=self.config.label_font_size) if display_x_ticks: - if diagonal_tick_labels: - _ = [l.set_rotation(45) for l in ax.get_xticklabels()] - _ = [l.set_fontsize(tick_font_size) for l in ax.get_xticklabels()] + if self.config.diagonal_tick_labels: + _ = [label.set_rotation(45) for label in ax.get_xticklabels()] + _ = [label.set_fontsize(self.config.tick_font_size) for label in ax.get_xticklabels()] if not logx: - ax.xaxis.set_major_locator(MaxNLocator(max_ticks, prune="lower")) + ax.xaxis.set_major_locator(MaxNLocator(self.config.max_ticks, prune="lower")) ax.xaxis.set_major_formatter(formatter_x) else: - ax.xaxis.set_major_locator(LogLocator(numticks=max_ticks)) + ax.xaxis.set_major_locator(LogLocator(numticks=self.config.max_ticks)) else: ax.set_xticks([]) if display_y_ticks: - if diagonal_tick_labels: - _ = [l.set_rotation(45) for l in ax.get_yticklabels()] - _ = [l.set_fontsize(tick_font_size) for l in ax.get_yticklabels()] + if self.config.diagonal_tick_labels: + _ = [label.set_rotation(45) for label in ax.get_yticklabels()] + _ = [label.set_fontsize(self.config.tick_font_size) for label in ax.get_yticklabels()] if not logy: - ax.yaxis.set_major_locator(MaxNLocator(max_ticks, prune="lower")) + ax.yaxis.set_major_locator(MaxNLocator(self.config.max_ticks, prune="lower")) ax.yaxis.set_major_formatter(formatter_y) else: - ax.yaxis.set_major_locator(LogLocator(numticks=max_ticks)) + ax.yaxis.set_major_locator(LogLocator(numticks=self.config.max_ticks)) else: ax.set_yticks([]) - if i != j or not plot_hists: - ax.set_ylim(extents[p1]) - elif flip and i == 1: - ax.set_ylim(extents[p1]) - ax.set_xlim(extents[p2]) + if (i != j or not self.config.plot_hists) or (self.config.flip and i == 1): + ax.set_ylim(base.extents[p1]) + ax.set_xlim(base.extents[p2]) - return fig, axes, params1, params2, extents + return fig, axes, params_x, params_y - def _get_parameter_extents(self, parameter, chains, wide_extents=True) -> tuple[float, float]: - min_val, max_val = None, None + def _get_parameter_extents( + self, column: ColumnName, chains: list[Chain], wide_extents: bool = True + ) -> tuple[float, float]: + min_val, max_val = np.inf, -np.inf for chain in chains: - if parameter not in chain.parameters: + if column not in chain.samples: continue # pragma: no cover - if not chain.config["plot_contour"]: - data = chain.get_data(parameter) - if data.size < 10: - min_prop, max_prop = np.min(data), np.max(data) - else: - if self.parent.config["global_point"]: - min_prop = chain.log_posterior_max_params.get(parameter) - max_prop = min_prop - else: - data = chain.get_data(parameter) - min_prop, max_prop = get_extents(data, chain.weights, tiny=True) - else: - data = chain.get_data(parameter) + + data = chain.get_data(column) + min_prop, max_prop = np.inf, -np.inf + if chain.plot_contour or chain.plot_cloud: if chain.grid: min_prop = data.min() max_prop = data.max() else: min_prop, max_prop = get_extents(data, chain.weights, plot=True, wide_extents=wide_extents) - if min_val is None or min_prop < min_val: + + else: + point = chain.get_max_posterior_point() + if point is not None and column in point.coordinate: + min_prop = point.coordinate[column] + max_prop = min_prop + + if min_prop < min_val: min_val = min_prop - if max_val is None or max_prop > max_val: + if max_prop > max_val: max_val = max_prop + return min_val, max_val - def _get_levels(self): - sigma2d = self.parent.config["sigma2d"] + def _get_levels(self, sigmas: list[float]) -> np.ndarray: + sigma2d = self.config.sigma2d if sigma2d: - levels = 1.0 - np.exp(-0.5 * self.parent.config["sigmas"] ** 2) + levels: np.ndarray = 1.0 - np.exp(-0.5 * np.array(sigmas) ** 2) else: - levels = 2 * norm.cdf(self.parent.config["sigmas"]) - 1.0 + levels: np.ndarray = 2 * norm.cdf(sigmas) - 1.0 return levels - def _plot_points(self, ax, chains_groups, markers, sizes, alphas, py, px): # pragma: no cover - global_point = self.parent.config["global_point"] - for marker, chains, size, alpha in zip(markers, chains_groups, sizes, alphas): - if global_point: - res = self.parent.analysis.get_max_posteriors(parameters=[px, py], chains=chains, squeeze=False) - xs = [r[px] for r in res if r is not None] - ys = [r[py] for r in res if r is not None] - else: - xs, ys, res = [], [], [] - for chain in chains: - if px in chain.parameters and py in chain.parameters: - x = chain.get_data(px) - y = chain.get_data(py) - if x.size <= 2: # Marker only - xs.append(x[0]) - ys.append(y[0]) - res.append({"px": x[0], "py": y[0]}) - else: - hist, x_centers, y_centers = self._get_smoothed_histogram2d(chain, py, px) - index = np.unravel_index(hist.argmax(), hist.shape) - ys.append(x_centers[index[0]]) - xs.append(y_centers[index[1]]) - res.append({"px": xs[-1], "py": ys[-1]}) - else: - res.append(None) - cs = [c.config["color"] for c, r in zip(chains, res) if r is not None] - h = ax.scatter(xs, ys, marker=marker, c=cs, s=size, linewidth=0.7, alpha=alpha) + def _plot_point(self, ax: Axes, chain: Chain, px: str, py: str) -> PathCollection: # pragma: no cover + point = chain.get_max_posterior_point() + if point is None or px not in point.coordinate or py not in point.coordinate: + return + h = ax.scatter( + [point.coordinate[px]], + point.coordinate[py], + marker=chain.marker_style, + c=colors.format(chain.color), + s=chain.marker_size, + alpha=chain.marker_alpha, + ) return h - def _sanitise_chains(self, chains): - if not self.parent._configured: - self.parent.configure() - if not self.parent._configured_truth: - self.parent.configure_truth() - - if chains is None: - chains = list(range(len(self.parent.chains))) + def _sanitise_chains(self, chains: list[Chain | ChainName] | dict[ChainName, Chain] | None) -> list[Chain]: + if isinstance(chains, list): + final_chains = [self.parent.chains[n] if isinstance(n, ChainName) else n for n in chains] + elif isinstance(chains, dict): + final_chains = list(chains.values()) else: - if isinstance(chains, int | str): - chains = [chains] - chains = [i for c in chains for i in self.parent._get_chain(c)] - - chains = [self.parent.chains[i] for i in chains] - return chains + final_chains = list(self.parent.chains.values()) + return [c for c in final_chains if not c.skip] - def plot_contour(self, ax, parameter_x, parameter_y, chains=None): + def plot_contour( + self, + ax: Axes, + column_x: str, + column_y: str, + chains: list[Chain | ChainName] | dict[ChainName, Chain] | None = None, + ) -> None: """A lightweight method to plot contours in an external axis given two specified parameters - Parameters - ========== - ax : matplotlib axis - The axis to plot on - parameter_x : str - The name of the parameter to plot for the x axis. Must be the string label of the parameter. - parameter_y : str - The name of the parameter to plot for the y axis. Must be the string label of the parameter. - chains : int|str, list[str|int], optional - Used to specify which chain to show if more than one chain is loaded in. - Can be an integer, specifying the chain index, or a str, specifying the chain name, or a list of either. + Args: + ax (Axes): The axis to plot on + column_x (str): The parameter to plot on the x axis + column_y (str): The parameter to plot on the y axis + chains (list[Chain | ChainName] | dict[ChainName, str], optional): The chains to plot. Defaults to None. """ - chains = self._sanitise_chains(chains) - for chain in chains: - self._plot_contour(ax, chain, parameter_y, parameter_x) - - def _plot_contour(self, ax, chain, px, py, color_extents=None): # pragma: no cover - levels = self._get_levels() - cloud = chain.config["cloud"] - colour = chain.config["color"] - shade = chain.config["shade"] - shade_alpha = chain.config["shade_alpha"] - shade_gradient = chain.config["shade_gradient"] - linestyle = chain.config["linestyle"] - linewidth = chain.config["linewidth"] - zorder = chain.config["zorder"] - cmap = chain.config["cmap"] - contour_labels = self.parent.config["contour_labels"] - if color_extents is None: - color_extents = {} - h = None - color_data = chain.get_color_data() + + final_chains = self._sanitise_chains(chains) + for chain in final_chains: + self._plot_contour(ax, chain, column_y, column_x) + + def _plot_scatter(self, ax: Axes, chain: Chain, color: str, x: pd.Series, y: pd.Series) -> PathCollection | None: + skip = max(1, int(x.size / chain.num_cloud)) + if chain.color_data is not None: + kwargs = {"c": chain.color_data[::skip], "cmap": chain.cmap} + else: + kwargs = {"c": color, "alpha": 0.3} + + h = ax.scatter(x[::skip], y[::skip], s=10, marker=".", edgecolors="none", **kwargs) + if chain.color_data is not None: + return h + else: + return None + + def _plot_contour(self, ax: Axes, chain: Chain, px: str, py: str) -> PathCollection | None: # pragma: no cover + levels = self._get_levels(chain.sigmas) x = chain.get_data(py) y = chain.get_data(px) - color_extent = color_extents.get(chain.config["color_params"]) - cf = self.parent.color_finder - colours = self._scale_colours(colour, len(levels), shade_gradient) - sub = max(0.1, 1 - 0.2 * shade_gradient) - if shade: + contour_colours = self._scale_colours(chain.color, len(levels), chain.shade_gradient) + sub = max(0.1, 1 - 0.2 * chain.shade_gradient) + paths = None + + if chain.cloud: + paths = self._plot_scatter(ax, chain, contour_colours[1], x, y) + + # TODO: Figure out whats going on here + if chain.shade: sub *= 0.9 - colours2 = [cf.scale_colour(colours[0], sub)] + [cf.scale_colour(c, sub) for c in colours[:-1]] + colours2 = [colors.scale_colour(contour_colours[0], sub)] + [ + colors.scale_colour(c, sub) for c in contour_colours[:-1] + ] hist, x_centers, y_centers = self._get_smoothed_histogram2d(chain, py, px) - hist[hist == 0] = 1e-16 vals = self._convert_to_stdev(hist.T) - if cloud: - n = chain.config["num_cloud"] - skip = max(1, int(x.size / n)) - kwargs = {"c": colours[1], "alpha": 0.3} - if color_data is not None: - kwargs["c"] = color_data[::skip] - kwargs["cmap"] = cmap - if color_extent is not None: - kwargs["vmin"] = color_extent[0] - kwargs["vmax"] = color_extent[1] - if color_extent[0] == color_extent[1]: - kwargs["vmax"] = kwargs["vmin"] + 1.0 - - h = ax.scatter(x[::skip], y[::skip], s=10, marker=".", edgecolors="none", **kwargs) - if color_data is None: - h = None - - if shade and shade_alpha > 0: - ax.contourf(x_centers, y_centers, vals, levels=levels, colors=colours, alpha=shade_alpha, zorder=zorder) + + if chain.shade and chain.shade_alpha > 0: + ax.contourf( + x_centers, + y_centers, + vals, + levels=levels, + colors=contour_colours, + alpha=chain.shade_alpha, + zorder=chain.zorder, + ) con = ax.contour( x_centers, y_centers, vals, levels=levels, colors=colours2, - linestyles=linestyle, - linewidths=linewidth, - zorder=zorder, + linestyles=chain.linestyle, + linewidths=chain.linewidth, + zorder=chain.zorder, ) - if contour_labels is not None: + if chain.show_contour_labels: lvls = [lvl for lvl in con.levels if lvl != 0.0] - if contour_labels == "sigma": - sigmas = self.parent.config["sigmas"] - fmt = dict( - [(lvl, ("$%.1f \\sigma$" % sigma).replace(".0", "")) for lvl, sigma in zip(lvls, sigmas[1:])] - ) - else: - fmt = dict([(lvl, "%d\\%%" % (100 * lvl)) for lvl in lvls]) - ax.clabel(con, lvls, inline=True, fmt=fmt, fontsize=self.parent.config["contour_label_font_size"]) - return h - - def _add_truth(self, ax, truth, px, py=None): # pragma: no cover - if truth is not None: - if px is not None: - truth_value = truth.get(px) - if truth_value is not None: - ax.axhline(truth_value, **self.parent.config_truth) - if py is not None: - truth_value = truth.get(py) - if truth_value is not None: - ax.axvline(truth_value, **self.parent.config_truth) - - def _plot_bars(self, ax, parameter, chain, flip=False, summary=False): # pragma: no cover + # TODO: see if this can just be "0.0%" + fmt = {lvl: f"{lvl:0.0%}" for lvl in lvls} + ax.clabel(con, lvls, inline=True, fmt=fmt, fontsize=self.config.contour_label_font_size) + + if chain.plot_point: + self._plot_point(ax, chain, px, py) + return paths + + def _add_truth( + self, ax: Axes, truth: Truth, px: str | None = None, py: str | None = None + ) -> None: # pragma: no cover + if px is not None: + val_x = truth.truth_value.get(px) + if val_x is not None: + ax.axhline(val_x, **truth.kwargs) + if py is not None: + val_y = truth.truth_value.get(py) + if val_y is not None: + ax.axvline(val_y, **truth.kwargs) + + def _plot_bars( + self, ax: Axes, column: str, chain: Chain, flip: bool = False, summary: bool = False + ) -> float: # pragma: no cover # Get values from config - colour = chain.config["color"] - linestyle = chain.config["linestyle"] - bar_shade = chain.config["bar_shade"] - linewidth = chain.config["linewidth"] - bins = chain.config["bins"] - smooth = chain.config["smooth"] - kde = chain.config["kde"] - zorder = chain.config["zorder"] - title_size = self.parent.config["label_font_size"] - chain_row = chain.get_data(parameter) - weights = chain.weights - if smooth or kde: - xs, ys, _ = self.parent.analysis._get_smoothed_histogram(chain, parameter, pad=True) + data = chain.get_data(column) + if chain.smooth or chain.kde: + xs, ys, _ = self.parent.analysis._get_smoothed_histogram(chain, column, pad=True) if flip: - ax.plot(ys, xs, color=colour, ls=linestyle, lw=linewidth, zorder=zorder) + ax.plot(ys, xs, color=chain.color, ls=chain.linestyle, lw=chain.linewidth, zorder=chain.zorder) else: - ax.plot(xs, ys, color=colour, ls=linestyle, lw=linewidth, zorder=zorder) + ax.plot(xs, ys, color=chain.color, ls=chain.linestyle, lw=chain.linewidth, zorder=chain.zorder) else: - orientation = "horizontal" if flip else "vertical" if chain.grid: - bins = get_grid_bins(chain_row) + bins = get_grid_bins(data) else: - bins, smooth = get_smoothed_bins(smooth, bins, chain_row, weights) - hist, edges = np.histogram(chain_row, bins=bins, density=True, weights=weights) + bins, _ = get_smoothed_bins(chain.smooth, int(chain.bins), data, chain.weights) + hist, edges = np.histogram(data, bins=bins, density=True, weights=chain.weights) if chain.power is not None: hist = hist**chain.power edge_center = 0.5 * (edges[:-1] + edges[1:]) @@ -1304,19 +1276,19 @@ def _plot_bars(self, ax, parameter, chain, flip=False, summary=False): # pragma ax.hist( xs, weights=ys, - bins=bins, + bins=bins, # type: ignore histtype="step", - color=colour, - orientation=orientation, - ls=linestyle, - lw=linewidth, - zorder=zorder, + color=chain.color, # type: ignore + orientation="horizontal" if flip else "vertical", + ls=chain.linestyle, + lw=chain.linewidth, + zorder=chain.zorder, ) - interp_type = "linear" if smooth else "nearest" + interp_type = "linear" if chain.smooth else "nearest" interpolator = interp1d(xs, ys, kind=interp_type) - if bar_shade: - fit_values = self.parent.analysis.get_parameter_summary(chain, parameter) + if chain.bar_shade: + fit_values = self.parent.analysis.get_parameter_summary(chain, column) if fit_values is not None: lower = fit_values[0] upper = fit_values[2] @@ -1327,36 +1299,31 @@ def _plot_bars(self, ax, parameter, chain, flip=False, summary=False): # pragma upper = xs.max() x = np.linspace(lower, upper, 1000) if flip: - ax.fill_betweenx(x, np.zeros(x.shape), interpolator(x), color=colour, alpha=0.2, zorder=zorder) + ax.fill_betweenx( + x, + np.zeros(x.shape), + interpolator(x), + color=chain.color, + alpha=0.2, + zorder=chain.zorder, + ) else: - ax.fill_between(x, np.zeros(x.shape), interpolator(x), color=colour, alpha=0.2, zorder=zorder) + ax.fill_between( + x, + np.zeros(x.shape), + interpolator(x), + color=chain.color, + alpha=0.2, + zorder=chain.zorder, + ) if summary: t = self.parent.analysis.get_parameter_text(*fit_values) - if isinstance(parameter, str): - ax.set_title(r"${} = {}$".format(parameter.strip("$"), t), fontsize=title_size) + if isinstance(column, str): + ax.set_title(r"${} = {}$".format(column.strip("$"), t), fontsize=self.config.title_size) else: - ax.set_title(r"$%s$" % t, fontsize=title_size) + ax.set_title(r"$%s$" % t, fontsize=self.config.title_size) return ys.max() - def _plot_point_histogram(self, ax, chains_groups, parameter, flip=False): # pragma: no cover - max_val = 0 - for chains in chains_groups: - if len(chains) < 10: # You probably dont want a contour if you only have a small group - continue # And even if you do, it'll be so inaccurate... - res = self.parent.analysis.get_max_posteriors(parameters=parameter, chains=chains, squeeze=False) - xs = [r[parameter] for r in res if r is not None] - colour = chains[0].config["color"] - num_bins = int(max(5, np.power(len(xs), 0.4))) - hist, bin_edges = np.histogram(xs, bins=num_bins, density=True) - if hist.max() > max_val: - max_val = hist.max() - orientation = "horizontal" if flip else "vertical" - - bin_center = 0.5 * (bin_edges[:-1] + bin_edges[1:]) - xs, ys = bin_center, hist - ax.hist(xs, weights=ys, bins=bin_edges, histtype="step", color=colour, orientation=orientation) - return max_val - def _plot_walk( self, ax, parameter, data, truth=None, extents=None, convolve=None, color=None, log_scale=False ): # pragma: no cover @@ -1369,7 +1336,7 @@ def _plot_walk( if color is None: color = "#0345A1" ax.scatter(x, data, c=color, s=2, marker=".", edgecolors="none", alpha=0.5) - max_ticks = self.parent.config["max_ticks"] + max_ticks = self.config.max_ticks if log_scale: ax.set_yscale("log") ax.yaxis.set_major_locator(LogLocator(numticks=max_ticks)) @@ -1377,15 +1344,15 @@ def _plot_walk( ax.yaxis.set_major_locator(MaxNLocator(max_ticks, prune="lower")) if convolve is not None: - color2 = self.parent.color_finder.scale_colour(color, 0.5) + color2 = colors.scale_colour(color, 0.5) filt = np.ones(convolve) / convolve filtered = np.convolve(data, filt, mode="same") ax.plot(x[:-1], filtered[:-1], ls=":", color=color2, alpha=1) - def _plot_walk_truth(self, ax, truth): - ax.axhline(truth, **self.parent.config_truth) + def _plot_walk_truth(self, ax: Axes, truth: Truth, col: str) -> None: + ax.axhline(truth.truth_value[col], **truth.kwargs) - def _convert_to_stdev(self, sigma): # pragma: no cover + def _convert_to_stdev(self, sigma: np.ndarray) -> np.ndarray: # pragma: no cover # From astroML shape = sigma.shape sigma = sigma.ravel() @@ -1397,21 +1364,31 @@ def _convert_to_stdev(self, sigma): # pragma: no cover return sigma_cumsum[i_unsort].reshape(shape) - def _scale_colours(self, colour, num, shade_gradient): # pragma: no cover + def _scale_colours(self, colour: ColorInput, num: int, shade_gradient: float) -> list[str]: # pragma: no cover # http://thadeusb.com/weblog/2010/10/10/python_scale_hex_color minv, maxv = 1 - 0.1 * shade_gradient, 1 + 0.5 * shade_gradient scales = np.logspace(np.log(minv), np.log(maxv), num) - colours = [self.parent.color_finder.scale_colour(colour, scale) for scale in scales] + colours = [colors.scale_colour(colour, scale) for scale in scales] return colours - def _get_smoothed_histogram2d(self, chain, param1, param2): # pragma: no cover - # No test coverage here because - smooth = chain.config["smooth"] - bins = chain.config["bins"] - kde = chain.config["kde"] - - x = chain.get_data(param1) - y = chain.get_data(param2) + def _get_smoothed_histogram2d( + self, + chain: Chain, + col1: str, + col2: str, + ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: # pragma: no cover + """Returns a smoothed 2D histogram of two parameters. + + Args: + chain (Chain): The chain to plot + col1 (str): The first parameter + col2 (str): The second parameter + + Returns: + tuple[np.ndarray, np.ndarray, np.ndarray]: The histogram, x bin enters, y bin centers + """ + x = chain.get_data(col1) + y = chain.get_data(col2) w = chain.weights if chain.grid: @@ -1419,8 +1396,8 @@ def _get_smoothed_histogram2d(self, chain, param1, param2): # pragma: no cover binsy = get_grid_bins(y) hist, x_bins, y_bins = np.histogram2d(x, y, bins=[binsx, binsy], weights=w) else: - binsx, smooth = get_smoothed_bins(smooth, bins, x, w, marginalised=False) - binsy, _ = get_smoothed_bins(smooth, bins, y, w, marginalised=False) + binsx, smooth = get_smoothed_bins(chain.smooth, int(chain.bins), x, w) + binsy, _ = get_smoothed_bins(smooth, int(chain.bins), y, w) hist, x_bins, y_bins = np.histogram2d(x, y, bins=[binsx, binsy], weights=w) if chain.power is not None: @@ -1429,17 +1406,21 @@ def _get_smoothed_histogram2d(self, chain, param1, param2): # pragma: no cover x_centers = 0.5 * (x_bins[:-1] + x_bins[1:]) y_centers = 0.5 * (y_bins[:-1] + y_bins[1:]) - if kde: + if chain.kde: nn = x_centers.size * 2 # Double samples for KDE because smooth x_centers = np.linspace(x_bins.min(), x_bins.max(), nn) y_centers = np.linspace(y_bins.min(), y_bins.max(), nn) xx, yy = meshgrid(x_centers, y_centers, indexing="ij") coords = np.vstack((xx.flatten(), yy.flatten())).T data = np.vstack((x, y)).T - hist = MegKDE(data, w, kde).evaluate(coords).reshape((nn, nn)) + hist = MegKDE(data, w, chain.kde).evaluate(coords).reshape((nn, nn)) if chain.power is not None: hist = hist**chain.power - elif smooth: - hist = gaussian_filter(hist, smooth, mode="reflect") + elif chain.smooth: + hist = gaussian_filter(hist, chain.smooth, mode="reflect") return hist, x_centers, y_centers + + +if __name__ == "__main__": + from .chainconsumer import ChainConsumer diff --git a/src/chainconsumer/truth.py b/src/chainconsumer/truth.py index 37098250..68e5d7aa 100644 --- a/src/chainconsumer/truth.py +++ b/src/chainconsumer/truth.py @@ -1,3 +1,4 @@ +from typing import Any import pandas as pd from pydantic import Field, ValidationError, field_validator @@ -23,3 +24,13 @@ def ensure_dict(cls, v): elif isinstance(v, pd.Series): return v.to_dict() raise ValidationError("Truth must be a dict or a pandas Series") + + @property + def kwargs(self) -> dict[str, Any]: + return { + "ls": self.line_style, + "c": self.line_color, + "lw": self.line_width, + "alpha": self.line_alpha, + "zorder": self.line_zorder, + } From 3589c2bcb46736c8eb9e388f37ce4ab6b6cfe70a Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Sat, 7 Oct 2023 21:45:40 +1000 Subject: [PATCH 06/22] Starting on doco --- .gitignore | 4 +- Makefile | 4 + docs/api/api.md | 13 + docs/examples/README.md | 1 + docs/examples/plot_contours.py | 117 ++ docs/index.md | 3 + docs/plugins/griffe_doclinks.py | 87 + examples/plot_introduction.py | 18 +- mkdocs.yml | 72 + poetry.lock | 2524 +++++++++++++++++++++++++++- pyproject.toml | 16 +- requirements.txt | 13 - src/chainconsumer/__init__.py | 3 +- src/chainconsumer/analysis.py | 45 +- src/chainconsumer/base.py | 11 + src/chainconsumer/chain.py | 165 +- src/chainconsumer/chainconsumer.py | 675 +------- src/chainconsumer/colors.py | 11 +- src/chainconsumer/comparisons.py | 6 +- src/chainconsumer/helpers.py | 2 + src/chainconsumer/plotter.py | 147 +- src/chainconsumer/py.typed | 0 src/chainconsumer/something.py | 13 + src/chainconsumer/summary_stats.py | 10 + src/chainconsumer/truth.py | 6 +- tests/test_chain.py | 22 +- tests/test_chainconsumer.py | 22 +- tests/test_plotter.py | 12 +- 28 files changed, 3168 insertions(+), 854 deletions(-) create mode 100644 docs/api/api.md create mode 100644 docs/examples/README.md create mode 100644 docs/examples/plot_contours.py create mode 100644 docs/index.md create mode 100644 docs/plugins/griffe_doclinks.py create mode 100644 mkdocs.yml delete mode 100644 requirements.txt create mode 100644 src/chainconsumer/py.typed create mode 100644 src/chainconsumer/something.py create mode 100644 src/chainconsumer/summary_stats.py diff --git a/.gitignore b/.gitignore index 84b113c8..4ae9311e 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,6 @@ qodana.yaml **/*.log **/*tmp* **/*secret* -**/*.pkl \ No newline at end of file +**/*.pkl + +generated/ \ No newline at end of file diff --git a/Makefile b/Makefile index 09153b84..e6e76e9c 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,10 @@ precommit: test: poetry run pytest +serve: + # rm -rf docs/generated/gallery; + poetry run mkdocs serve --clean + tests: test all: precommit tests \ No newline at end of file diff --git a/docs/api/api.md b/docs/api/api.md new file mode 100644 index 00000000..2b6d2cf3 --- /dev/null +++ b/docs/api/api.md @@ -0,0 +1,13 @@ + +The ChainConsumer acts as manager and state holder, to which you supply configured pydantic objects to dictate the behaviour of your plots and analyses. + +***** + +::: chainconsumer.chainconsumer.ChainConsumer + + +::: chainconsumer.chain.Chain + +::: chainconsumer.chain.ChainConfig + + diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 100644 index 00000000..a72638ab --- /dev/null +++ b/docs/examples/README.md @@ -0,0 +1 @@ +# ChainConsumer \ No newline at end of file diff --git a/docs/examples/plot_contours.py b/docs/examples/plot_contours.py new file mode 100644 index 00000000..7fe977cd --- /dev/null +++ b/docs/examples/plot_contours.py @@ -0,0 +1,117 @@ +""" +# Introduction to Contours + +At the most basic, we take a contour as a pandas DataFrame and let ChainConsumer +handle the defaults and display. + +""" + +import pandas as pd +from scipy.stats import multivariate_normal as mv + +from chainconsumer import Chain, ChainConfig, ChainConsumer, PlotConfig, Truth + +# Here's what you might start with +norm = mv(mean=[0.0, 4.0], cov=[[1.0, 0.7], [0.7, 1.5]]) # type: ignore +data = norm.rvs(size=1000000) +df = pd.DataFrame(data, columns=["x_1", "x_2"]) + +# And how we give this to chainconsumer +c = ChainConsumer() +c.add_chain(Chain(samples=df, name="An Example Contour")) +fig = c.plotter.plot() + +# %% Second cell +# +# If we wanted to customise the chain, that's easy enough to do. + +# Here's a convenience function for you +chain2 = Chain.from_covariance( + [3.0, 1.0], + [[1.0, -0.7], [-0.7, 1.5]], + columns=["x_1", "x_2"], + name="Another contour!", + color="#065f46", + linestyle=":", +) +c.add_chain(chain2) +fig = c.plotter.plot() + +# %% Third cell +# # Customising Chains +# +# There's a lot you can configure using chains, and to make it easy, Chains are defined as pydantic +# base models so you can easily see the default and values you can pass in. Don't worry, there will be +# plenty of very specific examples in a sub gallery you can check out, but as a final one for here, +# let's add markers and truth values. + +c.add_marker(location={"x_1": 4, "x_2": 4}, name="A point", color="orange", marker_style="P", marker_size=50) +c.add_truth(Truth(location={"x_1": 5, "x_2": 5})) +fig = c.plotter.plot() + + +# %% Fourth cell +# # Weights and Posteriors +# +# If you provide the log posteriors in the chain, you can ask for the maximum probability point +# to be plotted as well. Similarly, if you have samples with non-uniform weights, you can +# specify the weights column as well. +# +# To keep this clean, let's remake everything. I'm going to add an extra few columns into our +# dataframe. You'll see what they do + +df2 = df.assign( + x_3=lambda x: x["x_1"] + x["x_2"], + log_posterior=lambda x: norm.logpdf(x[["x_1", "x_2"]]), +) + +c = ChainConsumer() +# Customise the chain when you add it +chain = Chain( + samples=df2, + name="Example", + color="k", + plot_point=True, + plot_cloud=True, + marker_style="*", + marker_size=100, + num_cloud=30000, + shade=False, + linewidth=2.0, + cmap="magma", + show_contour_labels=True, + color_param="x_3", +) +c.add_chain(chain) +# You can also override *all* chains at once like so +# Notice that Chain is a child of ChainConfig +# So you could override base properties like line weights... but not samples +c.add_override(ChainConfig(sigmas=[0, 1, 2, 3])) +c.add_truth(Truth(location={"x_1": 0, "x_2": 4}, line_color="#500724")) + +# And if we want to change the plot itself in some way, we can do that via +c.set_plot_config( + PlotConfig( + flip=True, + labels={"x_1": "$x_1$", "x_2": "$x_2$", "x_3": "$x_3$"}, + contour_label_font_size=12, + ) +) +fig = c.plotter.plot() + +# %% Fifth cell +# Here the maximum posterior value is used to plot the star-shaped +# point. A truth line is added in dark red, and instead of plotting +# $x_3$ as another contour, we use it to provide coloured scatter plots. +# This is useful if the parameter isn't constrained and has some dependency. +# +# I have used this in the past to show how adding different priors to Hubble's +# constant (as the color parameter) would pull our fits in different directions. +# +# Now, I am expecting a few comments like: "But Sam, it was easier to do everything +# in one line before, instead of all these objects." +# +# Having everything as kwargs being passed through made the code a nightmare, +# type hints impossible, and extensibility poor. By trying to separate +# things out into appropriate dataclasses, I am hoping it is a lot easier +# for people to contribute to this code base in the future. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..f9def3b9 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,3 @@ +# ChainConsumer + +Welcome to the party \ No newline at end of file diff --git a/docs/plugins/griffe_doclinks.py b/docs/plugins/griffe_doclinks.py new file mode 100644 index 00000000..b7bfc456 --- /dev/null +++ b/docs/plugins/griffe_doclinks.py @@ -0,0 +1,87 @@ +import ast +import re +from functools import partial +from pathlib import Path + +from griffe.dataclasses import Object as GriffeObject +from griffe.extensions import VisitorExtension +from pymdownx.slugs import slugify + +DOCS_PATH = Path(__file__).parent.parent +slugifier = slugify(case="lower") + + +def find_heading(content: str, slug: str, file_path: Path) -> tuple[str, int]: + for m in re.finditer("^#+ (.+)", content, flags=re.M): + heading = m.group(1) + h_slug = slugifier(heading, "-") + if h_slug == slug: + return heading, m.end() + raise ValueError(f"heading with slug {slug!r} not found in {file_path}") + + +def insert_at_top(path: str, api_link: str) -> str: + rel_file = path.rstrip("/") + ".md" + file_path = DOCS_PATH / rel_file + content = file_path.read_text() + second_heading = re.search("^#+ ", content, flags=re.M) + assert second_heading, "unable to find second heading in file" + first_section = content[: second_heading.start()] + + if f"[{api_link}]" not in first_section: + file_path.write_text('??? api "API Documentation"\n' f" [`{api_link}`][{api_link}]
\n\n" f"{content}") + + heading = file_path.stem.replace("_", " ").title() + return f'!!! abstract "Usage Documentation"\n [{heading}](../{rel_file})\n' + + +def replace_links(m: re.Match, *, api_link: str) -> str: + path_group = m.group(1) + if "#" not in path_group: + # no heading id, put the content at the top of the page + return insert_at_top(path_group, api_link) + + usage_path, slug = path_group.split("#", 1) + rel_file = usage_path.rstrip("/") + ".md" + file_path = DOCS_PATH / rel_file + content = file_path.read_text() + heading, heading_end = find_heading(content, slug, file_path) + + next_heading = re.search("^#+ ", content[heading_end:], flags=re.M) + if next_heading: # noqa: SIM108 + next_section = content[heading_end : heading_end + next_heading.start()] + else: + next_section = content[heading_end:] + + if f"[{api_link}]" not in next_section: + file_path.write_text( + f"{content[:heading_end]}\n\n" + '??? api "API Documentation"\n' + f" [`{api_link}`][{api_link}]
" + f"{content[heading_end:]}" + ) + + return f'!!! abstract "Usage Documentation"\n [{heading}](../{rel_file}#{slug})\n' + + +def update_docstring(obj: GriffeObject) -> str: + return re.sub( + r"usage[\- ]docs: ?https://docs\.pydantic\.dev/.+?/(\S+)", + partial(replace_links, api_link=obj.path), + obj.docstring.value, # type: ignore + flags=re.I, + ) + + +def update_docstrings_recursively(obj: GriffeObject) -> None: + if obj.docstring: + obj.docstring.value = update_docstring(obj) + for member in obj.members.values(): + if not member.is_alias: + update_docstrings_recursively(member) # type: ignore + + +class Extension(VisitorExtension): + def visit_module(self, node: ast.AST) -> None: + module = self.visitor.current.module + update_docstrings_recursively(module) diff --git a/examples/plot_introduction.py b/examples/plot_introduction.py index 1e9c7ee7..2eaf801d 100644 --- a/examples/plot_introduction.py +++ b/examples/plot_introduction.py @@ -20,20 +20,18 @@ """ import numpy as np +import pandas as pd -from chainconsumer import ChainConsumer +from chainconsumer import Chain, ChainConsumer +# Here's what you might start with rng = np.random.default_rng(0) data = rng.multivariate_normal([0.0, 4.0], [[1.0, 0.7], [0.7, 1.5]], size=1000000) +df = pd.DataFrame(data, columns=["x_1", "x_2"]) +# And how we give this to chainconsumer c = ChainConsumer() -c.add_chain(data, parameters=["$x_1$", "$x_2$"]) -fig = c.plotter.plot(figsize="column", truth=[0.0, 4.0]) +c.add_chain(Chain(samples=df, name="An Example Contour")) +fig = c.plotter.plot() -# If we wanted to save to file, we would instead have written -# fig = c.plotter.plot(filename="location", figsize="column", truth=[0.0, 4.0]) - -# If we wanted to display the plot interactively... -# fig = c.plotter.plot(display=True, figsize="column", truth=[0.0, 4.0]) - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. +print("whoa") diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..0a86fa7d --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,72 @@ +site_name: ChainConsumer +theme: + name: material + icon: + repo: fontawesome/brands/github + features: + - search.suggest + - search.highlight + - search.tabs.link + - navigation.expand + - toc.follow + - navigation.tracking + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: teal + accent: light green + toggle: + icon: material/weather-sunny + name: Switch to light mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: teal + accent: light green + toggle: + icon: material/weather-night + name: Switch to dark mode +repo_name: chainconsumer +repo_url: https://github.com/samreay/chainconsumer +plugins: + # autorefs: {} + mkdocstrings: + handlers: + python: + paths: [.] + options: + members_order: source + separate_signature: true + filters: ["!^_"] + show_root_heading: true + show_if_no_docstring: true + show_signature_annotations: true + extensions: + - docs/plugins/griffe_doclinks.py + # gallery: + # examples_dirs: docs/examples # path to your example scripts + # gallery_dirs: docs/generated/gallery # where to save generated gallery + # image_srcset: ['2x'] + # image_scrapers: matplotlib + # compress_images: ['images', 'thumbnails'] + # doc_module: ['mkdocs_gallery', 'numpy'] + search: {} + +markdown_extensions: + - mdx_include: + base_path: docs + - toc: + permalink: true + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.details + - pymdownx.snippets + - pymdownx.superfences + +watch: + - docs + - src +nav: + - Home: index.md + - Python API: api/api.md + - Examples: generated/gallery diff --git a/poetry.lock b/poetry.lock index 16696c16..1a14c002 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,15 +2,215 @@ [[package]] name = "annotated-types" -version = "0.5.0" +version = "0.6.0" description = "Reusable constraint types to use with typing.Annotated" optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "anyio" +version = "4.0.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, + {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.22)"] + +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = "*" +files = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, +] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +description = "Argon2 for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, + {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, +] + +[package.dependencies] +argon2-cffi-bindings = "*" + +[package.extras] +dev = ["argon2-cffi[tests,typing]", "tox (>4)"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] +tests = ["hypothesis", "pytest"] +typing = ["mypy"] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +description = "Low-level CFFI bindings for Argon2" +optional = false +python-versions = ">=3.6" +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, +] + +[package.dependencies] +cffi = ">=1.0.1" + +[package.extras] +dev = ["cogapp", "pre-commit", "pytest", "wheel"] +tests = ["pytest"] + +[[package]] +name = "arrow" +version = "1.3.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, + {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +types-python-dateutil = ">=2.8.10" + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] + +[[package]] +name = "asttokens" +version = "2.4.0" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.4.0-py2.py3-none-any.whl", hash = "sha256:cf8fc9e61a86461aa9fb161a14a0841a03c405fa829ac6b202670b3495d2ce69"}, + {file = "asttokens-2.4.0.tar.gz", hash = "sha256:2e0171b991b2c959acc6c49318049236844a5da1d65ba2672c4880c1c894834e"}, +] + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +test = ["astroid", "pytest"] + +[[package]] +name = "async-lru" +version = "2.0.4" +description = "Simple LRU cache for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, + {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +optional = false python-versions = ">=3.7" files = [ - {file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"}, - {file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"}, + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, ] +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "babel" +version = "2.13.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Babel-2.13.0-py3-none-any.whl", hash = "sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec"}, + {file = "Babel-2.13.0.tar.gz", hash = "sha256:04c3e2d28d2b7681644508f836be388ae49e0cfe91465095340395b60d00f210"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +optional = false +python-versions = "*" +files = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.2" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, + {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "black" version = "23.9.1" @@ -57,6 +257,99 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "bleach" +version = "6.1.0" +description = "An easy safelist-based HTML-sanitizing tool." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, + {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, +] + +[package.dependencies] +six = ">=1.9.0" +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.3)"] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.4.0" @@ -68,6 +361,105 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "charset-normalizer" +version = "3.3.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"}, + {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"}, +] + [[package]] name = "click" version = "8.1.7" @@ -93,6 +485,25 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "comm" +version = "0.1.4" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.6" +files = [ + {file = "comm-0.1.4-py3-none-any.whl", hash = "sha256:6d52794cba11b36ed9860999cd10fd02d6b2eac177068fdd585e1e2f8a96e67a"}, + {file = "comm-0.1.4.tar.gz", hash = "sha256:354e40a59c9dd6db50c5cc6b4acc887d82e9603787f83b68c01a80a923984d15"}, +] + +[package.dependencies] +traitlets = ">=4" + +[package.extras] +lint = ["black (>=22.6.0)", "mdformat (>0.7)", "mdformat-gfm (>=0.3.5)", "ruff (>=0.0.156)"] +test = ["pytest"] +typing = ["mypy (>=0.990)"] + [[package]] name = "contourpy" version = "1.1.1" @@ -169,19 +580,79 @@ test-no-images = ["pytest", "pytest-cov", "wurlitzer"] [[package]] name = "cycler" -version = "0.12.0" +version = "0.12.1" description = "Composable style cycles" optional = false python-versions = ">=3.8" files = [ - {file = "cycler-0.12.0-py3-none-any.whl", hash = "sha256:7896994252d006771357777d0251f3e34d266f4fa5f2c572247a80ab01440947"}, - {file = "cycler-0.12.0.tar.gz", hash = "sha256:8cc3a7b4861f91b1095157f9916f748549a617046e67eb7619abed9b34d2c94a"}, + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, ] [package.extras] docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] tests = ["pytest", "pytest-cov", "pytest-xdist"] +[[package]] +name = "cyclic" +version = "1.0.0" +description = "Handle cyclic relations" +optional = false +python-versions = "*" +files = [ + {file = "cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed"}, + {file = "cyclic-1.0.0.tar.gz", hash = "sha256:ecddd56cb831ee3e6b79f61ecb0ad71caee606c507136867782911aa01c3e5eb"}, +] + +[[package]] +name = "debugpy" +version = "1.8.0" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "debugpy-1.8.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7fb95ca78f7ac43393cd0e0f2b6deda438ec7c5e47fa5d38553340897d2fbdfb"}, + {file = "debugpy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef9ab7df0b9a42ed9c878afd3eaaff471fce3fa73df96022e1f5c9f8f8c87ada"}, + {file = "debugpy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:a8b7a2fd27cd9f3553ac112f356ad4ca93338feadd8910277aff71ab24d8775f"}, + {file = "debugpy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5d9de202f5d42e62f932507ee8b21e30d49aae7e46d5b1dd5c908db1d7068637"}, + {file = "debugpy-1.8.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ef54404365fae8d45cf450d0544ee40cefbcb9cb85ea7afe89a963c27028261e"}, + {file = "debugpy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60009b132c91951354f54363f8ebdf7457aeb150e84abba5ae251b8e9f29a8a6"}, + {file = "debugpy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:8cd0197141eb9e8a4566794550cfdcdb8b3db0818bdf8c49a8e8f8053e56e38b"}, + {file = "debugpy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a64093656c4c64dc6a438e11d59369875d200bd5abb8f9b26c1f5f723622e153"}, + {file = "debugpy-1.8.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:b05a6b503ed520ad58c8dc682749113d2fd9f41ffd45daec16e558ca884008cd"}, + {file = "debugpy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c6fb41c98ec51dd010d7ed650accfd07a87fe5e93eca9d5f584d0578f28f35f"}, + {file = "debugpy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:46ab6780159eeabb43c1495d9c84cf85d62975e48b6ec21ee10c95767c0590aa"}, + {file = "debugpy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:bdc5ef99d14b9c0fcb35351b4fbfc06ac0ee576aeab6b2511702e5a648a2e595"}, + {file = "debugpy-1.8.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:61eab4a4c8b6125d41a34bad4e5fe3d2cc145caecd63c3fe953be4cc53e65bf8"}, + {file = "debugpy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:125b9a637e013f9faac0a3d6a82bd17c8b5d2c875fb6b7e2772c5aba6d082332"}, + {file = "debugpy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:57161629133113c97b387382045649a2b985a348f0c9366e22217c87b68b73c6"}, + {file = "debugpy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:e3412f9faa9ade82aa64a50b602544efcba848c91384e9f93497a458767e6926"}, + {file = "debugpy-1.8.0-py2.py3-none-any.whl", hash = "sha256:9c9b0ac1ce2a42888199df1a1906e45e6f3c9555497643a85e0bf2406e3ffbc4"}, + {file = "debugpy-1.8.0.zip", hash = "sha256:12af2c55b419521e33d5fb21bd022df0b5eb267c3e178f1d374a63a2a6bdccd0"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "distlib" version = "0.3.7" @@ -207,6 +678,34 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "executing" +version = "2.0.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = "*" +files = [ + {file = "executing-2.0.0-py2.py3-none-any.whl", hash = "sha256:06df6183df67389625f4e763921c6cf978944721abf3e714000200aab95b0657"}, + {file = "executing-2.0.0.tar.gz", hash = "sha256:0ff053696fdeef426cda5bd18eacd94f82c91f49823a2e9090124212ceea9b08"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[[package]] +name = "fastjsonschema" +version = "2.18.1" +description = "Fastest Python implementation of JSON schema" +optional = false +python-versions = "*" +files = [ + {file = "fastjsonschema-2.18.1-py3-none-any.whl", hash = "sha256:aec6a19e9f66e9810ab371cc913ad5f4e9e479b63a7072a2cd060a9369e329a8"}, + {file = "fastjsonschema-2.18.1.tar.gz", hash = "sha256:06dc8680d937628e993fa0cd278f196d20449a1adc087640710846b324d422ea"}, +] + +[package.extras] +devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] + [[package]] name = "filelock" version = "3.12.4" @@ -225,53 +724,53 @@ typing = ["typing-extensions (>=4.7.1)"] [[package]] name = "fonttools" -version = "4.43.0" +version = "4.43.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.43.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ab80e7d6bb01316d5fc8161a2660ca2e9e597d0880db4927bc866c76474472ef"}, - {file = "fonttools-4.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82d8e687a42799df5325e7ee12977b74738f34bf7fde1c296f8140efd699a213"}, - {file = "fonttools-4.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d08a694b280d615460563a6b4e2afb0b1b9df708c799ec212bf966652b94fc84"}, - {file = "fonttools-4.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d654d3e780e0ceabb1f4eff5a3c042c67d4428d0fe1ea3afd238a721cf171b3"}, - {file = "fonttools-4.43.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:20fc43783c432862071fa76da6fa714902ae587bc68441e12ff4099b94b1fcef"}, - {file = "fonttools-4.43.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:33c40a657fb87ff83185828c0323032d63a4df1279d5c1c38e21f3ec56327803"}, - {file = "fonttools-4.43.0-cp310-cp310-win32.whl", hash = "sha256:b3813f57f85bbc0e4011a0e1e9211f9ee52f87f402e41dc05bc5135f03fa51c1"}, - {file = "fonttools-4.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:05056a8c9af048381fdb17e89b17d45f6c8394176d01e8c6fef5ac96ea950d38"}, - {file = "fonttools-4.43.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da78f39b601ed0b4262929403186d65cf7a016f91ff349ab18fdc5a7080af465"}, - {file = "fonttools-4.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5056f69a18f3f28ab5283202d1efcfe011585d31de09d8560f91c6c88f041e92"}, - {file = "fonttools-4.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcc01cea0a121fb0c009993497bad93cae25e77db7dee5345fec9cce1aaa09cd"}, - {file = "fonttools-4.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee728d5af70f117581712966a21e2e07031e92c687ef1fdc457ac8d281016f64"}, - {file = "fonttools-4.43.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b5e760198f0b87e42478bb35a6eae385c636208f6f0d413e100b9c9c5efafb6a"}, - {file = "fonttools-4.43.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af38f5145258e9866da5881580507e6d17ff7756beef175d13213a43a84244e9"}, - {file = "fonttools-4.43.0-cp311-cp311-win32.whl", hash = "sha256:25620b738d4533cfc21fd2a4f4b667e481f7cb60e86b609799f7d98af657854e"}, - {file = "fonttools-4.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:635658464dccff6fa5c3b43fe8f818ae2c386ee6a9e1abc27359d1e255528186"}, - {file = "fonttools-4.43.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a682fb5cbf8837d1822b80acc0be5ff2ea0c49ca836e468a21ffd388ef280fd3"}, - {file = "fonttools-4.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3d7adfa342e6b3a2b36960981f23f480969f833d565a4eba259c2e6f59d2674f"}, - {file = "fonttools-4.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa67d1e720fdd902fde4a59d0880854ae9f19fc958f3e1538bceb36f7f4dc92"}, - {file = "fonttools-4.43.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77e5113233a2df07af9dbf493468ce526784c3b179c0e8b9c7838ced37c98b69"}, - {file = "fonttools-4.43.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57c22e5f9f53630d458830f710424dce4f43c5f0d95cb3368c0f5178541e4db7"}, - {file = "fonttools-4.43.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:206808f9717c9b19117f461246372a2c160fa12b9b8dbdfb904ab50ca235ba0a"}, - {file = "fonttools-4.43.0-cp312-cp312-win32.whl", hash = "sha256:f19c2b1c65d57cbea25cabb80941fea3fbf2625ff0cdcae8900b5fb1c145704f"}, - {file = "fonttools-4.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7c76f32051159f8284f1a5f5b605152b5a530736fb8b55b09957db38dcae5348"}, - {file = "fonttools-4.43.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e3f8acc6ef4a627394021246e099faee4b343afd3ffe2e517d8195b4ebf20289"}, - {file = "fonttools-4.43.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a68b71adc3b3a90346e4ac92f0a69ab9caeba391f3b04ab6f1e98f2c8ebe88e3"}, - {file = "fonttools-4.43.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ace0fd5afb79849f599f76af5c6aa5e865bd042c811e4e047bbaa7752cc26126"}, - {file = "fonttools-4.43.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9660e70a2430780e23830476332bc3391c3c8694769e2c0032a5038702a662"}, - {file = "fonttools-4.43.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:48078357984214ccd22d7fe0340cd6ff7286b2f74f173603a1a9a40b5dc25afe"}, - {file = "fonttools-4.43.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d27d960e10cf7617d70cf3104c32a69b008dde56f2d55a9bed4ba6e3df611544"}, - {file = "fonttools-4.43.0-cp38-cp38-win32.whl", hash = "sha256:a6a2e99bb9ea51e0974bbe71768df42c6dd189308c22f3f00560c3341b345646"}, - {file = "fonttools-4.43.0-cp38-cp38-win_amd64.whl", hash = "sha256:030355fbb0cea59cf75d076d04d3852900583d1258574ff2d7d719abf4513836"}, - {file = "fonttools-4.43.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52e77f23a9c059f8be01a07300ba4c4d23dc271d33eed502aea5a01ab5d2f4c1"}, - {file = "fonttools-4.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a530fa28c155538d32214eafa0964989098a662bd63e91e790e6a7a4e9c02da"}, - {file = "fonttools-4.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70f021a6b9eb10dfe7a411b78e63a503a06955dd6d2a4e130906d8760474f77c"}, - {file = "fonttools-4.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:812142a0e53cc853964d487e6b40963df62f522b1b571e19d1ff8467d7880ceb"}, - {file = "fonttools-4.43.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ace51902ab67ef5fe225e8b361039e996db153e467e24a28d35f74849b37b7ce"}, - {file = "fonttools-4.43.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8dfd8edfce34ad135bd69de20c77449c06e2c92b38f2a8358d0987737f82b49e"}, - {file = "fonttools-4.43.0-cp39-cp39-win32.whl", hash = "sha256:e5d53eddaf436fa131042f44a76ea1ead0a17c354ab9de0d80e818f0cb1629f1"}, - {file = "fonttools-4.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:93c5b6d77baf28f306bc13fa987b0b13edca6a39dc2324eaca299a74ccc6316f"}, - {file = "fonttools-4.43.0-py3-none-any.whl", hash = "sha256:e4bc589d8da09267c7c4ceaaaa4fc01a7908ac5b43b286ac9279afe76407c384"}, - {file = "fonttools-4.43.0.tar.gz", hash = "sha256:b62a53a4ca83c32c6b78cac64464f88d02929779373c716f738af6968c8c821e"}, + {file = "fonttools-4.43.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bf11e2cca121df35e295bd34b309046c29476ee739753bc6bc9d5050de319273"}, + {file = "fonttools-4.43.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10b3922875ffcba636674f406f9ab9a559564fdbaa253d66222019d569db869c"}, + {file = "fonttools-4.43.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f727c3e3d08fd25352ed76cc3cb61486f8ed3f46109edf39e5a60fc9fecf6ca"}, + {file = "fonttools-4.43.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad0b3f6342cfa14be996971ea2b28b125ad681c6277c4cd0fbdb50340220dfb6"}, + {file = "fonttools-4.43.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3b7ad05b2beeebafb86aa01982e9768d61c2232f16470f9d0d8e385798e37184"}, + {file = "fonttools-4.43.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4c54466f642d2116686268c3e5f35ebb10e49b0d48d41a847f0e171c785f7ac7"}, + {file = "fonttools-4.43.1-cp310-cp310-win32.whl", hash = "sha256:1e09da7e8519e336239fbd375156488a4c4945f11c4c5792ee086dd84f784d02"}, + {file = "fonttools-4.43.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cf9e974f63b1080b1d2686180fc1fbfd3bfcfa3e1128695b5de337eb9075cef"}, + {file = "fonttools-4.43.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5db46659cfe4e321158de74c6f71617e65dc92e54980086823a207f1c1c0e24b"}, + {file = "fonttools-4.43.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1952c89a45caceedf2ab2506d9a95756e12b235c7182a7a0fff4f5e52227204f"}, + {file = "fonttools-4.43.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c36da88422e0270fbc7fd959dc9749d31a958506c1d000e16703c2fce43e3d0"}, + {file = "fonttools-4.43.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bbbf8174501285049e64d174e29f9578495e1b3b16c07c31910d55ad57683d8"}, + {file = "fonttools-4.43.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d4071bd1c183b8d0b368cc9ed3c07a0f6eb1bdfc4941c4c024c49a35429ac7cd"}, + {file = "fonttools-4.43.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d21099b411e2006d3c3e1f9aaf339e12037dbf7bf9337faf0e93ec915991f43b"}, + {file = "fonttools-4.43.1-cp311-cp311-win32.whl", hash = "sha256:b84a1c00f832feb9d0585ca8432fba104c819e42ff685fcce83537e2e7e91204"}, + {file = "fonttools-4.43.1-cp311-cp311-win_amd64.whl", hash = "sha256:9a2f0aa6ca7c9bc1058a9d0b35483d4216e0c1bbe3962bc62ce112749954c7b8"}, + {file = "fonttools-4.43.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4d9740e3783c748521e77d3c397dc0662062c88fd93600a3c2087d3d627cd5e5"}, + {file = "fonttools-4.43.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:884ef38a5a2fd47b0c1291647b15f4e88b9de5338ffa24ee52c77d52b4dfd09c"}, + {file = "fonttools-4.43.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9648518ef687ba818db3fcc5d9aae27a369253ac09a81ed25c3867e8657a0680"}, + {file = "fonttools-4.43.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e974d70238fc2be5f444fa91f6347191d0e914d5d8ae002c9aa189572cc215"}, + {file = "fonttools-4.43.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:34f713dad41aa21c637b4e04fe507c36b986a40f7179dcc86402237e2d39dcd3"}, + {file = "fonttools-4.43.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:360201d46165fc0753229afe785900bc9596ee6974833124f4e5e9f98d0f592b"}, + {file = "fonttools-4.43.1-cp312-cp312-win32.whl", hash = "sha256:bb6d2f8ef81ea076877d76acfb6f9534a9c5f31dc94ba70ad001267ac3a8e56f"}, + {file = "fonttools-4.43.1-cp312-cp312-win_amd64.whl", hash = "sha256:25d3da8a01442cbc1106490eddb6d31d7dffb38c1edbfabbcc8db371b3386d72"}, + {file = "fonttools-4.43.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8da417431bfc9885a505e86ba706f03f598c85f5a9c54f67d63e84b9948ce590"}, + {file = "fonttools-4.43.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:51669b60ee2a4ad6c7fc17539a43ffffc8ef69fd5dbed186a38a79c0ac1f5db7"}, + {file = "fonttools-4.43.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748015d6f28f704e7d95cd3c808b483c5fb87fd3eefe172a9da54746ad56bfb6"}, + {file = "fonttools-4.43.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a58eb5e736d7cf198eee94844b81c9573102ae5989ebcaa1d1a37acd04b33d"}, + {file = "fonttools-4.43.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6bb5ea9076e0e39defa2c325fc086593ae582088e91c0746bee7a5a197be3da0"}, + {file = "fonttools-4.43.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5f37e31291bf99a63328668bb83b0669f2688f329c4c0d80643acee6e63cd933"}, + {file = "fonttools-4.43.1-cp38-cp38-win32.whl", hash = "sha256:9c60ecfa62839f7184f741d0509b5c039d391c3aff71dc5bc57b87cc305cff3b"}, + {file = "fonttools-4.43.1-cp38-cp38-win_amd64.whl", hash = "sha256:fe9b1ec799b6086460a7480e0f55c447b1aca0a4eecc53e444f639e967348896"}, + {file = "fonttools-4.43.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13a9a185259ed144def3682f74fdcf6596f2294e56fe62dfd2be736674500dba"}, + {file = "fonttools-4.43.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2adca1b46d69dce4a37eecc096fe01a65d81a2f5c13b25ad54d5430ae430b13"}, + {file = "fonttools-4.43.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18eefac1b247049a3a44bcd6e8c8fd8b97f3cad6f728173b5d81dced12d6c477"}, + {file = "fonttools-4.43.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2062542a7565091cea4cc14dd99feff473268b5b8afdee564f7067dd9fff5860"}, + {file = "fonttools-4.43.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18a2477c62a728f4d6e88c45ee9ee0229405e7267d7d79ce1f5ce0f3e9f8ab86"}, + {file = "fonttools-4.43.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a7a06f8d95b7496e53af80d974d63516ffb263a468e614978f3899a6df52d4b3"}, + {file = "fonttools-4.43.1-cp39-cp39-win32.whl", hash = "sha256:10003ebd81fec0192c889e63a9c8c63f88c7d72ae0460b7ba0cd2a1db246e5ad"}, + {file = "fonttools-4.43.1-cp39-cp39-win_amd64.whl", hash = "sha256:e117a92b07407a061cde48158c03587ab97e74e7d73cb65e6aadb17af191162a"}, + {file = "fonttools-4.43.1-py3-none-any.whl", hash = "sha256:4f88cae635bfe4bbbdc29d479a297bb525a94889184bb69fa9560c2d4834ddb9"}, + {file = "fonttools-4.43.1.tar.gz", hash = "sha256:17dbc2eeafb38d5d0e865dcce16e313c58265a6d2d20081c435f84dc5a9d8212"}, ] [package.extras] @@ -288,6 +787,48 @@ ufo = ["fs (>=2.2.0,<3)"] unicode = ["unicodedata2 (>=15.0.0)"] woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] +[[package]] +name = "fqdn" +version = "1.5.1" +description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" +optional = false +python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" +files = [ + {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, + {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "griffe" +version = "0.36.4" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.8" +files = [ + {file = "griffe-0.36.4-py3-none-any.whl", hash = "sha256:4e37a723891fa774fafdd67240571801a1d90d0236562c178707e5c37fb3ebe2"}, + {file = "griffe-0.36.4.tar.gz", hash = "sha256:7b5968f5cc6446637ed0d3ded9de07d6a928f10ccb24116b1dd843635bf1994a"}, +] + +[package.dependencies] +colorama = ">=0.4" + [[package]] name = "identify" version = "2.5.30" @@ -302,6 +843,17 @@ files = [ [package.extras] license = ["ukkonen"] +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -313,6 +865,486 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "ipykernel" +version = "6.25.2" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipykernel-6.25.2-py3-none-any.whl", hash = "sha256:2e2ee359baba19f10251b99415bb39de1e97d04e1fab385646f24f0596510b77"}, + {file = "ipykernel-6.25.2.tar.gz", hash = "sha256:f468ddd1f17acb48c8ce67fcfa49ba6d46d4f9ac0438c1f441be7c3d1372230b"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = "*" +packaging = "*" +psutil = "*" +pyzmq = ">=20" +tornado = ">=6.1" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "ipython" +version = "8.16.1" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.9" +files = [ + {file = "ipython-8.16.1-py3-none-any.whl", hash = "sha256:0852469d4d579d9cd613c220af7bf0c9cc251813e12be647cb9d463939db9b1e"}, + {file = "ipython-8.16.1.tar.gz", hash = "sha256:ad52f58fca8f9f848e256c629eff888efc0528c12fe0f8ec14f33205f23ef938"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" + +[package.extras] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] + +[[package]] +name = "ipython-genutils" +version = "0.2.0" +description = "Vestigial utilities from IPython" +optional = false +python-versions = "*" +files = [ + {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, + {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, +] + +[[package]] +name = "ipywidgets" +version = "8.1.1" +description = "Jupyter interactive widgets" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ipywidgets-8.1.1-py3-none-any.whl", hash = "sha256:2b88d728656aea3bbfd05d32c747cfd0078f9d7e159cf982433b58ad717eed7f"}, + {file = "ipywidgets-8.1.1.tar.gz", hash = "sha256:40211efb556adec6fa450ccc2a77d59ca44a060f4f9f136833df59c9f538e6e8"}, +] + +[package.dependencies] +comm = ">=0.1.3" +ipython = ">=6.1.0" +jupyterlab-widgets = ">=3.0.9,<3.1.0" +traitlets = ">=4.3.1" +widgetsnbextension = ">=4.0.9,<4.1.0" + +[package.extras] +test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] + +[[package]] +name = "isoduration" +version = "20.11.0" +description = "Operations with ISO 8601 durations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, + {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, +] + +[package.dependencies] +arrow = ">=0.15.0" + +[[package]] +name = "jedi" +version = "0.19.1" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "json5" +version = "0.9.14" +description = "A Python implementation of the JSON5 data format." +optional = false +python-versions = "*" +files = [ + {file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"}, + {file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"}, +] + +[package.extras] +dev = ["hypothesis"] + +[[package]] +name = "jsonpointer" +version = "2.4" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +files = [ + {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, + {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, +] + +[[package]] +name = "jsonschema" +version = "4.19.1" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.19.1-py3-none-any.whl", hash = "sha256:cd5f1f9ed9444e554b38ba003af06c0a8c2868131e56bfbef0550fb450c0330e"}, + {file = "jsonschema-4.19.1.tar.gz", hash = "sha256:ec84cc37cfa703ef7cd4928db24f9cb31428a5d0fa77747b8b51a847458e0bbf"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} +rpds-py = ">=0.7.1" +uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format-nongpl\""} + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.7.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.7.1-py3-none-any.whl", hash = "sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1"}, + {file = "jsonschema_specifications-2023.7.1.tar.gz", hash = "sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb"}, +] + +[package.dependencies] +referencing = ">=0.28.0" + +[[package]] +name = "jupyter" +version = "1.0.0" +description = "Jupyter metapackage. Install all the Jupyter components in one go." +optional = false +python-versions = "*" +files = [ + {file = "jupyter-1.0.0-py2.py3-none-any.whl", hash = "sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78"}, + {file = "jupyter-1.0.0.tar.gz", hash = "sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f"}, + {file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"}, +] + +[package.dependencies] +ipykernel = "*" +ipywidgets = "*" +jupyter-console = "*" +nbconvert = "*" +notebook = "*" +qtconsole = "*" + +[[package]] +name = "jupyter-client" +version = "8.3.1" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_client-8.3.1-py3-none-any.whl", hash = "sha256:5eb9f55eb0650e81de6b7e34308d8b92d04fe4ec41cd8193a913979e33d8e1a5"}, + {file = "jupyter_client-8.3.1.tar.gz", hash = "sha256:60294b2d5b869356c893f57b1a877ea6510d60d45cf4b38057f1672d85699ac9"}, +] + +[package.dependencies] +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-console" +version = "6.6.3" +description = "Jupyter terminal console" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485"}, + {file = "jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539"}, +] + +[package.dependencies] +ipykernel = ">=6.14" +ipython = "*" +jupyter-client = ">=7.0.0" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +prompt-toolkit = ">=3.0.30" +pygments = "*" +pyzmq = ">=17" +traitlets = ">=5.4" + +[package.extras] +test = ["flaky", "pexpect", "pytest"] + +[[package]] +name = "jupyter-core" +version = "5.3.2" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_core-5.3.2-py3-none-any.whl", hash = "sha256:a4af53c3fa3f6330cebb0d9f658e148725d15652811d1c32dc0f63bb96f2e6d6"}, + {file = "jupyter_core-5.3.2.tar.gz", hash = "sha256:0c28db6cbe2c37b5b398e1a1a5b22f84fd64cd10afc1f6c05b02fb09481ba45f"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["myst-parser", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "jupyter-events" +version = "0.7.0" +description = "Jupyter Event System library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_events-0.7.0-py3-none-any.whl", hash = "sha256:4753da434c13a37c3f3c89b500afa0c0a6241633441421f6adafe2fb2e2b924e"}, + {file = "jupyter_events-0.7.0.tar.gz", hash = "sha256:7be27f54b8388c03eefea123a4f79247c5b9381c49fb1cd48615ee191eb12615"}, +] + +[package.dependencies] +jsonschema = {version = ">=4.18.0", extras = ["format-nongpl"]} +python-json-logger = ">=2.0.4" +pyyaml = ">=5.3" +referencing = "*" +rfc3339-validator = "*" +rfc3986-validator = ">=0.1.1" +traitlets = ">=5.3" + +[package.extras] +cli = ["click", "rich"] +docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontrib-spelling"] +test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] + +[[package]] +name = "jupyter-lsp" +version = "2.2.0" +description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter-lsp-2.2.0.tar.gz", hash = "sha256:8ebbcb533adb41e5d635eb8fe82956b0aafbf0fd443b6c4bfa906edeeb8635a1"}, + {file = "jupyter_lsp-2.2.0-py3-none-any.whl", hash = "sha256:9e06b8b4f7dd50300b70dd1a78c0c3b0c3d8fa68e0f2d8a5d1fbab62072aca3f"}, +] + +[package.dependencies] +jupyter-server = ">=1.1.2" + +[[package]] +name = "jupyter-server" +version = "2.7.3" +description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_server-2.7.3-py3-none-any.whl", hash = "sha256:8e4b90380b59d7a1e31086c4692231f2a2ea4cb269f5516e60aba72ce8317fc9"}, + {file = "jupyter_server-2.7.3.tar.gz", hash = "sha256:d4916c8581c4ebbc534cebdaa8eca2478d9f3bfdd88eae29fcab0120eac57649"}, +] + +[package.dependencies] +anyio = ">=3.1.0" +argon2-cffi = "*" +jinja2 = "*" +jupyter-client = ">=7.4.4" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-events = ">=0.6.0" +jupyter-server-terminals = "*" +nbconvert = ">=6.4.4" +nbformat = ">=5.3.0" +overrides = "*" +packaging = "*" +prometheus-client = "*" +pywinpty = {version = "*", markers = "os_name == \"nt\""} +pyzmq = ">=24" +send2trash = ">=1.8.2" +terminado = ">=0.8.3" +tornado = ">=6.2.0" +traitlets = ">=5.6.0" +websocket-client = "*" + +[package.extras] +docs = ["ipykernel", "jinja2", "jupyter-client", "jupyter-server", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] +test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.4)", "pytest-timeout", "requests"] + +[[package]] +name = "jupyter-server-terminals" +version = "0.4.4" +description = "A Jupyter Server Extension Providing Terminals." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_server_terminals-0.4.4-py3-none-any.whl", hash = "sha256:75779164661cec02a8758a5311e18bb8eb70c4e86c6b699403100f1585a12a36"}, + {file = "jupyter_server_terminals-0.4.4.tar.gz", hash = "sha256:57ab779797c25a7ba68e97bcfb5d7740f2b5e8a83b5e8102b10438041a7eac5d"}, +] + +[package.dependencies] +pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} +terminado = ">=0.8.3" + +[package.extras] +docs = ["jinja2", "jupyter-server", "mistune (<3.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"] +test = ["coverage", "jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-cov", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] + +[[package]] +name = "jupyterlab" +version = "4.0.6" +description = "JupyterLab computational environment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab-4.0.6-py3-none-any.whl", hash = "sha256:7d9dacad1e3f30fe4d6d4efc97fda25fbb5012012b8f27cc03a2283abcdee708"}, + {file = "jupyterlab-4.0.6.tar.gz", hash = "sha256:6c43ae5a6a1fd2fdfafcb3454004958bde6da76331abb44cffc6f9e436b19ba1"}, +] + +[package.dependencies] +async-lru = ">=1.0.0" +ipykernel = "*" +jinja2 = ">=3.0.3" +jupyter-core = "*" +jupyter-lsp = ">=2.0.0" +jupyter-server = ">=2.4.0,<3" +jupyterlab-server = ">=2.19.0,<3" +notebook-shim = ">=0.2" +packaging = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} +tornado = ">=6.2.0" +traitlets = "*" + +[package.extras] +dev = ["black[jupyter] (==23.7.0)", "build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.0.286)"] +docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-tornasync", "sphinx (>=1.8,<7.2.0)", "sphinx-copybutton"] +docs-screenshots = ["altair (==5.0.1)", "ipython (==8.14.0)", "ipywidgets (==8.0.6)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.0.post0)", "matplotlib (==3.7.1)", "nbconvert (>=7.0.0)", "pandas (==2.0.2)", "scipy (==1.10.1)", "vega-datasets (==0.9.0)"] +test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] + +[[package]] +name = "jupyterlab-pygments" +version = "0.2.2" +description = "Pygments theme using JupyterLab CSS variables" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jupyterlab_pygments-0.2.2-py2.py3-none-any.whl", hash = "sha256:2405800db07c9f770863bcf8049a529c3dd4d3e28536638bd7c1c01d2748309f"}, + {file = "jupyterlab_pygments-0.2.2.tar.gz", hash = "sha256:7405d7fde60819d905a9fa8ce89e4cd830e318cdad22a0030f7a901da705585d"}, +] + +[[package]] +name = "jupyterlab-server" +version = "2.25.0" +description = "A set of server components for JupyterLab and JupyterLab like applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab_server-2.25.0-py3-none-any.whl", hash = "sha256:c9f67a98b295c5dee87f41551b0558374e45d449f3edca153dd722140630dcb2"}, + {file = "jupyterlab_server-2.25.0.tar.gz", hash = "sha256:77c2f1f282d610f95e496e20d5bf1d2a7706826dfb7b18f3378ae2870d272fb7"}, +] + +[package.dependencies] +babel = ">=2.10" +jinja2 = ">=3.0.3" +json5 = ">=0.9.0" +jsonschema = ">=4.18.0" +jupyter-server = ">=1.21,<3" +packaging = ">=21.3" +requests = ">=2.31" + +[package.extras] +docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi (>0.8)"] +openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] +test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.7.0)", "pytest (>=7.0)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.9" +description = "Jupyter interactive widgets for JupyterLab" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jupyterlab_widgets-3.0.9-py3-none-any.whl", hash = "sha256:3cf5bdf5b897bf3bccf1c11873aa4afd776d7430200f765e0686bd352487b58d"}, + {file = "jupyterlab_widgets-3.0.9.tar.gz", hash = "sha256:6005a4e974c7beee84060fdfba341a3218495046de8ae3ec64888e5fe19fdb4c"}, +] + [[package]] name = "kiwisolver" version = "1.4.5" @@ -426,6 +1458,80 @@ files = [ {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, ] +[[package]] +name = "markdown" +version = "3.5" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.5-py3-none-any.whl", hash = "sha256:4afb124395ce5fc34e6d9886dab977fd9ae987fc6e85689f08278cf0c69d4bf3"}, + {file = "Markdown-3.5.tar.gz", hash = "sha256:a807eb2e4778d9156c8f07876c6e4d50b5494c5665c4834f67b06459dfd877b3"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + [[package]] name = "matplotlib" version = "3.8.0" @@ -475,6 +1581,214 @@ pyparsing = ">=2.3.1" python-dateutil = ">=2.7" setuptools_scm = ">=7" +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mdx-include" +version = "1.4.2" +description = "Python Markdown extension to include local or remote files" +optional = false +python-versions = "*" +files = [ + {file = "mdx_include-1.4.2-py3-none-any.whl", hash = "sha256:cfbeadd59985f27a9b70cb7ab0a3d209892fe1bb1aa342df055e0b135b3c9f34"}, + {file = "mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297"}, +] + +[package.dependencies] +cyclic = "*" +Markdown = ">=2.6" +rcslice = ">=1.1.0" + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mistune" +version = "3.0.2" +description = "A sane and fast Markdown parser with useful plugins and renderers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"}, + {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, +] + +[[package]] +name = "mkdocs" +version = "1.5.3" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, + {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.2.1" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +packaging = ">=20.5" +pathspec = ">=0.11.1" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "0.5.0" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_autorefs-0.5.0-py3-none-any.whl", hash = "sha256:7930fcb8ac1249f10e683967aeaddc0af49d90702af111a5e390e8b20b3d97ff"}, + {file = "mkdocs_autorefs-0.5.0.tar.gz", hash = "sha256:9a5054a94c08d28855cfab967ada10ed5be76e2bfad642302a610b252c3274c0"}, +] + +[package.dependencies] +Markdown = ">=3.3" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-gallery" +version = "0.7.8" +description = "a `mkdocs` plugin to generate example galleries from python scripts, similar to `sphinx-gallery`." +optional = false +python-versions = "*" +files = [ + {file = "mkdocs-gallery-0.7.8.tar.gz", hash = "sha256:281af6c9917643f70bd52f56a81e7bacc4b74a61391d5fa288054079c6a96294"}, + {file = "mkdocs_gallery-0.7.8-py2.py3-none-any.whl", hash = "sha256:d9b137b50cef78f6985eeb6117a29af5575399950724cf87cb93b65f1e5406ca"}, +] + +[package.dependencies] +mkdocs = ">=1,<2" +mkdocs-material = "*" +tqdm = "*" + +[[package]] +name = "mkdocs-material" +version = "9.4.4" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.4.4-py3-none-any.whl", hash = "sha256:86fe79253afccc7f085f89a2d8e9e3300f82c4813d9b910d9081ce57a7e68380"}, + {file = "mkdocs_material-9.4.4.tar.gz", hash = "sha256:ab84a7cfaf009c47cd2926cdd7e6040b8cc12c3806cc533e8b16d57bd16d9c47"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.5.3,<2.0" +mkdocs-material-extensions = ">=1.2,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=9.4,<10.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.2" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs_material_extensions-1.2-py3-none-any.whl", hash = "sha256:c767bd6d6305f6420a50f0b541b0c9966d52068839af97029be14443849fb8a1"}, + {file = "mkdocs_material_extensions-1.2.tar.gz", hash = "sha256:27e2d1ed2d031426a6e10d5ea06989d67e90bb02acd588bc5673106b5ee5eedf"}, +] + +[[package]] +name = "mkdocs-render-swagger-plugin" +version = "0.1.1" +description = "MKDocs plugin for rendering swagger & openapi files." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_render_swagger_plugin-0.1.1-py3-none-any.whl", hash = "sha256:1b0f4f92bf69d7e0d56b13f520fdad072ddd1f4ccf031f11706d45d831d4df7b"}, +] + +[package.dependencies] +mkdocs = ">=1.4" + +[package.extras] +dev = ["coverage", "flake8", "mypy", "pyyaml"] + +[[package]] +name = "mkdocstrings" +version = "0.23.0" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings-0.23.0-py3-none-any.whl", hash = "sha256:051fa4014dfcd9ed90254ae91de2dbb4f24e166347dae7be9a997fe16316c65e"}, + {file = "mkdocstrings-0.23.0.tar.gz", hash = "sha256:d9c6a37ffbe7c14a7a54ef1258c70b8d394e6a33a1c80832bce40b9567138d1c"}, +] + +[package.dependencies] +Jinja2 = ">=2.11.1" +Markdown = ">=3.3" +MarkupSafe = ">=1.1" +mkdocs = ">=1.2" +mkdocs-autorefs = ">=0.3.1" +pymdown-extensions = ">=6.3" + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "1.7.2" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings_python-1.7.2-py3-none-any.whl", hash = "sha256:2d005729a90f1b86d6d71fad4953d787140996adec5b00a25fafc6ee48e1b79a"}, + {file = "mkdocstrings_python-1.7.2.tar.gz", hash = "sha256:75b6af86f9dcdc2d864072d8fed5b1d45ad94dd2ce97843ef52ca87ad53d9b26"}, +] + +[package.dependencies] +griffe = ">=0.35" +mkdocstrings = ">=0.20" + [[package]] name = "mypy" version = "1.5.1" @@ -532,6 +1846,97 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "nbclient" +version = "0.8.0" +description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "nbclient-0.8.0-py3-none-any.whl", hash = "sha256:25e861299e5303a0477568557c4045eccc7a34c17fc08e7959558707b9ebe548"}, + {file = "nbclient-0.8.0.tar.gz", hash = "sha256:f9b179cd4b2d7bca965f900a2ebf0db4a12ebff2f36a711cb66861e4ae158e55"}, +] + +[package.dependencies] +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +nbformat = ">=5.1" +traitlets = ">=5.4" + +[package.extras] +dev = ["pre-commit"] +docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] +test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] + +[[package]] +name = "nbconvert" +version = "7.9.2" +description = "Converting Jupyter Notebooks" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nbconvert-7.9.2-py3-none-any.whl", hash = "sha256:39fe4b8bdd1b0104fdd86fc8a43a9077ba64c720bda4c6132690d917a0a154ee"}, + {file = "nbconvert-7.9.2.tar.gz", hash = "sha256:e56cc7588acc4f93e2bb5a34ec69028e4941797b2bfaf6462f18a41d1cc258c9"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +bleach = "!=5.0.0" +defusedxml = "*" +jinja2 = ">=3.0" +jupyter-core = ">=4.7" +jupyterlab-pygments = "*" +markupsafe = ">=2.0" +mistune = ">=2.0.3,<4" +nbclient = ">=0.5.0" +nbformat = ">=5.7" +packaging = "*" +pandocfilters = ">=1.4.1" +pygments = ">=2.4.1" +tinycss2 = "*" +traitlets = ">=5.1" + +[package.extras] +all = ["nbconvert[docs,qtpdf,serve,test,webpdf]"] +docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] +qtpdf = ["nbconvert[qtpng]"] +qtpng = ["pyqtwebengine (>=5.15)"] +serve = ["tornado (>=6.1)"] +test = ["flaky", "ipykernel", "ipywidgets (>=7)", "pytest", "pytest-dependency"] +webpdf = ["playwright"] + +[[package]] +name = "nbformat" +version = "5.9.2" +description = "The Jupyter Notebook format" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nbformat-5.9.2-py3-none-any.whl", hash = "sha256:1c5172d786a41b82bcfd0c23f9e6b6f072e8fb49c39250219e4acfff1efe89e9"}, + {file = "nbformat-5.9.2.tar.gz", hash = "sha256:5f98b5ba1997dff175e77e0c17d5c10a96eaed2cbd1de3533d1fc35d5e111192"}, +] + +[package.dependencies] +fastjsonschema = "*" +jsonschema = ">=2.6" +jupyter-core = "*" +traitlets = ">=5.1" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["pep440", "pre-commit", "pytest", "testpath"] + +[[package]] +name = "nest-asyncio" +version = "1.5.8" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.5.8-py3-none-any.whl", hash = "sha256:accda7a339a70599cb08f9dd09a67e0c2ef8d8d6f4c07f96ab203f2ae254e48d"}, + {file = "nest_asyncio-1.5.8.tar.gz", hash = "sha256:25aa2ca0d2a5b5531956b9e273b45cf664cae2b145101d73b86b199978d48fdb"}, +] + [[package]] name = "nodeenv" version = "1.8.0" @@ -546,6 +1951,46 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "notebook" +version = "7.0.4" +description = "Jupyter Notebook - A web-based notebook environment for interactive computing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "notebook-7.0.4-py3-none-any.whl", hash = "sha256:ee738414ac01773c1ad6834cf76cc6f1ce140ac8197fd13b3e2d44d89e257f72"}, + {file = "notebook-7.0.4.tar.gz", hash = "sha256:0c1b458f72ce8774445c8ef9ed2492bd0b9ce9605ac996e2b066114f69795e71"}, +] + +[package.dependencies] +jupyter-server = ">=2.4.0,<3" +jupyterlab = ">=4.0.2,<5" +jupyterlab-server = ">=2.22.1,<3" +notebook-shim = ">=0.2,<0.3" +tornado = ">=6.2.0" + +[package.extras] +dev = ["hatch", "pre-commit"] +docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.22.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] + +[[package]] +name = "notebook-shim" +version = "0.2.3" +description = "A shim layer for notebook traits and config" +optional = false +python-versions = ">=3.7" +files = [ + {file = "notebook_shim-0.2.3-py3-none-any.whl", hash = "sha256:a83496a43341c1674b093bfcebf0fe8e74cbe7eda5fd2bbc56f8e39e1486c0c7"}, + {file = "notebook_shim-0.2.3.tar.gz", hash = "sha256:f69388ac283ae008cd506dda10d0288b09a017d822d5e8c7129a152cbd3ce7e9"}, +] + +[package.dependencies] +jupyter-server = ">=1.8,<3" + +[package.extras] +test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] + [[package]] name = "numpy" version = "1.26.0" @@ -587,6 +2032,17 @@ files = [ {file = "numpy-1.26.0.tar.gz", hash = "sha256:f93fc78fe8bf15afe2b8d6b6499f1c73953169fad1e9a8dd086cdff3190e7fdf"}, ] +[[package]] +name = "overrides" +version = "7.4.0" +description = "A decorator to automatically detect mismatch when overriding a method." +optional = false +python-versions = ">=3.6" +files = [ + {file = "overrides-7.4.0-py3-none-any.whl", hash = "sha256:3ad24583f86d6d7a49049695efe9933e67ba62f0c7625d53c59fa832ce4b8b7d"}, + {file = "overrides-7.4.0.tar.gz", hash = "sha256:9502a3cca51f4fac40b5feca985b6703a5c1f6ad815588a7ca9e285b9dca6757"}, +] + [[package]] name = "packaging" version = "23.2" @@ -598,6 +2054,16 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + [[package]] name = "pandas" version = "2.1.1" @@ -681,6 +2147,32 @@ files = [ numpy = {version = ">=1.26.0", markers = "python_version < \"3.13\""} types-pytz = ">=2022.1.1" +[[package]] +name = "pandocfilters" +version = "1.5.0" +description = "Utilities for writing pandoc filters in python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pandocfilters-1.5.0-py2.py3-none-any.whl", hash = "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f"}, + {file = "pandocfilters-1.5.0.tar.gz", hash = "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38"}, +] + +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + [[package]] name = "pathspec" version = "0.11.2" @@ -710,6 +2202,31 @@ six = "*" [package.extras] test = ["pytest", "pytest-cov", "scipy"] +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +optional = false +python-versions = "*" +files = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] + [[package]] name = "pillow" version = "10.0.1" @@ -825,6 +2342,96 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "prometheus-client" +version = "0.17.1" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.6" +files = [ + {file = "prometheus_client-0.17.1-py3-none-any.whl", hash = "sha256:e537f37160f6807b8202a6fc4764cdd19bac5480ddd3e0d463c3002b34462101"}, + {file = "prometheus_client-0.17.1.tar.gz", hash = "sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.39" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, + {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psutil" +version = "5.9.5" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "psutil-5.9.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f"}, + {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5"}, + {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4"}, + {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48"}, + {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4"}, + {file = "psutil-5.9.5-cp27-none-win32.whl", hash = "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f"}, + {file = "psutil-5.9.5-cp27-none-win_amd64.whl", hash = "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42"}, + {file = "psutil-5.9.5-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217"}, + {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da"}, + {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4"}, + {file = "psutil-5.9.5-cp36-abi3-win32.whl", hash = "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d"}, + {file = "psutil-5.9.5-cp36-abi3-win_amd64.whl", hash = "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9"}, + {file = "psutil-5.9.5-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30"}, + {file = "psutil-5.9.5.tar.gz", hash = "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pydantic" version = "2.4.2" @@ -960,7 +2567,39 @@ files = [ ] [package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pygments" +version = "2.16.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pymdown-extensions" +version = "10.3" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.3-py3-none-any.whl", hash = "sha256:77a82c621c58a83efc49a389159181d570e370fff9f810d3a4766a75fc678b66"}, + {file = "pymdown_extensions-10.3.tar.gz", hash = "sha256:94a0d8a03246712b64698af223848fd80aaf1ae4c4be29c8c61939b0467b5722"}, +] + +[package.dependencies] +markdown = ">=3.2" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] [[package]] name = "pyparsing" @@ -1012,6 +2651,17 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-json-logger" +version = "2.0.7" +description = "A python library adding a json log formatter" +optional = false +python-versions = ">=3.6" +files = [ + {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, + {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, +] + [[package]] name = "pytz" version = "2023.3.post1" @@ -1023,6 +2673,44 @@ files = [ {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, ] +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pywinpty" +version = "2.0.12" +description = "Pseudo terminal support for Windows from Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pywinpty-2.0.12-cp310-none-win_amd64.whl", hash = "sha256:21319cd1d7c8844fb2c970fb3a55a3db5543f112ff9cfcd623746b9c47501575"}, + {file = "pywinpty-2.0.12-cp311-none-win_amd64.whl", hash = "sha256:853985a8f48f4731a716653170cd735da36ffbdc79dcb4c7b7140bce11d8c722"}, + {file = "pywinpty-2.0.12-cp312-none-win_amd64.whl", hash = "sha256:1617b729999eb6713590e17665052b1a6ae0ad76ee31e60b444147c5b6a35dca"}, + {file = "pywinpty-2.0.12-cp38-none-win_amd64.whl", hash = "sha256:189380469ca143d06e19e19ff3fba0fcefe8b4a8cc942140a6b863aed7eebb2d"}, + {file = "pywinpty-2.0.12-cp39-none-win_amd64.whl", hash = "sha256:7520575b6546db23e693cbd865db2764097bd6d4ef5dc18c92555904cd62c3d4"}, + {file = "pywinpty-2.0.12.tar.gz", hash = "sha256:8197de460ae8ebb7f5d1701dfa1b5df45b157bb832e92acba316305e18ca00dd"}, +] + [[package]] name = "pyyaml" version = "6.0.1" @@ -1072,6 +2760,445 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "pyzmq" +version = "25.1.1" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyzmq-25.1.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:381469297409c5adf9a0e884c5eb5186ed33137badcbbb0560b86e910a2f1e76"}, + {file = "pyzmq-25.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:955215ed0604dac5b01907424dfa28b40f2b2292d6493445dd34d0dfa72586a8"}, + {file = "pyzmq-25.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:985bbb1316192b98f32e25e7b9958088431d853ac63aca1d2c236f40afb17c83"}, + {file = "pyzmq-25.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:afea96f64efa98df4da6958bae37f1cbea7932c35878b185e5982821bc883369"}, + {file = "pyzmq-25.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76705c9325d72a81155bb6ab48d4312e0032bf045fb0754889133200f7a0d849"}, + {file = "pyzmq-25.1.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:77a41c26205d2353a4c94d02be51d6cbdf63c06fbc1295ea57dad7e2d3381b71"}, + {file = "pyzmq-25.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:12720a53e61c3b99d87262294e2b375c915fea93c31fc2336898c26d7aed34cd"}, + {file = "pyzmq-25.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:57459b68e5cd85b0be8184382cefd91959cafe79ae019e6b1ae6e2ba8a12cda7"}, + {file = "pyzmq-25.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:292fe3fc5ad4a75bc8df0dfaee7d0babe8b1f4ceb596437213821f761b4589f9"}, + {file = "pyzmq-25.1.1-cp310-cp310-win32.whl", hash = "sha256:35b5ab8c28978fbbb86ea54958cd89f5176ce747c1fb3d87356cf698048a7790"}, + {file = "pyzmq-25.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:11baebdd5fc5b475d484195e49bae2dc64b94a5208f7c89954e9e354fc609d8f"}, + {file = "pyzmq-25.1.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:d20a0ddb3e989e8807d83225a27e5c2eb2260eaa851532086e9e0fa0d5287d83"}, + {file = "pyzmq-25.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e1c1be77bc5fb77d923850f82e55a928f8638f64a61f00ff18a67c7404faf008"}, + {file = "pyzmq-25.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d89528b4943d27029a2818f847c10c2cecc79fa9590f3cb1860459a5be7933eb"}, + {file = "pyzmq-25.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90f26dc6d5f241ba358bef79be9ce06de58d477ca8485e3291675436d3827cf8"}, + {file = "pyzmq-25.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2b92812bd214018e50b6380ea3ac0c8bb01ac07fcc14c5f86a5bb25e74026e9"}, + {file = "pyzmq-25.1.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2f957ce63d13c28730f7fd6b72333814221c84ca2421298f66e5143f81c9f91f"}, + {file = "pyzmq-25.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:047a640f5c9c6ade7b1cc6680a0e28c9dd5a0825135acbd3569cc96ea00b2505"}, + {file = "pyzmq-25.1.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7f7e58effd14b641c5e4dec8c7dab02fb67a13df90329e61c869b9cc607ef752"}, + {file = "pyzmq-25.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c2910967e6ab16bf6fbeb1f771c89a7050947221ae12a5b0b60f3bca2ee19bca"}, + {file = "pyzmq-25.1.1-cp311-cp311-win32.whl", hash = "sha256:76c1c8efb3ca3a1818b837aea423ff8a07bbf7aafe9f2f6582b61a0458b1a329"}, + {file = "pyzmq-25.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:44e58a0554b21fc662f2712814a746635ed668d0fbc98b7cb9d74cb798d202e6"}, + {file = "pyzmq-25.1.1-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:e1ffa1c924e8c72778b9ccd386a7067cddf626884fd8277f503c48bb5f51c762"}, + {file = "pyzmq-25.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1af379b33ef33757224da93e9da62e6471cf4a66d10078cf32bae8127d3d0d4a"}, + {file = "pyzmq-25.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cff084c6933680d1f8b2f3b4ff5bbb88538a4aac00d199ac13f49d0698727ecb"}, + {file = "pyzmq-25.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2400a94f7dd9cb20cd012951a0cbf8249e3d554c63a9c0cdfd5cbb6c01d2dec"}, + {file = "pyzmq-25.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d81f1ddae3858b8299d1da72dd7d19dd36aab654c19671aa8a7e7fb02f6638a"}, + {file = "pyzmq-25.1.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:255ca2b219f9e5a3a9ef3081512e1358bd4760ce77828e1028b818ff5610b87b"}, + {file = "pyzmq-25.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a882ac0a351288dd18ecae3326b8a49d10c61a68b01419f3a0b9a306190baf69"}, + {file = "pyzmq-25.1.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:724c292bb26365659fc434e9567b3f1adbdb5e8d640c936ed901f49e03e5d32e"}, + {file = "pyzmq-25.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ca1ed0bb2d850aa8471387882247c68f1e62a4af0ce9c8a1dbe0d2bf69e41fb"}, + {file = "pyzmq-25.1.1-cp312-cp312-win32.whl", hash = "sha256:b3451108ab861040754fa5208bca4a5496c65875710f76789a9ad27c801a0075"}, + {file = "pyzmq-25.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:eadbefd5e92ef8a345f0525b5cfd01cf4e4cc651a2cffb8f23c0dd184975d787"}, + {file = "pyzmq-25.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:db0b2af416ba735c6304c47f75d348f498b92952f5e3e8bff449336d2728795d"}, + {file = "pyzmq-25.1.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c133e93b405eb0d36fa430c94185bdd13c36204a8635470cccc200723c13bb"}, + {file = "pyzmq-25.1.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:273bc3959bcbff3f48606b28229b4721716598d76b5aaea2b4a9d0ab454ec062"}, + {file = "pyzmq-25.1.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cbc8df5c6a88ba5ae385d8930da02201165408dde8d8322072e3e5ddd4f68e22"}, + {file = "pyzmq-25.1.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:18d43df3f2302d836f2a56f17e5663e398416e9dd74b205b179065e61f1a6edf"}, + {file = "pyzmq-25.1.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:73461eed88a88c866656e08f89299720a38cb4e9d34ae6bf5df6f71102570f2e"}, + {file = "pyzmq-25.1.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c850ce7976d19ebe7b9d4b9bb8c9dfc7aac336c0958e2651b88cbd46682123"}, + {file = "pyzmq-25.1.1-cp36-cp36m-win32.whl", hash = "sha256:d2045d6d9439a0078f2a34b57c7b18c4a6aef0bee37f22e4ec9f32456c852c71"}, + {file = "pyzmq-25.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:458dea649f2f02a0b244ae6aef8dc29325a2810aa26b07af8374dc2a9faf57e3"}, + {file = "pyzmq-25.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7cff25c5b315e63b07a36f0c2bab32c58eafbe57d0dce61b614ef4c76058c115"}, + {file = "pyzmq-25.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1579413ae492b05de5a6174574f8c44c2b9b122a42015c5292afa4be2507f28"}, + {file = "pyzmq-25.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3d0a409d3b28607cc427aa5c30a6f1e4452cc44e311f843e05edb28ab5e36da0"}, + {file = "pyzmq-25.1.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21eb4e609a154a57c520e3d5bfa0d97e49b6872ea057b7c85257b11e78068222"}, + {file = "pyzmq-25.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:034239843541ef7a1aee0c7b2cb7f6aafffb005ede965ae9cbd49d5ff4ff73cf"}, + {file = "pyzmq-25.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f8115e303280ba09f3898194791a153862cbf9eef722ad8f7f741987ee2a97c7"}, + {file = "pyzmq-25.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1a5d26fe8f32f137e784f768143728438877d69a586ddeaad898558dc971a5ae"}, + {file = "pyzmq-25.1.1-cp37-cp37m-win32.whl", hash = "sha256:f32260e556a983bc5c7ed588d04c942c9a8f9c2e99213fec11a031e316874c7e"}, + {file = "pyzmq-25.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:abf34e43c531bbb510ae7e8f5b2b1f2a8ab93219510e2b287a944432fad135f3"}, + {file = "pyzmq-25.1.1-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:87e34f31ca8f168c56d6fbf99692cc8d3b445abb5bfd08c229ae992d7547a92a"}, + {file = "pyzmq-25.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c9c6c9b2c2f80747a98f34ef491c4d7b1a8d4853937bb1492774992a120f475d"}, + {file = "pyzmq-25.1.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5619f3f5a4db5dbb572b095ea3cb5cc035335159d9da950830c9c4db2fbb6995"}, + {file = "pyzmq-25.1.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5a34d2395073ef862b4032343cf0c32a712f3ab49d7ec4f42c9661e0294d106f"}, + {file = "pyzmq-25.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f0e6b78220aba09815cd1f3a32b9c7cb3e02cb846d1cfc526b6595f6046618"}, + {file = "pyzmq-25.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3669cf8ee3520c2f13b2e0351c41fea919852b220988d2049249db10046a7afb"}, + {file = "pyzmq-25.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2d163a18819277e49911f7461567bda923461c50b19d169a062536fffe7cd9d2"}, + {file = "pyzmq-25.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:df27ffddff4190667d40de7beba4a950b5ce78fe28a7dcc41d6f8a700a80a3c0"}, + {file = "pyzmq-25.1.1-cp38-cp38-win32.whl", hash = "sha256:a382372898a07479bd34bda781008e4a954ed8750f17891e794521c3e21c2e1c"}, + {file = "pyzmq-25.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:52533489f28d62eb1258a965f2aba28a82aa747202c8fa5a1c7a43b5db0e85c1"}, + {file = "pyzmq-25.1.1-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:03b3f49b57264909aacd0741892f2aecf2f51fb053e7d8ac6767f6c700832f45"}, + {file = "pyzmq-25.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:330f9e188d0d89080cde66dc7470f57d1926ff2fb5576227f14d5be7ab30b9fa"}, + {file = "pyzmq-25.1.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2ca57a5be0389f2a65e6d3bb2962a971688cbdd30b4c0bd188c99e39c234f414"}, + {file = "pyzmq-25.1.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d457aed310f2670f59cc5b57dcfced452aeeed77f9da2b9763616bd57e4dbaae"}, + {file = "pyzmq-25.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c56d748ea50215abef7030c72b60dd723ed5b5c7e65e7bc2504e77843631c1a6"}, + {file = "pyzmq-25.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8f03d3f0d01cb5a018debeb412441996a517b11c5c17ab2001aa0597c6d6882c"}, + {file = "pyzmq-25.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:820c4a08195a681252f46926de10e29b6bbf3e17b30037bd4250d72dd3ddaab8"}, + {file = "pyzmq-25.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17ef5f01d25b67ca8f98120d5fa1d21efe9611604e8eb03a5147360f517dd1e2"}, + {file = "pyzmq-25.1.1-cp39-cp39-win32.whl", hash = "sha256:04ccbed567171579ec2cebb9c8a3e30801723c575601f9a990ab25bcac6b51e2"}, + {file = "pyzmq-25.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:e61f091c3ba0c3578411ef505992d356a812fb200643eab27f4f70eed34a29ef"}, + {file = "pyzmq-25.1.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ade6d25bb29c4555d718ac6d1443a7386595528c33d6b133b258f65f963bb0f6"}, + {file = "pyzmq-25.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0c95ddd4f6e9fca4e9e3afaa4f9df8552f0ba5d1004e89ef0a68e1f1f9807c7"}, + {file = "pyzmq-25.1.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48e466162a24daf86f6b5ca72444d2bf39a5e58da5f96370078be67c67adc978"}, + {file = "pyzmq-25.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abc719161780932c4e11aaebb203be3d6acc6b38d2f26c0f523b5b59d2fc1996"}, + {file = "pyzmq-25.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ccf825981640b8c34ae54231b7ed00271822ea1c6d8ba1090ebd4943759abf5"}, + {file = "pyzmq-25.1.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c2f20ce161ebdb0091a10c9ca0372e023ce24980d0e1f810f519da6f79c60800"}, + {file = "pyzmq-25.1.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:deee9ca4727f53464daf089536e68b13e6104e84a37820a88b0a057b97bba2d2"}, + {file = "pyzmq-25.1.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aa8d6cdc8b8aa19ceb319aaa2b660cdaccc533ec477eeb1309e2a291eaacc43a"}, + {file = "pyzmq-25.1.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:019e59ef5c5256a2c7378f2fb8560fc2a9ff1d315755204295b2eab96b254d0a"}, + {file = "pyzmq-25.1.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:b9af3757495c1ee3b5c4e945c1df7be95562277c6e5bccc20a39aec50f826cd0"}, + {file = "pyzmq-25.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:548d6482dc8aadbe7e79d1b5806585c8120bafa1ef841167bc9090522b610fa6"}, + {file = "pyzmq-25.1.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:057e824b2aae50accc0f9a0570998adc021b372478a921506fddd6c02e60308e"}, + {file = "pyzmq-25.1.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2243700cc5548cff20963f0ca92d3e5e436394375ab8a354bbea2b12911b20b0"}, + {file = "pyzmq-25.1.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79986f3b4af059777111409ee517da24a529bdbd46da578b33f25580adcff728"}, + {file = "pyzmq-25.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:11d58723d44d6ed4dd677c5615b2ffb19d5c426636345567d6af82be4dff8a55"}, + {file = "pyzmq-25.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:49d238cf4b69652257db66d0c623cd3e09b5d2e9576b56bc067a396133a00d4a"}, + {file = "pyzmq-25.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fedbdc753827cf014c01dbbee9c3be17e5a208dcd1bf8641ce2cd29580d1f0d4"}, + {file = "pyzmq-25.1.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc16ac425cc927d0a57d242589f87ee093884ea4804c05a13834d07c20db203c"}, + {file = "pyzmq-25.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11c1d2aed9079c6b0c9550a7257a836b4a637feb334904610f06d70eb44c56d2"}, + {file = "pyzmq-25.1.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e8a701123029cc240cea61dd2d16ad57cab4691804143ce80ecd9286b464d180"}, + {file = "pyzmq-25.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:61706a6b6c24bdece85ff177fec393545a3191eeda35b07aaa1458a027ad1304"}, + {file = "pyzmq-25.1.1.tar.gz", hash = "sha256:259c22485b71abacdfa8bf79720cd7bcf4b9d128b30ea554f01ae71fdbfdaa23"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "qtconsole" +version = "5.4.4" +description = "Jupyter Qt console" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "qtconsole-5.4.4-py3-none-any.whl", hash = "sha256:a3b69b868e041c2c698bdc75b0602f42e130ffb256d6efa48f9aa756c97672aa"}, + {file = "qtconsole-5.4.4.tar.gz", hash = "sha256:b7ffb53d74f23cee29f4cdb55dd6fabc8ec312d94f3c46ba38e1dde458693dfb"}, +] + +[package.dependencies] +ipykernel = ">=4.1" +ipython-genutils = "*" +jupyter-client = ">=4.1" +jupyter-core = "*" +packaging = "*" +pygments = "*" +pyzmq = ">=17.1" +qtpy = ">=2.4.0" +traitlets = "<5.2.1 || >5.2.1,<5.2.2 || >5.2.2" + +[package.extras] +doc = ["Sphinx (>=1.3)"] +test = ["flaky", "pytest", "pytest-qt"] + +[[package]] +name = "qtpy" +version = "2.4.0" +description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)." +optional = false +python-versions = ">=3.7" +files = [ + {file = "QtPy-2.4.0-py3-none-any.whl", hash = "sha256:4d4f045a41e09ac9fa57fcb47ef05781aa5af294a0a646acc1b729d14225e741"}, + {file = "QtPy-2.4.0.tar.gz", hash = "sha256:db2d508167aa6106781565c8da5c6f1487debacba33519cedc35fa8997d424d4"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +test = ["pytest (>=6,!=7.0.0,!=7.0.1)", "pytest-cov (>=3.0.0)", "pytest-qt"] + +[[package]] +name = "rcslice" +version = "1.1.0" +description = "Slice a list of sliceables (1 indexed, start and end index both are inclusive)" +optional = false +python-versions = "*" +files = [ + {file = "rcslice-1.1.0-py3-none-any.whl", hash = "sha256:1b12fc0c0ca452e8a9fd2b56ac008162f19e250906a4290a7e7a98be3200c2a6"}, + {file = "rcslice-1.1.0.tar.gz", hash = "sha256:a2ce70a60690eb63e52b722e046b334c3aaec5e900b28578f529878782ee5c6e"}, +] + +[[package]] +name = "referencing" +version = "0.30.2" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.30.2-py3-none-any.whl", hash = "sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf"}, + {file = "referencing-0.30.2.tar.gz", hash = "sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "regex" +version = "2023.10.3" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.7" +files = [ + {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"}, + {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"}, + {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"}, + {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"}, + {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"}, + {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"}, + {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"}, + {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"}, + {file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"}, + {file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"}, + {file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"}, + {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"}, + {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"}, + {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"}, + {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"}, + {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[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 = "rfc3339-validator" +version = "0.1.4" +description = "A pure python RFC3339 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, + {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +description = "Pure python rfc3986 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, + {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, +] + +[[package]] +name = "rpds-py" +version = "0.10.4" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.10.4-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:e41824343c2c129599645373992b1ce17720bb8a514f04ff9567031e1c26951e"}, + {file = "rpds_py-0.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b9d8884d58ea8801e5906a491ab34af975091af76d1a389173db491ee7e316bb"}, + {file = "rpds_py-0.10.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5db93f9017b384a4f194e1d89e1ce82d0a41b1fafdbbd3e0c8912baf13f2950f"}, + {file = "rpds_py-0.10.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c31ecfc53ac03dad4928a1712f3a2893008bfba1b3cde49e1c14ff67faae2290"}, + {file = "rpds_py-0.10.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f92d2372ec992c82fd7c74aa21e2a1910b3dcdc6a7e6392919a138f21d528a3"}, + {file = "rpds_py-0.10.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7ea49ddf51d5ec0c3cbd95190dd15e077a3153c8d4b22a33da43b5dd2b3c640"}, + {file = "rpds_py-0.10.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c27942722cd5039bbf5098c7e21935a96243fed00ea11a9589f3c6c6424bd84"}, + {file = "rpds_py-0.10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:08f07150c8ebbdbce1d2d51b8e9f4d588749a2af6a98035485ebe45c7ad9394e"}, + {file = "rpds_py-0.10.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f3331a3684192659fa1090bf2b448db928152fcba08222e58106f44758ef25f7"}, + {file = "rpds_py-0.10.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:efffa359cc69840c8793f0c05a7b663de6afa7b9078fa6c80309ee38b9db677d"}, + {file = "rpds_py-0.10.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:86e8d6ff15fa7a9590c0addaf3ce52fb58bda4299cab2c2d0afa404db6848dab"}, + {file = "rpds_py-0.10.4-cp310-none-win32.whl", hash = "sha256:8f90fc6dd505867514c8b8ef68a712dc0be90031a773c1ae2ad469f04062daef"}, + {file = "rpds_py-0.10.4-cp310-none-win_amd64.whl", hash = "sha256:9f9184744fb800c9f28e155a5896ecb54816296ee79d5d1978be6a2ae60f53c4"}, + {file = "rpds_py-0.10.4-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:72e9b1e92830c876cd49565d8404e4dcc9928302d348ea2517bc3f9e3a873a2a"}, + {file = "rpds_py-0.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3650eae998dc718960e90120eb45d42bd57b18b21b10cb9ee05f91bff2345d48"}, + {file = "rpds_py-0.10.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f40413d2859737ce6d95c29ce2dde0ef7cdc3063b5830ae4342fef5922c3bba7"}, + {file = "rpds_py-0.10.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b953d11b544ca5f2705bb77b177d8e17ab1bfd69e0fd99790a11549d2302258c"}, + {file = "rpds_py-0.10.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28b4942ec7d9d6114c1e08cace0157db92ef674636a38093cab779ace5742d3a"}, + {file = "rpds_py-0.10.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e0e2e01c5f61ddf47e3ed2d1fe1c9136e780ca6222d57a2517b9b02afd4710c"}, + {file = "rpds_py-0.10.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:927e3461dae0c09b1f2e0066e50c1a9204f8a64a3060f596e9a6742d3b307785"}, + {file = "rpds_py-0.10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e69bbe0ede8f7fe2616e779421bbdb37f025c802335a90f6416e4d98b368a37"}, + {file = "rpds_py-0.10.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc688a59c100f038fa9fec9e4ab457c2e2d1fca350fe7ea395016666f0d0a2dc"}, + {file = "rpds_py-0.10.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ec001689402b9104700b50a005c2d3d0218eae90eaa8bdbbd776fe78fe8a74b7"}, + {file = "rpds_py-0.10.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:628fbb8be71a103499d10b189af7764996ab2634ed7b44b423f1e19901606e0e"}, + {file = "rpds_py-0.10.4-cp311-none-win32.whl", hash = "sha256:e3f9c9e5dd8eba4768e15f19044e1b5e216929a43a54b4ab329e103aed9f3eda"}, + {file = "rpds_py-0.10.4-cp311-none-win_amd64.whl", hash = "sha256:3bc561c183684636c0099f9c3fbab8c1671841942edbce784bb01b4707d17924"}, + {file = "rpds_py-0.10.4-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:36ff30385fb9fb3ac23a28bffdd4a230a5229ed5b15704b708b7c84bfb7fce51"}, + {file = "rpds_py-0.10.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db0589e0bf41ff6ce284ab045ca89f27be1adf19e7bce26c2e7de6739a70c18b"}, + {file = "rpds_py-0.10.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c330cb125983c5d380fef4a4155248a276297c86d64625fdaf500157e1981c"}, + {file = "rpds_py-0.10.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d230fddc60caced271cc038e43e6fb8f4dd6b2dbaa44ac9763f2d76d05b0365a"}, + {file = "rpds_py-0.10.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a9e864ec051a58fdb6bb2e6da03942adb20273897bc70067aee283e62bbac4d"}, + {file = "rpds_py-0.10.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e41d5b334e8de4bc3f38843f31b2afa9a0c472ebf73119d3fd55cde08974bdf"}, + {file = "rpds_py-0.10.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bb3f3cb6072c73e6ec1f865d8b80419b599f1597acf33f63fbf02252aab5a03"}, + {file = "rpds_py-0.10.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:576d48e1e45c211e99fc02655ade65c32a75d3e383ccfd98ce59cece133ed02c"}, + {file = "rpds_py-0.10.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b28b9668a22ca2cfca4433441ba9acb2899624a323787a509a3dc5fbfa79c49d"}, + {file = "rpds_py-0.10.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ddbd113a37307638f94be5ae232a325155fd24dbfae2c56455da8724b471e7be"}, + {file = "rpds_py-0.10.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd0ad98c7d72b0e4cbfe89cdfa12cd07d2fd6ed22864341cdce12b318a383442"}, + {file = "rpds_py-0.10.4-cp312-none-win32.whl", hash = "sha256:2a97406d5e08b7095428f01dac0d3c091dc072351151945a167e7968d2755559"}, + {file = "rpds_py-0.10.4-cp312-none-win_amd64.whl", hash = "sha256:aab24b9bbaa3d49e666e9309556591aa00748bd24ea74257a405f7fed9e8b10d"}, + {file = "rpds_py-0.10.4-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6c5ca3eb817fb54bfd066740b64a2b31536eb8fe0b183dc35b09a7bd628ed680"}, + {file = "rpds_py-0.10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd37ab9a24021821b715478357af1cf369d5a42ac7405e83e5822be00732f463"}, + {file = "rpds_py-0.10.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2573ec23ad3a59dd2bc622befac845695972f3f2d08dc1a4405d017d20a6c225"}, + {file = "rpds_py-0.10.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:362faeae52dc6ccc50c0b6a01fa2ec0830bb61c292033f3749a46040b876f4ba"}, + {file = "rpds_py-0.10.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40f6e53461b19ddbb3354fe5bcf3d50d4333604ae4bf25b478333d83ca68002c"}, + {file = "rpds_py-0.10.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6090ba604ea06b525a231450ae5d343917a393cbf50423900dea968daf61d16f"}, + {file = "rpds_py-0.10.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28e29dac59df890972f73c511948072897f512974714a803fe793635b80ff8c7"}, + {file = "rpds_py-0.10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f82abb5c5b83dc30e96be99ce76239a030b62a73a13c64410e429660a5602bfd"}, + {file = "rpds_py-0.10.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a3628815fd170a64624001bfb4e28946fd515bd672e68a1902d9e0290186eaf3"}, + {file = "rpds_py-0.10.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d37f27ad80f742ef82796af3fe091888864958ad0bc8bab03da1830fa00c6004"}, + {file = "rpds_py-0.10.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:255a23bded80605e9f3997753e3a4b89c9aec9efb07ec036b1ca81440efcc1a9"}, + {file = "rpds_py-0.10.4-cp38-none-win32.whl", hash = "sha256:049098dabfe705e9638c55a3321137a821399c50940041a6fcce267a22c70db2"}, + {file = "rpds_py-0.10.4-cp38-none-win_amd64.whl", hash = "sha256:aa45cc71bf23a3181b8aa62466b5a2b7b7fb90fdc01df67ca433cd4fce7ec94d"}, + {file = "rpds_py-0.10.4-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:3507c459767cf24c11e9520e2a37c89674266abe8e65453e5cb66398aa47ee7b"}, + {file = "rpds_py-0.10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2603e084054351cc65097da326570102c4c5bd07426ba8471ceaefdb0b642cc9"}, + {file = "rpds_py-0.10.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0f1d336786cb62613c72c00578c98e5bb8cd57b49c5bae5d4ab906ca7872f98"}, + {file = "rpds_py-0.10.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf032367f921201deaecf221d4cc895ea84b3decf50a9c73ee106f961885a0ad"}, + {file = "rpds_py-0.10.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f050ceffd8c730c1619a16bbf0b9cd037dcdb94b54710928ba38c7bde67e4a4"}, + {file = "rpds_py-0.10.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8709eb4ab477c533b7d0a76cd3065d7d95c9e25e6b9f6e27caeeb8c63e8799c9"}, + {file = "rpds_py-0.10.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc20dadb102140dff63529e08ce6f9745dbd36e673ebb2b1c4a63e134bca81c2"}, + {file = "rpds_py-0.10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd7da2adc721ccf19ac7ec86cae3a4fcaba03d9c477d5bd64ded6e9bb817bf3f"}, + {file = "rpds_py-0.10.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e5dba1c11e089b526379e74f6c636202e4c5bad9a48c7416502b8a5b0d026c91"}, + {file = "rpds_py-0.10.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ffd539d213c1ea2989ab92a5b9371ae7159c8c03cf2bcb9f2f594752f755ecd3"}, + {file = "rpds_py-0.10.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e791e3d13b14d0a7921804d0efe4d7bd15508bbcf8cb7a0c1ee1a27319a5f033"}, + {file = "rpds_py-0.10.4-cp39-none-win32.whl", hash = "sha256:2f2ac8bb01f705c5caaa7fe77ffd9b03f92f1b5061b94228f6ea5eaa0fca68ad"}, + {file = "rpds_py-0.10.4-cp39-none-win_amd64.whl", hash = "sha256:7c7ca791bedda059e5195cf7c6b77384657a51429357cdd23e64ac1d4973d6dc"}, + {file = "rpds_py-0.10.4-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:9c7e7bd1fa1f535af71dfcd3700fc83a6dc261a1204f8f5327d8ffe82e52905d"}, + {file = "rpds_py-0.10.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7089d8bfa8064b28b2e39f5af7bf12d42f61caed884e35b9b4ea9e6fb1175077"}, + {file = "rpds_py-0.10.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1f191befea279cb9669b57be97ab1785781c8bab805900e95742ebfaa9cbf1d"}, + {file = "rpds_py-0.10.4-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98c0aecf661c175ce9cb17347fc51a5c98c3e9189ca57e8fcd9348dae18541db"}, + {file = "rpds_py-0.10.4-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d81359911c3bb31c899c6a5c23b403bdc0279215e5b3bc0d2a692489fed38632"}, + {file = "rpds_py-0.10.4-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83da147124499fe41ed86edf34b4e81e951b3fe28edcc46288aac24e8a5c8484"}, + {file = "rpds_py-0.10.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49db6c0a0e6626c2b97f5e7f8f7074da21cbd8ec73340c25e839a2457c007efa"}, + {file = "rpds_py-0.10.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:125776d5db15162fdd9135372bef7fe4fb7c5f5810cf25898eb74a06a0816aec"}, + {file = "rpds_py-0.10.4-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:32819b662e3b4c26355a4403ea2f60c0a00db45b640fe722dd12db3d2ef807fb"}, + {file = "rpds_py-0.10.4-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:3bd38b80491ef9686f719c1ad3d24d14fbd0e069988fdd4e7d1a6ffcdd7f4a13"}, + {file = "rpds_py-0.10.4-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2e79eeeff8394284b09577f36316d410525e0cf0133abb3de10660e704d3d38e"}, + {file = "rpds_py-0.10.4-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3e37f1f134037601eb4b1f46854194f0cc082435dac2ee3de11e51529f7831f2"}, + {file = "rpds_py-0.10.4-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:ba3246c60303eab3d0e562addf25a983d60bddc36f4d1edc2510f056d19df255"}, + {file = "rpds_py-0.10.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9123ba0f3f98ff79780eebca9984a2b525f88563844b740f94cffb9099701230"}, + {file = "rpds_py-0.10.4-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d98802b78093c7083cc51f83da41a5be5a57d406798c9f69424bd75f8ae0812a"}, + {file = "rpds_py-0.10.4-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:58bae860d1d116e6b4e1aad0cdc48a187d5893994f56d26db0c5534df7a47afd"}, + {file = "rpds_py-0.10.4-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd7e62e7d5bcfa38a62d8397fba6d0428b970ab7954c2197501cd1624f7f0bbb"}, + {file = "rpds_py-0.10.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83f5228459b84fa6279e4126a53abfdd73cd9cc183947ee5084153880f65d7"}, + {file = "rpds_py-0.10.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bcb1abecd998a72ad4e36a0fca93577fd0c059a6aacc44f16247031b98f6ff4"}, + {file = "rpds_py-0.10.4-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9e7b3ad9f53ea9e085b3d27286dd13f8290969c0a153f8a52c8b5c46002c374b"}, + {file = "rpds_py-0.10.4-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:cbec8e43cace64e63398155dc585dc479a89fef1e57ead06c22d3441e1bd09c3"}, + {file = "rpds_py-0.10.4-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ad21c60fc880204798f320387164dcacc25818a7b4ec2a0bf6b6c1d57b007d23"}, + {file = "rpds_py-0.10.4-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:6baea8a4f6f01e69e75cfdef3edd4a4d1c4b56238febbdf123ce96d09fbff010"}, + {file = "rpds_py-0.10.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:94876c21512535955a960f42a155213315e6ab06a4ce8ce372341a2a1b143eeb"}, + {file = "rpds_py-0.10.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cb55454a20d1b935f9eaab52e6ceab624a2efd8b52927c7ae7a43e02828dbe0"}, + {file = "rpds_py-0.10.4-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13cbd79ccedc6b39c279af31ebfb0aec0467ad5d14641ddb15738bf6e4146157"}, + {file = "rpds_py-0.10.4-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00a88003db3cc953f8656b59fc9af9d0637a1fb93c235814007988f8c153b2f2"}, + {file = "rpds_py-0.10.4-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0f7f77a77c37159c9f417b8dd847f67a29e98c6acb52ee98fc6b91efbd1b2b6"}, + {file = "rpds_py-0.10.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70563a1596d2e0660ca2cebb738443437fc0e38597e7cbb276de0a7363924a52"}, + {file = "rpds_py-0.10.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3ece9aa6d07e18c966f14b4352a4c6f40249f6174d3d2c694c1062e19c6adbb"}, + {file = "rpds_py-0.10.4-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d5ad7b1a1f6964d19b1a8acfc14bf7864f39587b3e25c16ca04f6cd1815026b3"}, + {file = "rpds_py-0.10.4-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:60018626e637528a1fa64bb3a2b3e46ab7bf672052316d61c3629814d5e65052"}, + {file = "rpds_py-0.10.4-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ae8a32ab77a84cc870bbfb60645851ca0f7d58fd251085ad67464b1445d632ca"}, + {file = "rpds_py-0.10.4.tar.gz", hash = "sha256:18d5ff7fbd305a1d564273e9eb22de83ae3cd9cd6329fddc8f12f6428a711a6a"}, +] + [[package]] name = "ruff" version = "0.0.292" @@ -1140,6 +3267,22 @@ dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyl doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +[[package]] +name = "send2trash" +version = "1.8.2" +description = "Send file to trash natively under Mac OS X, Windows and Linux" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "Send2Trash-1.8.2-py3-none-any.whl", hash = "sha256:a384719d99c07ce1eefd6905d2decb6f8b7ed054025bb0e618919f945de4f679"}, + {file = "Send2Trash-1.8.2.tar.gz", hash = "sha256:c132d59fa44b9ca2b1699af5c86f57ce9f4c5eb56629d5d55fbb7a35f84e2312"}, +] + +[package.extras] +nativelib = ["pyobjc-framework-Cocoa", "pywin32"] +objc = ["pyobjc-framework-Cocoa"] +win32 = ["pywin32"] + [[package]] name = "setuptools" version = "68.2.2" @@ -1189,6 +3332,47 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + [[package]] name = "statsmodels" version = "0.14.0" @@ -1234,6 +3418,44 @@ build = ["cython (>=0.29.26)"] develop = ["colorama", "cython (>=0.29.26)", "cython (>=0.29.28,<3.0.0)", "flake8", "isort", "joblib", "matplotlib (>=3)", "oldest-supported-numpy (>=2022.4.18)", "pytest (>=7.0.1,<7.1.0)", "pytest-randomly", "pytest-xdist", "pywinpty", "setuptools-scm[toml] (>=7.0.0,<7.1.0)"] docs = ["ipykernel", "jupyter-client", "matplotlib", "nbconvert", "nbformat", "numpydoc", "pandas-datareader", "sphinx"] +[[package]] +name = "terminado" +version = "0.17.1" +description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." +optional = false +python-versions = ">=3.7" +files = [ + {file = "terminado-0.17.1-py3-none-any.whl", hash = "sha256:8650d44334eba354dd591129ca3124a6ba42c3d5b70df5051b6921d506fdaeae"}, + {file = "terminado-0.17.1.tar.gz", hash = "sha256:6ccbbcd3a4f8a25a5ec04991f39a0b8db52dfcd487ea0e578d977e6752380333"}, +] + +[package.dependencies] +ptyprocess = {version = "*", markers = "os_name != \"nt\""} +pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} +tornado = ">=6.1.0" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] + +[[package]] +name = "tinycss2" +version = "1.2.1" +description = "A tiny CSS parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, + {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, +] + +[package.dependencies] +webencodings = ">=0.4" + +[package.extras] +doc = ["sphinx", "sphinx_rtd_theme"] +test = ["flake8", "isort", "pytest"] + [[package]] name = "tomli" version = "2.0.1" @@ -1245,6 +3467,72 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "tornado" +version = "6.3.3" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">= 3.8" +files = [ + {file = "tornado-6.3.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:502fba735c84450974fec147340016ad928d29f1e91f49be168c0a4c18181e1d"}, + {file = "tornado-6.3.3-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:805d507b1f588320c26f7f097108eb4023bbaa984d63176d1652e184ba24270a"}, + {file = "tornado-6.3.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd19ca6c16882e4d37368e0152f99c099bad93e0950ce55e71daed74045908f"}, + {file = "tornado-6.3.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ac51f42808cca9b3613f51ffe2a965c8525cb1b00b7b2d56828b8045354f76a"}, + {file = "tornado-6.3.3-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71a8db65160a3c55d61839b7302a9a400074c9c753040455494e2af74e2501f2"}, + {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ceb917a50cd35882b57600709dd5421a418c29ddc852da8bcdab1f0db33406b0"}, + {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:7d01abc57ea0dbb51ddfed477dfe22719d376119844e33c661d873bf9c0e4a16"}, + {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9dc4444c0defcd3929d5c1eb5706cbe1b116e762ff3e0deca8b715d14bf6ec17"}, + {file = "tornado-6.3.3-cp38-abi3-win32.whl", hash = "sha256:65ceca9500383fbdf33a98c0087cb975b2ef3bfb874cb35b8de8740cf7f41bd3"}, + {file = "tornado-6.3.3-cp38-abi3-win_amd64.whl", hash = "sha256:22d3c2fa10b5793da13c807e6fc38ff49a4f6e1e3868b0a6f4164768bb8e20f5"}, + {file = "tornado-6.3.3.tar.gz", hash = "sha256:e7d8db41c0181c80d76c982aacc442c0783a2c54d6400fe028954201a2e032fe"}, +] + +[[package]] +name = "tqdm" +version = "4.66.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, + {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "traitlets" +version = "5.11.2" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.11.2-py3-none-any.whl", hash = "sha256:98277f247f18b2c5cabaf4af369187754f4fb0e85911d473f72329db8a7f4fae"}, + {file = "traitlets-5.11.2.tar.gz", hash = "sha256:7564b5bf8d38c40fa45498072bf4dc5e8346eb087bbf1e2ae2d8774f6a0f078e"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.5.1)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "types-python-dateutil" +version = "2.8.19.14" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = "*" +files = [ + {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, + {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, +] + [[package]] name = "types-pytz" version = "2023.3.1.1" @@ -1278,6 +3566,37 @@ files = [ {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, ] +[[package]] +name = "uri-template" +version = "1.3.0" +description = "RFC 6570 URI Template Processor" +optional = false +python-versions = ">=3.7" +files = [ + {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, + {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, +] + +[package.extras] +dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] + +[[package]] +name = "urllib3" +version = "2.0.6" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"}, + {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "virtualenv" version = "20.24.5" @@ -1298,7 +3617,110 @@ platformdirs = ">=3.9.1,<4" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[[package]] +name = "watchdog" +version = "3.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.7" +files = [ + {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"}, + {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"}, + {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"}, + {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"}, + {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"}, + {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"}, + {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"}, + {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, + {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "wcwidth" +version = "0.2.8" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.8-py2.py3-none-any.whl", hash = "sha256:77f719e01648ed600dfa5402c347481c0992263b81a027344f3e1ba25493a704"}, + {file = "wcwidth-0.2.8.tar.gz", hash = "sha256:8705c569999ffbb4f6a87c6d1b80f324bd6db952f5eb0b95bc07517f4c1813d4"}, +] + +[[package]] +name = "webcolors" +version = "1.13" +description = "A library for working with the color formats defined by HTML and CSS." +optional = false +python-versions = ">=3.7" +files = [ + {file = "webcolors-1.13-py3-none-any.whl", hash = "sha256:29bc7e8752c0a1bd4a1f03c14d6e6a72e93d82193738fa860cbff59d0fcc11bf"}, + {file = "webcolors-1.13.tar.gz", hash = "sha256:c225b674c83fa923be93d235330ce0300373d02885cef23238813b0d5668304a"}, +] + +[package.extras] +docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[[package]] +name = "websocket-client" +version = "1.6.3" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket-client-1.6.3.tar.gz", hash = "sha256:3aad25d31284266bcfcfd1fd8a743f63282305a364b8d0948a43bd606acc652f"}, + {file = "websocket_client-1.6.3-py3-none-any.whl", hash = "sha256:6cfc30d051ebabb73a5fa246efdcc14c8fbebbd0330f8984ac3bb6d9edd2ad03"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "widgetsnbextension" +version = "4.0.9" +description = "Jupyter interactive widgets for Jupyter Notebook" +optional = false +python-versions = ">=3.7" +files = [ + {file = "widgetsnbextension-4.0.9-py3-none-any.whl", hash = "sha256:91452ca8445beb805792f206e560c1769284267a30ceb1cec9f5bcc887d15175"}, + {file = "widgetsnbextension-4.0.9.tar.gz", hash = "sha256:3c1f5e46dc1166dfd40a42d685e6a51396fd34ff878742a3e47c6f0cc4a2a385"}, +] + [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "1fecd7dfb911894538c627c8dbeb3bdffbf5d2250f950eb2e9e806d33ebe3940" +content-hash = "8006b96eff435b820dd9ae4893db8bafcf39657d9167fb3bc21987162ed70c40" diff --git a/pyproject.toml b/pyproject.toml index 5765eb94..b8ce99f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,17 @@ ruff = ">=0.0.276, <1" mypy = "^1.4.1" pandas-stubs = "^2.1.1.230928" + +[tool.poetry.group.docs.dependencies] +jupyter = "^1.0.0" +Markdown = "^3.3.7" +mkdocs = "^1.1.2" +mkdocs-material = "^9.1.19" +pyyaml = "^6.0.1" +mdx-include = ">=1.4.1" +mkdocstrings-python = ">=0.8.3" +mkdocs-gallery = ">=0.7.8" + [tool.black] line-length = 120 target-version = ['py310'] @@ -47,17 +58,18 @@ target-version = "py310" "test/***" = ["INP001"] "__init__.py" = ["E402", "F401"] "examples/***" = ["T201"] +"**/plot_*" = ["T201"] [tool.mypy] plugins = ["numpy.typing.mypy_plugin", "pydantic.mypy"] -check_untyped_defs = true +ignore_missing_imports = true +disallow_untyped_defs = false disallow_any_unimported = false disallow_any_generics = false disallow_incomplete_defs = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = false -disallow_untyped_defs = false implicit_reexport = false no_implicit_optional = false python_version = "3.10" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 72a713e8..00000000 --- a/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -numpy -scipy -matplotlib -statsmodels -pytest -pytest-cov -sphinx -numpydoc -sphinx_rtd_theme -codecov -Pillow -sphinx_gallery -pandas \ No newline at end of file diff --git a/src/chainconsumer/__init__.py b/src/chainconsumer/__init__.py index 951b5b39..a9866382 100644 --- a/src/chainconsumer/__init__.py +++ b/src/chainconsumer/__init__.py @@ -1,5 +1,6 @@ from .chain import Chain, ChainConfig from .chainconsumer import ChainConsumer from .truth import Truth +from .plotter import PlotConfig -__all__ = ["ChainConsumer", "Chain", "ChainConfig", "Truth"] +__all__ = ["ChainConsumer", "Chain", "ChainConfig", "Truth", "PlotConfig"] diff --git a/src/chainconsumer/analysis.py b/src/chainconsumer/analysis.py index 8fc055cd..e5892099 100644 --- a/src/chainconsumer/analysis.py +++ b/src/chainconsumer/analysis.py @@ -1,26 +1,19 @@ +from __future__ import annotations + import logging -from enum import Enum from pathlib import Path import numpy as np from pydantic import Field from scipy.integrate import simps from scipy.interpolate import interp1d -from scipy.ndimage.filters import gaussian_filter +from scipy.ndimage import gaussian_filter from .base import BetterBase from .chain import Chain, ChainName, ColumnName, MaxPosterior, Named2DMatrix from .helpers import get_bins, get_grid_bins, get_latex_table_frame, get_smoothed_bins from .kde import MegKDE - - -class SummaryStatistic(Enum): - MAX = "max" - MEAN = "mean" - CUMULATIVE = "cumulative" - MAX_SYMMETRIC = "max_symmetric" - MAX_SHORTEST = "max_shortest" - MAX_CENTRAL = "max_central" +from .summary_stats import SummaryStatistic class Bound(BetterBase): @@ -80,13 +73,13 @@ def get_latex_table( str: the LaTeX table. """ if columns is None: - columns = self.parent.all_columns + columns = self.parent._all_columns elif isinstance(columns, int): - columns = self.parent.all_columns[:columns] + columns = self.parent._all_columns[:columns] # TODO: ensure labels are a thin we can add num_parameters = len(columns) - chains = self.parent.chains + chains = self.parent._chains num_chains = len(chains) fit_values = self.get_summary(chains=chains) if label is None: @@ -153,9 +146,9 @@ def get_summary( """ results = {} if chains is None: - chains = self.parent.chains + chains = self.parent._chains if isinstance(chains, list): - chains = {c: self.parent.chains[c] for c in chains} + chains = {c: self.parent._chains[c] for c in chains} for name, chain in chains.items(): res = {} @@ -183,9 +176,9 @@ def get_max_posteriors(self, chains: dict[str, Chain] | list[str] | None = None) results = {} if chains is None: - chains = self.parent.chains + chains = self.parent._chains if isinstance(chains, list): - chains = {c: self.parent.chains[c] for c in chains} + chains = {c: self.parent._chains[c] for c in chains} for chain_name, chain in chains.items(): max_posterior = chain.get_max_posterior_point() @@ -219,11 +212,11 @@ def get_correlation_table( str: The LaTeX table ready to go! """ if isinstance(chain, str): - assert chain in self.parent.chains, f"Chain {chain} not found!" - chain = self.parent.chains[chain] + assert chain in self.parent._chains, f"Chain {chain} not found!" + chain = self.parent._chains[chain] if chain is None: - assert len(self.parent.chains) == 1, "You must specify a chain if there are multiple chains" - chain = next(iter(self.parent.chains.values())) + assert len(self.parent._chains) == 1, "You must specify a chain if there are multiple chains" + chain = next(iter(self.parent._chains.values())) correlations = chain.get_correlation(columns=columns) return self._get_2d_latex_table(correlations, caption, label) @@ -248,11 +241,11 @@ def get_covariance_table( str: The LaTeX table ready to go! """ if isinstance(chain, str): - assert chain in self.parent.chains, f"Chain {chain} not found!" - chain = self.parent.chains[chain] + assert chain in self.parent._chains, f"Chain {chain} not found!" + chain = self.parent._chains[chain] if chain is None: - assert len(self.parent.chains) == 1, "You must specify a chain if there are multiple chains" - chain = next(iter(self.parent.chains.values())) + assert len(self.parent._chains) == 1, "You must specify a chain if there are multiple chains" + chain = next(iter(self.parent._chains.values())) covariance = chain.get_covariance(columns=columns) return self._get_2d_latex_table(covariance, caption, label) diff --git a/src/chainconsumer/base.py b/src/chainconsumer/base.py index 0f0233ed..a3985448 100644 --- a/src/chainconsumer/base.py +++ b/src/chainconsumer/base.py @@ -1,5 +1,16 @@ +from typing import Any + from pydantic import BaseModel, ConfigDict class BetterBase(BaseModel): + _user_specified: set[str] = set() + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._user_specified = set(kwargs.keys()) + model_config = ConfigDict(arbitrary_types_allowed=True) + + def get_user_specified_dump(self) -> dict[str, Any]: + return {key: getattr(self, key) for key in self._user_specified} diff --git a/src/chainconsumer/chain.py b/src/chainconsumer/chain.py index c418145d..c388e710 100644 --- a/src/chainconsumer/chain.py +++ b/src/chainconsumer/chain.py @@ -1,3 +1,12 @@ +"""# Chains and ChainConfigs + +This file is where I expect most people will focus their time. It contains a general configuration class, +which stores non-unique things that chains use. Like line styles, colours, etc. + +It is then extended by the `Chain` class, which contains the actual data. + +There are also a few helper functions and objects in here, like the `MaxPosterior` class which +provides the log posterior and the coordinate at which it can be found for the chain.""" from __future__ import annotations import logging @@ -7,24 +16,17 @@ import pandas as pd from pydantic import Field, field_validator, model_validator -from .analysis import SummaryStatistic from .base import BetterBase from .colors import ColorInput, colors +from .summary_stats import SummaryStatistic ChainName: TypeAlias = str ColumnName: TypeAlias = str -class MaxPosterior(BetterBase): - log_posterior: float - coordinate: dict[ColumnName, float] - - @property - def vec_coordinate(self) -> np.ndarray: - return np.array(list(self.coordinate.values())) - - class Named2DMatrix(BetterBase): + """A 2D matrix with named columns. Used for covariance and correlation matrices.""" + columns: list[str] matrix: np.ndarray # type: ignore @@ -41,16 +43,15 @@ class ChainConfig(BetterBase): statistics: SummaryStatistic = Field(default=SummaryStatistic.MAX, description="The summary statistic to use") summary_area: float = Field(default=0.6827, description="The area to use for summary statistics") - sigmas: list[float] = Field(default=None, description="The sigmas to use for summary statistics") - color: ColorInput = Field(default=None, description="The color of the chain") + sigmas: list[float] = Field(default=[0, 1, 2], description="The sigmas to use for summary statistics") + color: ColorInput = Field(default=None, description="The color of the chain") # type: ignore linestyle: str = Field(default="-", description="The line style of the chain") linewidth: float = Field(default=1.0, description="The line width of the chain") - cloud: bool = Field(default=False, description="Whether to show the cloud of the chain") show_contour_labels: bool = Field(default=False, description="Whether to show contour labels") - shade: bool = Field(default=None, description="Whether to shade the chain") - shade_alpha: float = Field(default=None, description="The alpha of the shading") + shade: bool = Field(default=None, description="Whether to shade the chain") # type: ignore + shade_alpha: float = Field(default=None, description="The alpha of the shading") # type: ignore shade_gradient: float = Field(default=1.0, description="The contrast between contour levels") - bar_shade: bool = Field(default=None, description="Whether to shade marginalised distributions") + bar_shade: bool = Field(default=None, description="Whether to shade marginalised distributions") # type: ignore bins: int | float = Field( default=1.0, description="The number of bins to use for histograms. If a float, used to scale the default bins" ) @@ -62,11 +63,10 @@ class ChainConfig(BetterBase): plot_cloud: bool = Field(default=False, description="Whether to plot the cloud") plot_contour: bool = Field(default=True, description="Whether to plot contours") plot_point: bool = Field(default=False, description="Whether to plot points") - show_as_1d_prior: bool = Field(default=False, description="Whether to show as a 1D prior") - marker_style: str = Field(default=None, description="The marker style to use") - marker_size: int | float = Field(default=None, description="The marker size to use") - marker_alpha: int | float = Field(default=None, description="The marker alpha to use") - zorder: int = Field(default=None, description="The zorder to use") + marker_style: str = Field(default=None, description="The marker style to use") # type: ignore + marker_size: int | float = Field(default=10.0, ge=1, description="The marker size to use") + marker_alpha: int | float = Field(default=None, description="The marker alpha to use") # type: ignore + zorder: int = Field(default=10, description="The zorder to use") shift_params: bool = Field( default=False, description="Whether to shift the parameters by subtracting each parameters mean", @@ -74,22 +74,24 @@ class ChainConfig(BetterBase): @field_validator("color") @classmethod - def convert_color(cls, v: ColorInput | None) -> str | None: + def _convert_color(cls, v: ColorInput | None) -> str | None: if v is None: return None return colors.format(v) - def apply_if_none(self, **kwargs: dict[str, Any]) -> None: + def _apply_if_none(self, **kwargs: Any) -> None: for key, value in kwargs.items(): if getattr(self, key) is None: setattr(self, key, value) - def apply(self, **kwargs: dict[str, Any]) -> None: + def _apply(self, **kwargs: Any) -> None: for key, value in kwargs.items(): setattr(self, key, value) class Chain(ChainConfig): + """The numerical chain with its configuration.""" + samples: pd.DataFrame = Field( default=..., description="The chain data as a pandas DataFrame", @@ -104,7 +106,7 @@ class Chain(ChainConfig): description="The name of the weight column, if it exists", ) posterior_column: ColumnName = Field( - default="posterior", + default="log_posterior", description="The name of the log posterior column, if it exists", ) walkers: int = Field( @@ -131,43 +133,78 @@ class Chain(ChainConfig): description="Raise the posterior surface to this. Useful for inflating or deflating uncertainty for debugging.", ) + @property + def data_columns(self) -> list[str]: + """The columns in the dataframe which are not weights or posteriors.""" + results = [] + for c in self.samples.columns: + if c in {self.weight_column, self.posterior_column}: + continue + if c.lower() in { + "weight", + "weights", + "posterior", + "posteriors", + "log_weights", + "log_posterior", + "log_posteriors", + }: + continue + results.append(c) + return results + + @property + def plotting_columns(self) -> list[str]: + """The columns to be plotted, which are the dataframe columns + with the weights, posterior and colour coloumns removed.""" + cols = self.data_columns + if not self.plot_cloud: + return cols + return [c for c in cols if c != self.color_param] + @property def skip(self) -> bool: + """If the chain will be skipped in plotting because it has nothing to plot.""" return self.samples.empty or not (self.plot_contour or self.plot_cloud or self.plot_point) @property def max_posterior_row(self) -> pd.Series | None: + """The row of samples which correspond to the maximum posterior value. + None if the posterior is not supplied.""" if self.posterior_column not in self.samples.columns: - logging.warning("No posterior column found, cannot find max posterior row") + logging.warning("No posterior column found, cannot find max posterior") return None argmax = self.samples[self.posterior_column].argmax() - return self.samples.loc[argmax] + return self.samples.loc[argmax] # type: ignore @property def weights(self) -> np.ndarray: + """The column of weights in the samples.""" return self.samples[self.weight_column].to_numpy() @property def log_posterior(self) -> np.ndarray | None: + """The column of log posteriors in the samples. None if not set.""" if self.posterior_column not in self.samples.columns: return None return self.samples[self.posterior_column].to_numpy() @property def color_data(self) -> np.ndarray | None: + """The data from the color column. None if not set.""" if self.color_param is None: return None return self.samples[self.color_param].to_numpy() @field_validator("color") @classmethod - def validate_color(cls, v: str | np.ndarray | list[float] | None) -> str | None: + def _validate_color(cls, v: str | np.ndarray | list[float] | None) -> str | None: if v is None: return None return colors.format(v) @model_validator(mode="after") - def validate_model(self) -> Chain: + def _validate_model(self) -> Chain: assert not self.samples.empty, "Your chain is empty. This is not ideal." # If weights aren't set, add them all as one @@ -200,33 +237,34 @@ def validate_model(self) -> Chain: return self - def get_data(self, columns: str) -> pd.Series[float]: - return self.samples[columns] + def get_data(self, column: str) -> pd.Series[float]: + """Extracts a single columns from the samples dataframe.""" + return self.samples[column] @classmethod def from_covariance( cls, - mean: np.ndarray, - covariance: np.ndarray, - columns: list[str], - name: str, - **kwargs: dict[str, Any], + mean: np.ndarray | list[float], + covariance: np.ndarray | list[list[float]], + columns: list[ColumnName], + name: ChainName, + **kwargs: Any, ) -> Chain: """Generate samples as per mean and covariance supplied. Useful for Fisher matrix forecasts. Args: - mean (np.ndarray): The an array of mean values. - covariance (np.ndarray): The 2D array describing the covariance. + mean: The an array of mean values. + covariance: The 2D array describing the covariance. Dimensions should agree with the `mean` input. - columns (list[str]): A list of parameter names, one for each column (dimension) in the mean array. - name (str): The name of the chain. Defaults to None. + columns: A list of parameter names, one for each column (dimension) in the mean array. + name: The name of the chain. kwargs: Any other arguments to pass to the Chain constructor. Returns: - Chain: The generated chain. + The generated chain. """ rng = np.random.default_rng() - samples = rng.multivariate_normal(mean, covariance, size=1000000) + samples = rng.multivariate_normal(mean, covariance, size=1000000) # type: ignore df = pd.DataFrame(samples, columns=columns) return cls(samples=df, name=name, **kwargs) # type: ignore @@ -241,7 +279,7 @@ def divide(self) -> list[Chain]: you would see agree. Returns: - list[Chain]: One chain per walker, split evenly + One chain per walker, split evenly """ assert self.walkers > 1, "Cannot divide a chain with only one walker" assert not self.grid, "Cannot divide a grid chain" @@ -257,21 +295,56 @@ def divide(self) -> list[Chain]: return chains def get_max_posterior_point(self) -> MaxPosterior | None: + """Returns the maximum posterior point in the chain. If the posterior + + Returns: + MaxPosterior: The maximum posterior point + """ if self.max_posterior_row is None: return None row = self.max_posterior_row.to_dict() log_posterior = row.pop(self.posterior_column) + row = {k: v for k, v in row.items() if k in self.plotting_columns} return MaxPosterior(log_posterior=log_posterior, coordinate=row) - def get_covariance(self, columns: list[str] | None) -> Named2DMatrix: + def get_covariance(self, columns: list[str] | None = None) -> Named2DMatrix: + """Returns the covariance matrix of the chain. + + Args: + columns: The columns to use. None means all data columns. + + Returns: + Named2DMatrix: The covariance matrix + """ if columns is None: - columns = list(self.samples.columns) + columns = self.data_columns cov = np.cov(self.samples[columns], rowvar=False, aweights=self.weights) return Named2DMatrix(columns=columns, matrix=cov) - def get_correlation(self, columns: list[str] | None) -> Named2DMatrix: + def get_correlation(self, columns: list[str] | None = None) -> Named2DMatrix: + """Returns the correlation matrix of the chain. + + Args: + columns: The columns to use. None means all data columns. + + Returns: + Named2DMatrix: The correlation matrix + """ cov = self.get_covariance(columns) diag = np.sqrt(np.diag(cov.matrix)) divisor = diag[None, :] * diag[:, None] correlations = cov.matrix / divisor return Named2DMatrix(columns=cov.columns, matrix=correlations) + + +class MaxPosterior(BetterBase): + """A class that bundles the value of the + log posterior with the coordinate you can find it at.""" + + log_posterior: float + coordinate: dict[ColumnName, float] + + @property + def vec_coordinate(self) -> np.ndarray: + """The coordinate as a numpy array, in the order the columns were given.""" + return np.array(list(self.coordinate.values())) diff --git a/src/chainconsumer/chainconsumer.py b/src/chainconsumer/chainconsumer.py index fe970ddc..0c541ccc 100644 --- a/src/chainconsumer/chainconsumer.py +++ b/src/chainconsumer/chainconsumer.py @@ -1,3 +1,4 @@ +from typing import Any import numpy as np import pandas as pd @@ -17,66 +18,66 @@ class ChainConsumer: """A class for consuming chains produced by an MCMC walk. Or grid searches. To make plots, figures, tables, diagnostics, you name it.""" - def __init__(self): - self.chains: dict[ChainName, Chain] = {} - self.truths: list[Truth] = [] - self.labels = {} - self.global_chain_override: ChainConfig | None = None + def __init__(self) -> None: + self._chains: dict[ChainName, Chain] = {} + self._truths: list[Truth] = [] + self._global_chain_override: ChainConfig | None = None self.plotter = Plotter(self) + """Use this to access all the plotting functions""" self.diagnostic = Diagnostic(self) + """Use this to access your diagnostics to see if chains have converged.""" self.comparison = Comparison(self) + """Use this to compare chains to each other, like ranking the AIC, BIC, and DIC.""" self.analysis = Analysis(self) + """Use this to access the analysis functions, like getting summary statistics from your chains.""" @property - def all_columns(self) -> list[str]: - return list(set([c for chain in self.chains.values() for c in chain.samples.columns])) + def _all_columns(self) -> list[str]: + """All the columns across all chains""" + return list(set([c for chain in self._chains.values() for c in chain.samples.columns])) - def get_label(self, str: ColumnName) -> str: - return self.labels.get(str, str) - - def set_labels(self, labels: dict[str, str]) -> "ChainConsumer": - """Set the labels for the chains. + def add_truth(self, truth: Truth) -> "ChainConsumer": + """Add a truth to ChainConsumer. Args: - labels (dict[str, str]): A dictionary mapping column names to labels. + truth: The truth to add. Returns: - ChainConsumer: Itself, to allow chaining calls. + Itself, to allow chaining calls. """ - self.labels = labels + self._truths.append(truth) return self - def add_chain(self, chain: Chain): + def add_chain(self, chain: Chain) -> "ChainConsumer": """Add a chain to ChainConsumer. Args: - chain (Chain): The chain to add. + chain: The chain to add. Returns: - ChainConsumer: Itself, to allow chaining calls. + Itself, to allow chaining calls. """ key = chain.name - assert key not in self.chains, f"Chain with name {key} already exists!" - self.chains[key] = chain + assert key not in self._chains, f"Chain with name {key} already exists!" + self._chains[key] = chain return self def set_plot_config(self, plot_config: PlotConfig) -> "ChainConsumer": """Set the plot config for ChainConsumer. Args: - plot_config (PlotConfig): The plot config to use. + plot_config: The plot config to use. Returns: - ChainConsumer: Itself, to allow chaining calls. + Itself, to allow chaining calls. """ self.plotter.set_config(plot_config) return self def add_marker( self, - location: np.ndarray, - columns: list[str], + location: dict[ColumnName, float], name: str, color: ColorInput | None = None, marker_size: float = 20.0, @@ -86,51 +87,51 @@ def add_marker( r"""Add a marker to the plot at the given location. Args: - location (np.ndarray): The location of the marker. - columns (list[str]): The names of the columns in the chain that correspond to the location. - name (str): The name of the marker. - color (ColourInput, optional): The colour of the marker. Defaults to None. - marker_size (float, optional): The size of the marker. Defaults to 20.0. - marker_style (str, optional): The style of the marker. Defaults to ".". - marker_alpha (float, optional): The alpha of the marker. Defaults to 1.0. + location: The location of the marker. + columns: The names of the columns in the chain that correspond to the location. + name: The name of the marker. + color: The colour of the marker. Defaults to None. + marker_size: The size of the marker. Defaults to 20.0. + marker_style: The style of the marker. Defaults to ".". + marker_alpha: The alpha of the marker. Defaults to 1.0. Returns: - ChainConsumer: Itself, to allow chaining calls. + Itself, to allow chaining calls. """ - assert len(location.shape) == 1, "Location should be a 1D array" - assert len(location) == len(columns), "Location and columns should be the same length" - samples = pd.DataFrame(np.atleast_2d(location), columns=columns) - samples["weight"] = 1.0 - samples["posterior"] = 1.0 + samples = pd.DataFrame(location, index=[0]) + samples["weights"] = 1.0 + samples["log_posterior"] = 1.0 + kwargs = {} + if color is not None: + kwargs["color"] = color chain = Chain( samples=samples, name=name, - color=color, # type: ignore # ignoring the None override as this means we figure out colour later marker_size=marker_size, marker_style=marker_style, marker_alpha=marker_alpha, plot_contour=False, plot_point=True, + **kwargs, ) - self.add_chain(chain) - return self + return self.add_chain(chain) def remove_chain(self, remove: str | Chain) -> "ChainConsumer": r"""Removes a chain from ChainConsumer. Args: - remove (str|Chain): The name of the chain to remove, or the chain itself. + remove: The name of the chain to remove, or the chain itself. Returns: - ChainConsumer: Itself, to allow chaining calls. + Itself, to allow chaining calls. """ if isinstance(remove, Chain): remove = remove.name - assert remove in self.chains, f"Chain with name {remove} does not exist!" - self.chains.pop(remove) + assert remove in self._chains, f"Chain with name {remove} does not exist!" + self._chains.pop(remove) return self def add_override( @@ -140,23 +141,22 @@ def add_override( """Apply a custom override config Args: - override (Config, optional): The override config. Defaults to None. + override: The override config. Defaults to None. Returns: - ChainConsumer: Itself, to allow chaining calls. + Itself, to allow chaining calls. """ - self.global_chain_override = override + self._global_chain_override = override return self def _get_final_chains(self) -> dict[ChainName, Chain]: # Copy the original chain list - final_chains = {k: v.model_copy() for k, v in self.chains.items()} - chain_list = list(final_chains.values()) - num_chains = len(self.chains) + final_chains = {k: v.model_copy() for k, v in self._chains.items()} + num_chains = len(self._chains) # Note we only have to override things without a default # and things which should change as the number of chains change - global_config = {} + global_config: dict[str, Any] = {} global_config["bar_shade"] = num_chains < 5 global_config["sigmas"] = [0, 1, 2] global_config["shade"] = num_chains < 5 @@ -177,566 +177,29 @@ def _get_final_chains(self) -> dict[ChainName, Chain]: if chain.color is None: local_config["color"] = next(colors.next_colour()) - chain.apply_if_none(**local_config) + chain._apply_if_none(**local_config) # Apply user overrides - if self.global_chain_override is not None: - chain.apply(**self.global_chain_override.model_dump()) + if self._global_chain_override is not None: + chain._apply(**self._global_chain_override.get_user_specified_dump()) return final_chains - # def configure_overrides( - # self, - # statistics="max", - # max_ticks=5, - # plot_hists=True, - # flip=True, - # serif=False, - # sigma2d=False, - # sigmas=None, - # summary=None, - # bins=None, - # cmap=None, - # colors=None, - # linestyles=None, - # linewidths=None, - # kde=False, - # smooth=None, - # cloud=None, - # shade=None, - # shade_alpha=None, - # shade_gradient=None, - # bar_shade=None, - # num_cloud=None, - # color_params=None, - # plot_color_params=False, - # cmaps=None, - # plot_contour=None, - # plot_point=None, - # show_as_1d_prior=None, - # global_point=True, - # marker_style=None, - # marker_size=None, - # marker_alpha=None, - # usetex=False, - # diagonal_tick_labels=True, - # label_font_size=12, - # tick_font_size=10, - # spacing=None, - # contour_labels=None, - # contour_label_font_size=10, - # legend_kwargs=None, - # legend_location=None, - # legend_artists=None, - # legend_color_text=True, - # watermark_text_kwargs=None, - # summary_area=0.6827, - # zorder=None, - # ): # pragma: no cover - # r"""Configure the general plotting parameters common across the bar - # and contour plots. - - # If you do not call this explicitly, the :func:`plot` - # method will invoke this method automatically. - - # Please ensure that you call this method *after* adding all the relevant data to the - # chain consumer, as the consume changes configuration values depending on - # the supplied data. - - # Parameters - # ---------- - # statistics : string|list[str], optional - # Which sort of statistics to use. Defaults to `"max"` for maximum likelihood - # statistics. Other available options are `"mean"`, `"cumulative"`, `"max_symmetric"`, - # `"max_closest"` and `"max_central"`. In the - # very, very rare case you want to enable different statistics for different - # chains, you can pass in a list of strings. - # max_ticks : int, optional - # The maximum number of ticks to use on the plots - # plot_hists : bool, optional - # Whether to plot marginalised distributions or not - # flip : bool, optional - # Set to false if, when plotting only two parameters, you do not want it to - # rotate the histogram so that it is horizontal. - # sigma2d: bool, optional - # Defaults to `False`. When `False`, uses :math:`\sigma` levels for 1D Gaussians - ie confidence - # levels of 68% and 95%. When `True`, uses the confidence levels for 2D Gaussians, where 1 and 2 - # :math:`\sigma` represents 39% and 86% confidence levels respectively. - # sigmas : np.array, optional - # The :math:`\sigma` contour levels to plot. Defaults to [0, 1, 2, 3] for a single chain - # and [0, 1, 2] for multiple chains. - # serif : bool, optional - # Whether to display ticks and labels with serif font. - # summary : bool, optional - # If overridden, sets whether parameter summaries should be set as axis titles. - # Will not work if you have multiple chains - # bins : int|float,list[int|float], optional - # The number of bins to use. By default uses :math:`\frac{\sqrt{n}}{10}`, where - # :math:`n` are the number of data points. Giving an integer will set the number - # of bins to the given value. Giving a float will scale the number of bins, such - # that giving ``bins=1.5`` will result in using :math:`\frac{1.5\sqrt{n}}{10}` bins. - # Note this parameter is most useful if `kde=False` is also passed, so you - # can actually see the bins and not a KDE. - # cmap : str, optional - # Set to the matplotlib colour map you want to use to overwrite the default colours. - # Note that this parameter overwrites colours. The `cmaps` parameters is different, - # and used when you ask for an extra dimension to be used to colour scatter points. - # See the online examples to see the difference. - # colors : str(hex)|list[str(hex)], optional - # Provide a list of colours to use for each chain. If you provide more chains - # than colours, you *will* get the rainbow colour spectrum. If you only pass - # one colour, all chains are set to this colour. This probably won't look good. - # linestyles : str|list[str], optional - # Provide a list of line styles to plot the contours and marginalised - # distributions with. By default, this will become a list of solid lines. If a - # string is passed instead of a list, this style is used for all chains. - # linewidths : float|list[float], optional - # Provide a list of line widths to plot the contours and marginalised - # distributions with. By default, this is a width of 1. If a float - # is passed instead of a list, this width is used for all chains. - # kde : bool|float|list[bool|float], optional - # Whether to use a Gaussian KDE to smooth marginalised posteriors. If false, uses - # bins and linear interpolation, so ensure you have plenty of samples if your - # distribution is highly non-gaussian. Due to the slowness of performing a - # KDE on all data, it is often useful to disable this before producing final - # plots. If float, scales the width of the KDE bandpass manually. - # smooth : int|list[int], optional - # Defaults to 3. How much to smooth the marginalised distributions using a gaussian filter. - # If ``kde`` is set to true, this parameter is ignored. Setting it to either - # ``0``, ``False`` disables smoothing. For grid data, smoothing - # is set to 0 by default, not 3. - # cloud : bool|list[bool], optional - # If set, overrides the default behaviour and plots the cloud or not - # shade : bool|list[bool] optional - # If set, overrides the default behaviour and plots filled contours or not. If a list of - # bools is passed, you can turn shading on or off for specific chains. - # shade_alpha : float|list[float], optional - # Filled contour alpha value override. Default is 1.0. If a list is passed, you can set the - # shade opacity for specific chains. - # shade_gradient : float|list[float], optional - # How much to vary colours in different contour levels. - # bar_shade : bool|list[bool], optional - # If set to true, shades in confidence regions in under histogram. By default - # this happens if you less than 3 chains, but is disabled if you are comparing - # more chains. You can pass a list if you wish to shade some chains but not others. - # num_cloud : int|list[int], optional - # The number of scatter points to show when enabling `cloud` or setting one of the parameters - # to colour scatter. Defaults to 15k per chain. - # color_params : str|list[str], optional - # The name of the parameter to use for the colour scatter. Defaults to none, for no colour. If set - # to 'weights', 'log_weights', or 'posterior' (without the quotes), and that is not a parameter in the chain, - # it will respectively use the weights, log weights, or posterior, to colour the points. - # plot_color_params : bool|list[bool], optional - # Whether or not the colour parameter should also be plotted as a posterior surface. - # cmaps : str|list[str], optional - # The matplotlib colourmap to use in the `colour_param`. If you have multiple `color_param`s, you can - # specific a different cmap for each variable. By default ChainConsumer will cycle between several - # cmaps. - # plot_contour : bool|list[bool], optional - # Whether to plot the whole contour (as opposed to a point). Defaults to true for less than - # 25 concurrent chains. - # plot_point : bool|list[bool], optional - # Whether to plot a maximum likelihood point. Defaults to true for more then 24 chains. - # show_as_1d_prior : bool|list[bool], optional - # Showing as a 1D prior will show the 1D histograms, but won't plot the 2D contours. - # global_point : bool, optional - # Whether the point which gets plotted is the global posterior maximum, or the marginalised 2D - # posterior maximum. Note that when you use marginalised 2D maximums for the points, you do not - # get the 1D histograms. Defaults to `True`, for a global maximum value. - # marker_style : str|list[str], optional - # The marker style to use when plotting points. Defaults to `'.'` - # marker_size : numeric|list[numeric], optional - # Size of markers, if plotted. Defaults to `20`. - # marker_alpha : numeric|list[numeric], optional - # The alpha values when plotting markers. - # usetex : bool, optional - # Whether or not to parse text as LaTeX in plots. - # diagonal_tick_labels : bool, optional - # Whether to display tick labels on a 45 degree angle. - # label_font_size : int|float, optional - # The font size for plot axis labels and axis titles if summaries are configured to display. - # tick_font_size : int|float, optional - # The font size for the tick labels in the plots. - # spacing : float, optional - # The amount of spacing to add between plots. Defaults to `None`, which equates to 1.0 for less - # than 6 dimensions and 0.0 for higher dimensions. - # contour_labels : string, optional - # If unset do not plot contour labels. If set to "confidence", label the using confidence - # intervals. If set to "sigma", labels using sigma. - # contour_label_font_size : int|float, optional - # The font size for contour labels, if they are enabled. - # legend_kwargs : dict, optional - # Extra arguments to pass to the legend api. - # legend_location : tuple(int,int), optional - # Specifies the subplot in which to locate the legend. By default, this will be (0, -1), - # corresponding to the top right subplot if there are more than two parameters, - # and the bottom left plot for only two parameters with flip on. - # For having the legend in the primary subplot - # in the bottom left, set to (-1,0). - # legend_artists : bool, optional - # Whether to include hide artists in the legend. If all linestyles and line widths are identical, - # this will default to false (as only the colours change). Otherwise it will be true. - # legend_color_text : bool, optional - # Whether to colour the legend text. - # watermark_text_kwargs : dict, optional - # Options to pass to the fontdict property when generating text for the watermark. - # summary_area : float, optional - # The confidence interval used when generating parameter summaries. Defaults to 1 sigma, aka 0.6827 - # zorder : int, optional - # The zorder to pass to `matplotlib` to determine visual ordering when plotting. - - # Returns - # ------- - # ChainConsumer - # Itself, to allow chaining calls. - # """ - # # Warn the user if configure has been invoked multiple times - # self._num_configure_calls += 1 - # if self._num_configure_calls > 1: - # logger.warning( - # "Configure has been called %d times - this is not good - it should be once!" % self._num_configure_calls - # ) - # logger.warning("To avoid this, load your chains in first, then call analysis/plotting methods") - - # # Dirty way of ensuring overrides happen when requested - # l = locals() - # explicit = [] - # for k in l: - # if l[k] is not None: - # explicit.append(k) - # if k.endswith("s"): - # explicit.append(k[:-1]) - # self._init_params() - - # num_chains = len(self.chains) - - # assert cmap is None or colors is None, "You cannot both ask for cmap colours and then give explicit colours" - - # # Determine statistics - # assert statistics is not None, "statistics should be a string or list of strings!" - # if isinstance(statistics, str): - # assert statistics in list(Analysis.summaries), "statistics {} not recognised. Should be in {}".format( - # statistics, - # Analysis.summaries, - # ) - # statistics = [statistics.lower()] * len(self.chains) - # elif isinstance(statistics, list): - # for i, l in enumerate(statistics): - # statistics[i] = l.lower() - # else: - # raise ValueError("statistics is not a string or a list!") - - # # Determine KDEs - # if isinstance(kde, bool | float): - # kde = [False if c.grid else kde for c in self.chains] - - # kde_override = [c.kde for c in self.chains] - # kde = [c2 if c2 is not None else c1 for c1, c2 in zip(kde, kde_override)] - - # # Determine bins - # if bins is None: - # bins = get_bins(self.chains) - # elif isinstance(bins, list): - # bins = [b2 if isinstance(b2, int) else np.floor(b2 * b1) for b1, b2 in zip(get_bins(self.chains), bins)] - # elif isinstance(bins, float): - # bins = [np.floor(b * bins) for b in get_bins(self.chains)] - # elif isinstance(bins, int): - # bins = [bins] * len(self.chains) - # else: - # raise ValueError("bins value is not a recognised class (float or int)") - - # # Determine smoothing - # if smooth is None: - # smooth = [0 if c.grid or k else 3 for c, k in zip(self.chains, kde)] - # else: - # if smooth is not None and not smooth: - # smooth = 0 - # if isinstance(smooth, list): - # smooth = [0 if k else s for s, k in zip(smooth, kde)] - # else: - # smooth = [0 if k else smooth for k in kde] - - # # Determine color parameters - # if color_params is None: - # color_params = [None] * num_chains - # else: - # if isinstance(color_params, str): - # color_params = [ - # color_params if color_params in [*cs.parameters, "log_weights", "weights", "posterior"] else None - # for cs in self.chains - # ] - # color_params = [ - # None if c == "posterior" and self.chains[i].posterior is None else c - # for i, c in enumerate(color_params) - # ] - # elif isinstance(color_params, list | tuple): - # for c, chain in zip(color_params, self.chains): - # p = chain.parameters - # if c is not None: - # assert c in p, f"Color parameter {c} not in parameters {p}" - # # Determine if we should plot color parameters - # if isinstance(plot_color_params, bool): - # plot_color_params = [plot_color_params] * len(color_params) - - # # Determine cmaps - # if cmaps is None: - # param_cmaps = {} - # cmaps = [] - # i = 0 - # for cp in color_params: - # if cp is None: - # cmaps.append(None) - # elif cp in param_cmaps: - # cmaps.append(param_cmaps[cp]) - # else: - # param_cmaps[cp] = self._cmaps[i] - # cmaps.append(self._cmaps[i]) - # i = (i + 1) % len(self._cmaps) - - # # Determine colours - # if colors is None: - # if cmap: - # colors = colors.get_colormap(num_chains, cmap) - # else: - # if num_chains > len(self._all_colours): - # num_needed_colours = np.sum([c is None for c in color_params]) - # colour_list = colors.get_colormap(num_needed_colours, "inferno") - # else: - # colour_list = self._all_colours - # colors = [] - # ci = 0 - # for c in color_params: - # if c: - # colors.append("#000000") - # else: - # colors.append(colour_list[ci]) - # ci += 1 - # elif isinstance(colors, str): - # colors = [colors] * len(self.chains) - # colors = colors.get_formatted(colors) - - # # Determine linestyles - # if linestyles is None: - # i = 0 - # linestyles = [] - # for c in color_params: - # if c is None: - # linestyles.append(self._linestyles[0]) - # else: - # linestyles.append(self._linestyles[i]) - # i = (i + 1) % len(self._linestyles) - # elif isinstance(linestyles, str): - # linestyles = [linestyles] * len(self.chains) - - # # Determine linewidths - # if linewidths is None: - # linewidths = [1.0] * len(self.chains) - # elif isinstance(linewidths, float | int): - # linewidths = [linewidths] * len(self.chains) - - # # Determine clouds - # if cloud is None: - # cloud = False - # cloud = [cloud or c is not None for c in color_params] - - # # Determine cloud points - # if num_cloud is None: - # num_cloud = 30000 - # if isinstance(num_cloud, float | int): - # num_cloud = [int(num_cloud)] * num_chains - - # # Should we shade the contours - # if shade is None: - # shade = num_chains <= 3 if shade_alpha is None else True - # if isinstance(shade, bool): - # # If not overridden, do not shade chains with colour scatter points - # shade = [shade and c is None for c in color_params] - - # # Modify shade alpha based on how many chains we have - # if shade_alpha is None: - # if num_chains == 1: - # shade_alpha = 0.75 if contour_labels is not None else 1.0 - # else: - # shade_alpha = 1.0 / np.sqrt(num_chains) - # # Decrease the shading amount if there are colour scatter points - # if isinstance(shade_alpha, float | int): - # shade_alpha = [shade_alpha if c is None else 0.25 * shade_alpha for c in color_params] - - # if shade_gradient is None: - # shade_gradient = 1.0 - # if isinstance(shade_gradient, float): - # shade_gradient = [shade_gradient] * num_chains - # elif isinstance(shade_gradient, list): - # assert len(shade_gradient) == num_chains, "Have %d shade_gradient but % chains" % ( - # len(shade_gradient), - # num_chains, - # ) - - # contour_over_points = num_chains < 20 - - # if plot_contour is None: - # plot_contour = [contour_over_points if chain.log_posterior is not None else True for chain in self.chains] - # elif isinstance(plot_contour, bool): - # plot_contour = [plot_contour] * num_chains - - # if plot_point is None: - # plot_point = [not contour_over_points] * num_chains - # elif isinstance(plot_point, bool): - # plot_point = [plot_point] * num_chains - - # if show_as_1d_prior is None: - # show_as_1d_prior = [not contour_over_points] * num_chains - # elif isinstance(show_as_1d_prior, bool): - # show_as_1d_prior = [show_as_1d_prior] * num_chains - - # if marker_style is None: - # marker_style = ["."] * num_chains - # elif isinstance(marker_style, str): - # marker_style = [marker_style] * num_chains - - # if marker_size is None: - # marker_size = [20] * num_chains - # elif isinstance(marker_style, int | float): - # marker_size = [marker_size] * num_chains - - # if marker_alpha is None: - # marker_alpha = [1.0] * num_chains - # elif isinstance(marker_alpha, int | float): - # marker_alpha = [marker_alpha] * num_chains - - # # Figure out if we should display parameter summaries - # if summary is not None: - # summary = summary and num_chains == 1 - - # # Figure out bar shading - # if bar_shade is None: - # bar_shade = num_chains <= 3 - # if isinstance(bar_shade, bool): - # bar_shade = [bar_shade] * num_chains - - # if zorder is None: - # zorder = [1] * num_chains - - # # Figure out how many sigmas to plot - # if sigmas is None: - # sigmas = np.array([0, 1, 2]) if num_chains == 1 else np.array([0, 1, 2]) - # if sigmas[0] != 0: - # sigmas = np.concatenate(([0], sigmas)) - # sigmas = np.sort(sigmas) - - # if contour_labels is not None: - # assert isinstance(contour_labels, str), "contour_labels parameter should be a string" - # contour_labels = contour_labels.lower() - # assert contour_labels in [ - # "sigma", - # "confidence", - # ], "contour_labels should be either sigma or confidence" - # assert isinstance(contour_label_font_size, float | int), "contour_label_font_size needs to be numeric" - - # if legend_artists is None: - # legend_artists = len(set(linestyles)) > 1 or len(set(linewidths)) > 1 - - # if legend_kwargs is not None: - # assert isinstance(legend_kwargs, dict), "legend_kwargs should be a dict" - # else: - # legend_kwargs = {} - - # if num_chains < 3: - # labelspacing = 0.5 - # elif num_chains == 3: - # labelspacing = 0.2 - # else: - # labelspacing = 0.15 - # legend_kwargs_default = { - # "labelspacing": labelspacing, - # "loc": "upper right", - # "frameon": False, - # "fontsize": label_font_size, - # "handlelength": 1, - # "handletextpad": 0.2, - # "borderaxespad": 0.0, - # } - # legend_kwargs_default.update(legend_kwargs) - - # watermark_text_kwargs_default = { - # "color": "#333333", - # "alpha": 0.7, - # "verticalalignment": "center", - # "horizontalalignment": "center", - # } - # if watermark_text_kwargs is None: - # watermark_text_kwargs = {} - # watermark_text_kwargs_default.update(watermark_text_kwargs) - - # assert isinstance(summary_area, float), "summary_area needs to be a float, not %s!" % type(summary_area) - # assert summary_area > 0, "summary_area should be a positive number, instead is %s!" % summary_area - # assert summary_area < 1, "summary_area must be less than unity, instead is %s!" % summary_area - # assert isinstance(global_point, bool), "global_point should be a bool" - - # # List options - # for i, c in enumerate(self.chains): - # try: - # c.update_unset_config("statistics", statistics[i], override=explicit) - # c.update_unset_config("color", colors[i], override=explicit) - # c.update_unset_config("linestyle", linestyles[i], override=explicit) - # c.update_unset_config("linewidth", linewidths[i], override=explicit) - # c.update_unset_config("cloud", cloud[i], override=explicit) - # c.update_unset_config("shade", shade[i], override=explicit) - # c.update_unset_config("shade_alpha", shade_alpha[i], override=explicit) - # c.update_unset_config("shade_gradient", shade_gradient[i], override=explicit) - # c.update_unset_config("bar_shade", bar_shade[i], override=explicit) - # c.update_unset_config("bins", bins[i], override=explicit) - # c.update_unset_config("kde", kde[i], override=explicit) - # c.update_unset_config("smooth", smooth[i], override=explicit) - # c.update_unset_config("color_params", color_params[i], override=explicit) - # c.update_unset_config("plot_color_params", plot_color_params[i], override=explicit) - # c.update_unset_config("cmap", cmaps[i], override=explicit) - # c.update_unset_config("num_cloud", num_cloud[i], override=explicit) - # c.update_unset_config("marker_style", marker_style[i], override=explicit) - # c.update_unset_config("marker_size", marker_size[i], override=explicit) - # c.update_unset_config("marker_alpha", marker_alpha[i], override=explicit) - # c.update_unset_config("plot_contour", plot_contour[i], override=explicit) - # c.update_unset_config("plot_point", plot_point[i], override=explicit) - # c.update_unset_config("show_as_1d_prior", show_as_1d_prior[i], override=explicit) - # c.update_unset_config("zorder", zorder[i], override=explicit) - # c.config["summary_area"] = summary_area - - # except IndentationError: - # print( - # "Index error when assigning chain properties, make sure you " - # "have enough properties set for the number of chains you have loaded! " - # "See the stack trace for which config item has the wrong number of entries." - # ) - # raise - - # # Non list options - # self.config["sigma2d"] = sigma2d - # self.config["sigmas"] = sigmas - # self.config["summary"] = summary - # self.config["flip"] = flip - # self.config["serif"] = serif - # self.config["plot_hists"] = plot_hists - # self.config["max_ticks"] = max_ticks - # self.config["usetex"] = usetex - # self.config["diagonal_tick_labels"] = diagonal_tick_labels - # self.config["label_font_size"] = label_font_size - # self.config["tick_font_size"] = tick_font_size - # self.config["spacing"] = spacing - # self.config["contour_labels"] = contour_labels - # self.config["contour_label_font_size"] = contour_label_font_size - # self.config["legend_location"] = legend_location - # self.config["legend_kwargs"] = legend_kwargs_default - # self.config["legend_artists"] = legend_artists - # self.config["legend_color_text"] = legend_color_text - # self.config["watermark_text_kwargs"] = watermark_text_kwargs_default - # self.config["global_point"] = global_point - - # self._configured = True - # return self - def get_chain(self, name: str) -> Chain: - assert name in self.chains, f"Chain with name {name} does not exist!" - return self.chains[name] + """Get a chain by name. + + Args: + name: The name of the chain. + + Returns: + The chain. + """ + assert name in self._chains, f"Chain with name {name} does not exist!" + return self._chains[name] def get_names(self) -> list[str]: - return list(self.chains.keys()) + """Get the names of all chains. + + Returns: + The names of all chains.""" + return list(self._chains.keys()) diff --git a/src/chainconsumer/colors.py b/src/chainconsumer/colors.py index f83a4adf..0abf3b0c 100644 --- a/src/chainconsumer/colors.py +++ b/src/chainconsumer/colors.py @@ -41,6 +41,7 @@ def __init__(self) -> None: "r": "red", "g": "green", "k": "gray", + "f": "gray", "m": "rose", "c": "cyan", "o": "orange", @@ -73,8 +74,10 @@ def next_colour(self) -> Generator[str, None, None]: for color in self.default_colors: yield ALL_COLOURS[color][index] - def format(self, color: ColorInput) -> str: - if isinstance(color, np.ndarray | list): + def format(self, color: ColorInput | None) -> str: + if color is None: + return next(iter(self.next_colour())) + elif isinstance(color, np.ndarray | list): color = rgb2hex(color) # type: ignore if color[0] == "#": return color @@ -83,9 +86,9 @@ def format(self, color: ColorInput) -> str: elif color in self.aliases: alias = self.aliases[color] index = 4 - if color.lower() == "black": + if color.lower() in ["k", "black"]: index = -1 - elif color.lower() == "white": + elif color.lower() in ["f", "white"]: index = 0 return ALL_COLOURS[alias][index] else: diff --git a/src/chainconsumer/comparisons.py b/src/chainconsumer/comparisons.py index 7f07919b..283c0044 100644 --- a/src/chainconsumer/comparisons.py +++ b/src/chainconsumer/comparisons.py @@ -36,7 +36,7 @@ def dic(self) -> dict[str, float]: [1] Andrew R. Liddle, "Information criteria for astrophysical model selection", MNRAS (2007) """ dics = {} - for name, chain in self.parent.chains.items(): + for name, chain in self.parent._chains.items(): p = chain.log_posterior if p is None: logger.warning("You need to set the posterior for chain %s to get the DIC" % chain.name) @@ -72,7 +72,7 @@ def bic(self) -> dict[str, float]: """ bics = {} - for name, chain in self.parent.chains.items(): + for name, chain in self.parent._chains.items(): p, n_data, n_free = chain.log_posterior, chain.num_eff_data_points, chain.num_free_params if p is None or n_data is None or n_free is None: missing = "" @@ -116,7 +116,7 @@ def aic(self) -> dict[str, float]: """ aics = {} - for name, chain in self.parent.chains.items(): + for name, chain in self.parent._chains.items(): p, n_data, n_free = chain.log_posterior, chain.num_eff_data_points, chain.num_free_params if p is None or n_data is None or n_free is None: missing = "" diff --git a/src/chainconsumer/helpers.py b/src/chainconsumer/helpers.py index 2188a611..8bf804e7 100644 --- a/src/chainconsumer/helpers.py +++ b/src/chainconsumer/helpers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numpy as np import pandas as pd diff --git a/src/chainconsumer/plotter.py b/src/chainconsumer/plotter.py index db028b2d..0b9b02b1 100644 --- a/src/chainconsumer/plotter.py +++ b/src/chainconsumer/plotter.py @@ -15,9 +15,9 @@ from matplotlib.ticker import LogLocator, MaxNLocator, ScalarFormatter from numpy import meshgrid from pydantic import Field -from scipy.interpolate import interp1d -from scipy.ndimage import gaussian_filter -from scipy.stats import norm +from scipy.interpolate import interp1d # type: ignore +from scipy.ndimage import gaussian_filter # type: ignore +from scipy.stats import norm # type: ignore from chainconsumer.truth import Truth @@ -115,7 +115,7 @@ def get_size( return input # Otherwise it must be grow, which is the default - return grow_factor * 1.5 * num_columns + (1 if has_cax else 0), grow_factor * 1.5 * num_columns + return 3 + grow_factor * 2 * num_columns + (1 if has_cax else 0), 3 + grow_factor * 2 * num_columns def get_artists_from_chains(chains: list[Chain]): @@ -123,7 +123,14 @@ def get_artists_from_chains(chains: list[Chain]): for chain in chains: if chain.plot_contour and not chain.plot_point: artists.append( - Line2D((0, 1), (0, 0), color=colors.format(chain.color), ls=chain.linestyle, lw=chain.linewidth) + Line2D( + (0, 1), + (0, 0), + color=colors.format(chain.color), + ls=chain.linestyle, + lw=chain.linewidth, + label=" " + chain.name, + ) ) elif not chain.plot_contour and chain.plot_point: artists.append( @@ -134,7 +141,8 @@ def get_artists_from_chains(chains: list[Chain]): ls=chain.linestyle, lw=0, marker=chain.marker_style, - markersize=chain.marker_size, + markersize=np.sqrt(chain.marker_size), + label=" " + chain.name, ) ) else: @@ -146,14 +154,15 @@ def get_artists_from_chains(chains: list[Chain]): ls=chain.linestyle, lw=chain.linewidth, marker=chain.marker_style, - markersize=chain.marker_size, + markersize=np.sqrt(chain.marker_size), + label=" " + chain.name, ) ) return artists class Plotter: - def __init__(self, parent): + def __init__(self, parent: "ChainConsumer") -> None: self.parent: "ChainConsumer" = parent self._config: PlotConfig | None = None self._default_config = PlotConfig() @@ -161,7 +170,7 @@ def __init__(self, parent): self.usetex_old = matplotlib.rcParams["text.usetex"] self.serif_old = matplotlib.rcParams["font.family"] - def set_config(self, config: PlotConfig): + def set_config(self, config: PlotConfig) -> None: self._config = config @property @@ -177,12 +186,11 @@ def plot( chains: list[ChainName | Chain] | None = None, extents: dict[ColumnName, tuple[float, float]] | None = None, filename: list[str | Path] | str | Path | None = None, - display: bool = False, show_legend: bool | None = None, blind: bool | list[str] | None = None, watermark: str | None = None, log_scales: list[ColumnName] | None = None, - ): # pragma: no cover + ) -> Figure: # pragma: no cover """Plot the chain! Parameters @@ -256,7 +264,7 @@ def plot( # Plot the histograms if plot_hists and i == j: - for truth in self.parent.truths: + for truth in self.parent._truths: if do_flip: self._add_truth(ax, truth, px=p1) else: @@ -284,28 +292,32 @@ def plot( for chain in base.chains: if p1 not in chain.samples or p2 not in chain.samples: continue - if not chain.plot_contour or chain.show_as_1d_prior: - continue - h = self._plot_contour(ax, chain, p1, p2) - cp = chain.color_param - if h is not None and cp is not None and cp not in cbar_done: - cbar_done.append(cp) - aspect = fig_size[1] / 0.15 - fraction = 0.85 / fig_size[0] - cbar = fig.colorbar(h, ax=axl, aspect=aspect, pad=0.03, fraction=fraction, drawedges=False) - label = self.config.get_label(cp) - if label == "weights": - label = "Weights" - elif label == "log_weights": - label = "log(Weights)" - elif label == "posterior": - label = "log(Posterior)" - cbar.set_label(label, fontsize=self.config.label_font_size) - if cbar.solids is not None: - cbar.solids.set(alpha=1) - - for truth in self.parent.truths: + if chain.plot_contour: + h = self._plot_contour(ax, chain, p1, p2) + cp = chain.color_param + if h is not None and cp is not None and cp not in cbar_done: + cbar_done.append(cp) + aspect = fig_size[1] / 0.15 + fraction = 0.85 / fig_size[0] + cbar = fig.colorbar( + h, ax=axl, aspect=aspect, pad=0.03, fraction=fraction, drawedges=False + ) + label = self.config.get_label(cp) + if label == "weights": + label = "Weights" + elif label == "log_weights": + label = "log(Weights)" + elif label == "posterior": + label = "log(Posterior)" + cbar.set_label(label, fontsize=self.config.label_font_size) + if cbar.solids is not None: + cbar.solids.set(alpha=1) + + if chain.plot_point: + self._plot_point(ax, chain, p2, p1) + + for truth in self.parent._truths: self._add_truth(ax, truth, px=p1, py=p2) legend_location = self.config.legend_location @@ -320,7 +332,7 @@ def plot( legend_kwargs["markerfirst"] = legend_outside or not self.config.legend_artists artists = get_artists_from_chains(base.chains) - leg = ax.legend(artists, **legend_kwargs) + leg = ax.legend(handles=artists, **legend_kwargs) if self.config.legend_color_text: for text, chain in zip(leg.get_texts(), base.chains): text.set_fontweight("medium") @@ -353,8 +365,6 @@ def plot( filename = [filename] for f in filename: self._save_fig(fig, f, dpi) - if display: - plt.show() return fig @@ -917,7 +927,7 @@ def _get_size_of_texts(self, texts: list[str]) -> float: # pragma: no cover def _sanitise_columns(self, columns: list[ColumnName] | None, chains: list[Chain]) -> list[ColumnName]: if columns is None: - return list(set([c for chain in chains for c in chain.samples.columns])) + return list(set([c for chain in chains for c in chain.plotting_columns])) return columns def _sanitise_logscale(self, log_scales: list[ColumnName] | None) -> list[ColumnName]: @@ -1006,7 +1016,16 @@ def _get_figure( gridspec_kw = {"width_ratios": [3, 1], "height_ratios": [1, 3]} fig, axes = plt.subplots(n, n, figsize=figsize, squeeze=False, gridspec_kw=gridspec_kw) - fig.subplots_adjust(left=0.1, right=0.95, top=0.95, bottom=0.1, wspace=0.05 * spacing, hspace=0.05 * spacing) + min_left_for_axes = min(max(0.85 / figsize[0], 0.1), 0.3) + min_bottom_for_axes = min(max(0.85 / figsize[1], 0.1), 0.3) + fig.subplots_adjust( + left=min_left_for_axes, + right=0.95, + top=0.9, + bottom=min_bottom_for_axes, + wspace=0.05 * spacing, + hspace=0.05 * spacing, + ) if self.config.plot_hists: params_x = base.columns @@ -1054,7 +1073,7 @@ def _get_figure( else: display_x_ticks = True if isinstance(p2, str): - ax.set_xlabel(p2, fontsize=self.config.label_font_size) + ax.set_xlabel(self.config.get_label(p2), fontsize=self.config.label_font_size) if j != 0 or (self.config.plot_hists and i == 0): ax.set_yticks([]) else: @@ -1063,7 +1082,7 @@ def _get_figure( else: display_y_ticks = True if isinstance(p1, str): - ax.set_ylabel(p1, fontsize=self.config.label_font_size) + ax.set_ylabel(self.config.get_label(p1), fontsize=self.config.label_font_size) if display_x_ticks: if self.config.diagonal_tick_labels: _ = [label.set_rotation(45) for label in ax.get_xticklabels()] @@ -1130,27 +1149,34 @@ def _get_levels(self, sigmas: list[float]) -> np.ndarray: levels: np.ndarray = 2 * norm.cdf(sigmas) - 1.0 return levels - def _plot_point(self, ax: Axes, chain: Chain, px: str, py: str) -> PathCollection: # pragma: no cover + def _plot_point(self, ax: Axes, chain: Chain, px: str, py: str) -> PathCollection | None: # pragma: no cover point = chain.get_max_posterior_point() if point is None or px not in point.coordinate or py not in point.coordinate: - return + return None + # Determine if we need to darken the point + c = colors.format(chain.color) + if chain.plot_contour: + c = colors.scale_colour(chain.color, 0.5) h = ax.scatter( [point.coordinate[px]], - point.coordinate[py], + [point.coordinate[py]], marker=chain.marker_style, - c=colors.format(chain.color), + c=c, s=chain.marker_size, alpha=chain.marker_alpha, + zorder=chain.zorder + 1, ) return h def _sanitise_chains(self, chains: list[Chain | ChainName] | dict[ChainName, Chain] | None) -> list[Chain]: + overriden_chains = self.parent._get_final_chains() + final_chains = [] if isinstance(chains, list): - final_chains = [self.parent.chains[n] if isinstance(n, ChainName) else n for n in chains] + final_chains = [overriden_chains[c if isinstance(c, ChainName) else c.name] for c in chains] elif isinstance(chains, dict): - final_chains = list(chains.values()) + final_chains = [overriden_chains[c.name] for c in chains.values()] else: - final_chains = list(self.parent.chains.values()) + final_chains = list(overriden_chains.values()) return [c for c in final_chains if not c.skip] def plot_contour( @@ -1180,7 +1206,7 @@ def _plot_scatter(self, ax: Axes, chain: Chain, color: str, x: pd.Series, y: pd. else: kwargs = {"c": color, "alpha": 0.3} - h = ax.scatter(x[::skip], y[::skip], s=10, marker=".", edgecolors="none", **kwargs) + h = ax.scatter(x[::skip], y[::skip], s=10, marker=".", edgecolors="none", zorder=chain.zorder - 1, **kwargs) if chain.color_data is not None: return h else: @@ -1195,7 +1221,7 @@ def _plot_contour(self, ax: Axes, chain: Chain, px: str, py: str) -> PathCollect sub = max(0.1, 1 - 0.2 * chain.shade_gradient) paths = None - if chain.cloud: + if chain.plot_cloud: paths = self._plot_scatter(ax, chain, contour_colours[1], x, y) # TODO: Figure out whats going on here @@ -1217,7 +1243,7 @@ def _plot_contour(self, ax: Axes, chain: Chain, px: str, py: str) -> PathCollect levels=levels, colors=contour_colours, alpha=chain.shade_alpha, - zorder=chain.zorder, + zorder=chain.zorder - 2, ) con = ax.contour( x_centers, @@ -1232,23 +1258,22 @@ def _plot_contour(self, ax: Axes, chain: Chain, px: str, py: str) -> PathCollect if chain.show_contour_labels: lvls = [lvl for lvl in con.levels if lvl != 0.0] - # TODO: see if this can just be "0.0%" - fmt = {lvl: f"{lvl:0.0%}" for lvl in lvls} - ax.clabel(con, lvls, inline=True, fmt=fmt, fontsize=self.config.contour_label_font_size) + fmt = {lvl: f" {lvl:0.0%} " if lvl < 0.991 else f" {lvl:0.1%} " for lvl in lvls} + texts = ax.clabel(con, lvls, inline=True, fmt=fmt, fontsize=self.config.contour_label_font_size) + for text in texts: + text.set_fontweight("semibold") - if chain.plot_point: - self._plot_point(ax, chain, px, py) return paths def _add_truth( self, ax: Axes, truth: Truth, px: str | None = None, py: str | None = None ) -> None: # pragma: no cover if px is not None: - val_x = truth.truth_value.get(px) + val_x = truth.location.get(px) if val_x is not None: ax.axhline(val_x, **truth.kwargs) if py is not None: - val_y = truth.truth_value.get(py) + val_y = truth.location.get(py) if val_y is not None: ax.axvline(val_y, **truth.kwargs) @@ -1290,8 +1315,8 @@ def _plot_bars( if chain.bar_shade: fit_values = self.parent.analysis.get_parameter_summary(chain, column) if fit_values is not None: - lower = fit_values[0] - upper = fit_values[2] + lower = fit_values.lower + upper = fit_values.upper if lower is not None and upper is not None: if lower < xs.min(): lower = xs.min() @@ -1317,7 +1342,7 @@ def _plot_bars( zorder=chain.zorder, ) if summary: - t = self.parent.analysis.get_parameter_text(*fit_values) + t = self.parent.analysis.get_parameter_text(fit_values) if isinstance(column, str): ax.set_title(r"${} = {}$".format(column.strip("$"), t), fontsize=self.config.title_size) else: @@ -1350,7 +1375,7 @@ def _plot_walk( ax.plot(x[:-1], filtered[:-1], ls=":", color=color2, alpha=1) def _plot_walk_truth(self, ax: Axes, truth: Truth, col: str) -> None: - ax.axhline(truth.truth_value[col], **truth.kwargs) + ax.axhline(truth.location[col], **truth.kwargs) def _convert_to_stdev(self, sigma: np.ndarray) -> np.ndarray: # pragma: no cover # From astroML diff --git a/src/chainconsumer/py.typed b/src/chainconsumer/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/chainconsumer/something.py b/src/chainconsumer/something.py new file mode 100644 index 00000000..0e4fe37e --- /dev/null +++ b/src/chainconsumer/something.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel + + +class Something(BaseModel): + message: str + + def get_message(self) -> str: + """Gets the message. + + Returns: + str: The message. + """ + return self.message diff --git a/src/chainconsumer/summary_stats.py b/src/chainconsumer/summary_stats.py new file mode 100644 index 00000000..32316385 --- /dev/null +++ b/src/chainconsumer/summary_stats.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class SummaryStatistic(Enum): + MAX = "max" + MEAN = "mean" + CUMULATIVE = "cumulative" + MAX_SYMMETRIC = "max_symmetric" + MAX_SHORTEST = "max_shortest" + MAX_CENTRAL = "max_central" diff --git a/src/chainconsumer/truth.py b/src/chainconsumer/truth.py index 68e5d7aa..addff441 100644 --- a/src/chainconsumer/truth.py +++ b/src/chainconsumer/truth.py @@ -1,4 +1,5 @@ from typing import Any + import pandas as pd from pydantic import Field, ValidationError, field_validator @@ -6,7 +7,7 @@ class Truth(BetterBase): - truth_value: dict[str, float] = Field( + location: dict[str, float] = Field( default=..., description="The truth value, either as dictionary or pandas series" ) truth_name: str | None = Field(default=None, description="The name of the truth line") @@ -15,8 +16,9 @@ class Truth(BetterBase): line_alpha: float = Field(default=1.0, description="The alpha of the truth line") line_style: str = Field(default="--", description="The style of the truth line") line_zorder: int = Field(default=100, description="The zorder of the truth line") + name: str | None = Field(default=None, description="The name of the truth line") - @field_validator("truth_value") + @field_validator("location") @classmethod def ensure_dict(cls, v): if isinstance(v, dict): diff --git a/tests/test_chain.py b/tests/test_chain.py index 2966f4b3..59c063fc 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -207,66 +207,66 @@ def test_color_data_none(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) c.configure_overrides(color_params=None) - chain = c.chains[0] + chain = c._chains[0] assert chain.get_color_data() is None def test_color_data_p1(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) c.configure_overrides(color_params=self.p[0]) - chain = c.chains[0] + chain = c._chains[0] assert np.all(chain.get_color_data() == self.d[:, 0]) def test_color_data_w(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) c.configure_overrides(color_params="weights") - chain = c.chains[0] + chain = c._chains[0] assert np.all(chain.get_color_data() == self.w) def test_color_data_logw(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) c.configure_overrides(color_params="log_weights") - chain = c.chains[0] + chain = c._chains[0] assert np.all(chain.get_color_data() == np.log(self.w)) def test_color_data_posterior(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) c.configure_overrides(color_params="posterior") - chain = c.chains[0] + chain = c._chains[0] assert np.all(chain.get_color_data() == np.ones(100)) def test_override_color(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, color="#4286f4") c.configure_overrides() - assert c.chains[0].config["color"] == "#4286f4" + assert c._chains[0].config["color"] == "#4286f4" def test_override_linewidth(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, linewidth=2.0) c.configure_overrides(linewidths=[100]) - assert c.chains[0].config["linewidth"] == 100 + assert c._chains[0].config["linewidth"] == 100 def test_override_linestyle(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, linestyle="--") c.configure_overrides() - assert c.chains[0].config["linestyle"] == "--" + assert c._chains[0].config["linestyle"] == "--" def test_override_shade_alpha(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, shade_alpha=0.8) c.configure_overrides() - assert c.chains[0].config["shade_alpha"] == 0.8 + assert c._chains[0].config["shade_alpha"] == 0.8 def test_override_kde(self): c = ChainConsumer() c.add_chain(self.d, parameters=self.p, kde=2.0) c.configure_overrides() - assert c.chains[0].config["kde"] == 2.0 + assert c._chains[0].config["kde"] == 2.0 def test_override_kde_grid(self): c = ChainConsumer() @@ -274,7 +274,7 @@ def test_override_kde_grid(self): z = np.ones((10, 10)) c.add_chain([x, y], weights=z, grid=True, kde=2.0) c.configure_overrides() - assert not c.chains[0].config["kde"] + assert not c._chains[0].config["kde"] def test_cache_invalidation(self): c = ChainConsumer() diff --git a/tests/test_chainconsumer.py b/tests/test_chainconsumer.py index 3b6fa599..d0465342 100644 --- a/tests/test_chainconsumer.py +++ b/tests/test_chainconsumer.py @@ -28,10 +28,10 @@ def test_get_chain_via_object(self): c = ChainConsumer() c.add_chain(self.data, name="A") c.add_chain(self.data, name="B") - assert c.get_chain(c.chains[0])[0] == 0 - assert c.get_chain(c.chains[1])[0] == 1 - assert len(c.get_chain(c.chains[0])) == 1 - assert len(c.get_chain(c.chains[1])) == 1 + assert c.get_chain(c._chains[0])[0] == 0 + assert c.get_chain(c._chains[1])[0] == 1 + assert len(c.get_chain(c._chains[0])) == 1 + assert len(c.get_chain(c._chains[1])) == 1 def test_summary_bad_input1(self): with pytest.raises(AssertionError): @@ -173,7 +173,7 @@ def test_shade_alpha_algorithm1(self): consumer = ChainConsumer() consumer.add_chain(self.data) consumer.configure_overrides() - alpha = consumer.chains[0].config["shade_alpha"] + alpha = consumer._chains[0].config["shade_alpha"] assert alpha == 1.0 def test_shade_alpha_algorithm2(self): @@ -181,8 +181,8 @@ def test_shade_alpha_algorithm2(self): consumer.add_chain(self.data) consumer.add_chain(self.data) consumer.configure_overrides() - alpha0 = consumer.chains[0].config["shade_alpha"] - alpha1 = consumer.chains[0].config["shade_alpha"] + alpha0 = consumer._chains[0].config["shade_alpha"] + alpha1 = consumer._chains[0].config["shade_alpha"] assert alpha0 == 1.0 / np.sqrt(2.0) assert alpha1 == 1.0 / np.sqrt(2.0) @@ -192,7 +192,7 @@ def test_shade_alpha_algorithm3(self): consumer.add_chain(self.data) consumer.add_chain(self.data) consumer.configure_overrides() - alphas = [c.config["shade_alpha"] for c in consumer.chains] + alphas = [c.config["shade_alpha"] for c in consumer._chains] assert len(alphas) == 3 assert alphas[0] == 1.0 / np.sqrt(3.0) assert alphas[1] == 1.0 / np.sqrt(3.0) @@ -203,8 +203,8 @@ def test_covariance(self): cov = [[1, 1], [1, 2.5]] c = ChainConsumer() c.add_covariance(mean, cov) - mean_obs = np.mean(c.chains[0].chain, axis=0) - cov_obs = np.cov(c.chains[0].chain.T) + mean_obs = np.mean(c._chains[0].chain, axis=0) + cov_obs = np.cov(c._chains[0].chain.T) assert np.all(np.isclose(mean, mean_obs, atol=1e-2)) assert np.all(np.isclose(cov, cov_obs, atol=1e-2)) @@ -212,4 +212,4 @@ def test_marker(self): loc = [0, 1, 2] c = ChainConsumer() c.add_marker(loc) - assert np.all(np.equal(loc, c.chains[0].chain[0, :])) + assert np.all(np.equal(loc, c._chains[0].chain[0, :])) diff --git a/tests/test_plotter.py b/tests/test_plotter.py index 180c7b85..9f45cb74 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -14,7 +14,7 @@ def test_plotter_extents1(self): c = ChainConsumer() c.add_chain(self.data, parameters=["x"]) c.configure_overrides() - minv, maxv = c.plotter._get_parameter_extents("x", c.chains) + minv, maxv = c.plotter._get_parameter_extents("x", c._chains) assert np.isclose(minv, (5.0 - 1.5 * 3.7), atol=0.2) assert np.isclose(maxv, (5.0 + 1.5 * 3.7), atol=0.2) @@ -23,7 +23,7 @@ def test_plotter_extents2(self): c.add_chain(self.data, parameters=["x"]) c.add_chain(self.data + 5, parameters=["y"]) c.configure_overrides() - minv, maxv = c.plotter._get_parameter_extents("x", c.chains) + minv, maxv = c.plotter._get_parameter_extents("x", c._chains) assert np.isclose(minv, (5.0 - 1.5 * 3.7), atol=0.2) assert np.isclose(maxv, (5.0 + 1.5 * 3.7), atol=0.2) @@ -32,7 +32,7 @@ def test_plotter_extents3(self): c.add_chain(self.data, parameters=["x"]) c.add_chain(self.data + 5, parameters=["x"]) c.configure_overrides() - minv, maxv = c.plotter._get_parameter_extents("x", c.chains) + minv, maxv = c.plotter._get_parameter_extents("x", c._chains) assert np.isclose(minv, (5.0 - 1.5 * 3.7), atol=0.2) assert np.isclose(maxv, (10.0 + 1.5 * 3.7), atol=0.2) @@ -41,7 +41,7 @@ def test_plotter_extents4(self): c.add_chain(self.data, parameters=["x"]) c.add_chain(self.data + 5, parameters=["y"]) c.configure_overrides() - minv, maxv = c.plotter._get_parameter_extents("x", c.chains[:1]) + minv, maxv = c.plotter._get_parameter_extents("x", c._chains[:1]) assert np.isclose(minv, (5.0 - 1.5 * 3.7), atol=0.2) assert np.isclose(maxv, (5.0 + 1.5 * 3.7), atol=0.2) @@ -54,7 +54,7 @@ def test_plotter_extents5(self): c = ChainConsumer() c.add_chain(chain, parameters=["x", "y"], weights=pdf, grid=True) c.configure_overrides() - minv, maxv = c.plotter._get_parameter_extents("x", c.chains) + minv, maxv = c.plotter._get_parameter_extents("x", c._chains) assert np.isclose(minv, -3, atol=0.001) assert np.isclose(maxv, 3, atol=0.001) @@ -67,6 +67,6 @@ def test_plotter_extents6(self): c.add_chain(data, parameters=["x"], posterior=posterior, plot_point=True, plot_contour=False) c.configure_overrides() - minv, maxv = c.plotter._get_parameter_extents("x", c.chains) + minv, maxv = c.plotter._get_parameter_extents("x", c._chains) assert np.isclose(minv, -1, atol=0.01) assert np.isclose(maxv, 1, atol=0.01) From c6a62ef02f6da04bb388c58d9f8ad3a59db55253 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Sat, 7 Oct 2023 23:38:25 +1000 Subject: [PATCH 07/22] Fixing more tests --- .gitignore | 4 +- Makefile | 2 +- doc_sphinx/Makefile | 178 -------------- doc_sphinx/_static/theme_override.css | 67 ------ doc_sphinx/build.bat | 2 - doc_sphinx/chain_api.rst | 115 --------- doc_sphinx/conf.py | 289 ---------------------- doc_sphinx/index.rst | 107 --------- doc_sphinx/make.bat | 285 ---------------------- doc_sphinx/usage.rst | 63 ----- docs/{api => }/api.md | 3 +- docs/examples/plot_contours.py | 34 ++- docs/index.md | 64 ++++- docs/resources/example.png | Bin 0 -> 107842 bytes {examples => docs}/resources/stats.png | Bin docs/statistics.md | 7 + mkdocs.yml | 16 +- src/chainconsumer/__init__.py | 5 +- src/chainconsumer/analysis.py | 29 +-- src/chainconsumer/chain.py | 26 +- src/chainconsumer/chainconsumer.py | 10 +- src/chainconsumer/diagnostic.py | 1 - src/chainconsumer/examples.py | 22 ++ src/chainconsumer/plotter.py | 11 +- src/chainconsumer/something.py | 13 - src/chainconsumer/statistics.py | 20 ++ src/chainconsumer/summary_stats.py | 10 - src/chainconsumer/truth.py | 23 +- tests/test_chain.py | 316 ++++--------------------- tests/test_chainconsumer.py | 218 ++--------------- tests/test_colours.py | 23 +- tests/test_kde.py | 4 +- 32 files changed, 263 insertions(+), 1704 deletions(-) delete mode 100644 doc_sphinx/Makefile delete mode 100644 doc_sphinx/_static/theme_override.css delete mode 100644 doc_sphinx/build.bat delete mode 100644 doc_sphinx/chain_api.rst delete mode 100644 doc_sphinx/conf.py delete mode 100644 doc_sphinx/index.rst delete mode 100644 doc_sphinx/make.bat delete mode 100644 doc_sphinx/usage.rst rename docs/{api => }/api.md (90%) create mode 100644 docs/resources/example.png rename {examples => docs}/resources/stats.png (100%) create mode 100644 docs/statistics.md create mode 100644 src/chainconsumer/examples.py delete mode 100644 src/chainconsumer/something.py create mode 100644 src/chainconsumer/statistics.py delete mode 100644 src/chainconsumer/summary_stats.py diff --git a/.gitignore b/.gitignore index 4ae9311e..504b1c9d 100644 --- a/.gitignore +++ b/.gitignore @@ -53,11 +53,11 @@ ipython_config.py __pypackages__/ .python-version qodana.yaml -.idea **/*.log **/*tmp* **/*secret* **/*.pkl -generated/ \ No newline at end of file +generated/ +.mypy_cache \ No newline at end of file diff --git a/Makefile b/Makefile index e6e76e9c..c5110b08 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ test: poetry run pytest serve: - # rm -rf docs/generated/gallery; + rm -rf docs/generated/gallery; poetry run mkdocs serve --clean tests: test diff --git a/doc_sphinx/Makefile b/doc_sphinx/Makefile deleted file mode 100644 index f96a19f5..00000000 --- a/doc_sphinx/Makefile +++ /dev/null @@ -1,178 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = out - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - rm -rf api/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sep.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sep.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/sep" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sep" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." \ No newline at end of file diff --git a/doc_sphinx/_static/theme_override.css b/doc_sphinx/_static/theme_override.css deleted file mode 100644 index 822d11cc..00000000 --- a/doc_sphinx/_static/theme_override.css +++ /dev/null @@ -1,67 +0,0 @@ -.wy-nav-top { - background-color: #208ae0 !important; - background: linear-gradient(to bottom, #1a7ccc 0%,#4ba3ea 100%); -} -.wy-side-nav-search { - background-color: #208ae0 !important; - background: linear-gradient(to bottom, #1a7ccc 0%,#4ba3ea 100%); -} - -.wy-menu-vertical a { - line-height: 1.7; - font-size: 100%; -} - -li>strong { - font-weight: bold; - color: #208ae0; - font-size: 1.1em; - padding-right: 0.5em; -} - -.field-body ul li { - padding-bottom: 0.5em; -} - -.wy-nav-side { - box-shadow: 0px 0px 15px 0px #444444; -} - -table.borderless { - border: none !important; -} - -table.borderless td { - background-color: transparent !important; - border: none !important; - border-bottom: none !important; - border-top: none !important; - border-right: none !important; -} - -.bounding img { - padding: 5px; -} - -table.borderless p { - text-align: center; -} - -.wy-table-responsive { - overflow: auto !important; -} - -.wy-nav-content { - max-width: 1000px !important; -} - -.rst-content dl.class > dt { - font-size: 1.2em; - margin-top: 30px !important; - margin-bottom: 30px; - width: 100%; -} - -.rst-content dl.method > dt { - width: 100%; -} \ No newline at end of file diff --git a/doc_sphinx/build.bat b/doc_sphinx/build.bat deleted file mode 100644 index d67126a9..00000000 --- a/doc_sphinx/build.bat +++ /dev/null @@ -1,2 +0,0 @@ -pip uninstall -y chainconsumer -cd .. && python setup.py install && cd doc && make clean && make rst && make htmlfull \ No newline at end of file diff --git a/doc_sphinx/chain_api.rst b/doc_sphinx/chain_api.rst deleted file mode 100644 index 9dd26ecd..00000000 --- a/doc_sphinx/chain_api.rst +++ /dev/null @@ -1,115 +0,0 @@ - -.. _chain_api: - -================== -Chain Consumer API -================== - -ChainConsumer has a number of different methods that can be access. In the latest version -of ChainConsumer, the increasing number of methods has had them put into smaller classes within -ChainConsumer. - -Basic Methods -------------- - -The methods found in the ChainConsumer class itself all relate to add, manipulating, and configuring -the chains fed in. - -* :func:`chainconsumer.ChainConsumer.add_chain` - Add a chain! -* :func:`chainconsumer.ChainConsumer.add_marker` - Add a marker! -* :func:`chainconsumer.ChainConsumer.add_covariance` - Add a Gaussian to the mix. -* :func:`chainconsumer.ChainConsumer.divide_chain` - Split a chain into multiple chains to inspect each walk. -* :func:`chainconsumer.ChainConsumer.remove_chain` - Remove a chain. -* :func:`chainconsumer.ChainConsumer.configure` - Configure ChainConsumer. -* :func:`chainconsumer.ChainConsumer.configure_truth` - Configure how truth values are plotted. - -Plotter Class -------------- - -The plotter class, accessible via `chainConsumer.plotter` contains the methods -used for generating plots. - - -* :func:`chainconsumer.plotter.Plotter.plot` - Plot the posterior surfaces -* :func:`chainconsumer.plotter.Plotter.plot_walks` - Plot the walks to visually inspect convergence. -* :func:`chainconsumer.plotter.Plotter.plot_distributions` - Plot the marginalised distributions only. -* :func:`chainconsumer.plotter.Plotter.plot_summary` - Plot the marginalised distributions only. -* :func:`chainconsumer.plotter.Plotter.plot_contour` - Pass in an axis for a contour plot on an external figure. - -Analysis Class --------------- - -The plotter class, accessible via `chainConsumer.analysis` contains the methods -used for getting data or LaTeX analysis of the chains fed in. - -* :func:`chainconsumer.analysis.Analysis.get_latex_table` - Return a LaTeX table of the parameter summaries. -* :func:`chainconsumer.analysis.Analysis.get_parameter_text` - Return LaTeX text for specified parameter bounds. -* :func:`chainconsumer.analysis.Analysis.get_summary` - Get the parameter bounds for your chains. -* :func:`chainconsumer.analysis.Analysis.get_max_posteriors` - Get the parameters for the point with greatest posterior. -* :func:`chainconsumer.analysis.Analysis.get_correlations` - Get the parameters and correlation matrix for a chain. -* :func:`chainconsumer.analysis.Analysis.get_correlation_table` - Get a chain's correlation matrix as a LaTeX table. -* :func:`chainconsumer.analysis.Analysis.get_covariance` - Get the parameters and covariance matrix for a chain. -* :func:`chainconsumer.analysis.Analysis.get_covariance_table` - Get a chain's covariance matrix as a LaTeX table. - - - -Diagnostic Class ----------------- - -The plotter class, accessible via `chainConsumer.diagnostic` contains the methods -used for checking chain convergence - -* :func:`chainconsumer.diagnostic.Diagnostic.gelman_rubin` - Run the Gelman-Rubin statistic on your chains. -* :func:`chainconsumer.diagnostic.Diagnostic.geweke` - Run the Geweke statistic on your chains. - -Model Comparison Class ----------------------- - - -The plotter class, accessible via `chainConsumer.comparison` contains the methods -used for comparing the chains from various models. - -* :func:`chainconsumer.comparisons.Comparison.comparison.aic` - Return the AICc values for all chains. -* :func:`chainconsumer.comparisons.Comparison.comparison.bic` - Return the BIC values for all chains. -* :func:`chainconsumer.comparisons.Comparison.comparison.dic` - Return the DIC values for all chains. -* :func:`chainconsumer.comparisons.Comparison.comparison.comparison_table` - Return a LaTeX table comparing models as per the above methods. - - -The full documentation can be found below. - -Full Documentation ------------------- - -.. autoclass:: chainconsumer.ChainConsumer - :members: - - ------- - - -.. autoclass:: chainconsumer.analysis.Analysis - :members: - - ------- - - -.. autoclass:: chainconsumer.comparisons.Comparison - :members: - - ------- - - -.. autoclass:: chainconsumer.diagnostic.Diagnostic - :members: - - ------- - - -.. autoclass:: chainconsumer.plotter.Plotter - :members: - - ------- diff --git a/doc_sphinx/conf.py b/doc_sphinx/conf.py deleted file mode 100644 index 23e229a5..00000000 --- a/doc_sphinx/conf.py +++ /dev/null @@ -1,289 +0,0 @@ -# -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import re - -import sphinx_rtd_theme - -# 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. -# sys.path.insert(0, os.path.abspath('.')) -# sys.path.append(os.path.abspath('ext')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' - -# intersphinx_mapping = { -# 'python': ('http://docs.python.org/', None), -# 'numpy': ('http://docs.scipy.org/doc/numpy/', None), -# 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None)} - -# 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.autosummary", - "sphinx.ext.intersphinx", - "sphinx.ext.mathjax", - "sphinx.ext.napoleon", - # 'numpydoc', - "sphinx_gallery.gen_gallery", -] -numpydoc_show_class_members = False -autosummary_generate = True -autoclass_content = "class" -autodoc_default_flags = ["members", "no-special-members"] -sphinx_gallery_conf = { - "filename_pattern": "plot_", - "examples_dirs": "../examples", # path to examples scripts - "gallery_dirs": "examples", # path to gallery generated examples -} -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# The suffix of source filenames. -source_suffix = ".rst" - -# The encoding of source files. -# source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = "index" - -# General information about the project. -version = re.findall(r"__version__ = \"(.*?)\"", open("../chainconsumer/chainconsumer.py").read())[0] - -project = "ChainConsumer" -copyright = "2016-2017, Samuel Hinton and contributors" - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = ".".join(version.split(".")[0:2]) - -# The full version, including alpha/beta/rc tags. -release = version - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ["_build"] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -default_role = "obj" - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -html_theme = "sphinx_rtd_theme" -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - - -def setup(app): - app.add_stylesheet("theme_override.css") - - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None - -# 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"] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = "ChainConsumerDoc" - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - #'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - #'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - #'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ("index", "chainConsumer.tex", "ChainConsumer Documentation", "Samuel Hinton", "manual"), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [("index", "ChainConsumer", "ChainConsumer Documentation", ["Samuel Hinton"], 1)] - -# If true, show URL addresses after external links. -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - "index", - "ChainConsumer", - "ChainConsumer Documentation", - "Samuel Hinton", - "ChainConsumer", - "One line description of project.", - "Miscellaneous", - ), -] - -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False diff --git a/doc_sphinx/index.rst b/doc_sphinx/index.rst deleted file mode 100644 index f5bc1bcd..00000000 --- a/doc_sphinx/index.rst +++ /dev/null @@ -1,107 +0,0 @@ -============= -ChainConsumer -============= - -ChainConsumer is a python package designed to do one thing - consume the chains -output from Monte Carlo processes like MCMC. ChainConsumer can utilise these chains -to produce plots of the posterior surface inferred from the chain distributions, -to plot the chains as walks (to check for mixing and convergence), and to output -parameter summaries in the form of LaTeX tables. On top of all of this, -if you have multiple models (chains), you can load them all in and perform some -model comparison using AIC, BIC or DIC metrics. - -To get things started, here is a basic example: - -.. code-block:: python - - import numpy as np - from chainconsumer import ChainConsumer - - mean = [0.0, 4.0] - data = np.random.multivariate_normal(mean, [[1.0, 0.7], [0.7, 1.5]], size=100000) - - c = ChainConsumer() - c.add_chain(data, parameters=["$x_1$", "$x_2$"]) - c.plotter.plot(filename="example.png", figsize="column", truth=mean) - - -The output figure is displayed below. - -.. figure:: ../paper/example.png - :align: center - :width: 80% - -Or you can add more models and look at the summaries between them. Or a ton more, check the examples! - -.. figure:: ../examples/resources/summary.png - :align: center - :width: 60% - -Check out the API and far more :ref:`examples-index` below: - -Contents --------- - -.. toctree:: - :maxdepth: 2 - - usage - examples/index - chain_api - -Installation ------------- - -ChainConsumer requires the following dependencies, along with a LaTeX installation and `dvipng` (a maptlotlib dependency):: - - numpy - scipy - matplotlib - statsmodels - -ChainConsumer can be installed as follows:: - - pip install chainconsumer - -Common Issues -------------- - -Users on some Linux platforms have reported issues rendering plots using ChainConsumer. -The common error states that `dvipng: not found`, and as per `StackOverflow `_ -post, it can be solved by explicitly install the `matplotlib` dependency `dvipng` via `sudo apt-get install dvipng`. - -If you are running on HPC or clusters where you can't install things yourself, -users may run into issues where LaTeX or other optional dependencies aren't installed. -In this case, ensure `usetex=False` in `configure` to request matplotlib not try to use TeX. -If this does not work, also set `serif=False`, which has helped some uses. - - - -Citing ------- - -You can cite ChainConsumer using the following BibTeX:: - - @ARTICLE{Hinton2016, - author = {{Hinton}, S.~R.}, - title = "{ChainConsumer}", - journal = {The Journal of Open Source Software}, - year = 2016, - month = aug, - volume = 1, - eid = {00045}, - pages = {00045}, - doi = {10.21105/joss.00045}, - adsurl = {http://adsabs.harvard.edu/abs/2016JOSS....1...45H}, - } - -Contributing ------------- - -Users that wish to contribute to this project may do so in a number of ways. -Firstly, for any feature requests, bugs or general ideas, please raise an issue -via `Github `_. - -If you wish to contribute code to the project, please simple fork the project on -Github and then raise a pull request. Pull requests will be reviewed to determine -whether the changes are major or minor in nature, and to ensure all changes are tested. \ No newline at end of file diff --git a/doc_sphinx/make.bat b/doc_sphinx/make.bat deleted file mode 100644 index a6a1eadc..00000000 --- a/doc_sphinx/make.bat +++ /dev/null @@ -1,285 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=out -set BUILDDIR2=examples -set BUILDDIR3=modules -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - echo. coverage to run coverage check of the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - for /d %%i in (%BUILDDIR2%\*) do rmdir /q /s %%i - for /d %%i in (%BUILDDIR3%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - del /q /s %BUILDDIR2%\* - del /q /s %BUILDDIR3%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 1>NUL 2>NUL -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "htmlfull" ( - %SPHINXBUILD% -E -a -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "rst" ( - sphinx-apidoc -fM -o . ../chainconsumer - if errorlevel 1 exit /b 1 - echo. - echo.Made rst - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\dessn.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\dessn.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ -results in %BUILDDIR%/coverage/python.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/doc_sphinx/usage.rst b/doc_sphinx/usage.rst deleted file mode 100644 index bcc3d852..00000000 --- a/doc_sphinx/usage.rst +++ /dev/null @@ -1,63 +0,0 @@ -===== -Usage -===== - -I recommend going straight to the :ref:`chain_api` and -the :ref:`examples-index` page for details on how to use ChainConsumer. - -The Process ------------ - -The process of using ChainConsumer should be straightforward: - -1. Create an instance of ChainConsumer. -2. Add your chains to this instance. -3. Run convergence diagnostics, if desired. -4. Update the configurations if needed (make sure you do this *after* loading in the data). -5. Plot. - -The main page and the examples page has code demonstrating these, -so I won't repeat it here. - - - - -Statistics ----------- - -An area of some difference in analyses is how to generate summary statistics -from marginalised posterior distributions. ChainConsumer comes equipped -with the several different methods that can be configured with the -`configure` method. The three methods are: - -Maximum Likelihood Statistics - The default statistic used by ChainConsumer, maximum likelihood statistics - report asymmetric uncertainty, from the point of maximum likelihood to the - iso-likelihood levels above and below the maximal point. -Cumulative Statistics - For cumulative statistics , the lower :math:`1\sigma` confidence bound, mean value, - and upper bound are respectively given by the value of the cumulative function - at points :math:`C(x) = 0.15865`, :math:`0.5`, and :math:`0.84135`. -Mean Statistics - Mean statistics report the same upper and lower confidence bound as cumulative - statistics, however report symmetric error bars by having the primary statistic - reported as the mean of the lower and upper bound. -Max Symmetric - See `Figure 6(1) of Andrae (2010) `_. Maximum - likelihood with error with symmetric errors to get the desired confidence interval. -Max Shortest - See `Figure 6(2) of Andrae (2010) `_. Maximum - likelihood with uncertainty bounds that minimise the distance between bounds. -Max Central - See `Figure 6(3) of Andrae (2010) `_. Maximum - likelihood with uncertainty bounds from the CDF (i.e. same as cumulative stats - but the central point is the maximum likelihood point and not the :math:`x` such that - :math:`C(x)=0.5`. - - -All three methods are illustrated below. - -.. figure:: ../examples/resources/stats.png - :align: center - :width: 80% - diff --git a/docs/api/api.md b/docs/api.md similarity index 90% rename from docs/api/api.md rename to docs/api.md index 2b6d2cf3..993fe281 100644 --- a/docs/api/api.md +++ b/docs/api.md @@ -5,9 +5,10 @@ The ChainConsumer acts as manager and state holder, to which you supply configur ::: chainconsumer.chainconsumer.ChainConsumer - ::: chainconsumer.chain.Chain ::: chainconsumer.chain.ChainConfig +::: chainconsumer.truth.Truth + diff --git a/docs/examples/plot_contours.py b/docs/examples/plot_contours.py index 7fe977cd..118aa6bf 100644 --- a/docs/examples/plot_contours.py +++ b/docs/examples/plot_contours.py @@ -5,18 +5,15 @@ handle the defaults and display. """ - -import pandas as pd -from scipy.stats import multivariate_normal as mv - -from chainconsumer import Chain, ChainConfig, ChainConsumer, PlotConfig, Truth +from chainconsumer import Chain, ChainConfig, ChainConsumer, PlotConfig, Truth, make_sample # Here's what you might start with -norm = mv(mean=[0.0, 4.0], cov=[[1.0, 0.7], [0.7, 1.5]]) # type: ignore -data = norm.rvs(size=1000000) -df = pd.DataFrame(data, columns=["x_1", "x_2"]) +df = make_sample(num_dimensions=2, seed=1) +print(df.head()) -# And how we give this to chainconsumer +# %% New cell + +# And now we give this to chainconsumer c = ChainConsumer() c.add_chain(Chain(samples=df, name="An Example Contour")) fig = c.plotter.plot() @@ -29,7 +26,7 @@ chain2 = Chain.from_covariance( [3.0, 1.0], [[1.0, -0.7], [-0.7, 1.5]], - columns=["x_1", "x_2"], + columns=["A", "B"], name="Another contour!", color="#065f46", linestyle=":", @@ -45,8 +42,8 @@ # plenty of very specific examples in a sub gallery you can check out, but as a final one for here, # let's add markers and truth values. -c.add_marker(location={"x_1": 4, "x_2": 4}, name="A point", color="orange", marker_style="P", marker_size=50) -c.add_truth(Truth(location={"x_1": 5, "x_2": 5})) +c.add_marker(location={"A": 0, "B": 2}, name="A point", color="orange", marker_style="P", marker_size=50) +c.add_truth(Truth(location={"A": 0, "B": 1})) fig = c.plotter.plot() @@ -60,10 +57,7 @@ # To keep this clean, let's remake everything. I'm going to add an extra few columns into our # dataframe. You'll see what they do -df2 = df.assign( - x_3=lambda x: x["x_1"] + x["x_2"], - log_posterior=lambda x: norm.logpdf(x[["x_1", "x_2"]]), -) +df2 = df.assign(C=lambda x: x["A"] + x["B"]) c = ChainConsumer() # Customise the chain when you add it @@ -80,20 +74,20 @@ linewidth=2.0, cmap="magma", show_contour_labels=True, - color_param="x_3", + color_param="C", ) c.add_chain(chain) # You can also override *all* chains at once like so # Notice that Chain is a child of ChainConfig # So you could override base properties like line weights... but not samples -c.add_override(ChainConfig(sigmas=[0, 1, 2, 3])) -c.add_truth(Truth(location={"x_1": 0, "x_2": 4}, line_color="#500724")) +c.set_override(ChainConfig(sigmas=[0, 1, 2, 3])) +c.add_truth(Truth(location={"A": 0, "B": 1}, color="#500724")) # And if we want to change the plot itself in some way, we can do that via c.set_plot_config( PlotConfig( flip=True, - labels={"x_1": "$x_1$", "x_2": "$x_2$", "x_3": "$x_3$"}, + labels={"A": "$A$", "B": "$B$", "C": r"$\alpha^2$"}, contour_label_font_size=12, ) ) diff --git a/docs/index.md b/docs/index.md index f9def3b9..9505a8dc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,65 @@ # ChainConsumer -Welcome to the party \ No newline at end of file + +ChainConsumer is a python package designed to do one thing - consume the chains output from Monte Carlo processes like MCMC. ChainConsumer can utilise these chains to produce plots of the posterior surface inferred from the chain distributions, to plot the chains as walks (to check for mixing and convergence), and to output parameter summaries in the form of LaTeX tables. On top of all of this, if you have multiple models (chains), you can load them all in and perform some model comparison using AIC, BIC or DIC metrics. + +## Installation + +The latest version of ChainConsumer requires at least Python 3.10. + +`pip install chainconsumer` + +## Basic Example + +If you have some samples, analysing them should be straightforward: + +```python +from chainconsumer import Chain, ChainConsumer, make_sample + + +df = make_sample() +c = ChainConsumer() +c.add_chain(Chain(samples=df, name="An Example Contour")) +fig = c.plotter.plot() +``` + +![](resources/example.png) + + +## Common Issues + +Users on some Linux platforms have reported issues rendering plots using ChainConsumer. The common error states that `dvipng: not found`, and as per this [StackOverflow](http://stackoverflow.com/a/32915992/3339667) +post, it can be solved by explicitly installing the `matplotlib` dependency `dvipng` via `sudo apt-get install dvipng`. + +If you are running on HPC or clusters where you can't install things yourself, +users may run into issues where LaTeX or other optional dependencies aren't installed. In this case, ensure `usetex=False` in your `PlotConfig` (which is the default). If this does not work, also set `serif=False`, which has helped some uses. + +## Citing + + +You can cite ChainConsumer using the following BibTeX: + +```bash + @ARTICLE{Hinton2016, + author = {{Hinton}, S.~R.}, + title = "{ChainConsumer}", + journal = {The Journal of Open Source Software}, + year = 2016, + month = aug, + volume = 1, + eid = {00045}, + pages = {00045}, + doi = {10.21105/joss.00045}, + adsurl = {http://adsabs.harvard.edu/abs/2016JOSS....1...45H}, + } +``` + +## Contributing + + +Users that wish to contribute to this project may do so in a number of ways. +Firstly, for any feature requests, bugs or general ideas, please raise an issue via [Github](https://github.com/samreay/ChainConsumer/issues). + +If you wish to contribute code to the project, please simple fork the project on Github and then raise a pull request. Pull requests will be reviewed to determine whether the changes are major or minor in nature, and to ensure all changes are tested. + +After cloning down the project, run `make install` to install all dependencies and pre-commit hook. \ No newline at end of file diff --git a/docs/resources/example.png b/docs/resources/example.png new file mode 100644 index 0000000000000000000000000000000000000000..11eb1779109eaec5c1077afe83c179c6d5e9e7ba GIT binary patch literal 107842 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⁡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 literal 0 HcmV?d00001 diff --git a/examples/resources/stats.png b/docs/resources/stats.png similarity index 100% rename from examples/resources/stats.png rename to docs/resources/stats.png diff --git a/docs/statistics.md b/docs/statistics.md new file mode 100644 index 00000000..391212c6 --- /dev/null +++ b/docs/statistics.md @@ -0,0 +1,7 @@ +# Statistics + +When summarising chains, ChainConsumer offers several different methods. The below image shows the upper and lower bounds and central points for the "MEAN", "CUMULATIVE", and "MAX" methods respectively. The "MAX_CENTRAL" method is the blue central value and the red bounds. + +![](resources/stats.png) + +::: chainconsumer.statistics.SummaryStatistic \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 0a86fa7d..835e60de 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,15 +40,10 @@ plugins: show_root_heading: true show_if_no_docstring: true show_signature_annotations: true - extensions: - - docs/plugins/griffe_doclinks.py - # gallery: - # examples_dirs: docs/examples # path to your example scripts - # gallery_dirs: docs/generated/gallery # where to save generated gallery - # image_srcset: ['2x'] - # image_scrapers: matplotlib - # compress_images: ['images', 'thumbnails'] - # doc_module: ['mkdocs_gallery', 'numpy'] + gallery: + examples_dirs: docs/examples # path to your example scripts + gallery_dirs: docs/generated/gallery # where to save generated gallery + image_srcset: ['2x'] search: {} markdown_extensions: @@ -68,5 +63,6 @@ watch: - src nav: - Home: index.md - - Python API: api/api.md + - Statistics: statistics.md - Examples: generated/gallery + - Python API: api.md diff --git a/src/chainconsumer/__init__.py b/src/chainconsumer/__init__.py index a9866382..d7a5c555 100644 --- a/src/chainconsumer/__init__.py +++ b/src/chainconsumer/__init__.py @@ -1,6 +1,7 @@ from .chain import Chain, ChainConfig from .chainconsumer import ChainConsumer -from .truth import Truth +from .examples import make_sample from .plotter import PlotConfig +from .truth import Truth -__all__ = ["ChainConsumer", "Chain", "ChainConfig", "Truth", "PlotConfig"] +__all__ = ["ChainConsumer", "Chain", "ChainConfig", "Truth", "PlotConfig", "make_sample"] diff --git a/src/chainconsumer/analysis.py b/src/chainconsumer/analysis.py index e5892099..95222e31 100644 --- a/src/chainconsumer/analysis.py +++ b/src/chainconsumer/analysis.py @@ -13,7 +13,7 @@ from .chain import Chain, ChainName, ColumnName, MaxPosterior, Named2DMatrix from .helpers import get_bins, get_grid_bins, get_latex_table_frame, get_smoothed_bins from .kde import MegKDE -from .summary_stats import SummaryStatistic +from .statistics import SummaryStatistic class Bound(BetterBase): @@ -23,7 +23,7 @@ class Bound(BetterBase): class Analysis: - def __init__(self, parent: "ChainConsumer"): + def __init__(self, parent: ChainConsumer): self.parent = parent self._logger = logging.getLogger("chainconsumer") @@ -31,7 +31,6 @@ def __init__(self, parent: "ChainConsumer"): SummaryStatistic.MAX: self.get_parameter_summary_max, SummaryStatistic.MEAN: self.get_parameter_summary_mean, SummaryStatistic.CUMULATIVE: self.get_parameter_summary_cumulative, - SummaryStatistic.MAX_SHORTEST: self.get_parameter_summary_max_shortest, SummaryStatistic.MAX_CENTRAL: self.get_parameter_summary_max_central, } @@ -99,7 +98,7 @@ def get_latex_table( if hlines: center_text += "\t\t" + hline_text for p in columns: - arr = ["\t\t" + self.parent.get_label(p)] + arr = ["\t\t" + self.parent.plotter.config.get_label(p)] for _, column_results in fit_values.items(): if p in column_results: arr.append(self.get_parameter_text(column_results[p], wrap=True)) @@ -107,7 +106,7 @@ def get_latex_table( arr.append(blank_fill) center_text += " & ".join(arr) + end_text else: - center_text += " & ".join(["Model", *[self.parent.get_label(c) for c in columns]]) + end_text + center_text += " & ".join(["Model", *[self.parent.plotter.config.get_label(c) for c in columns]]) + end_text if hlines: center_text += "\t\t" + hline_text for name, chain_res in fit_values.items(): @@ -281,7 +280,7 @@ def _get_smoothed_histogram( return xs, ys, cs def _get_2d_latex_table(self, named_matrix: Named2DMatrix, caption: str, label: str): - parameters = [self.parent.get_label(c) for c in named_matrix.columns] + parameters = [self.parent.plotter.config.get_label(c) for c in named_matrix.columns] matrix = named_matrix.matrix latex_table = get_latex_table_frame(caption=caption, label=label) column_def = "c|%s" % ("c" * len(parameters)) @@ -428,28 +427,10 @@ def get_parameter_summary_max(self, chain: Chain, column: ColumnName) -> Bound | return Bound(lower=x1, center=float(xs[start_index]), upper=x2) - def get_parameter_summary_max_shortest(self, chain, parameter): - xs, ys, cs = self._get_smoothed_histogram(chain, parameter) - - c_to_x = interp1d(cs, xs, bounds_error=False, fill_value=(-np.inf, np.inf)) # type: ignore - - # Get max likelihood x - max_index = ys.argmax() - x = xs[max_index] - - # Pair each lower bound with an upper to get the right area - x2 = c_to_x(cs + chain.summary_area) - dists = x2 - xs - mask = (xs > x) | (x2 < x) # Ensure max point is inside the area - dists[mask] = np.inf - ind = dists.argmin() - return Bound(lower=xs[ind], center=x, upper=x2[ind]) - def get_parameter_summary_max_central(self, chain, parameter): xs, ys, cs = self._get_smoothed_histogram(chain, parameter) c_to_x = interp1d(cs, xs) - # Get max likelihood x max_index = ys.argmax() x = xs[max_index] diff --git a/src/chainconsumer/chain.py b/src/chainconsumer/chain.py index c388e710..8694b84e 100644 --- a/src/chainconsumer/chain.py +++ b/src/chainconsumer/chain.py @@ -18,7 +18,7 @@ from .base import BetterBase from .colors import ColorInput, colors -from .summary_stats import SummaryStatistic +from .statistics import SummaryStatistic ChainName: TypeAlias = str ColumnName: TypeAlias = str @@ -42,7 +42,7 @@ class ChainConfig(BetterBase): """ statistics: SummaryStatistic = Field(default=SummaryStatistic.MAX, description="The summary statistic to use") - summary_area: float = Field(default=0.6827, description="The area to use for summary statistics") + summary_area: float = Field(default=0.6827, ge=0, le=1.0, description="The area to use for summary statistics") sigmas: list[float] = Field(default=[0, 1, 2], description="The sigmas to use for summary statistics") color: ColorInput = Field(default=None, description="The color of the chain") # type: ignore linestyle: str = Field(default="-", description="The line style of the chain") @@ -102,7 +102,7 @@ class Chain(ChainConfig): ) weight_column: ColumnName = Field( - default="weights", + default="weight", description="The name of the weight column, if it exists", ) posterior_column: ColumnName = Field( @@ -209,10 +209,17 @@ def _validate_model(self) -> Chain: # If weights aren't set, add them all as one if self.weight_column not in self.samples: + assert ( + self.weight_column == "weight" + ), f"weight column has been changed to {self.weight_column}, but its not in the dataframe" + self.samples[self.weight_column] = 1.0 else: assert np.all(self.weights > 0), "Weights must be positive and non-zero" - assert np.all(np.isfinite(self.weights)), "Weights must be finite" + + for column in self.samples.columns: + assert isinstance(column, str), f"Column {column} is not a string" + assert np.all(np.isfinite(self.samples[column])), f"Column {column} has NaN or inf in it" # Apply the mean shift if it is set to true if self.shift_params: @@ -225,16 +232,19 @@ def _validate_model(self) -> Chain: "which is not divisible by {self.walkers} walkers. This is not good." ) - # And the log posterior - if self.log_posterior is not None: - assert np.all(np.isfinite(self.log_posterior)), f"Chain {self.name} has NaN or inf in the log-posterior" - # And if the color_params are set, ensure they're in the dataframe if self.color_param is not None: assert ( self.color_param in self.samples.columns ), f"Chain {self.name} does not have color parameter {self.color_param}" + # more nan checks + if self.num_eff_data_points is not None: + assert np.isfinite(self.num_eff_data_points), "num_eff_data_points is not finite" + + if self.num_free_params is not None: + assert np.isfinite(self.num_free_params), "num_free_params is not finite" + return self def get_data(self, column: str) -> pd.Series[float]: diff --git a/src/chainconsumer/chainconsumer.py b/src/chainconsumer/chainconsumer.py index 0c541ccc..747e5512 100644 --- a/src/chainconsumer/chainconsumer.py +++ b/src/chainconsumer/chainconsumer.py @@ -1,4 +1,5 @@ from typing import Any + import numpy as np import pandas as pd @@ -83,12 +84,11 @@ def add_marker( marker_size: float = 20.0, marker_style: str = ".", marker_alpha: float = 1.0, - ): + ) -> "ChainConsumer": r"""Add a marker to the plot at the given location. Args: location: The location of the marker. - columns: The names of the columns in the chain that correspond to the location. name: The name of the marker. color: The colour of the marker. Defaults to None. marker_size: The size of the marker. Defaults to 20.0. @@ -101,7 +101,7 @@ def add_marker( """ samples = pd.DataFrame(location, index=[0]) - samples["weights"] = 1.0 + samples["weight"] = 1.0 samples["log_posterior"] = 1.0 kwargs = {} if color is not None: @@ -134,7 +134,7 @@ def remove_chain(self, remove: str | Chain) -> "ChainConsumer": self._chains.pop(remove) return self - def add_override( + def set_override( self, override: ChainConfig, ) -> "ChainConsumer": @@ -201,5 +201,5 @@ def get_names(self) -> list[str]: """Get the names of all chains. Returns: - The names of all chains.""" + The names of all chains.""" return list(self._chains.keys()) diff --git a/src/chainconsumer/diagnostic.py b/src/chainconsumer/diagnostic.py index ee4dec33..8109f494 100644 --- a/src/chainconsumer/diagnostic.py +++ b/src/chainconsumer/diagnostic.py @@ -1,4 +1,3 @@ -import logging import numpy as np from scipy.stats import normaltest diff --git a/src/chainconsumer/examples.py b/src/chainconsumer/examples.py new file mode 100644 index 00000000..c2914908 --- /dev/null +++ b/src/chainconsumer/examples.py @@ -0,0 +1,22 @@ +import numpy as np +import pandas as pd +from scipy.stats import multivariate_normal as mv + + +def make_sample(num_dimensions: int = 2, seed: int | None = None) -> pd.DataFrame: + gen = np.random.default_rng(seed) + vals = gen.random((num_dimensions, num_dimensions)) - 0.5 + cov = np.dot(vals, vals.T) + diag = np.sqrt(np.diag(cov)) + outer = np.outer(diag, diag) + cor = cov / outer + means = np.arange(num_dimensions) + norm = mv(mean=means, cov=cor) + samples = norm.rvs(size=1000000) + # Use the letters of the alphabet as the column names + columns = [chr(65 + i) for i in range(num_dimensions)] + return pd.DataFrame(samples, columns=columns).assign(log_posterior=norm.logpdf(samples)) + + +if __name__ == "__main__": + make_sample() diff --git a/src/chainconsumer/plotter.py b/src/chainconsumer/plotter.py index 0b9b02b1..a2896548 100644 --- a/src/chainconsumer/plotter.py +++ b/src/chainconsumer/plotter.py @@ -26,7 +26,6 @@ from .colors import ColorInput, colors from .helpers import get_extents, get_grid_bins, get_smoothed_bins from .kde import MegKDE -from .log import logger class PlottingBase(BetterBase): @@ -304,9 +303,9 @@ def plot( h, ax=axl, aspect=aspect, pad=0.03, fraction=fraction, drawedges=False ) label = self.config.get_label(cp) - if label == "weights": + if label == "weight": label = "Weights" - elif label == "log_weights": + elif label == "log_weight": label = "log(Weights)" elif label == "posterior": label = "log(Posterior)" @@ -1271,11 +1270,11 @@ def _add_truth( if px is not None: val_x = truth.location.get(px) if val_x is not None: - ax.axhline(val_x, **truth.kwargs) + ax.axhline(val_x, **truth._kwargs) if py is not None: val_y = truth.location.get(py) if val_y is not None: - ax.axvline(val_y, **truth.kwargs) + ax.axvline(val_y, **truth._kwargs) def _plot_bars( self, ax: Axes, column: str, chain: Chain, flip: bool = False, summary: bool = False @@ -1375,7 +1374,7 @@ def _plot_walk( ax.plot(x[:-1], filtered[:-1], ls=":", color=color2, alpha=1) def _plot_walk_truth(self, ax: Axes, truth: Truth, col: str) -> None: - ax.axhline(truth.location[col], **truth.kwargs) + ax.axhline(truth.location[col], **truth._kwargs) def _convert_to_stdev(self, sigma: np.ndarray) -> np.ndarray: # pragma: no cover # From astroML diff --git a/src/chainconsumer/something.py b/src/chainconsumer/something.py deleted file mode 100644 index 0e4fe37e..00000000 --- a/src/chainconsumer/something.py +++ /dev/null @@ -1,13 +0,0 @@ -from pydantic import BaseModel - - -class Something(BaseModel): - message: str - - def get_message(self) -> str: - """Gets the message. - - Returns: - str: The message. - """ - return self.message diff --git a/src/chainconsumer/statistics.py b/src/chainconsumer/statistics.py new file mode 100644 index 00000000..5dbcedab --- /dev/null +++ b/src/chainconsumer/statistics.py @@ -0,0 +1,20 @@ +from enum import Enum + + +class SummaryStatistic(Enum): + MAX = "max" + """The max value summary statistic is the default. The central point is set + to your maxmimum likelihood, and the upper and lower bounds are determined by + finding an iso-likelihood surface which encapsulates the required volume.""" + + MAX_CENTRAL = "max_central" + """"As per the MAX method, this has the centre point at the maximum likelihood. + However the lower and upper values come from the CDF, like the cumulative method.""" + + CUMULATIVE = "cumulative" + """The lower, central, and upper bound are determined by finding where on the marginalised + sample CDF the points lie. This means the central point is the median.""" + + MEAN = "mean" + """As per the cumulative method, except the central value is placed in the midpoint between + the upper and lower boundary. Not recommended, but was requested.""" diff --git a/src/chainconsumer/summary_stats.py b/src/chainconsumer/summary_stats.py deleted file mode 100644 index 32316385..00000000 --- a/src/chainconsumer/summary_stats.py +++ /dev/null @@ -1,10 +0,0 @@ -from enum import Enum - - -class SummaryStatistic(Enum): - MAX = "max" - MEAN = "mean" - CUMULATIVE = "cumulative" - MAX_SYMMETRIC = "max_symmetric" - MAX_SHORTEST = "max_shortest" - MAX_CENTRAL = "max_central" diff --git a/src/chainconsumer/truth.py b/src/chainconsumer/truth.py index addff441..0dc6c670 100644 --- a/src/chainconsumer/truth.py +++ b/src/chainconsumer/truth.py @@ -4,23 +4,24 @@ from pydantic import Field, ValidationError, field_validator from .base import BetterBase +from .colors import ColorInput class Truth(BetterBase): location: dict[str, float] = Field( - default=..., description="The truth value, either as dictionary or pandas series" + default=..., + description="The truth value, either as dictionary or pandas series which will be converted to a dict)", ) - truth_name: str | None = Field(default=None, description="The name of the truth line") - line_color: str = Field(default="black", description="The color of the truth line") + name: str | None = Field(default=None, description="The name of the truth line") + color: ColorInput = Field(default="black", description="The color of the truth line") line_width: float = Field(default=1.0, description="The width of the truth line") - line_alpha: float = Field(default=1.0, description="The alpha of the truth line") line_style: str = Field(default="--", description="The style of the truth line") - line_zorder: int = Field(default=100, description="The zorder of the truth line") - name: str | None = Field(default=None, description="The name of the truth line") + alpha: float = Field(default=1.0, description="The alpha of the truth line") + zorder: int = Field(default=100, description="The zorder of the truth line") @field_validator("location") @classmethod - def ensure_dict(cls, v): + def _ensure_dict(cls, v): if isinstance(v, dict): return v elif isinstance(v, pd.Series): @@ -28,11 +29,11 @@ def ensure_dict(cls, v): raise ValidationError("Truth must be a dict or a pandas Series") @property - def kwargs(self) -> dict[str, Any]: + def _kwargs(self) -> dict[str, Any]: return { "ls": self.line_style, - "c": self.line_color, + "c": self.color, "lw": self.line_width, - "alpha": self.line_alpha, - "zorder": self.line_zorder, + "alpha": self.alpha, + "zorder": self.zorder, } diff --git a/tests/test_chain.py b/tests/test_chain.py index 59c063fc..4597ab8c 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -1,9 +1,10 @@ import numpy as np import pandas as pd import pytest +from pydantic import ValidationError from scipy.stats import norm -from chainconsumer.chain import Chain +from chainconsumer.chain import Chain, ChainConfig from chainconsumer.chainconsumer import ChainConsumer @@ -11,314 +12,89 @@ class TestChain: rng = np.random.default_rng(0) d = rng.normal(size=(100, 3)) d2 = rng.normal(size=(1000000, 3)) - bad = d.copy() - bad[0, 0] = np.nan p = ("a", "b", "c") + df = pd.DataFrame(d, columns=p) + dfw = df.assign(weight=1) + dfp = df.assign(log_posterior=1) + dfp_bad = dfp.copy() + dfp_bad["log_posterior"][0] = np.nan + bad = df.copy() + bad["a"][0] = np.nan n = "A" - w = np.ones(100) - w2 = np.ones(1000000) def test_good_chain(self): - Chain(self.d, self.p, self.n) + Chain(samples=self.df, name=self.n) def test_good_chain_weights1(self): - Chain(self.d, self.p, self.n, self.w) - - def test_good_chain_weights2(self): - Chain(self.d, self.p, self.n, self.w[None]) - - def test_good_chain_weights3(self): - Chain(self.d, self.p, self.n, self.w[None].T) + Chain(samples=self.dfw, name=self.n) def test_chain_with_bad_weights1(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, weights=np.ones((50, 1))) + with pytest.raises(ValidationError): + Chain(samples=self.dfw, name=self.n, weight_column="potato") def test_chain_with_bad_weights2(self): - with pytest.raises(AssertionError): - w = self.w.copy() - w[10] = np.inf - Chain(self.d, self.p, self.n, weights=w) - - def test_chain_with_bad_weights3(self): - with pytest.raises(AssertionError): - w = self.w.copy() - w[10] = np.nan - Chain(self.d, self.p, self.n, weights=w) - - def test_chain_with_bad_weights4(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, weights=np.ones((50, 2))) - - def test_chain_with_bad_name1(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, 1) - - def test_chain_with_bad_name2(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, None) - - def test_chain_with_bad_params1(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p[:-1], self.n) + with pytest.raises(ValidationError): + df2 = self.dfw.copy() + df2["weight"][10] = np.inf + Chain(samples=df2, name=self.n) def test_chain_with_bad_params2(self): - with pytest.raises(AssertionError): - Chain(self.d, ["A", "B", 0], self.n) - - def test_chain_with_bad_params3(self): - with pytest.raises(AssertionError): - Chain(self.d, None, self.n) + with pytest.raises(ValidationError): + df2 = self.df.copy() + df2.columns = ["A", "B", 0] + Chain(samples=df2, name=self.n) def test_chain_with_bad_chain_initial_success1(self): - Chain(self.bad, self.p, self.n) - - def test_chain_with_bad_chain_initial_success2(self): - c = Chain(self.bad, self.p, self.n) - c.get_data(1) - - def test_chain_with_bad_chain_fails_on_access1(self): - c = Chain(self.bad, self.p, self.n) - with pytest.raises(AssertionError): - c.get_data(0) - - def test_chain_with_bad_chain_fails_on_access2(self): - c = Chain(self.bad, self.p, self.n) - with pytest.raises(AssertionError): - c.get_data(self.p[0]) + Chain(samples=self.bad, name=self.n) def test_good_grid(self): - Chain(self.d, self.p, self.n, grid=False) - - def test_bad_grid1(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, grid=0) - - def test_bad_grid2(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, grid=None) - - def test_bad_grid3(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, grid="False") + Chain(samples=self.df, name=self.n, grid=False) def test_good_walkers1(self): - Chain(self.d, self.p, self.n, walkers=10) - - def test_good_walkers2(self): - Chain(self.d, self.p, self.n, walkers=10.0) + Chain(samples=self.df, name=self.n, walkers=10) def test_bad_walkers1(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, walkers=2000) + with pytest.raises(ValidationError): + Chain(samples=self.df, name=self.n, walkers=2000) def test_bad_walkers2(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, walkers=11) - - def test_bad_walkers3(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, walkers="5") - - def test_bad_walkers4(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, walkers=2.5) + with pytest.raises(ValidationError): + Chain(samples=self.df, name=self.n, walkers=11) def test_good_posterior1(self): - Chain(self.d, self.p, self.n, posterior=np.ones(100)) - - def test_good_posterior2(self): - Chain(self.d, self.p, self.n, posterior=np.ones((100, 1))) - - def test_good_posterior3(self): - Chain(self.d, self.p, self.n, posterior=np.ones((1, 100))) + Chain(samples=self.dfp, name=self.n) def test_bad_posterior1(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, posterior=np.ones((2, 50))) - - def test_bad_posterior2(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, posterior=np.ones(50)) - - def test_bad_posterior3(self): - posterior = np.ones(100) - posterior[0] = np.nan - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, posterior=posterior) - - def test_bad_posterior4(self): - posterior = np.ones(100) - posterior[0] = np.inf - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, posterior=posterior) - - def test_bad_posterior5(self): - posterior = np.ones(100) - posterior[0] = -np.inf - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, posterior=posterior) + with pytest.raises(ValidationError): + Chain(samples=self.dfp_bad, name=self.n) def test_good_num_free_params1(self): - Chain(self.d, self.p, self.n, num_free_params=2) - - def test_good_num_free_params2(self): - Chain(self.d, self.p, self.n, num_free_params=2.0) - - def test_bad_num_free_params1(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, num_free_params="2.5") - - def test_bad_num_free_params2(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, num_free_params=np.inf) - - def test_bad_num_free_params3(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, num_free_params=np.nan) + Chain(samples=self.df, name=self.n, num_free_params=2) def test_bad_num_free_params4(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, num_free_params=-10) + with pytest.raises(ValidationError): + Chain(samples=self.df, name=self.n, num_free_params=-10) def test_good_num_eff_data_points1(self): - Chain(self.d, self.p, self.n, num_eff_data_points=2) + Chain(samples=self.df, name=self.n, num_eff_data_points=2) def test_good_num_eff_data_points2(self): - Chain(self.d, self.p, self.n, num_eff_data_points=20.4) - - def test_bad_num_eff_data_points1(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, num_eff_data_points="2.5") + Chain(samples=self.df, name=self.n, num_eff_data_points=20.4) def test_bad_num_eff_data_points2(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, num_eff_data_points=np.nan) + with pytest.raises(ValidationError): + Chain(samples=self.df, name=self.n, num_eff_data_points=np.nan) def test_bad_num_eff_data_points3(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, num_eff_data_points=np.inf) + with pytest.raises(ValidationError): + Chain(samples=self.df, name=self.n, num_eff_data_points=np.inf) def test_bad_num_eff_data_points4(self): - with pytest.raises(AssertionError): - Chain(self.d, self.p, self.n, num_eff_data_points=-100) - - def test_color_data_none(self): - c = ChainConsumer() - c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) - c.configure_overrides(color_params=None) - chain = c._chains[0] - assert chain.get_color_data() is None + with pytest.raises(ValidationError): + Chain(samples=self.df, name=self.n, num_eff_data_points=-100) def test_color_data_p1(self): - c = ChainConsumer() - c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) - c.configure_overrides(color_params=self.p[0]) - chain = c._chains[0] - assert np.all(chain.get_color_data() == self.d[:, 0]) - - def test_color_data_w(self): - c = ChainConsumer() - c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) - c.configure_overrides(color_params="weights") - chain = c._chains[0] - assert np.all(chain.get_color_data() == self.w) - - def test_color_data_logw(self): - c = ChainConsumer() - c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) - c.configure_overrides(color_params="log_weights") - chain = c._chains[0] - assert np.all(chain.get_color_data() == np.log(self.w)) - - def test_color_data_posterior(self): - c = ChainConsumer() - c.add_chain(self.d, parameters=self.p, name=self.n, weights=self.w, posterior=np.ones(100)) - c.configure_overrides(color_params="posterior") - chain = c._chains[0] - assert np.all(chain.get_color_data() == np.ones(100)) - - def test_override_color(self): - c = ChainConsumer() - c.add_chain(self.d, parameters=self.p, color="#4286f4") - c.configure_overrides() - assert c._chains[0].config["color"] == "#4286f4" - - def test_override_linewidth(self): - c = ChainConsumer() - c.add_chain(self.d, parameters=self.p, linewidth=2.0) - c.configure_overrides(linewidths=[100]) - assert c._chains[0].config["linewidth"] == 100 - - def test_override_linestyle(self): - c = ChainConsumer() - c.add_chain(self.d, parameters=self.p, linestyle="--") - c.configure_overrides() - assert c._chains[0].config["linestyle"] == "--" - - def test_override_shade_alpha(self): - c = ChainConsumer() - c.add_chain(self.d, parameters=self.p, shade_alpha=0.8) - c.configure_overrides() - assert c._chains[0].config["shade_alpha"] == 0.8 - - def test_override_kde(self): - c = ChainConsumer() - c.add_chain(self.d, parameters=self.p, kde=2.0) - c.configure_overrides() - assert c._chains[0].config["kde"] == 2.0 - - def test_override_kde_grid(self): - c = ChainConsumer() - x, y = np.linspace(0, 10, 10), np.linspace(0, 10, 10) - z = np.ones((10, 10)) - c.add_chain([x, y], weights=z, grid=True, kde=2.0) - c.configure_overrides() - assert not c._chains[0].config["kde"] - - def test_cache_invalidation(self): - c = ChainConsumer() - c.add_chain(self.rng.normal(size=(1000000, 1)), parameters=["a"]) - c.configure_overrides(summary_area=0.68) - summary1 = c.analysis.get_summary() - c.configure_overrides(summary_area=0.95) - summary2 = c.analysis.get_summary() - assert np.isclose(summary1["a"][0], -1, atol=0.03) - assert np.isclose(summary2["a"][0], -2, atol=0.03) - assert np.isclose(summary1["a"][2], 1, atol=0.03) - assert np.isclose(summary2["a"][2], 2, atol=0.03) - - def test_pass_in_dataframe1(self): - df = pd.DataFrame(self.d2, columns=self.p) - c = ChainConsumer() - c.add_chain(df) - summary1 = c.analysis.get_summary() - assert np.isclose(summary1["a"][0], -1, atol=0.03) - assert np.isclose(summary1["a"][1], 0, atol=0.05) - assert np.isclose(summary1["a"][2], 1, atol=0.03) - assert np.isclose(summary1["b"][0], -1, atol=0.03) - assert np.isclose(summary1["c"][0], -1, atol=0.03) - - def test_pass_in_dataframe2(self): - df = pd.DataFrame(self.d2, columns=self.p) - df["weight"] = self.w2 - c = ChainConsumer() - c.add_chain(df) - summary1 = c.analysis.get_summary() - assert np.isclose(summary1["a"][0], -1, atol=0.03) - assert np.isclose(summary1["a"][1], 0, atol=0.05) - assert np.isclose(summary1["a"][2], 1, atol=0.03) - assert np.isclose(summary1["b"][0], -1, atol=0.03) - assert np.isclose(summary1["c"][0], -1, atol=0.03) - - def test_pass_in_dataframe3(self): - data = self.rng.uniform(-4, 6, size=(1000000, 1)) - weight = norm.pdf(data) - df = pd.DataFrame(data, columns=["a"]) - df["weight"] = weight - c = ChainConsumer() - c.add_chain(df) - summary1 = c.analysis.get_summary() - assert np.isclose(summary1["a"][0], -1, atol=0.03) - assert np.isclose(summary1["a"][1], 0, atol=0.05) - assert np.isclose(summary1["a"][2], 1, atol=0.03) + chain = Chain(samples=self.df, name=self.n, color_param="a") + color_data = chain.color_data + assert color_data is not None + assert np.allclose(self.df["a"].to_numpy(), color_data) diff --git a/tests/test_chainconsumer.py b/tests/test_chainconsumer.py index d0465342..c718239c 100644 --- a/tests/test_chainconsumer.py +++ b/tests/test_chainconsumer.py @@ -1,8 +1,8 @@ import numpy as np -import pytest +import pandas as pd from scipy.stats import skewnorm -from chainconsumer import ChainConsumer +from chainconsumer import Chain, ChainConsumer class TestChainConsumer: @@ -13,203 +13,27 @@ class TestChainConsumer: data_combined = np.vstack((data, data2)).T data_skew = skewnorm.rvs(5, loc=1, scale=1.5, size=n) - def test_get_chain_name(self): - c = ChainConsumer() - c.add_chain(self.data, name="A") - assert c._get_chain_name(0) == "A" - - def test_get_names(self): - c = ChainConsumer() - c.add_chain(self.data, name="A") - c.add_chain(self.data, name="B") - assert c._all_names() == ["A", "B"] - - def test_get_chain_via_object(self): - c = ChainConsumer() - c.add_chain(self.data, name="A") - c.add_chain(self.data, name="B") - assert c.get_chain(c._chains[0])[0] == 0 - assert c.get_chain(c._chains[1])[0] == 1 - assert len(c.get_chain(c._chains[0])) == 1 - assert len(c.get_chain(c._chains[1])) == 1 - - def test_summary_bad_input1(self): - with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data).configure_overrides(summary_area=None) - - def test_summary_bad_input2(self): - with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data).configure_overrides(summary_area="Nope") - - def test_summary_bad_input3(self): - with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data).configure_overrides(summary_area=0) - - def test_summary_bad_input4(self): - with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data).configure_overrides(summary_area=1) - - def test_summary_bad_input5(self): - with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data).configure_overrides(summary_area=-0.2) - - def test_remove_last_chain(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data) - consumer.add_chain(self.data * 2) - consumer.remove_chain() - consumer.configure_overrides() - summary = consumer.analysis.get_summary() - assert isinstance(summary, dict) - actual = np.array(next(iter(summary.values()))) - expected = np.array([3.5, 5.0, 6.5]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) + chain1 = Chain(samples=pd.DataFrame({"a": data}), name="A") + chain2 = Chain(samples=pd.DataFrame({"b": data2}), name="B") + chain3 = Chain(samples=pd.DataFrame({"c": data_skew}), name="C") + chain4 = Chain(samples=pd.DataFrame(data_combined, columns=["a", "b"]), name="D") - def test_remove_first_chain(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data * 2) - consumer.add_chain(self.data) - consumer.remove_chain(chain=0) - consumer.configure_overrides() - summary = consumer.analysis.get_summary() - assert isinstance(summary, dict) - actual = np.array(next(iter(summary.values()))) - expected = np.array([3.5, 5.0, 6.5]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) + def get(self) -> ChainConsumer: + c = ChainConsumer().add_chain(self.chain1).add_chain(self.chain2).add_chain(self.chain3).add_chain(self.chain4) + return c - def test_remove_chain_by_name(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data * 2, name="a") - consumer.add_chain(self.data, name="b") - consumer.remove_chain(chain="a") - consumer.configure_overrides() - summary = consumer.analysis.get_summary() - assert isinstance(summary, dict) - actual = np.array(next(iter(summary.values()))) - expected = np.array([3.5, 5.0, 6.5]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_remove_chain_recompute_params(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data * 2, parameters=["p1"], name="a") - consumer.add_chain(self.data, parameters=["p2"], name="b") - consumer.remove_chain(chain="a") - consumer.configure_overrides() - summary = consumer.analysis.get_summary() - assert isinstance(summary, dict) - assert "p2" in summary - assert "p1" not in summary - actual = np.array(next(iter(summary.values()))) - expected = np.array([3.5, 5.0, 6.5]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_remove_multiple_chains(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data * 2, parameters=["p1"], name="a") - consumer.add_chain(self.data, parameters=["p2"], name="b") - consumer.add_chain(self.data * 3, parameters=["p3"], name="c") - consumer.remove_chain(chain=["a", "c"]) - consumer.configure_overrides() - summary = consumer.analysis.get_summary() - assert isinstance(summary, dict) - assert "p2" in summary - assert "p1" not in summary - assert "p3" not in summary - actual = np.array(next(iter(summary.values()))) - expected = np.array([3.5, 5.0, 6.5]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_remove_multiple_chains2(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data * 2, parameters=["p1"], name="a") - consumer.add_chain(self.data, parameters=["p2"], name="b") - consumer.add_chain(self.data * 3, parameters=["p3"], name="c") - consumer.remove_chain(chain=[0, 2]) - consumer.configure_overrides() - summary = consumer.analysis.get_summary() - assert isinstance(summary, dict) - assert "p2" in summary - assert "p1" not in summary - assert "p3" not in summary - actual = np.array(next(iter(summary.values()))) - expected = np.array([3.5, 5.0, 6.5]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_remove_multiple_chains3(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data * 2, parameters=["p1"], name="a") - consumer.add_chain(self.data, parameters=["p2"], name="b") - consumer.add_chain(self.data * 3, parameters=["p3"], name="c") - consumer.remove_chain(chain=["a", 2]) - consumer.configure_overrides() - summary = consumer.analysis.get_summary() - assert isinstance(summary, dict) - assert "p2" in summary - assert "p1" not in summary - assert "p3" not in summary - actual = np.array(next(iter(summary.values()))) - expected = np.array([3.5, 5.0, 6.5]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_remove_multiple_chains_fails(self): - with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data).remove_chain(chain=[0, 0]) - - def test_shade_alpha_algorithm1(self): - consumer = ChainConsumer() - consumer.add_chain(self.data) - consumer.configure_overrides() - alpha = consumer._chains[0].config["shade_alpha"] - assert alpha == 1.0 - - def test_shade_alpha_algorithm2(self): - consumer = ChainConsumer() - consumer.add_chain(self.data) - consumer.add_chain(self.data) - consumer.configure_overrides() - alpha0 = consumer._chains[0].config["shade_alpha"] - alpha1 = consumer._chains[0].config["shade_alpha"] - assert alpha0 == 1.0 / np.sqrt(2.0) - assert alpha1 == 1.0 / np.sqrt(2.0) + def test_get_chain_name(self): + assert self.get().get_chain(self.chain1.name) == self.chain1 - def test_shade_alpha_algorithm3(self): - consumer = ChainConsumer() - consumer.add_chain(self.data) - consumer.add_chain(self.data) - consumer.add_chain(self.data) - consumer.configure_overrides() - alphas = [c.config["shade_alpha"] for c in consumer._chains] - assert len(alphas) == 3 - assert alphas[0] == 1.0 / np.sqrt(3.0) - assert alphas[1] == 1.0 / np.sqrt(3.0) - assert alphas[2] == 1.0 / np.sqrt(3.0) + def test_get_chain_names(self): + assert self.get().get_names() == ["A", "B", "C", "D"] - def test_covariance(self): - mean = [0, 1] - cov = [[1, 1], [1, 2.5]] - c = ChainConsumer() - c.add_covariance(mean, cov) - mean_obs = np.mean(c._chains[0].chain, axis=0) - cov_obs = np.cov(c._chains[0].chain.T) - assert np.all(np.isclose(mean, mean_obs, atol=1e-2)) - assert np.all(np.isclose(cov, cov_obs, atol=1e-2)) + def test_remove_chain_str(self): + c = self.get() + c.remove_chain("A") + assert c.get_names() == ["B", "C", "D"] - def test_marker(self): - loc = [0, 1, 2] - c = ChainConsumer() - c.add_marker(loc) - assert np.all(np.equal(loc, c._chains[0].chain[0, :])) + def test_remove_chain_obj(self): + c = self.get() + c.remove_chain(self.chain1) + assert c.get_names() == ["B", "C", "D"] diff --git a/tests/test_colours.py b/tests/test_colours.py index ede4230d..b61f42c0 100644 --- a/tests/test_colours.py +++ b/tests/test_colours.py @@ -1,44 +1,39 @@ import numpy as np import pytest -from chainconsumer.colors import Colors +from chainconsumer.colors import ALL_COLOURS, colors def test_colors_rgb2hex_1(): c = np.array([1, 1, 1, 1]) - colourmap = Colors() - assert colourmap.get_formatted([c])[0] == "#ffffff" + assert colors.get_formatted([c])[0] == "#ffffff" def test_colors_rgb2hex_2(): c = np.array([0, 0, 0.5, 1]) - colourmap = Colors() - assert colourmap.get_formatted([c])[0] == "#000080" + assert colors.get_formatted([c])[0] == "#000080" def test_colors_alias_works(): - colourmap = Colors() - assert colourmap.get_formatted(["b"])[0] == colourmap.color_map["blue"] + assert colors.format("b") in ALL_COLOURS["blue"] def test_colors_name_works(): - colourmap = Colors() - assert colourmap.get_formatted(["blue"])[0] == colourmap.color_map["blue"] + assert colors.format("blue") in ALL_COLOURS["blue"] def test_colors_error_on_garbage(): - colourmap = Colors() with pytest.raises(ValueError): - colourmap.get_formatted(["java"]) + colors.get_formatted(["java"]) def test_clamp1(): - assert Colors()._clamp(-10) == 0 + assert colors._clamp(-10) == 0 def test_clamp2(): - assert Colors()._clamp(10) == 10 + assert colors._clamp(10) == 10 def test_clamp3(): - assert Colors()._clamp(1000) == 255 + assert colors._clamp(1000) == 255 diff --git a/tests/test_kde.py b/tests/test_kde.py index 74716811..3a14ba36 100644 --- a/tests/test_kde.py +++ b/tests/test_kde.py @@ -7,7 +7,7 @@ @pytest.fixture -def rng(): +def rng() -> np.random.Generator: return np.random.default_rng(0) @@ -53,7 +53,7 @@ def test_megkde_1d_changing_weights(rng): assert np.isclose(std, 1.0, atol=0.1) -def test_megkde_2d_basic(): +def test_megkde_2d_basic(rng): # Draw from normal, fit KDE, see if sampling from kde's pdf recovers norm data = rng.multivariate_normal([0, 1], [[1.0, 0.0], [0.0, 0.75**2]], size=10000) xs, ys = np.linspace(-4, 4, 50), np.linspace(-4, 4, 50) From 80d9659f16560f5ca09f8dd84ace7c5762780c3d Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Sun, 8 Oct 2023 17:00:56 +1000 Subject: [PATCH 08/22] Fixing up more things --- docs/examples/README.md | 3 +- .../{plot_contours.py => contours.py} | 0 docs/examples/plot_summary.py | 47 + pyproject.toml | 3 +- src/chainconsumer/__init__.py | 3 +- src/chainconsumer/analysis.py | 27 +- src/chainconsumer/chain.py | 26 +- src/chainconsumer/chainconsumer.py | 6 +- src/chainconsumer/colors.py | 5 +- src/chainconsumer/comparisons.py | 7 +- src/chainconsumer/diagnostic.py | 181 ++- src/chainconsumer/plotter.py | 521 +++--- tests/test_analysis.py | 1437 ++++++++--------- tests/test_chain.py | 9 +- tests/test_comparisons.py | 279 ++-- tests/test_diagnostic.py | 132 +- tests/test_plotter.py | 53 +- 17 files changed, 1290 insertions(+), 1449 deletions(-) rename docs/examples/{plot_contours.py => contours.py} (100%) create mode 100644 docs/examples/plot_summary.py diff --git a/docs/examples/README.md b/docs/examples/README.md index a72638ab..65afe604 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -1 +1,2 @@ -# ChainConsumer \ No newline at end of file +# Examples + diff --git a/docs/examples/plot_contours.py b/docs/examples/contours.py similarity index 100% rename from docs/examples/plot_contours.py rename to docs/examples/contours.py diff --git a/docs/examples/plot_summary.py b/docs/examples/plot_summary.py new file mode 100644 index 00000000..35e84b87 --- /dev/null +++ b/docs/examples/plot_summary.py @@ -0,0 +1,47 @@ +""" +# Introduction to Summaries + +When you have a few chains and want to contrast them all with +each other, you probably want a summary plot. + +To show you how they work, let's make some sample data that all +has the same average. +""" +from chainconsumer import Chain, ChainConfig, ChainConsumer, PlotConfig, Truth, make_sample + +# Here's what you might start with +df_1 = make_sample(num_dimensions=4, seed=1) +df_2 = make_sample(num_dimensions=5, seed=2) +print(df_1.head()) + +# %% New cell +## Using distributions + + +# And now we give this to chainconsumer +c = ChainConsumer() +c.add_chain(Chain(samples=df_1, name="An Example Contour")) +c.add_chain(Chain(samples=df_2, name="A Different Contour")) +fig = c.plotter.plot_summary() + +# %% New cell +# ## Using Errorbars +# +# Note that because the errorbar kwarg is specific to this function +# it is not part of the `PlotConfig` class. + +fig = c.plotter.plot_summary(errorbar=True) + +# %% New cell +# The other features of ChainConsumer should work with summaries too. +# +# For example, truth values should work just fine. + +c.add_truth(Truth(location={"A": 0, "B": 1}, line_style=":", color="red")) +fig = c.plotter.plot_summary(errorbar=True, vertical_spacing_ratio=2.0) + +# %% New cell +# And similarly, our overrides are generic and so effect this method too. +c.set_override(ChainConfig(bar_shade=False)) +c.set_plot_config(PlotConfig(watermark="Preliminary", blind=["E"])) +fig = c.plotter.plot_summary() diff --git a/pyproject.toml b/pyproject.toml index b8ce99f1..eb094678 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,8 +57,9 @@ target-version = "py310" [tool.ruff.extend-per-file-ignores] "test/***" = ["INP001"] "__init__.py" = ["E402", "F401"] -"examples/***" = ["T201"] +"examples/***" = ["T201", "NPY002"] "**/plot_*" = ["T201"] +"docs/examples/***" = ["T201"] [tool.mypy] plugins = ["numpy.typing.mypy_plugin", "pydantic.mypy"] diff --git a/src/chainconsumer/__init__.py b/src/chainconsumer/__init__.py index d7a5c555..63d6859a 100644 --- a/src/chainconsumer/__init__.py +++ b/src/chainconsumer/__init__.py @@ -1,7 +1,8 @@ +from .analysis import Bound from .chain import Chain, ChainConfig from .chainconsumer import ChainConsumer from .examples import make_sample from .plotter import PlotConfig from .truth import Truth -__all__ = ["ChainConsumer", "Chain", "ChainConfig", "Truth", "PlotConfig", "make_sample"] +__all__ = ["ChainConsumer", "Chain", "ChainConfig", "Truth", "PlotConfig", "make_sample", "Bound"] diff --git a/src/chainconsumer/analysis.py b/src/chainconsumer/analysis.py index 95222e31..618dda87 100644 --- a/src/chainconsumer/analysis.py +++ b/src/chainconsumer/analysis.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +from collections.abc import Callable from pathlib import Path import numpy as np @@ -21,13 +22,33 @@ class Bound(BetterBase): center: float | None = Field(default=None) upper: float | None = Field(default=None) + @property + def array(self) -> np.ndarray: + return np.array( + [ + self.lower if self.lower is not None else np.NaN, + self.center if self.center is not None else np.NaN, + self.upper if self.upper is not None else np.NaN, + ] + ) + + @property + def all_none(self) -> bool: + return self.lower is None and self.center is None and self.upper is None + + @classmethod + def from_array(cls, array: np.ndarray | list[float]) -> Bound: + assert len(array) == 3, "Array must have 3 elements" + lower, center, upper = array + return cls(lower=lower, center=center, upper=upper) + class Analysis: def __init__(self, parent: ChainConsumer): self.parent = parent self._logger = logging.getLogger("chainconsumer") - self._summaries = { + self._summaries: dict[SummaryStatistic, Callable[[Chain, ColumnName], Bound | None]] = { SummaryStatistic.MAX: self.get_parameter_summary_max, SummaryStatistic.MEAN: self.get_parameter_summary_mean, SummaryStatistic.CUMULATIVE: self.get_parameter_summary_cumulative, @@ -151,7 +172,7 @@ def get_summary( for name, chain in chains.items(): res = {} - params_to_find = columns if columns is not None else chain.samples.columns + params_to_find = columns if columns is not None else chain.data_columns for p in params_to_find: if p not in chain.samples: continue @@ -187,7 +208,7 @@ def get_max_posteriors(self, chains: dict[str, Chain] | list[str] | None = None) return results - def get_parameter_summary(self, chain: Chain, column: ColumnName): + def get_parameter_summary(self, chain: Chain, column: ColumnName) -> Bound | None: callback = self._summaries[chain.statistics] return callback(chain, column) diff --git a/src/chainconsumer/chain.py b/src/chainconsumer/chain.py index 8694b84e..5ac74874 100644 --- a/src/chainconsumer/chain.py +++ b/src/chainconsumer/chain.py @@ -44,14 +44,14 @@ class ChainConfig(BetterBase): statistics: SummaryStatistic = Field(default=SummaryStatistic.MAX, description="The summary statistic to use") summary_area: float = Field(default=0.6827, ge=0, le=1.0, description="The area to use for summary statistics") sigmas: list[float] = Field(default=[0, 1, 2], description="The sigmas to use for summary statistics") - color: ColorInput = Field(default=None, description="The color of the chain") # type: ignore + color: ColorInput | None = Field(default=None, description="The color of the chain") # type: ignore linestyle: str = Field(default="-", description="The line style of the chain") linewidth: float = Field(default=1.0, description="The line width of the chain") show_contour_labels: bool = Field(default=False, description="Whether to show contour labels") - shade: bool = Field(default=None, description="Whether to shade the chain") # type: ignore - shade_alpha: float = Field(default=None, description="The alpha of the shading") # type: ignore + shade: bool = Field(default=True, description="Whether to shade the chain") + shade_alpha: float = Field(default=0.5, description="The alpha of the shading") shade_gradient: float = Field(default=1.0, description="The contrast between contour levels") - bar_shade: bool = Field(default=None, description="Whether to shade marginalised distributions") # type: ignore + bar_shade: bool = Field(default=True, description="Whether to shade marginalised distributions") bins: int | float = Field( default=1.0, description="The number of bins to use for histograms. If a float, used to scale the default bins" ) @@ -63,9 +63,9 @@ class ChainConfig(BetterBase): plot_cloud: bool = Field(default=False, description="Whether to plot the cloud") plot_contour: bool = Field(default=True, description="Whether to plot contours") plot_point: bool = Field(default=False, description="Whether to plot points") - marker_style: str = Field(default=None, description="The marker style to use") # type: ignore + marker_style: str = Field(default=".", description="The marker style to use") marker_size: int | float = Field(default=10.0, ge=1, description="The marker size to use") - marker_alpha: int | float = Field(default=None, description="The marker alpha to use") # type: ignore + marker_alpha: int | float = Field(default=1.0, description="The marker alpha to use") zorder: int = Field(default=10, description="The zorder to use") shift_params: bool = Field( default=False, @@ -79,9 +79,9 @@ def _convert_color(cls, v: ColorInput | None) -> str | None: return None return colors.format(v) - def _apply_if_none(self, **kwargs: Any) -> None: + def _apply_if_not_user_set(self, **kwargs: Any) -> None: for key, value in kwargs.items(): - if getattr(self, key) is None: + if key not in self._user_specified: setattr(self, key, value) def _apply(self, **kwargs: Any) -> None: @@ -153,6 +153,11 @@ def data_columns(self) -> list[str]: results.append(c) return results + @property + def data_samples(self) -> pd.DataFrame: + """The subsection of the dataframe with data points (ie excluding weights and posterior)""" + return self.samples[self.data_columns] + @property def plotting_columns(self) -> list[str]: """The columns to be plotted, which are the dataframe columns @@ -203,6 +208,11 @@ def _validate_color(cls, v: str | np.ndarray | list[float] | None) -> str | None return None return colors.format(v) + @field_validator("samples") + @classmethod + def _copy_df(cls, v: pd.DataFrame) -> pd.DataFrame: + return v.copy() + @model_validator(mode="after") def _validate_model(self) -> Chain: assert not self.samples.empty, "Your chain is empty. This is not ideal." diff --git a/src/chainconsumer/chainconsumer.py b/src/chainconsumer/chainconsumer.py index 747e5512..0c9f0c86 100644 --- a/src/chainconsumer/chainconsumer.py +++ b/src/chainconsumer/chainconsumer.py @@ -162,6 +162,8 @@ def _get_final_chains(self) -> dict[ChainName, Chain]: global_config["shade"] = num_chains < 5 global_config["shade_alpha"] = 1.0 / np.sqrt(num_chains) + color_iter = colors.next_colour() + for _, chain in final_chains.items(): # copy global config into local config local_config = global_config.copy() @@ -175,9 +177,9 @@ def _get_final_chains(self) -> dict[ChainName, Chain]: # Check to see if the color is set if chain.color is None: - local_config["color"] = next(colors.next_colour()) + local_config["color"] = next(color_iter) - chain._apply_if_none(**local_config) + chain._apply_if_not_user_set(**local_config) # Apply user overrides if self._global_chain_override is not None: diff --git a/src/chainconsumer/colors.py b/src/chainconsumer/colors.py index 0abf3b0c..1020a606 100644 --- a/src/chainconsumer/colors.py +++ b/src/chainconsumer/colors.py @@ -60,17 +60,18 @@ def __init__(self) -> None: "red", "purple", "amber", - "grey", + "gray", "cyan", "teal", "green", "orange", "indigo", ) + self.iter = self.next_colour() def next_colour(self) -> Generator[str, None, None]: """A generator to return a sequence of colors""" - for index in [4, 7, 2]: + for index in [4, 7, 2, 8, 3, 6, 1, 5]: for color in self.default_colors: yield ALL_COLOURS[color][index] diff --git a/src/chainconsumer/comparisons.py b/src/chainconsumer/comparisons.py index 283c0044..876162de 100644 --- a/src/chainconsumer/comparisons.py +++ b/src/chainconsumer/comparisons.py @@ -41,10 +41,9 @@ def dic(self) -> dict[str, float]: if p is None: logger.warning("You need to set the posterior for chain %s to get the DIC" % chain.name) else: - num_params = chain.samples.shape[1] - means = np.array([np.average(chain.samples[:, ii], weights=chain.weights) for ii in range(num_params)]) + means = np.array([np.average(chain.samples[c], weights=chain.weights) for c in chain.data_columns]) d = -2 * p - d_of_mean = griddata(chain.samples, d, means, method="nearest")[0] + d_of_mean = griddata(chain.data_samples, d, means, method="nearest")[0] mean_d = np.average(d, weights=chain.weights) p_d = mean_d - d_of_mean dic = mean_d + p_d @@ -68,7 +67,7 @@ def bic(self) -> dict[str, float]: the number of independent data points used in the model fitting. Returns: - dict[str, float]: A dict of chain name to BIC value. + A dict of chain name to BIC value. """ bics = {} diff --git a/src/chainconsumer/diagnostic.py b/src/chainconsumer/diagnostic.py index 8109f494..16765776 100644 --- a/src/chainconsumer/diagnostic.py +++ b/src/chainconsumer/diagnostic.py @@ -1,131 +1,150 @@ - import numpy as np +from pydantic import Field from scipy.stats import normaltest +from .base import BetterBase +from .chain import Chain, ChainName from .log import logger +class TestResult(BetterBase): + passed: bool = Field(default=..., description="Whether or not the test passed in general") + results: dict[ChainName, bool] = Field( + default=..., description="For each chain, whether the test passed and its numerical value" + ) + + def __bool__(self) -> bool: + return self.passed + + +def _sanitise_chains( + chains: list[Chain | ChainName] | Chain | ChainName | None, parent: "ChainConsumer" +) -> list[Chain]: + if chains is None: + return list(parent._chains.values()) + elif isinstance(chains, list): + return [c if isinstance(c, Chain) else parent.get_chain(c) for c in chains] + return [parent.get_chain(chains) if isinstance(chains, str) else chains] + + class Diagnostic: - def __init__(self, parent): - self.parent = parent + def __init__(self, parent: "ChainConsumer"): + self.parent: "ChainConsumer" = parent - def gelman_rubin(self, chain=None, threshold=0.05): + def gelman_rubin( + self, chains: list[Chain | ChainName] | Chain | ChainName | None = None, threshold: float = 0.05 + ) -> TestResult: r"""Runs the Gelman Rubin diagnostic on the supplied chains. - Parameters - ---------- - chain : int|str, optional - Which chain to run the diagnostic on. By default, this is `None`, - which will run the diagnostic on all chains. You can also - supply and integer (the chain index) or a string, for the chain - name (if you set one). - threshold : float, optional - The maximum deviation permitted from 1 for the final value - :math:`\hat{R}` + Args: + chains: Which chain to run the diagnostic on. None will run on all chains + threshold: The maximum deviation permitted from 1 for the final value $\hat{R}$ - Returns - ------- - float - whether or not the chains pass the test + Returns: + The test results - Notes - ----- + Notes: - I follow PyMC in calculating the Gelman-Rubin statistic, where, - having :math:`m` chains of length :math:`n`, we compute + I follow PyMC in calculating the Gelman-Rubin statistic, where, + having :math:`m` chains of length :math:`n`, we compute - .. math:: + .. math:: - B = \frac{n}{m-1} \sum_{j=1}^{m} \left(\bar{\theta}_{.j} - \bar{\theta}_{..}\right)^2 + B = \frac{n}{m-1} \sum_{j=1}^{m} \left(\bar{\theta}_{.j} - \bar{\theta}_{..}\right)^2 - W = \frac{1}{m} \sum_{j=1}^{m} \left[ \frac{1}{n-1} \sum_{i=1}^{n} \left( \theta_{ij} - \bar{\theta_{.j}}\right)^2 \right] + W = \frac{1}{m} \sum_{j=1}^{m} \left[ \frac{1}{n-1} \sum_{i=1}^{n} \left( \theta_{ij} - \bar{\theta_{.j}}\right)^2 \right] - where :math:`\theta` represents each model parameter. We then compute - :math:`\hat{V} = \frac{n_1}{n}W + \frac{1}{n}B`, and have our convergence ratio - :math:`\hat{R} = \sqrt{\frac{\hat{V}}{W}}`. We check that for all parameters, - this ratio deviates from unity by less than the supplied threshold. + where :math:`\theta` represents each model parameter. We then compute + :math:`\hat{V} = \frac{n_1}{n}W + \frac{1}{n}B`, and have our convergence ratio + :math:`\hat{R} = \sqrt{\frac{\hat{V}}{W}}`. We check that for all parameters, + this ratio deviates from unity by less than the supplied threshold. """ # noqa: E501 - if chain is None: - return np.all([self.gelman_rubin(k, threshold=threshold) for k in range(len(self.parent.chains))]) - - index = self.parent._get_chain(chain) - assert len(index) == 1, "Please specify only one chain, have %d chains" % len(index) - chain = self.parent.chains[index[0]] + final_chains = _sanitise_chains(chains, parent=self.parent) + if len(final_chains) > 1: + results = [self.gelman_rubin(c, threshold=threshold) for c in final_chains] + passed = all([r.passed for r in results]) + combined_dict: dict[ChainName, bool] = {} + for result in results: + combined_dict |= result.results + return TestResult(passed=passed, results=combined_dict) + chain = final_chains[0] num_walkers = chain.walkers - parameters = chain.parameters + parameters = chain.data_columns name = chain.name - data = chain.chain - chains = np.split(data, num_walkers) + data = chain.data_samples + split_samples = np.split(data, num_walkers) assert num_walkers > 1, "Cannot run Gelman-Rubin statistic with only one walker" - m = 1.0 * len(chains) - n = 1.0 * chains[0].shape[0] - all_mean = np.mean(data, axis=0) - chain_means = np.array([np.mean(c, axis=0) for c in chains]) - chain_var = np.array([np.var(c, axis=0, ddof=1) for c in chains]) + m = 1.0 * len(split_samples) + n = 1.0 * split_samples[0].shape[0] + all_mean = np.mean(data.to_numpy(), axis=0) + chain_means = np.array([np.mean(c, axis=0) for c in split_samples]) + chain_var = np.array([np.var(c, axis=0, ddof=1) for c in split_samples]) b = n / (m - 1) * ((chain_means - all_mean) ** 2).sum(axis=0) w = (1 / m) * chain_var.sum(axis=0) var = (n - 1) * w / n + b / n v = var + b / (n * m) - r = np.sqrt(v / w) + r: float = np.sqrt(v / w) passed = np.abs(r - 1) < threshold logger.info("Gelman-Rubin Statistic values for chain %s" % name) for p, v, pas in zip(parameters, r, passed): param = "Param %d" % p if isinstance(p, int) else p logger.info(f"{param}: {v:7.5f} ({'Passed' if pas else 'Failed'})") - return np.all(passed) - - def geweke(self, chain=None, first=0.1, last=0.5, threshold=0.05): + all_passed: bool = np.all(passed) + return TestResult(passed=all_passed, results={chain.name: all_passed}) + + def geweke( + self, + chains: list[Chain | ChainName] | Chain | ChainName | None = None, + first: float = 0.1, + last: float = 0.5, + threshold: float = 0.05, + ) -> TestResult: """Runs the Geweke diagnostic on the supplied chains. - Parameters - ---------- - chain : int|str, optional - Which chain to run the diagnostic on. By default, this is `None`, - which will run the diagnostic on all chains. You can also - supply and integer (the chain index) or a string, for the chain - name (if you set one). - first : float, optional - The amount of the start of the chain to use - last : float, optional - The end amount of the chain to use - threshold : float, optional - The p-value to use when testing for normality. - - Returns - ------- - float - whether or not the chains pass the test + Args: + chains: Which chain to run the diagnostic on. None will run on all chains + first: The amount of the start of the chain to use + last: The end amount of the chain to use + threshold: The p-value to use when testing for normality. + Returns: + The test results """ - if chain is None: - return np.all([self.geweke(k, threshold=threshold) for k in range(len(self.parent.chains))]) - - index = self.parent._get_chain(chain) - assert len(index) == 1, "Please specify only one chain, have %d chains" % len(index) - chain = self.parent.chains[index[0]] + final_chains = _sanitise_chains(chains, parent=self.parent) + if len(final_chains) > 1: + results = [self.geweke(c, first=first, last=last, threshold=threshold) for c in final_chains] + passed = all([r.passed for r in results]) + combined_dict = {} + for r in results: + combined_dict |= r.results + return TestResult(passed=passed, results=combined_dict) + chain = final_chains[0] num_walkers = chain.walkers assert ( num_walkers is not None and num_walkers > 0 ), "You need to specify the number of walkers to use the Geweke diagnostic." name = chain.name - data = chain.chain - chains = np.split(data, num_walkers) - n = 1.0 * chains[0].shape[0] + data = chain.data_samples + split_samples = np.split(data.to_numpy(), num_walkers) + n = 1.0 * split_samples[0].shape[0] n_start = int(np.floor(first * n)) n_end = int(np.floor((1 - last) * n)) - mean_start = np.array([np.mean(c[:n_start, i]) for c in chains for i in range(c.shape[1])]) + mean_start = np.array([np.mean(c[:n_start, i]) for c in split_samples for i in range(c.shape[1])]) var_start = np.array( - [self._spec(c[:n_start, i]) / c[:n_start, i].size for c in chains for i in range(c.shape[1])] + [self._spec(c[:n_start, i]) / c[:n_start, i].size for c in split_samples for i in range(c.shape[1])] + ) + mean_end = np.array([np.mean(c[n_end:, i]) for c in split_samples for i in range(c.shape[1])]) + var_end = np.array( + [self._spec(c[n_end:, i]) / c[n_end:, i].size for c in split_samples for i in range(c.shape[1])] ) - mean_end = np.array([np.mean(c[n_end:, i]) for c in chains for i in range(c.shape[1])]) - var_end = np.array([self._spec(c[n_end:, i]) / c[n_end:, i].size for c in chains for i in range(c.shape[1])]) zs = (mean_start - mean_end) / (np.sqrt(var_start + var_end)) _, pvalue = normaltest(zs) logger.info(f"Gweke Statistic for chain {name} has p-value {pvalue:e}") - return pvalue > threshold + passed = pvalue > threshold + return TestResult(passed=passed, results={chain.name: passed}) # Method of estimating spectral density following PyMC. # See https://github.com/pymc-devs/pymc/blob/master/pymc/diagnostics.py @@ -134,3 +153,7 @@ def _spec(self, x, order=2): beta, sigma = yule_walker(x, order) # type: ignore return sigma**2 / (1.0 - np.sum(beta)) ** 2 + + +if __name__ == "__main__": + from chainconsumer import ChainConsumer diff --git a/src/chainconsumer/plotter.py b/src/chainconsumer/plotter.py index a2896548..42e28f2a 100644 --- a/src/chainconsumer/plotter.py +++ b/src/chainconsumer/plotter.py @@ -38,13 +38,6 @@ class PlottingBase(BetterBase): class PlotConfig(BetterBase): labels: dict[ColumnName, str] = Field(default={}, description="Labels for parameters") - sigma2d: bool | None = Field( - default=None, - description=( - "Whether to use 2D sigmas for summary statistics. Ie in 2D a 1sigma contour" - r" does *not* encapsulate 68% of the volume, it covers 39.3% of the volume." - ), - ) max_ticks: int = Field(default=5, ge=0, description="Maximum number of ticks to use on axes") plot_hists: bool = Field(default=True, description="Whether to plot the 1D histograms") flip: bool = Field(default=False, description="Whether to flip the 1D histograms") @@ -55,13 +48,31 @@ class PlotConfig(BetterBase): tick_font_size: int = Field(default=10, ge=0, description="Font size for axis ticks") spacing: float = Field(default=None, ge=0, description="Spacing between subplots") contour_label_font_size: int = Field(default=10, ge=0, description="Font size for contour labels") + show_legend: bool | None = Field( + default=None, + description="Whether to show the legend. None means determine automatically", + ) legend_kwargs: dict[str, Any] = Field(default={}, description="Kwargs to pass to the legend") legend_location: tuple[int, int] | None = Field(default=None, description="Which subplot to put the legend in") legend_artists: bool | None = Field(default=None, description="Whether to show artists in the legend") legend_color_text: bool = Field(default=True, description="Whether to color the legend text") + watermark: str | None = Field(default=None, description="Watermark text to add to the plot") watermark_text_kwargs: dict[str, Any] = Field(default={}, description="Kwargs to pass to the watermark text") - title_size: int = Field(default=14, ge=0, description="Font size for titles") summarise: bool = Field(default=True, description="Whether to annotate the plot with summary statistics") + summary_font_size: int = Field(default=12, ge=0, description="Font size for parameter summaries") + sigma2d: bool | None = Field( + default=None, + description=( + "Whether to use 2D sigmas for summary statistics. Ie in 2D a 1sigma contour" + r" does *not* encapsulate 68% of the volume, it covers 39.3% of the volume." + ), + ) + blind: bool | list[str] = Field(default=False, description="Whether to blind some parameters") + log_scales: list[ColumnName] = Field(default=[], description="Whether to use log scales for some parameters") + extents: dict[ColumnName, tuple[float, float]] = Field( + default={}, description="Extents for parameters. Any you don't specify are determined automatically" + ) + dpi: int = Field(default=300, ge=0, description="DPI for the figure") @property def legend_kwargs_final(self) -> dict[str, Any]: @@ -83,6 +94,7 @@ def watermark_text_kwargs_final(self) -> dict[str, Any]: "alpha": 0.7, "verticalalignment": "center", "horizontalalignment": "center", + "weight": "bold", } return default | self.watermark_text_kwargs @@ -180,67 +192,41 @@ def config(self) -> PlotConfig: def plot( self, - figsize: FigSize | float | int | tuple[float, float] = FigSize.GROW, - columns: list[ColumnName] | None = None, chains: list[ChainName | Chain] | None = None, - extents: dict[ColumnName, tuple[float, float]] | None = None, + columns: list[ColumnName] | None = None, filename: list[str | Path] | str | Path | None = None, - show_legend: bool | None = None, - blind: bool | list[str] | None = None, - watermark: str | None = None, - log_scales: list[ColumnName] | None = None, + figsize: FigSize | float | int | tuple[float, float] = FigSize.GROW, ) -> Figure: # pragma: no cover """Plot the chain! - Parameters - ---------- - figsize : str|tuple(float)|float, optional - The figure size to generate. Accepts a regular two tuple of size in inches, - or one of several key words. The default value of ``COLUMN`` creates a figure - of appropriate size of insertion into an A4 LaTeX document in two-column mode. - ``PAGE`` creates a full page width figure. ``GROW`` creates an image that - scales with parameters (1.5 inches per parameter). String arguments are not - case sensitive. If you pass a float, it will scale the default ``GROW`` by - that amount, so ``2.0`` would result in a plot 3 inches per parameter. - columns : list[str], optional - If set, only creates a plot for those specific parameters (if list). If an - integer is given, only plots the fist so many parameters. - chains : int|str, list[str|int], optional - Used to specify which chain to show if more than one chain is loaded in. - Can be an integer, specifying the - chain index, or a str, specifying the chain name. - extents : list[tuple[float]] or dict[str], optional - Extents are given as two-tuples. You can pass in a list the same size as - parameters (or default parameters if you don't specify parameters), - or as a dictionary. - filename : str, optional - If set, saves the figure to this location - display : bool, optional - If True, shows the figure using ``plt.show()``. - truth : list[float] or dict[str], optional - A list of truth values corresponding to parameters, or a dictionary of - truth values indexed by key - legend : bool, optional - If true, creates a legend in your plot using the chain names. - blind : bool|string|list[string], optional - Whether to blind axes values. Can be set to `True` to blind all parameters, - or can pass in a string (or list of strings) which specify the parameters to blind. - watermark : str, optional - A watermark to add to the figure - log_scales : bool, list[ColumnName], optional - Whether or not to use a log scale on any given axis. Can be a list of True/False, a list of param - names to set to true, a dictionary of param names with true/false - or just a bool (just `True` would set everything to log scales). - - Returns - ------- - figure + Args: + chains: + Used to specify which chain to show if more than one chain is loaded in. + Can be an integer, specifying the + chain index, or a str, specifying the chain name. + columns: + If set, only creates a plot for those specific parameters (if list). If an + integer is given, only plots the fist so many parameters. + filename: + If set, saves the figure to this location + figsize: + The figure size to generate. Accepts a regular two tuple of size in inches, + or one of several key words. The default value of ``COLUMN`` creates a figure + of appropriate size of insertion into an A4 LaTeX document in two-column mode. + ``PAGE`` creates a full page width figure. ``GROW`` creates an image that + scales with parameters (1.5 inches per parameter). String arguments are not + case sensitive. If you pass a float, it will scale the default ``GROW`` by + that amount, so ``2.0`` would result in a plot 3 inches per parameter. + + Returns: the matplotlib figure """ + base = self._sanitise( + chains, columns, self.config.extents, blind=self.config.blind, log_scales=self.config.log_scales + ) - base = self._sanitise(chains, columns, extents, blind=blind, log_scales=log_scales) - + show_legend = self.config.show_legend if show_legend is None: show_legend = len(base.chains) > 1 @@ -336,14 +322,6 @@ def plot( for text, chain in zip(leg.get_texts(), base.chains): text.set_fontweight("medium") text.set_color(colors.format(chain.color)) - - # TODO: This seems like behaviour which no longer works - # if not legend_outside: - # loc = legend_kwargs.get("loc") or "" - # if isinstance(loc, str) and "right" in loc.lower(): - # vp = leg._legend_box._children[-1]._children[0] - # vp.align = "right" - fig.canvas.draw() for ax in axes[-1, :]: offset = ax.get_xaxis().get_offset_text() @@ -354,16 +332,15 @@ def plot( ax.set_ylabel("{} {}".format(ax.get_ylabel(), f"[{offset.get_text()}]" if offset.get_text() else "")) offset.set_visible(False) - dpi = 300 - if watermark: + if self.config.watermark is not None: ax_watermark = axes[-1, 0] if flip and len(base.columns) == 2 else None - self._add_watermark(fig, ax_watermark, fig_size, watermark, dpi=dpi) + self._add_watermark(fig, ax_watermark, fig_size, self.config.watermark, dpi=self.config.dpi) if filename is not None: if not isinstance(filename, list): filename = [filename] for f in filename: - self._save_fig(fig, f, dpi) + self._save_fig(fig, f, self.config.dpi) return fig @@ -377,7 +354,7 @@ def _add_watermark( dx, dy = fig_size dy, dx = dy * dpi, dx * dpi rotation = 180 / np.pi * np.arctan2(-dy, dx) - property_dict = self.config.watermark_text_kwargs + property_dict = self.config.watermark_text_kwargs_final keys_in_font_dict = ["family", "style", "variant", "weight", "stretch", "size"] fontdict = {k: property_dict[k] for k in keys_in_font_dict if k in property_dict} @@ -392,7 +369,7 @@ def _add_watermark( bb1 = TextPath((0, 0), text, size=51, prop=font_prop, usetex=usetex).get_extents() dw = (bb1.width - bb0.width) * (dpi / 100) dh = (bb1.height - bb0.height) * (dpi / 100) - size = np.sqrt(dy**2 + dx**2) / (dh * abs(dy / dx) + dw) * 0.6 * scale * size_scale + size = np.sqrt(dy**2 + dx**2) / (dh * abs(dy / dx) + dw) * 0.7 * scale * size_scale if axes is not None: if usetex: size *= 0.7 @@ -687,236 +664,164 @@ def _add_watermark( # return fig - # def plot_summary( - # self, - # parameters=None, - # truth=None, - # extents=None, - # display=False, - # filename=None, - # chains=None, - # figsize=1.0, - # errorbar=False, - # include_truth_chain=True, - # blind=None, - # watermark=None, - # extra_parameter_spacing=0.5, - # vertical_spacing_ratio=1.0, - # show_names=True, - # log_scales=None, - # ): # pragma: no cover - # """Plots parameter summaries + def plot_summary( + self, + chains: list[ChainName | Chain] | None = None, + columns: list[ColumnName] | None = None, + filename: list[str | Path] | str | Path | None = None, + figsize: float = 1.0, + errorbar: bool = False, + extra_parameter_spacing: float = 1.0, + vertical_spacing_ratio: float = 1.0, + ): # pragma: no cover + """Plots parameter summaries - # This plot is more for a sanity or consistency check than for use with final results. - # Plotting this before plotting with :func:`plot` allows you to quickly see if the - # chains give well behaved distributions, or if certain parameters are suspect - # or require a greater burn in period. + This plot is more for a sanity or consistency check than for use with final results. + Plotting this before plotting with :func:`plot` allows you to quickly see if the + chains give well behaved distributions, or if certain parameters are suspect + or require a greater burn in period. - # Parameters - # ---------- - # parameters : list[str]|int, optional - # Specify a subset of parameters to plot. If not set, all parameters are plotted. - # If an integer is given, only the first so many parameters are plotted. - # truth : list[float]|list|list[float]|dict[str]|str, optional - # A list of truth values corresponding to parameters, or a dictionary of - # truth values keyed by the parameter. Each "truth value" can be either a float (will - # draw a vertical line), two floats (a shaded interval) or three floats (min, mean, max), - # which renders as a shaded interval with a line for the mean. Or, supply a string - # which matches a chain name, and the results for that chain will be used as the 'truth' - # extents : list[tuple]|dict[str], optional - # A list of two-tuples for plot extents per parameter, or a dictionary of - # extents keyed by the parameter. - # display : bool, optional - # If set, shows the plot using ``plt.show()`` - # filename : str, optional - # If set, saves the figure to the filename - # chains : int|str, list[str|int], optional - # Used to specify which chain to show if more than one chain is loaded in. - # Can be an integer, specifying the - # chain index, or a str, specifying the chain name. - # figsize : float, optional - # Scale horizontal and vertical figure size. - # errorbar : bool, optional - # Whether to onle plot an error bar, instead of the marginalised distribution. - # include_truth_chain : bool, optional - # If you specify another chain as the truth chain, determine if it should still - # be plotted. - # blind : bool|string|list[string], optional - # Whether to blind axes values. Can be set to `True` to blind all parameters, - # or can pass in a string (or list of strings) which specify the parameters to blind. - # watermark : str, optional - # A watermark to add to the figure - # extra_parameter_spacing : float, optional - # Increase horizontal space for parameter values - # vertical_spacing_ratio : float, optional - # Increase vertical space for each model - # show_names : bool, optional - # Whether to show chain names or not. Defaults to `True`. - # log_scales : bool, list[bool] or dict[bool], optional - # Whether or not to use a log scale on any given axis. Can be a list of True/False, a list of param - # names to set to true, a dictionary of param names with true/false - # or just a bool (just `True` would set everything to log scales). - - # Returns - # ------- - # figure - # the matplotlib figure created + Args: + chains: + Used to specify which chain to show if more than one chain is loaded in. + Can be an integer, specifying the + chain index, or a str, specifying the chain name. + columns: + If set, only creates a plot for those specific parameters (if list). If an + integer is given, only plots the fist so many parameters. + filename: + If set, saves the figure to this location + figsize: + Scale horizontal and vertical figure size. + errorbar: + Whether to onle plot an error bar, instead of the marginalised distribution. + include_truth_chain: + If you specify another chain as the truth chain, determine if it should still + be plotted. + extra_parameter_spacing: + Increase horizontal space for parameter values + vertical_spacing_ratio: + Increase vertical space for each model + Returns: + the matplotlib figure created - # """ - # wide_extents = not errorbar - # chains, parameters, truth, extents, blind, log_scales = self._sanitise( - # chains, parameters, truth, extents, blind=blind, wide_extents=wide_extents, log_scales=log_scales - # ) + """ + wide_extents = not errorbar + base = self._sanitise( + chains, + columns, + self.config.extents, + blind=self.config.blind, + log_scales=self.config.log_scales, + wide_extents=wide_extents, + ) - # all_names = [c.name for c in self.parent.chains] - - # # Check if we're using a chain for truth values - # if isinstance(truth, str): - # assert truth in all_names, f"Truth chain {truth} is not in the list of added chains {all_names}" - # if not include_truth_chain: - # chains = [c for c in chains if c.name != truth] - # truth = self.parent.analysis.get_summary(chains=truth, parameters=parameters) - - # max_param = self._get_size_of_texts(parameters) - # fid_dpi = 65 # Seriously I have no idea what value this should be - # param_width = extra_parameter_spacing + max(0.5, max_param / fid_dpi) - - # if show_names: - # max_model_name = self._get_size_of_texts([chain.name for chain in chains]) - # model_width = 0.25 + (max_model_name / fid_dpi) - # gridspec_kw = { - # "width_ratios": [model_width] + [param_width] * len(parameters), - # "height_ratios": [1] * len(chains), - # } - # ncols = 1 + len(parameters) - # else: - # model_width = 0 - # gridspec_kw = {"width_ratios": [param_width] * len(parameters), "height_ratios": [1] * len(chains)} - # ncols = len(parameters) - - # top_spacing = 0.3 - # bottom_spacing = 0.2 - # row_height = (0.5 if not errorbar else 0.3) * vertical_spacing_ratio - # width = param_width * len(parameters) + model_width - # height = top_spacing + bottom_spacing + row_height * len(chains) - # top_ratio = 1 - (top_spacing / height) - # bottom_ratio = bottom_spacing / height - - # figsize = (width * figsize, height * figsize) - # fig, axes = plt.subplots( - # nrows=len(chains), ncols=ncols, figsize=figsize, squeeze=False, gridspec_kw=gridspec_kw - # ) - # fig.subplots_adjust(left=0.05, right=0.95, top=top_ratio, bottom=bottom_ratio, wspace=0.0, hspace=0.0) - # label_font_size = self.parent.config["label_font_size"] - # legend_color_text = self.parent.config["legend_color_text"] - - # max_vals = {} - # for i, row in enumerate(axes): - # chain = chains[i] - - # ( - # cs, - # ws, - # ps, - # ) = ( - # chain.chain, - # chain.weights, - # chain.parameters, - # ) - # gs, ns = chain.grid, chain.name - - # colour = chain.config["color"] - - # # First one put name of model - # if show_names: - # ax_first = row[0] - # ax_first.set_axis_off() - # text_colour = "k" if not legend_color_text else colour - # ax_first.text( - # 0, - # 0.5, - # ns, - # transform=ax_first.transAxes, - # fontsize=label_font_size, - # verticalalignment="center", - # color=text_colour, - # weight="medium", - # ) - # cols = row[1:] - # else: - # cols = row - - # for ax, p in zip(cols, parameters): - # # Set up the frames - # if i > 0: - # ax.spines["top"].set_visible(False) - # if i < (len(chains) - 1): - # ax.spines["bottom"].set_visible(False) - # if i < (len(chains) - 1) or p in blind: - # ax.set_xticks([]) - # ax.set_yticks([]) - # ax.set_xlim(extents[p]) - # if log_scales.get(p): - # ax.set_xscale("log") - - # # Put title in - # if i == 0: - # ax.set_title(r"$%s$" % p, fontsize=label_font_size) - - # # Add truth values - # truth_value = truth.get(p) - # if truth_value is not None: - # if isinstance(truth_value, float | int): - # truth_mean = truth_value - # truth_min, truth_max = None, None - # else: - # if len(truth_value) == 1: - # truth_mean = truth_value - # truth_min, truth_max = None, None - # elif len(truth_value) == 2: - # truth_min, truth_max = truth_value - # truth_mean = None - # else: - # truth_min, truth_mean, truth_max = truth_value - # if truth_mean is not None: - # ax.axvline(truth_mean, **self.parent.config_truth) - # if truth_min is not None and truth_max is not None: - # ax.axvspan(truth_min, truth_max, color=self.parent.config_truth["color"], alpha=0.15, lw=0) - # # Skip if this chain doesnt have the parameter - # if p not in ps: - # continue + # We have a bit of fun to go from chain names to the width of the + # subplot used to display said names + max_param = self._get_size_of_texts(base.columns) + fid_dpi = 65 # Seriously I have no idea what value this should be + param_width = extra_parameter_spacing + max(0.5, max_param / fid_dpi) + max_model_name = self._get_size_of_texts([chain.name for chain in base.chains]) + model_width = 0.25 + (max_model_name / fid_dpi) + gridspec_kw = { + "width_ratios": [model_width] + [param_width] * len(base.columns), + "height_ratios": [1] * len(base.chains), + } + ncols = 1 + len(base.columns) + top_spacing = 0.3 + bottom_spacing = 0.2 + row_height = (0.5 if errorbar else 0.8) * vertical_spacing_ratio + width = param_width * len(base.columns) + model_width + height = top_spacing + bottom_spacing + row_height * len(base.chains) + top_ratio = 1 - (top_spacing / height) + bottom_ratio = bottom_spacing / height + + fig_size = (width * figsize, height * figsize) + fig, axes = plt.subplots( + nrows=len(base.chains), ncols=ncols, figsize=fig_size, squeeze=False, gridspec_kw=gridspec_kw + ) + fig.subplots_adjust(left=0.05, right=0.95, top=top_ratio, bottom=bottom_ratio, wspace=0.0, hspace=0.0) + label_font_size = self.config.label_font_size + legend_color_text = self.config.legend_color_text + + max_vals: dict[ColumnName, float] = {} + num_chains = len(base.chains) + for i, axes_row in enumerate(axes): + chain = base.chains[i] + colour = colors.format(chain.color) + + # First one put name of model + ax_first = axes_row[0] + ax_first.set_axis_off() + text_colour = "k" if not legend_color_text else colour + ax_first.text( + 0, + 0.5, + chain.name, + transform=ax_first.transAxes, + fontsize=label_font_size, + verticalalignment="center", + color=text_colour, + weight="medium", + ) + axes_for_summaries = axes_row[1:] + + for ax, p in zip(axes_for_summaries, base.columns): + # Set up the frames + if i > 0: + ax.spines["top"].set_visible(False) + if i < (num_chains - 1): + ax.spines["bottom"].set_visible(False) + if i < (num_chains - 1) or p in base.blind: + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_xlim(base.extents[p]) + if p in base.log_scales: + ax.set_xscale("log") + + # Put title in + if i == 0: + ax.set_title(r"$%s$" % p, fontsize=label_font_size) + + # Add truth values + for truth in self.parent._truths: + truth_value = truth.location.get(p) + if truth_value is not None: + ax.axvline(truth_value, **truth._kwargs) + + # Skip if this chain doesnt have the parameter + if p not in chain.data_columns: + continue - # # Plot the good stuff - # if errorbar: - # fv = self.parent.analysis.get_parameter_summary(chain, p) - # if fv[0] is not None and fv[2] is not None: - # diff = np.abs(np.diff(fv)) - # ax.errorbar([fv[1]], 0, xerr=[[diff[0]], [diff[1]]], fmt="o", color=colour) - # else: - # m = self._plot_bars(ax, p, chain) - # if max_vals.get(p) is None or m > max_vals.get(p): - # max_vals[p] = m + # Plot the good stuff + if errorbar: + fv = self.parent.analysis.get_parameter_summary(chain, p) + if fv is None or fv.all_none: + continue + if fv.lower is not None and fv.upper is not None: + diff = np.abs(np.diff(fv.array)) + ax.errorbar([fv.center], 0, xerr=[[diff[0]], [diff[1]]], fmt="o", color=colour) + else: + m = self._plot_bars(ax, p, chain) + if max_vals.get(p) is None or m > max_vals[p]: + max_vals[p] = m - # for i, row in enumerate(axes): - # index = 1 if show_names else 0 - # for ax, p in zip(row[index:], parameters): - # if not errorbar: - # ax.set_ylim(0, 1.1 * max_vals[p]) + for i, axes_row in enumerate(axes): + for ax, p in zip(axes_row, base.columns): + if not errorbar: + ax.set_ylim(0, 1.1 * max_vals[p]) - # dpi = 300 - # if watermark: - # ax = None - # self._add_watermark(fig, ax, figsize, watermark, dpi=dpi, size_scale=0.8) + if self.config.watermark: + ax = None + self._add_watermark(fig, ax, fig_size, self.config.watermark, dpi=self.config.dpi, size_scale=0.8) - # if filename is not None: - # if isinstance(filename, str): - # filename = [filename] - # for f in filename: - # self._save_fig(fig, f, dpi) - # if display: - # plt.show() + if filename is not None: + if not isinstance(filename, list): + filename = [filename] + for f in filename: + self._save_fig(fig, f, self.config.dpi) - # return fig + return fig def _get_size_of_texts(self, texts: list[str]) -> float: # pragma: no cover usetex = self.config.usetex @@ -926,7 +831,12 @@ def _get_size_of_texts(self, texts: list[str]) -> float: # pragma: no cover def _sanitise_columns(self, columns: list[ColumnName] | None, chains: list[Chain]) -> list[ColumnName]: if columns is None: - return list(set([c for chain in chains for c in chain.plotting_columns])) + res = [] # Doing it without set to preserve order + for chain in chains: + for column in chain.plotting_columns: + if column not in res: + res.append(column) + return res return columns def _sanitise_logscale(self, log_scales: list[ColumnName] | None) -> list[ColumnName]: @@ -989,13 +899,14 @@ def _get_custom_extents( self, columns: list[ColumnName], chains: list[Chain], - extents: dict[ColumnName, tuple[float, float]] | None, + initial_extents: dict[ColumnName, tuple[float, float]] | None, wide_extents: bool = True, ) -> dict[ColumnName, tuple[float, float]]: # pragma: no cover - if extents is None: - extents = {} + if initial_extents is None: + initial_extents = {} + extents = {} | initial_extents for p in columns: - if p not in extents: + if p not in initial_extents: extents[p] = self._get_parameter_extents(p, chains, wide_extents=wide_extents) return extents @@ -1155,7 +1066,7 @@ def _plot_point(self, ax: Axes, chain: Chain, px: str, py: str) -> PathCollectio # Determine if we need to darken the point c = colors.format(chain.color) if chain.plot_contour: - c = colors.scale_colour(chain.color, 0.5) + c = colors.scale_colour(colors.format(chain.color), 0.5) h = ax.scatter( [point.coordinate[px]], [point.coordinate[py]], @@ -1216,7 +1127,7 @@ def _plot_contour(self, ax: Axes, chain: Chain, px: str, py: str) -> PathCollect x = chain.get_data(py) y = chain.get_data(px) - contour_colours = self._scale_colours(chain.color, len(levels), chain.shade_gradient) + contour_colours = self._scale_colours(colors.format(chain.color), len(levels), chain.shade_gradient) sub = max(0.1, 1 - 0.2 * chain.shade_gradient) paths = None @@ -1343,9 +1254,11 @@ def _plot_bars( if summary: t = self.parent.analysis.get_parameter_text(fit_values) if isinstance(column, str): - ax.set_title(r"${} = {}$".format(column.strip("$"), t), fontsize=self.config.title_size) + ax.set_title( + r"${} = {}$".format(column.strip("$"), t), fontsize=self.config.summary_font_size + ) else: - ax.set_title(r"$%s$" % t, fontsize=self.config.title_size) + ax.set_title(r"$%s$" % t, fontsize=self.config.summary_font_size) return ys.max() def _plot_walk( diff --git a/tests/test_analysis.py b/tests/test_analysis.py index f63fe0e3..706a78a3 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -1,12 +1,8 @@ -import os -import tempfile - import numpy as np -import pytest -from scipy.interpolate import interp1d -from scipy.stats import norm, skewnorm +import pandas as pd +from scipy.stats import skewnorm -from chainconsumer import ChainConsumer +from chainconsumer import Bound, Chain, ChainConfig, ChainConsumer class TestChain: @@ -17,13 +13,17 @@ class TestChain: data_combined = np.vstack((data, data2)).T data_skew = skewnorm.rvs(5, loc=1, scale=1.5, size=n) + chain = Chain(samples=pd.DataFrame(data, columns=["x"]), name="a") + chain2 = Chain(samples=pd.DataFrame(data2, columns=["x"]), name="b") + chain_combined = Chain(samples=pd.DataFrame(data_combined, columns=["a", "b"]), name="combined") + chain_skew = Chain(samples=pd.DataFrame(data_skew, columns=["x"]), name="skew") # type: ignore + def test_summary(self): tolerance = 4e-2 consumer = ChainConsumer() - consumer.add_chain(self.data[::10]) - consumer.configure_overrides(kde=True) + consumer.add_chain(Chain(samples=pd.DataFrame(self.data[::10], columns=["x"]), name="a", kde=True)) summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) + actual = summary["a"]["x"].array expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -31,10 +31,10 @@ def test_summary(self): def test_summary_no_smooth(self): tolerance = 5e-2 consumer = ChainConsumer() - consumer.add_chain(self.data) - consumer.configure_overrides(smooth=0, bins=2.4) + consumer.add_chain(self.chain) + consumer.set_override(ChainConfig(smooth=0, bins=2.4)) summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) + actual = summary["a"]["x"].array expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -42,65 +42,40 @@ def test_summary_no_smooth(self): def test_summary2(self): tolerance = 5e-2 consumer = ChainConsumer() - consumer.add_chain(self.data_combined, parameters=["a", "b"], name="chain1") - consumer.add_chain(self.data_combined, name="chain2") + consumer.add_chain(self.chain_combined) + c2 = self.chain_combined.model_copy() + c2.name = "chain2" + consumer.add_chain(c2) summary = consumer.analysis.get_summary() - k1 = list(summary[0].keys()) - k2 = list(summary[1].keys()) - assert len(k1) == 2 - assert "a" in k1 - assert "b" in k1 - assert len(k2) == 2 - assert "a" in k2 - assert "b" in k2 + + assert len(summary) == 2 + assert "combined" in summary + assert "chain2" in summary + assert "a" in summary["combined"] + assert "b" in summary["combined"] + assert "a" in summary["chain2"] + assert "b" in summary["chain2"] + expected1 = np.array([3.5, 5.0, 6.5]) expected2 = np.array([2.0, 3.0, 4.0]) - diff1 = np.abs(expected1 - np.array(list(summary[0]["a"]))) - diff2 = np.abs(expected2 - np.array(list(summary[0]["b"]))) + diff1 = np.abs(expected1 - summary["combined"]["a"].array) + diff2 = np.abs(expected2 - summary["chain2"]["b"].array) assert np.all(diff1 < tolerance) assert np.all(diff2 < tolerance) - def test_summary_some_params(self): - consumer = ChainConsumer() - consumer.add_chain(self.data_combined, parameters=["a", "b"], name="chain1") - summary = consumer.analysis.get_summary(columns=["a"], squeeze=False) - k1 = list(summary[0].keys()) - assert len(k1) == 1 - assert "a" in k1 - assert "b" not in k1 - - def test_summary1(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data) - consumer.configure_overrides(bins=0.8) - summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - expected = np.array([3.5, 5.0, 6.5]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_summary_specific(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data, name="A") - consumer.configure_overrides(bins=0.8) - summary = consumer.analysis.get_summary(chains="A") - actual = np.array(next(iter(summary.values()))) - expected = np.array([3.5, 5.0, 6.5]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - def test_summary_disjoint(self): tolerance = 5e-2 consumer = ChainConsumer() - consumer.add_chain(self.data, parameters="A") - consumer.add_chain(self.data, parameters="B") - consumer.configure_overrides(bins=0.8) - summary = consumer.analysis.get_summary(columns="A") + consumer.add_chain(self.chain) + c2 = self.chain.model_copy() + c2.name = "chain2" + c2.samples = c2.samples.rename(columns={"x": "y"}) + consumer.add_chain(c2) + consumer.set_override(ChainConfig(bins=0.8)) + summary = consumer.analysis.get_summary(columns=["x"]) assert len(summary) == 2 # Two chains - assert summary[1] == {} # Second chain doesnt have param A - actual = summary[0]["A"] + assert not summary["chain2"] # but this one has no cols + actual = summary["a"]["x"].array expected = np.array([3.5, 5.0, 6.5]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) @@ -109,73 +84,73 @@ def test_summary_power(self): tolerance = 5e-2 consumer = ChainConsumer() data = self.rng.normal(loc=0, scale=np.sqrt(2), size=1000000) - consumer.add_chain(data, power=2.0) + consumer.add_chain(Chain(samples=pd.DataFrame(data, columns=["x"]), name="A", power=2.0)) summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) + actual = summary["A"]["x"].array expected = np.array([-1.0, 0.0, 1.0]) diff = np.abs(expected - actual) assert np.all(diff < tolerance) def test_output_text(self): consumer = ChainConsumer() - consumer.add_chain(self.data, parameters=["a"]) - consumer.configure_overrides(bins=0.8) + consumer.add_chain(self.chain) + consumer.set_override(ChainConfig(bins=0.8)) vals = consumer.analysis.get_summary()["a"] - text = consumer.analysis.get_parameter_text(*vals) + text = consumer.analysis.get_parameter_text(vals["x"]) assert text == r"5.0\pm 1.5" def test_output_text_asymmetric(self): - p1 = [1.0, 2.0, 3.5] + bound = Bound.from_array([1.0, 2.0, 3.5]) consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(bound) assert text == r"2.0^{+1.5}_{-1.0}" def test_output_format1(self): p1 = [1.0e-1, 2.0e-1, 3.5e-1] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"0.20^{+0.15}_{-0.10}" def test_output_format2(self): p1 = [1.0e-2, 2.0e-2, 3.5e-2] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"0.020^{+0.015}_{-0.010}" def test_output_format3(self): p1 = [1.0e-3, 2.0e-3, 3.5e-3] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"\left( 2.0^{+1.5}_{-1.0} \right) \times 10^{-3}" def test_output_format4(self): p1 = [1.0e3, 2.0e3, 3.5e3] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"\left( 2.0^{+1.5}_{-1.0} \right) \times 10^{3}" def test_output_format5(self): p1 = [1.1e6, 2.2e6, 3.3e6] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"\left( 2.2\pm 1.1 \right) \times 10^{6}" def test_output_format6(self): p1 = [1.0e-2, 2.0e-2, 3.5e-2] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1, wrap=True) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1), wrap=True) assert text == r"$0.020^{+0.015}_{-0.010}$" def test_output_format7(self): p1 = [None, 2.0e-2, 3.5e-2] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == "" def test_output_format8(self): p1 = [-1, -0.0, 1] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"0.0\pm 1.0" def test_output_format9(self): @@ -183,7 +158,7 @@ def test_output_format9(self): d = 123.321 p1 = [x - d, x, x + d] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"123460\pm 120" def test_output_format10(self): @@ -191,7 +166,7 @@ def test_output_format10(self): d = 1234.321 p1 = [x - d, x, x + d] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"\left( 123.5\pm 1.2 \right) \times 10^{3}" def test_output_format11(self): @@ -199,7 +174,7 @@ def test_output_format11(self): d = 111.111 p1 = [x - d, x, x + d] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"220\pm 110" def test_output_format12(self): @@ -207,7 +182,7 @@ def test_output_format12(self): d = 11.111 p1 = [x - d, x, x + d] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"222\pm 11" def test_output_format13(self): @@ -215,7 +190,7 @@ def test_output_format13(self): d = 11.111 p1 = [x - d, x, x + d] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"2222\pm 11" def test_output_format14(self): @@ -223,7 +198,7 @@ def test_output_format14(self): d = 1.111 p1 = [x - d, x, x + d] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"222.2\pm 1.1" def test_output_format15(self): @@ -231,7 +206,7 @@ def test_output_format15(self): d = 0.111 p1 = [x - d, x, x + d] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"222.22\pm 0.11" def test_output_format16(self): @@ -239,734 +214,612 @@ def test_output_format16(self): d = 0.0111 p1 = [x - d, x, x + d] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"222.222\pm 0.011" def test_output_format17(self): p1 = [1.0, 1.0, 2.0] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"1.0^{+1.0}_{-0.0}" def test_output_format18(self): p1 = [10000.0, 10000.0, 10000.0] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"\left( 1.0\pm 0.0 \right) \times 10^{4}" def test_output_format19(self): p1 = [1.0, 2.0, 2.0] consumer = ChainConsumer() - text = consumer.analysis.get_parameter_text(*p1) + text = consumer.analysis.get_parameter_text(Bound.from_array(p1)) assert text == r"2.0^{+0.0}_{-1.0}" - def test_file_loading1(self): - data = self.data[:1000] - directory = tempfile._get_default_tempdir() - filename = next(tempfile._get_candidate_names()) - filename = directory + os.sep + filename + ".txt" - np.savetxt(filename, data) - consumer = ChainConsumer() - consumer.add_chain(filename) - summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - assert np.abs(actual[1] - 5.0) < 0.5 - - def test_file_loading2(self): - data = self.data[:1000] - directory = tempfile._get_default_tempdir() - filename = next(tempfile._get_candidate_names()) - filename = directory + os.sep + filename + ".npy" - np.save(filename, data) - consumer = ChainConsumer() - consumer.add_chain(filename) - summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - assert np.abs(actual[1] - 5.0) < 0.5 - - def test_using_list(self): - data = self.data.tolist() - c = ChainConsumer() - c.add_chain(data) - summary = c.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - assert np.abs(actual[1] - 5.0) < 0.1 - - def test_using_dict(self): - data = {"x": self.data, "y": self.data2} - c = ChainConsumer() - c.add_chain(data) - summary = c.analysis.get_summary() - deviations = np.abs([summary["x"][1] - 5, summary["y"][1] - 3]) - assert np.all(deviations < 0.1) - - def test_summary_when_no_parameter_names(self): - c = ChainConsumer() - c.add_chain(self.data) - summary = c.analysis.get_summary() - assert list(summary.keys()) == ["0"] - - def test_squeeze_squeezes(self): - sum = ChainConsumer().add_chain(self.data).analysis.get_summary() - assert isinstance(sum, dict) - - def test_squeeze_doesnt(self): - sum = ChainConsumer().add_chain(self.data).analysis.get_summary(squeeze=False) - assert isinstance(sum, list) - assert len(sum) == 1 - - def test_squeeze_doesnt_squeeze_multi(self): - c = ChainConsumer() - c.add_chain(self.data).add_chain(self.data) - sum = c.analysis.get_summary() - assert isinstance(sum, list) - assert len(sum) == 2 - - def test_dictionary_and_parameters_fail(self): - with pytest.raises(AssertionError): - ChainConsumer().add_chain({"x": self.data}, parameters=["$x$"]) - - def test_convergence_failure(self): - data = np.concatenate((self.rng.normal(loc=0.0, size=10000), self.rng.normal(loc=4.0, size=10000))) - consumer = ChainConsumer() - consumer.add_chain(data) - summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - assert actual[0] is None and actual[2] is None - - def test_divide_chains_default(self): - data = np.concatenate((self.rng.normal(loc=0.0, size=100000), self.rng.normal(loc=1.0, size=100000))) - consumer = ChainConsumer() - num_walkers = 2 - consumer.add_chain(data, walkers=num_walkers) - - c = consumer.divide_chain() - c.configure_overrides(bins=0.7) - means = [0, 1.0] - for i in range(num_walkers): - stats = next(iter(c.analysis.get_summary()[i].values())) - assert np.abs(stats[1] - means[i]) < 1e-1 - assert np.abs(c.chains[i].chain[:, 0].mean() - means[i]) < 1e-2 - - def test_divide_chains_index(self): - data = np.concatenate((self.rng.normal(loc=0.0, size=100000), self.rng.normal(loc=1.0, size=100000))) - consumer = ChainConsumer() - num_walkers = 2 - consumer.add_chain(data, walkers=num_walkers) - - c = consumer.divide_chain(chain=0) - c.configure_overrides(bins=0.7) - means = [0, 1.0] - for i in range(num_walkers): - stats = next(iter(c.analysis.get_summary()[i].values())) - assert np.abs(stats[1] - means[i]) < 1e-1 - assert np.abs(c.chains[i].chain[:, 0].mean() - means[i]) < 1e-2 - def test_divide_chains_name(self): data = np.concatenate((self.rng.normal(loc=0.0, size=100000), self.rng.normal(loc=1.0, size=100000))) consumer = ChainConsumer() num_walkers = 2 - consumer.add_chain(data, walkers=num_walkers, name="test") - c = consumer.divide_chain(chain="test") - c.configure_overrides(bins=0.7) + chain = Chain(samples=pd.DataFrame(data, columns=["x"]), walkers=num_walkers, name="test") + for c in chain.divide(): + consumer.add_chain(c) + consumer.set_override(ChainConfig(bins=0.7)) means = [0, 1.0] - for i in range(num_walkers): - stats = next(iter(c.analysis.get_summary()[i].values())) - assert np.abs(stats[1] - means[i]) < 1e-1 - assert np.abs(c.chains[i].chain[:, 0].mean() - means[i]) < 1e-2 + stats = consumer.analysis.get_summary() - def test_divide_chains_fail(self): - data = np.concatenate((self.rng.normal(loc=0.0, size=100000), self.rng.normal(loc=1.0, size=100000))) - consumer = ChainConsumer() - consumer.add_chain(data, walkers=2) - with pytest.raises(ValueError): - consumer.divide_chain(chain=2.0) - - def test_divide_chains_name_fail(self): - data = np.concatenate((self.rng.normal(loc=0.0, size=200000), self.rng.normal(loc=1.0, size=200000))) - consumer = ChainConsumer() - consumer.add_chain(data, walkers=2) - with pytest.raises(AssertionError): - consumer.divide_chain(chain="notexist") - - def test_stats_max_normal(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data) - consumer.configure_overrides(statistics="max") - summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - expected = np.array([3.5, 5.0, 6.5]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_stats_max_cliff(self): - tolerance = 5e-2 - n = 100000 - data = np.linspace(0, 10, n) - weights = norm.pdf(data, 1, 2) - consumer = ChainConsumer() - consumer.add_chain(data, weights=weights) - consumer.configure_overrides(statistics="max", bins=4.0, smooth=1) - summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - expected = np.array([0.0, 1.0, 2.73]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_stats_mean_normal(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data) - consumer.configure_overrides(statistics="mean") - summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - expected = np.array([3.5, 5.0, 6.5]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_stats_cum_normal(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data) - consumer.configure_overrides(statistics="cumulative") - summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - expected = np.array([3.5, 5.0, 6.5]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_reject_bad_satst(self): - consumer = ChainConsumer() - consumer.add_chain(self.data) - with pytest.raises(AssertionError): - consumer.configure_overrides(statistics="monkey") - - def test_stats_max_skew(self): - tolerance = 3e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data_skew) - consumer.configure_overrides(statistics="max") - summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - expected = np.array([1.01, 1.55, 2.72]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_stats_mean_skew(self): - tolerance = 3e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data_skew) - consumer.configure_overrides(statistics="mean") - summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - expected = np.array([1.27, 2.19, 3.11]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_stats_cum_skew(self): - tolerance = 3e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data_skew) - consumer.configure_overrides(statistics="cumulative") - summary = consumer.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - expected = np.array([1.27, 2.01, 3.11]) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_stats_list_skew(self): - tolerance = 3e-2 - consumer = ChainConsumer() - consumer.add_chain(self.data_skew) - consumer.add_chain(self.data_skew) - consumer.configure_overrides(statistics=["cumulative", "mean"]) - summary = consumer.analysis.get_summary() - actual0 = np.array(next(iter(summary[0].values()))) - actual1 = np.array(next(iter(summary[1].values()))) - expected0 = np.array([1.27, 2.01, 3.11]) - expected1 = np.array([1.27, 2.19, 3.11]) - diff0 = np.abs(expected0 - actual0) - diff1 = np.abs(expected1 - actual1) - assert np.all(diff0 < tolerance) - assert np.all(diff1 < tolerance) - - def test_weights(self): - tolerance = 3e-2 - samples = np.linspace(-4, 4, 200000) - weights = norm.pdf(samples) - c = ChainConsumer() - c.add_chain(samples, weights=weights) - expected = np.array([-1.0, 0.0, 1.0]) - summary = c.analysis.get_summary() - actual = np.array(next(iter(summary.values()))) - diff = np.abs(expected - actual) - assert np.all(diff < tolerance) - - def test_grid_data(self): - x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) - xx, yy = np.meshgrid(x, y, indexing="ij") - xs, ys = xx.flatten(), yy.flatten() - chain = np.vstack((xs, ys)).T - pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xs * xs + ys * ys / 4)) - c = ChainConsumer() - c.add_chain(chain, parameters=["x", "y"], weights=pdf, grid=True) - summary = c.analysis.get_summary() - x_sum = summary["x"] - y_sum = summary["y"] - expected_x = np.array([-1.0, 0.0, 1.0]) - expected_y = np.array([-2.0, 0.0, 2.0]) - threshold = 0.1 - assert np.all(np.abs(expected_x - x_sum) < threshold) - assert np.all(np.abs(expected_y - y_sum) < threshold) - - def test_grid_list_input(self): - x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) - xx, yy = np.meshgrid(x, y, indexing="ij") - pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xx * xx + yy * yy / 4)) - c = ChainConsumer() - c.add_chain([x, y], parameters=["x", "y"], weights=pdf, grid=True) - summary = c.analysis.get_summary() - x_sum = summary["x"] - y_sum = summary["y"] - expected_x = np.array([-1.0, 0.0, 1.0]) - expected_y = np.array([-2.0, 0.0, 2.0]) - threshold = 0.05 - assert np.all(np.abs(expected_x - x_sum) < threshold) - assert np.all(np.abs(expected_y - y_sum) < threshold) - - def test_grid_dict_input(self): - x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) - xx, yy = np.meshgrid(x, y, indexing="ij") - pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xx * xx + yy * yy / 4)) - c = ChainConsumer() - with pytest.raises(AssertionError): - c.add_chain({"x": x, "y": y}, weights=pdf, grid=True) - - def test_grid_dict_input2(self): - x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) - xx, yy = np.meshgrid(x, y, indexing="ij") - pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xx * xx + yy * yy / 4)) - c = ChainConsumer() - c.add_chain({"x": xx.flatten(), "y": yy.flatten()}, weights=pdf.flatten(), grid=True) - summary = c.analysis.get_summary() - x_sum = summary["x"] - y_sum = summary["y"] - expected_x = np.array([-1.0, 0.0, 1.0]) - expected_y = np.array([-2.0, 0.0, 2.0]) - threshold = 0.05 - assert np.all(np.abs(expected_x - x_sum) < threshold) - assert np.all(np.abs(expected_y - y_sum) < threshold) - - def test_normal_list_input(self): - tolerance = 5e-2 - consumer = ChainConsumer() - consumer.add_chain([self.data, self.data2], parameters=["x", "y"]) - # consumer.configure(bins=1.6) - summary = consumer.analysis.get_summary() - actual1 = summary["x"] - actual2 = summary["y"] - expected1 = np.array([3.5, 5.0, 6.5]) - expected2 = np.array([2.0, 3.0, 4.0]) - diff1 = np.abs(expected1 - actual1) - diff2 = np.abs(expected2 - actual2) - assert np.all(diff1 < tolerance) - assert np.all(diff2 < tolerance) - - def test_grid_3d(self): - x, y, z = np.linspace(-3, 3, 30), np.linspace(-3, 3, 30), np.linspace(-3, 3, 30) - xx, yy, zz = np.meshgrid(x, y, z, indexing="ij") - pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xx * xx + yy * yy + zz * zz)) - c = ChainConsumer() - c.add_chain([x, y, z], parameters=["x", "y", "z"], weights=pdf, grid=True) - summary = c.analysis.get_summary() - expected = np.array([-1.0, 0.0, 1.0]) - for k in summary: - assert np.all(np.abs(summary[k] - expected) < 0.2) - - def test_correlations_1d(self): - data = self.rng.normal(0, 1, size=100000) - parameters = ["x"] - c = ChainConsumer() - c.add_chain(data, parameters=parameters) - p, cor = c.analysis.get_correlations() - assert p[0] == "x" - assert np.isclose(cor[0, 0], 1, atol=1e-2) - assert cor.shape == (1, 1) - - def test_correlations_2d(self): - data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=100000) - parameters = ["x", "y"] - c = ChainConsumer() - c.add_chain(data, parameters=parameters) - p, cor = c.analysis.get_correlations() - assert p[0] == "x" - assert p[1] == "y" - assert np.isclose(cor[0, 0], 1, atol=1e-2) - assert np.isclose(cor[1, 1], 1, atol=1e-2) - assert np.abs(cor[0, 1]) < 0.01 - assert cor.shape == (2, 2) - - def test_correlations_3d(self): - data = self.rng.multivariate_normal([0, 0, 1], [[1, 0.5, 0.2], [0.5, 1, 0.3], [0.2, 0.3, 1.0]], size=100000) - parameters = ["x", "y", "z"] - c = ChainConsumer() - c.add_chain(data, parameters=parameters, name="chain1") - p, cor = c.analysis.get_correlations(chain="chain1", parameters=["y", "z", "x"]) - assert p[0] == "y" - assert p[1] == "z" - assert p[2] == "x" - assert np.isclose(cor[0, 0], 1, atol=1e-2) - assert np.isclose(cor[1, 1], 1, atol=1e-2) - assert np.isclose(cor[2, 2], 1, atol=1e-2) - assert cor.shape == (3, 3) - assert np.abs(cor[0, 1] - 0.3) < 0.01 - assert np.abs(cor[0, 2] - 0.5) < 0.01 - assert np.abs(cor[1, 2] - 0.2) < 0.01 - - def test_correlations_2d_non_unitary(self): - data = self.rng.multivariate_normal([0, 0], [[4, 0], [0, 4]], size=100000) - parameters = ["x", "y"] - c = ChainConsumer() - c.add_chain(data, parameters=parameters) - p, cor = c.analysis.get_correlations() - assert p[0] == "x" - assert p[1] == "y" - assert np.isclose(cor[0, 0], 1, atol=1e-2) - assert np.isclose(cor[1, 1], 1, atol=1e-2) - assert np.abs(cor[0, 1]) < 0.01 - assert cor.shape == (2, 2) - - def test_correlation_latex_table(self): - data = self.rng.multivariate_normal([0, 0, 1], [[1, 0.5, 0.2], [0.5, 1, 0.3], [0.2, 0.3, 1.0]], size=1000000) - parameters = ["x", "y", "z"] - c = ChainConsumer() - c.add_chain(data, parameters=parameters) - latex_table = c.analysis.get_correlation_table() - - actual = r"""\begin{table} - \centering - \caption{Parameter Correlations} - \label{tab:parameter_correlations} - \begin{tabular}{c|ccc} - & x & y & z\\ - \hline - x & 1.00 & 0.50 & 0.20 \\ - y & 0.50 & 1.00 & 0.30 \\ - z & 0.20 & 0.30 & 1.00 \\ - \hline - \end{tabular} - \end{table}""" - assert latex_table.replace(" ", "") == actual.replace(" ", "") - - def test_covariance_1d(self): - data = self.rng.normal(0, 2, size=2000000) - parameters = ["x"] - c = ChainConsumer() - c.add_chain(data, parameters=parameters) - p, cor = c.analysis.get_covariance() - assert p[0] == "x" - assert np.isclose(cor[0, 0], 4, atol=1e-2) - assert cor.shape == (1, 1) - - def test_covariance_2d(self): - data = self.rng.multivariate_normal([0, 0], [[3, 0], [0, 9]], size=2000000) - parameters = ["x", "y"] - c = ChainConsumer() - c.add_chain(data, parameters=parameters) - p, cor = c.analysis.get_covariance() - assert p[0] == "x" - assert p[1] == "y" - assert np.isclose(cor[0, 0], 3, atol=2e-2) - assert np.isclose(cor[1, 1], 9, atol=2e-2) - assert np.isclose(cor[0, 1], 0, atol=2e-2) - assert cor.shape == (2, 2) - - def test_covariance_3d(self): - cov = [[3, 0.5, 0.2], [0.5, 4, 0.3], [0.2, 0.3, 5]] - data = self.rng.multivariate_normal([0, 0, 1], cov, size=2000000) - parameters = ["x", "y", "z"] - c = ChainConsumer() - c.add_chain(data, parameters=parameters, name="chain1") - p, cor = c.analysis.get_covariance(chain="chain1", parameters=["y", "z", "x"]) - assert p[0] == "y" - assert p[1] == "z" - assert p[2] == "x" - assert np.isclose(cor[0, 0], 4, atol=2e-2) - assert np.isclose(cor[1, 1], 5, atol=2e-2) - assert np.isclose(cor[2, 2], 3, atol=2e-2) - assert cor.shape == (3, 3) - assert np.abs(cor[0, 1] - 0.3) < 0.01 - assert np.abs(cor[0, 2] - 0.5) < 0.01 - assert np.abs(cor[1, 2] - 0.2) < 0.01 - - def test_covariance_latex_table(self): - cov = [[2, 0.5, 0.2], [0.5, 3, 0.3], [0.2, 0.3, 4.0]] - data = self.rng.multivariate_normal([0, 0, 1], cov, size=20000000) - parameters = ["x", "y", "z"] - c = ChainConsumer() - c.add_chain(data, parameters=parameters) - latex_table = c.analysis.get_covariance_table() - - actual = r"""\begin{table} - \centering - \caption{Parameter Covariance} - \label{tab:parameter_covariance} - \begin{tabular}{c|ccc} - & x & y & z\\ - \hline - x & 2.00 & 0.50 & 0.20 \\ - y & 0.50 & 3.00 & 0.30 \\ - z & 0.20 & 0.30 & 4.00 \\ - \hline - \end{tabular} - \end{table}""" - assert latex_table.replace(" ", "") == actual.replace(" ", "") - - def test_fail_if_more_parameters_than_data(self): - with pytest.raises(AssertionError): - ChainConsumer().add_chain(self.data_combined, parameters=["x", "y", "z"]) - - def test_covariant_covariance_calc(self): - data1 = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) - data2 = self.rng.multivariate_normal([0, 0], [[2, 1], [1, 2]], size=10000) - weights = np.concatenate((np.ones(10000), np.zeros(10000))) - data = np.concatenate((data1, data2)) - c = ChainConsumer() - c.add_chain(data, weights=weights, parameters=["x", "y"]) - p, cor = c.analysis.get_covariance() - assert p[0] == "x" - assert p[1] == "y" - assert np.isclose(cor[0, 0], 1, atol=4e-2) - assert np.isclose(cor[1, 1], 1, atol=4e-2) - assert np.isclose(cor[0, 1], 0, atol=4e-2) - assert cor.shape == (2, 2) - - def test_2d_levels(self): - c = ChainConsumer() - c.add_chain(self.data) - c.configure_overrides(sigmas=[0, 1, 2], sigma2d=True) - levels = c.plotter._get_levels() - assert np.allclose(levels, [0, 0.39, 0.86], atol=0.01) - - def test_1d_levels(self): - c = ChainConsumer() - c.add_chain(self.data) - c.configure_overrides(sigmas=[0, 1, 2], sigma2d=False) - levels = c.plotter._get_levels() - assert np.allclose(levels, [0, 0.68, 0.95], atol=0.01) - - def test_summary_area(self): - c = ChainConsumer() - c.add_chain(self.data) - summary = c.analysis.get_summary()["0"] - expected = [3.5, 5, 6.5] - assert np.all(np.isclose(summary, expected, atol=0.1)) - - def test_summary_area_default(self): - c = ChainConsumer() - c.add_chain(self.data) - c.configure_overrides(summary_area=0.6827) - summary = c.analysis.get_summary()["0"] - expected = [3.5, 5, 6.5] - assert np.all(np.isclose(summary, expected, atol=0.1)) - - def test_summary_area_95(self): - c = ChainConsumer() - c.add_chain(self.data) - c.configure_overrides(summary_area=0.95) - summary = c.analysis.get_summary()["0"] - expected = [2, 5, 8] - assert np.all(np.isclose(summary, expected, atol=0.1)) - - def test_summary_max_symmetric_1(self): - c = ChainConsumer() - c.add_chain(self.data) - c.configure_overrides(statistics="max_symmetric") - summary = c.analysis.get_summary()["0"] - expected = [3.5, 5, 6.5] - assert np.all(np.isclose(summary, expected, atol=0.1)) - assert np.isclose(summary[2] - summary[1], summary[1] - summary[0]) - - def test_summary_max_symmetric_2(self): - c = ChainConsumer() - c.add_chain(self.data_skew) - summary_area = 0.6827 - c.configure_overrides(statistics="max_symmetric", bins=1.0, summary_area=summary_area) - summary = c.analysis.get_summary()["0"] - - xs = np.linspace(0, 2, 1000) - pdf = skewnorm.pdf(xs, 5, 1, 1.5) - xmax = xs[pdf.argmax()] - cdf_top = skewnorm.cdf(summary[2], 5, 1, 1.5) - cdf_bottom = skewnorm.cdf(summary[0], 5, 1, 1.5) - area = cdf_top - cdf_bottom - - assert np.isclose(xmax, summary[1], atol=0.05) - assert np.isclose(area, summary_area, atol=0.05) - assert np.isclose(summary[2] - summary[1], summary[1] - summary[0]) - - def test_summary_max_symmetric_3(self): - c = ChainConsumer() - c.add_chain(self.data_skew) - summary_area = 0.95 - c.configure_overrides(statistics="max_symmetric", bins=1.0, summary_area=summary_area) - summary = c.analysis.get_summary()["0"] - - xs = np.linspace(0, 2, 1000) - pdf = skewnorm.pdf(xs, 5, 1, 1.5) - xmax = xs[pdf.argmax()] - cdf_top = skewnorm.cdf(summary[2], 5, 1, 1.5) - cdf_bottom = skewnorm.cdf(summary[0], 5, 1, 1.5) - area = cdf_top - cdf_bottom - - assert np.isclose(xmax, summary[1], atol=0.05) - assert np.isclose(area, summary_area, atol=0.05) - assert np.isclose(summary[2] - summary[1], summary[1] - summary[0]) - - def test_summary_max_shortest_1(self): - c = ChainConsumer() - c.add_chain(self.data) - c.configure_overrides(statistics="max_shortest") - summary = c.analysis.get_summary()["0"] - expected = [3.5, 5, 6.5] - assert np.all(np.isclose(summary, expected, atol=0.1)) - - def test_summary_max_shortest_2(self): - c = ChainConsumer() - c.add_chain(self.data_skew) - summary_area = 0.6827 - c.configure_overrides(statistics="max_shortest", bins=1.0, summary_area=summary_area) - summary = c.analysis.get_summary()["0"] - - xs = np.linspace(-1, 5, 1000) - pdf = skewnorm.pdf(xs, 5, 1, 1.5) - cdf = skewnorm.cdf(xs, 5, 1, 1.5) - x2 = interp1d(cdf, xs, bounds_error=False, fill_value=np.inf)(cdf + summary_area) - dist = x2 - xs - ind = np.argmin(dist) - x0 = xs[ind] - x2 = x2[ind] - xmax = xs[pdf.argmax()] - - assert np.isclose(xmax, summary[1], atol=0.05) - assert np.isclose(x0, summary[0], atol=0.05) - assert np.isclose(x2, summary[2], atol=0.05) - - def test_summary_max_shortest_3(self): - c = ChainConsumer() - c.add_chain(self.data_skew) - summary_area = 0.95 - c.configure_overrides(statistics="max_shortest", bins=1.0, summary_area=summary_area) - summary = c.analysis.get_summary()["0"] - - xs = np.linspace(-1, 5, 1000) - pdf = skewnorm.pdf(xs, 5, 1, 1.5) - cdf = skewnorm.cdf(xs, 5, 1, 1.5) - x2 = interp1d(cdf, xs, bounds_error=False, fill_value=np.inf)(cdf + summary_area) - dist = x2 - xs - ind = np.argmin(dist) - x0 = xs[ind] - x2 = x2[ind] - xmax = xs[pdf.argmax()] - - assert np.isclose(xmax, summary[1], atol=0.05) - assert np.isclose(x0, summary[0], atol=0.05) - assert np.isclose(x2, summary[2], atol=0.05) - - def test_summary_max_central_1(self): - c = ChainConsumer() - c.add_chain(self.data) - c.configure_overrides(statistics="max_central") - summary = c.analysis.get_summary()["0"] - expected = [3.5, 5, 6.5] - assert np.all(np.isclose(summary, expected, atol=0.1)) - - def test_summary_max_central_2(self): - c = ChainConsumer() - c.add_chain(self.data_skew) - summary_area = 0.6827 - c.configure_overrides(statistics="max_central", bins=1.0, summary_area=summary_area) - summary = c.analysis.get_summary()["0"] - - xs = np.linspace(-1, 5, 1000) - pdf = skewnorm.pdf(xs, 5, 1, 1.5) - cdf = skewnorm.cdf(xs, 5, 1, 1.5) - xval = interp1d(cdf, xs)([0.5 - 0.5 * summary_area, 0.5 + 0.5 * summary_area]) - xmax = xs[pdf.argmax()] - - assert np.isclose(xmax, summary[1], atol=0.05) - assert np.isclose(xval[0], summary[0], atol=0.05) - assert np.isclose(xval[1], summary[2], atol=0.05) - - def test_summary_max_central_3(self): - c = ChainConsumer() - c.add_chain(self.data_skew) - summary_area = 0.95 - c.configure_overrides(statistics="max_central", bins=1.0, summary_area=summary_area) - summary = c.analysis.get_summary()["0"] - - xs = np.linspace(-1, 5, 1000) - pdf = skewnorm.pdf(xs, 5, 1, 1.5) - cdf = skewnorm.cdf(xs, 5, 1, 1.5) - xval = interp1d(cdf, xs)([0.5 - 0.5 * summary_area, 0.5 + 0.5 * summary_area]) - xmax = xs[pdf.argmax()] - - assert np.isclose(xmax, summary[1], atol=0.05) - assert np.isclose(xval[0], summary[0], atol=0.05) - assert np.isclose(xval[1], summary[2], atol=0.05) - - def test_max_likelihood_1(self): - c = ChainConsumer() - data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) - posterior = norm.logpdf(data).sum(axis=1) - data[:, 1] += 1 - c.add_chain(data, parameters=["x", "y"], posterior=posterior, name="A") - result = c.analysis.get_max_posteriors() - x, y = result["x"], result["y"] - assert np.isclose(x, 0, atol=0.05) - assert np.isclose(y, 1, atol=0.05) - - def test_max_likelihood_2(self): - c = ChainConsumer() - data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) - posterior = norm.logpdf(data).sum(axis=1) - data[:, 1] += 2 - c.add_chain(data, parameters=["x", "y"], posterior=posterior, name="A") - c.add_chain(data + 3, parameters=["x", "y"], name="B") - result = c.analysis.get_max_posteriors(parameters=["x", "y"], chains="A") - x, y = result["x"], result["y"] - assert np.isclose(x, 0, atol=0.05) - assert np.isclose(y, 2, atol=0.05) - - def test_max_likelihood_3(self): - c = ChainConsumer() - data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) - posterior = norm.logpdf(data).sum(axis=1) - data[:, 1] += 3 - c.add_chain(data, parameters=["x", "y"], posterior=posterior, name="A") - c.add_chain(data + 3, parameters=["x", "y"], name="B") - result = c.analysis.get_max_posteriors(chains="A") - x, y = result["x"], result["y"] - assert np.isclose(x, 0, atol=0.05) - assert np.isclose(y, 3, atol=0.05) - - def test_max_likelihood_4(self): - c = ChainConsumer() - data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) - posterior = norm.logpdf(data).sum(axis=1) - data[:, 1] += 2 - c.add_chain(data, parameters=["x", "y"], posterior=posterior, name="A") - c.add_chain(data + 3, parameters=["x", "y"], name="B") - result = c.analysis.get_max_posteriors(parameters="x", chains="A", squeeze=False) - assert len(result) == 1 - x = result[0]["x"] - assert np.isclose(x, 0, atol=0.05) - - def test_max_likelihood_5_failure(self): - c = ChainConsumer() - data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) - data[:, 1] += 2 - c.add_chain(data, parameters=["x", "y"], name="A") - result = c.analysis.get_max_posteriors(parameters="x", chains="A") - assert result is None + for i in range(num_walkers): + name = f"test Walker {i}" + assert name in stats + array = stats[name]["x"] + assert np.all(np.abs(array.center - means[i]) < 1e-1) + assert np.abs(consumer.get_chain(name).get_data("x").mean() - means[i]) < 1e-2 + + # def test_stats_max_cliff(self): + # tolerance = 5e-2 + # n = 100000 + # data = np.linspace(0, 10, n) + # weights = norm.pdf(data, 1, 2) + # consumer = ChainConsumer() + # consumer.add_chain(data, weights=weights) + # consumer.configure_overrides(statistics="max", bins=4.0, smooth=1) + # summary = consumer.analysis.get_summary() + # actual = np.array(next(iter(summary.values()))) + # expected = np.array([0.0, 1.0, 2.73]) + # diff = np.abs(expected - actual) + # assert np.all(diff < tolerance) + + # def test_stats_mean_normal(self): + # tolerance = 5e-2 + # consumer = ChainConsumer() + # consumer.add_chain(self.data) + # consumer.configure_overrides(statistics="mean") + # summary = consumer.analysis.get_summary() + # actual = np.array(next(iter(summary.values()))) + # expected = np.array([3.5, 5.0, 6.5]) + # diff = np.abs(expected - actual) + # assert np.all(diff < tolerance) + + # def test_stats_cum_normal(self): + # tolerance = 5e-2 + # consumer = ChainConsumer() + # consumer.add_chain(self.data) + # consumer.configure_overrides(statistics="cumulative") + # summary = consumer.analysis.get_summary() + # actual = np.array(next(iter(summary.values()))) + # expected = np.array([3.5, 5.0, 6.5]) + # diff = np.abs(expected - actual) + # assert np.all(diff < tolerance) + + # def test_reject_bad_satst(self): + # consumer = ChainConsumer() + # consumer.add_chain(self.data) + # with pytest.raises(AssertionError): + # consumer.configure_overrides(statistics="monkey") + + # def test_stats_max_skew(self): + # tolerance = 3e-2 + # consumer = ChainConsumer() + # consumer.add_chain(self.data_skew) + # consumer.configure_overrides(statistics="max") + # summary = consumer.analysis.get_summary() + # actual = np.array(next(iter(summary.values()))) + # expected = np.array([1.01, 1.55, 2.72]) + # diff = np.abs(expected - actual) + # assert np.all(diff < tolerance) + + # def test_stats_mean_skew(self): + # tolerance = 3e-2 + # consumer = ChainConsumer() + # consumer.add_chain(self.data_skew) + # consumer.configure_overrides(statistics="mean") + # summary = consumer.analysis.get_summary() + # actual = np.array(next(iter(summary.values()))) + # expected = np.array([1.27, 2.19, 3.11]) + # diff = np.abs(expected - actual) + # assert np.all(diff < tolerance) + + # def test_stats_cum_skew(self): + # tolerance = 3e-2 + # consumer = ChainConsumer() + # consumer.add_chain(self.data_skew) + # consumer.configure_overrides(statistics="cumulative") + # summary = consumer.analysis.get_summary() + # actual = np.array(next(iter(summary.values()))) + # expected = np.array([1.27, 2.01, 3.11]) + # diff = np.abs(expected - actual) + # assert np.all(diff < tolerance) + + # def test_stats_list_skew(self): + # tolerance = 3e-2 + # consumer = ChainConsumer() + # consumer.add_chain(self.data_skew) + # consumer.add_chain(self.data_skew) + # consumer.configure_overrides(statistics=["cumulative", "mean"]) + # summary = consumer.analysis.get_summary() + # actual0 = np.array(next(iter(summary[0].values()))) + # actual1 = np.array(next(iter(summary[1].values()))) + # expected0 = np.array([1.27, 2.01, 3.11]) + # expected1 = np.array([1.27, 2.19, 3.11]) + # diff0 = np.abs(expected0 - actual0) + # diff1 = np.abs(expected1 - actual1) + # assert np.all(diff0 < tolerance) + # assert np.all(diff1 < tolerance) + + # def test_weights(self): + # tolerance = 3e-2 + # samples = np.linspace(-4, 4, 200000) + # weights = norm.pdf(samples) + # c = ChainConsumer() + # c.add_chain(samples, weights=weights) + # expected = np.array([-1.0, 0.0, 1.0]) + # summary = c.analysis.get_summary() + # actual = np.array(next(iter(summary.values()))) + # diff = np.abs(expected - actual) + # assert np.all(diff < tolerance) + + # def test_grid_data(self): + # x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) + # xx, yy = np.meshgrid(x, y, indexing="ij") + # xs, ys = xx.flatten(), yy.flatten() + # chain = np.vstack((xs, ys)).T + # pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xs * xs + ys * ys / 4)) + # c = ChainConsumer() + # c.add_chain(chain, parameters=["x", "y"], weights=pdf, grid=True) + # summary = c.analysis.get_summary() + # x_sum = summary["x"] + # y_sum = summary["y"] + # expected_x = np.array([-1.0, 0.0, 1.0]) + # expected_y = np.array([-2.0, 0.0, 2.0]) + # threshold = 0.1 + # assert np.all(np.abs(expected_x - x_sum) < threshold) + # assert np.all(np.abs(expected_y - y_sum) < threshold) + + # def test_grid_list_input(self): + # x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) + # xx, yy = np.meshgrid(x, y, indexing="ij") + # pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xx * xx + yy * yy / 4)) + # c = ChainConsumer() + # c.add_chain([x, y], parameters=["x", "y"], weights=pdf, grid=True) + # summary = c.analysis.get_summary() + # x_sum = summary["x"] + # y_sum = summary["y"] + # expected_x = np.array([-1.0, 0.0, 1.0]) + # expected_y = np.array([-2.0, 0.0, 2.0]) + # threshold = 0.05 + # assert np.all(np.abs(expected_x - x_sum) < threshold) + # assert np.all(np.abs(expected_y - y_sum) < threshold) + + # def test_grid_dict_input(self): + # x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) + # xx, yy = np.meshgrid(x, y, indexing="ij") + # pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xx * xx + yy * yy / 4)) + # c = ChainConsumer() + # with pytest.raises(AssertionError): + # c.add_chain({"x": x, "y": y}, weights=pdf, grid=True) + + # def test_grid_dict_input2(self): + # x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) + # xx, yy = np.meshgrid(x, y, indexing="ij") + # pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xx * xx + yy * yy / 4)) + # c = ChainConsumer() + # c.add_chain({"x": xx.flatten(), "y": yy.flatten()}, weights=pdf.flatten(), grid=True) + # summary = c.analysis.get_summary() + # x_sum = summary["x"] + # y_sum = summary["y"] + # expected_x = np.array([-1.0, 0.0, 1.0]) + # expected_y = np.array([-2.0, 0.0, 2.0]) + # threshold = 0.05 + # assert np.all(np.abs(expected_x - x_sum) < threshold) + # assert np.all(np.abs(expected_y - y_sum) < threshold) + + # def test_normal_list_input(self): + # tolerance = 5e-2 + # consumer = ChainConsumer() + # consumer.add_chain([self.data, self.data2], parameters=["x", "y"]) + # # consumer.configure(bins=1.6) + # summary = consumer.analysis.get_summary() + # actual1 = summary["x"] + # actual2 = summary["y"] + # expected1 = np.array([3.5, 5.0, 6.5]) + # expected2 = np.array([2.0, 3.0, 4.0]) + # diff1 = np.abs(expected1 - actual1) + # diff2 = np.abs(expected2 - actual2) + # assert np.all(diff1 < tolerance) + # assert np.all(diff2 < tolerance) + + # def test_grid_3d(self): + # x, y, z = np.linspace(-3, 3, 30), np.linspace(-3, 3, 30), np.linspace(-3, 3, 30) + # xx, yy, zz = np.meshgrid(x, y, z, indexing="ij") + # pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xx * xx + yy * yy + zz * zz)) + # c = ChainConsumer() + # c.add_chain([x, y, z], parameters=["x", "y", "z"], weights=pdf, grid=True) + # summary = c.analysis.get_summary() + # expected = np.array([-1.0, 0.0, 1.0]) + # for k in summary: + # assert np.all(np.abs(summary[k] - expected) < 0.2) + + # def test_correlations_1d(self): + # data = self.rng.normal(0, 1, size=100000) + # parameters = ["x"] + # c = ChainConsumer() + # c.add_chain(data, parameters=parameters) + # p, cor = c.analysis.get_correlations() + # assert p[0] == "x" + # assert np.isclose(cor[0, 0], 1, atol=1e-2) + # assert cor.shape == (1, 1) + + # def test_correlations_2d(self): + # data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=100000) + # parameters = ["x", "y"] + # c = ChainConsumer() + # c.add_chain(data, parameters=parameters) + # p, cor = c.analysis.get_correlations() + # assert p[0] == "x" + # assert p[1] == "y" + # assert np.isclose(cor[0, 0], 1, atol=1e-2) + # assert np.isclose(cor[1, 1], 1, atol=1e-2) + # assert np.abs(cor[0, 1]) < 0.01 + # assert cor.shape == (2, 2) + + # def test_correlations_3d(self): + # data = self.rng.multivariate_normal([0, 0, 1], [[1, 0.5, 0.2], [0.5, 1, 0.3], [0.2, 0.3, 1.0]], size=100000) + # parameters = ["x", "y", "z"] + # c = ChainConsumer() + # c.add_chain(data, parameters=parameters, name="chain1") + # p, cor = c.analysis.get_correlations(chain="chain1", parameters=["y", "z", "x"]) + # assert p[0] == "y" + # assert p[1] == "z" + # assert p[2] == "x" + # assert np.isclose(cor[0, 0], 1, atol=1e-2) + # assert np.isclose(cor[1, 1], 1, atol=1e-2) + # assert np.isclose(cor[2, 2], 1, atol=1e-2) + # assert cor.shape == (3, 3) + # assert np.abs(cor[0, 1] - 0.3) < 0.01 + # assert np.abs(cor[0, 2] - 0.5) < 0.01 + # assert np.abs(cor[1, 2] - 0.2) < 0.01 + + # def test_correlations_2d_non_unitary(self): + # data = self.rng.multivariate_normal([0, 0], [[4, 0], [0, 4]], size=100000) + # parameters = ["x", "y"] + # c = ChainConsumer() + # c.add_chain(data, parameters=parameters) + # p, cor = c.analysis.get_correlations() + # assert p[0] == "x" + # assert p[1] == "y" + # assert np.isclose(cor[0, 0], 1, atol=1e-2) + # assert np.isclose(cor[1, 1], 1, atol=1e-2) + # assert np.abs(cor[0, 1]) < 0.01 + # assert cor.shape == (2, 2) + + # def test_correlation_latex_table(self): + # data = self.rng.multivariate_normal([0, 0, 1], [[1, 0.5, 0.2], [0.5, 1, 0.3], [0.2, 0.3, 1.0]], size=1000000) + # parameters = ["x", "y", "z"] + # c = ChainConsumer() + # c.add_chain(data, parameters=parameters) + # latex_table = c.analysis.get_correlation_table() + + # actual = r"""\begin{table} + # \centering + # \caption{Parameter Correlations} + # \label{tab:parameter_correlations} + # \begin{tabular}{c|ccc} + # & x & y & z\\ + # \hline + # x & 1.00 & 0.50 & 0.20 \\ + # y & 0.50 & 1.00 & 0.30 \\ + # z & 0.20 & 0.30 & 1.00 \\ + # \hline + # \end{tabular} + # \end{table}""" + # assert latex_table.replace(" ", "") == actual.replace(" ", "") + + # def test_covariance_1d(self): + # data = self.rng.normal(0, 2, size=2000000) + # parameters = ["x"] + # c = ChainConsumer() + # c.add_chain(data, parameters=parameters) + # p, cor = c.analysis.get_covariance() + # assert p[0] == "x" + # assert np.isclose(cor[0, 0], 4, atol=1e-2) + # assert cor.shape == (1, 1) + + # def test_covariance_2d(self): + # data = self.rng.multivariate_normal([0, 0], [[3, 0], [0, 9]], size=2000000) + # parameters = ["x", "y"] + # c = ChainConsumer() + # c.add_chain(data, parameters=parameters) + # p, cor = c.analysis.get_covariance() + # assert p[0] == "x" + # assert p[1] == "y" + # assert np.isclose(cor[0, 0], 3, atol=2e-2) + # assert np.isclose(cor[1, 1], 9, atol=2e-2) + # assert np.isclose(cor[0, 1], 0, atol=2e-2) + # assert cor.shape == (2, 2) + + # def test_covariance_3d(self): + # cov = [[3, 0.5, 0.2], [0.5, 4, 0.3], [0.2, 0.3, 5]] + # data = self.rng.multivariate_normal([0, 0, 1], cov, size=2000000) + # parameters = ["x", "y", "z"] + # c = ChainConsumer() + # c.add_chain(data, parameters=parameters, name="chain1") + # p, cor = c.analysis.get_covariance(chain="chain1", parameters=["y", "z", "x"]) + # assert p[0] == "y" + # assert p[1] == "z" + # assert p[2] == "x" + # assert np.isclose(cor[0, 0], 4, atol=2e-2) + # assert np.isclose(cor[1, 1], 5, atol=2e-2) + # assert np.isclose(cor[2, 2], 3, atol=2e-2) + # assert cor.shape == (3, 3) + # assert np.abs(cor[0, 1] - 0.3) < 0.01 + # assert np.abs(cor[0, 2] - 0.5) < 0.01 + # assert np.abs(cor[1, 2] - 0.2) < 0.01 + + # def test_covariance_latex_table(self): + # cov = [[2, 0.5, 0.2], [0.5, 3, 0.3], [0.2, 0.3, 4.0]] + # data = self.rng.multivariate_normal([0, 0, 1], cov, size=20000000) + # parameters = ["x", "y", "z"] + # c = ChainConsumer() + # c.add_chain(data, parameters=parameters) + # latex_table = c.analysis.get_covariance_table() + + # actual = r"""\begin{table} + # \centering + # \caption{Parameter Covariance} + # \label{tab:parameter_covariance} + # \begin{tabular}{c|ccc} + # & x & y & z\\ + # \hline + # x & 2.00 & 0.50 & 0.20 \\ + # y & 0.50 & 3.00 & 0.30 \\ + # z & 0.20 & 0.30 & 4.00 \\ + # \hline + # \end{tabular} + # \end{table}""" + # assert latex_table.replace(" ", "") == actual.replace(" ", "") + + # def test_fail_if_more_parameters_than_data(self): + # with pytest.raises(AssertionError): + # ChainConsumer().add_chain(self.data_combined, parameters=["x", "y", "z"]) + + # def test_covariant_covariance_calc(self): + # data1 = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) + # data2 = self.rng.multivariate_normal([0, 0], [[2, 1], [1, 2]], size=10000) + # weights = np.concatenate((np.ones(10000), np.zeros(10000))) + # data = np.concatenate((data1, data2)) + # c = ChainConsumer() + # c.add_chain(data, weights=weights, parameters=["x", "y"]) + # p, cor = c.analysis.get_covariance() + # assert p[0] == "x" + # assert p[1] == "y" + # assert np.isclose(cor[0, 0], 1, atol=4e-2) + # assert np.isclose(cor[1, 1], 1, atol=4e-2) + # assert np.isclose(cor[0, 1], 0, atol=4e-2) + # assert cor.shape == (2, 2) + + # def test_2d_levels(self): + # c = ChainConsumer() + # c.add_chain(self.data) + # c.configure_overrides(sigmas=[0, 1, 2], sigma2d=True) + # levels = c.plotter._get_levels() + # assert np.allclose(levels, [0, 0.39, 0.86], atol=0.01) + + # def test_1d_levels(self): + # c = ChainConsumer() + # c.add_chain(self.data) + # c.configure_overrides(sigmas=[0, 1, 2], sigma2d=False) + # levels = c.plotter._get_levels() + # assert np.allclose(levels, [0, 0.68, 0.95], atol=0.01) + + # def test_summary_area(self): + # c = ChainConsumer() + # c.add_chain(self.data) + # summary = c.analysis.get_summary()["0"] + # expected = [3.5, 5, 6.5] + # assert np.all(np.isclose(summary, expected, atol=0.1)) + + # def test_summary_area_default(self): + # c = ChainConsumer() + # c.add_chain(self.data) + # c.configure_overrides(summary_area=0.6827) + # summary = c.analysis.get_summary()["0"] + # expected = [3.5, 5, 6.5] + # assert np.all(np.isclose(summary, expected, atol=0.1)) + + # def test_summary_area_95(self): + # c = ChainConsumer() + # c.add_chain(self.data) + # c.configure_overrides(summary_area=0.95) + # summary = c.analysis.get_summary()["0"] + # expected = [2, 5, 8] + # assert np.all(np.isclose(summary, expected, atol=0.1)) + + # def test_summary_max_symmetric_1(self): + # c = ChainConsumer() + # c.add_chain(self.data) + # c.configure_overrides(statistics="max_symmetric") + # summary = c.analysis.get_summary()["0"] + # expected = [3.5, 5, 6.5] + # assert np.all(np.isclose(summary, expected, atol=0.1)) + # assert np.isclose(summary[2] - summary[1], summary[1] - summary[0]) + + # def test_summary_max_symmetric_2(self): + # c = ChainConsumer() + # c.add_chain(self.data_skew) + # summary_area = 0.6827 + # c.configure_overrides(statistics="max_symmetric", bins=1.0, summary_area=summary_area) + # summary = c.analysis.get_summary()["0"] + + # xs = np.linspace(0, 2, 1000) + # pdf = skewnorm.pdf(xs, 5, 1, 1.5) + # xmax = xs[pdf.argmax()] + # cdf_top = skewnorm.cdf(summary[2], 5, 1, 1.5) + # cdf_bottom = skewnorm.cdf(summary[0], 5, 1, 1.5) + # area = cdf_top - cdf_bottom + + # assert np.isclose(xmax, summary[1], atol=0.05) + # assert np.isclose(area, summary_area, atol=0.05) + # assert np.isclose(summary[2] - summary[1], summary[1] - summary[0]) + + # def test_summary_max_symmetric_3(self): + # c = ChainConsumer() + # c.add_chain(self.data_skew) + # summary_area = 0.95 + # c.configure_overrides(statistics="max_symmetric", bins=1.0, summary_area=summary_area) + # summary = c.analysis.get_summary()["0"] + + # xs = np.linspace(0, 2, 1000) + # pdf = skewnorm.pdf(xs, 5, 1, 1.5) + # xmax = xs[pdf.argmax()] + # cdf_top = skewnorm.cdf(summary[2], 5, 1, 1.5) + # cdf_bottom = skewnorm.cdf(summary[0], 5, 1, 1.5) + # area = cdf_top - cdf_bottom + + # assert np.isclose(xmax, summary[1], atol=0.05) + # assert np.isclose(area, summary_area, atol=0.05) + # assert np.isclose(summary[2] - summary[1], summary[1] - summary[0]) + + # def test_summary_max_shortest_1(self): + # c = ChainConsumer() + # c.add_chain(self.data) + # c.configure_overrides(statistics="max_shortest") + # summary = c.analysis.get_summary()["0"] + # expected = [3.5, 5, 6.5] + # assert np.all(np.isclose(summary, expected, atol=0.1)) + + # def test_summary_max_shortest_2(self): + # c = ChainConsumer() + # c.add_chain(self.data_skew) + # summary_area = 0.6827 + # c.configure_overrides(statistics="max_shortest", bins=1.0, summary_area=summary_area) + # summary = c.analysis.get_summary()["0"] + + # xs = np.linspace(-1, 5, 1000) + # pdf = skewnorm.pdf(xs, 5, 1, 1.5) + # cdf = skewnorm.cdf(xs, 5, 1, 1.5) + # x2 = interp1d(cdf, xs, bounds_error=False, fill_value=np.inf)(cdf + summary_area) + # dist = x2 - xs + # ind = np.argmin(dist) + # x0 = xs[ind] + # x2 = x2[ind] + # xmax = xs[pdf.argmax()] + + # assert np.isclose(xmax, summary[1], atol=0.05) + # assert np.isclose(x0, summary[0], atol=0.05) + # assert np.isclose(x2, summary[2], atol=0.05) + + # def test_summary_max_shortest_3(self): + # c = ChainConsumer() + # c.add_chain(self.data_skew) + # summary_area = 0.95 + # c.configure_overrides(statistics="max_shortest", bins=1.0, summary_area=summary_area) + # summary = c.analysis.get_summary()["0"] + + # xs = np.linspace(-1, 5, 1000) + # pdf = skewnorm.pdf(xs, 5, 1, 1.5) + # cdf = skewnorm.cdf(xs, 5, 1, 1.5) + # x2 = interp1d(cdf, xs, bounds_error=False, fill_value=np.inf)(cdf + summary_area) + # dist = x2 - xs + # ind = np.argmin(dist) + # x0 = xs[ind] + # x2 = x2[ind] + # xmax = xs[pdf.argmax()] + + # assert np.isclose(xmax, summary[1], atol=0.05) + # assert np.isclose(x0, summary[0], atol=0.05) + # assert np.isclose(x2, summary[2], atol=0.05) + + # def test_summary_max_central_1(self): + # c = ChainConsumer() + # c.add_chain(self.data) + # c.configure_overrides(statistics="max_central") + # summary = c.analysis.get_summary()["0"] + # expected = [3.5, 5, 6.5] + # assert np.all(np.isclose(summary, expected, atol=0.1)) + + # def test_summary_max_central_2(self): + # c = ChainConsumer() + # c.add_chain(self.data_skew) + # summary_area = 0.6827 + # c.configure_overrides(statistics="max_central", bins=1.0, summary_area=summary_area) + # summary = c.analysis.get_summary()["0"] + + # xs = np.linspace(-1, 5, 1000) + # pdf = skewnorm.pdf(xs, 5, 1, 1.5) + # cdf = skewnorm.cdf(xs, 5, 1, 1.5) + # xval = interp1d(cdf, xs)([0.5 - 0.5 * summary_area, 0.5 + 0.5 * summary_area]) + # xmax = xs[pdf.argmax()] + + # assert np.isclose(xmax, summary[1], atol=0.05) + # assert np.isclose(xval[0], summary[0], atol=0.05) + # assert np.isclose(xval[1], summary[2], atol=0.05) + + # def test_summary_max_central_3(self): + # c = ChainConsumer() + # c.add_chain(self.data_skew) + # summary_area = 0.95 + # c.configure_overrides(statistics="max_central", bins=1.0, summary_area=summary_area) + # summary = c.analysis.get_summary()["0"] + + # xs = np.linspace(-1, 5, 1000) + # pdf = skewnorm.pdf(xs, 5, 1, 1.5) + # cdf = skewnorm.cdf(xs, 5, 1, 1.5) + # xval = interp1d(cdf, xs)([0.5 - 0.5 * summary_area, 0.5 + 0.5 * summary_area]) + # xmax = xs[pdf.argmax()] + + # assert np.isclose(xmax, summary[1], atol=0.05) + # assert np.isclose(xval[0], summary[0], atol=0.05) + # assert np.isclose(xval[1], summary[2], atol=0.05) + + # def test_max_likelihood_1(self): + # c = ChainConsumer() + # data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) + # posterior = norm.logpdf(data).sum(axis=1) + # data[:, 1] += 1 + # c.add_chain(data, parameters=["x", "y"], posterior=posterior, name="A") + # result = c.analysis.get_max_posteriors() + # x, y = result["x"], result["y"] + # assert np.isclose(x, 0, atol=0.05) + # assert np.isclose(y, 1, atol=0.05) + + # def test_max_likelihood_2(self): + # c = ChainConsumer() + # data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) + # posterior = norm.logpdf(data).sum(axis=1) + # data[:, 1] += 2 + # c.add_chain(data, parameters=["x", "y"], posterior=posterior, name="A") + # c.add_chain(data + 3, parameters=["x", "y"], name="B") + # result = c.analysis.get_max_posteriors(parameters=["x", "y"], chains="A") + # x, y = result["x"], result["y"] + # assert np.isclose(x, 0, atol=0.05) + # assert np.isclose(y, 2, atol=0.05) + + # def test_max_likelihood_3(self): + # c = ChainConsumer() + # data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) + # posterior = norm.logpdf(data).sum(axis=1) + # data[:, 1] += 3 + # c.add_chain(data, parameters=["x", "y"], posterior=posterior, name="A") + # c.add_chain(data + 3, parameters=["x", "y"], name="B") + # result = c.analysis.get_max_posteriors(chains="A") + # x, y = result["x"], result["y"] + # assert np.isclose(x, 0, atol=0.05) + # assert np.isclose(y, 3, atol=0.05) + + # def test_max_likelihood_4(self): + # c = ChainConsumer() + # data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) + # posterior = norm.logpdf(data).sum(axis=1) + # data[:, 1] += 2 + # c.add_chain(data, parameters=["x", "y"], posterior=posterior, name="A") + # c.add_chain(data + 3, parameters=["x", "y"], name="B") + # result = c.analysis.get_max_posteriors(parameters="x", chains="A", squeeze=False) + # assert len(result) == 1 + # x = result[0]["x"] + # assert np.isclose(x, 0, atol=0.05) + + # def test_max_likelihood_5_failure(self): + # c = ChainConsumer() + # data = self.rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=10000) + # data[:, 1] += 2 + # c.add_chain(data, parameters=["x", "y"], name="A") + # result = c.analysis.get_max_posteriors(parameters="x", chains="A") + # assert result is None diff --git a/tests/test_chain.py b/tests/test_chain.py index 4597ab8c..bfff9820 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -2,10 +2,8 @@ import pandas as pd import pytest from pydantic import ValidationError -from scipy.stats import norm -from chainconsumer.chain import Chain, ChainConfig -from chainconsumer.chainconsumer import ChainConsumer +from chainconsumer.chain import Chain class TestChain: @@ -44,8 +42,9 @@ def test_chain_with_bad_params2(self): df2.columns = ["A", "B", 0] Chain(samples=df2, name=self.n) - def test_chain_with_bad_chain_initial_success1(self): - Chain(samples=self.bad, name=self.n) + def test_chain_with_bad_chain(self): + with pytest.raises(ValidationError): + Chain(samples=self.bad, name=self.n) def test_good_grid(self): Chain(samples=self.df, name=self.n, grid=False) diff --git a/tests/test_comparisons.py b/tests/test_comparisons.py index 4bcf72e3..1ceae849 100644 --- a/tests/test_comparisons.py +++ b/tests/test_comparisons.py @@ -1,209 +1,198 @@ import numpy as np +import pandas as pd +import pytest from scipy.stats import norm -from chainconsumer import ChainConsumer +from chainconsumer import Chain, ChainConsumer -def test_aic_fail_no_posterior(): +@pytest.fixture +def cc_noposterior() -> ChainConsumer: d = norm.rvs(size=1000) - c = ChainConsumer() - c.add_chain(d, num_eff_data_points=1000, num_free_params=1) - aics = c.comparison.aic() - assert len(aics) == 1 - assert aics[0] is None + df = pd.DataFrame({"a": d}) + chain = Chain(samples=df, name="A") + return ChainConsumer().add_chain(chain) -def test_aic_fail_no_data_points(): +@pytest.fixture +def cc_no_effective_data_points() -> ChainConsumer: d = norm.rvs(size=1000) p = norm.logpdf(d) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_free_params=1) - aics = c.comparison.aic() - assert len(aics) == 1 - assert aics[0] is None + df = pd.DataFrame({"a": d, "log_posterior": p}) + chain = Chain(samples=df, name="A") + return ChainConsumer().add_chain(chain) -def test_aic_fail_no_num_params(): +@pytest.fixture +def cc_no_free_params() -> ChainConsumer: d = norm.rvs(size=1000) p = norm.logpdf(d) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_eff_data_points=1000) - aics = c.comparison.aic() - assert len(aics) == 1 - assert aics[0] is None + df = pd.DataFrame({"a": d, "log_posterior": p}) + chain = Chain(samples=df, name="A", num_eff_data_points=1000) + return ChainConsumer().add_chain(chain) -def test_aic_0(): +@pytest.fixture +def cc() -> ChainConsumer: d = norm.rvs(size=1000) p = norm.logpdf(d) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_free_params=1, num_eff_data_points=1000) - aics = c.comparison.aic() + df = pd.DataFrame({"a": d, "log_posterior": p}) + chain = Chain(samples=df, name="A", num_eff_data_points=1000, num_free_params=1) + return ChainConsumer().add_chain(chain) + + +def test_aic_fail_no_posterior(cc_noposterior) -> None: + aics = cc_noposterior.comparison.aic() + assert len(aics) == 0 + + +def test_aic_fail_no_data_points(cc_no_effective_data_points) -> None: + aics = cc_no_effective_data_points.comparison.aic() + assert len(aics) == 0 + + +def test_aic_fail_no_num_params(cc_no_free_params) -> None: + aics = cc_no_free_params.comparison.aic() + assert len(aics) == 0 + + +def test_aic_0(cc) -> None: + aics = cc.comparison.aic() assert len(aics) == 1 - assert aics[0] == 0 + assert aics["A"] == 0 -def test_aic_posterior_dependence(): - d = norm.rvs(size=1000) - p = norm.logpdf(d) - p2 = norm.logpdf(d, scale=2) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_free_params=1, num_eff_data_points=1000) - c.add_chain(d, posterior=p2, num_free_params=1, num_eff_data_points=1000) - aics = c.comparison.aic() +def test_aic_posterior_dependence(cc: ChainConsumer) -> None: + chain1 = cc.get_chain("A") + df = chain1.samples.assign(log_posterior=lambda x: norm.logpdf(x["a"], scale=2)) + cc.add_chain(Chain(samples=df, name="B", num_eff_data_points=1000, num_free_params=1)) + aics = cc.comparison.aic() assert len(aics) == 2 - assert aics[0] == 0 + assert aics["A"] == 0 expected = 2 * np.log(2) - assert np.isclose(aics[1], expected, atol=1e-3) + assert np.isclose(aics["B"], expected, atol=1e-3) -def test_aic_parameter_dependence(): - d = norm.rvs(size=1000) - p = norm.logpdf(d) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_free_params=1, num_eff_data_points=1000) - c.add_chain(d, posterior=p, num_free_params=2, num_eff_data_points=1000) - aics = c.comparison.aic() +def test_aic_parameter_dependence(cc: ChainConsumer) -> None: + chain1 = cc.get_chain("A") + chain2 = chain1.model_copy() + chain2.num_free_params = 2 + chain2.name = "B" + cc.add_chain(chain2) + + aics = cc.comparison.aic() assert len(aics) == 2 - assert aics[0] == 0 + assert aics["A"] == 0 expected = 1 * 2 + (2.0 * 2 * 3 / (1000 - 2 - 1)) - (2.0 * 1 * 2 / (1000 - 1 - 1)) - assert np.isclose(aics[1], expected, atol=1e-3) + assert np.isclose(aics["B"], expected, atol=1e-3) -def test_aic_data_dependence(): - d = norm.rvs(size=1000) - p = norm.logpdf(d) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_free_params=1, num_eff_data_points=1000) - c.add_chain(d, posterior=p, num_free_params=1, num_eff_data_points=500) - aics = c.comparison.aic() +def test_aic_data_dependence(cc: ChainConsumer) -> None: + chain1 = cc.get_chain("A") + chain2 = chain1.model_copy() + chain2.num_eff_data_points = 500 + chain2.name = "B" + cc.add_chain(chain2) + + aics = cc.comparison.aic() assert len(aics) == 2 - assert aics[0] == 0 + assert aics["A"] == 0 expected = (2.0 * 1 * 2 / (500 - 1 - 1)) - (2.0 * 1 * 2 / (1000 - 1 - 1)) - assert np.isclose(aics[1], expected, atol=1e-3) + assert np.isclose(aics["B"], expected, atol=1e-3) -def test_bic_fail_no_posterior(): - d = norm.rvs(size=1000) - c = ChainConsumer() - c.add_chain(d, num_eff_data_points=1000, num_free_params=1) - bics = c.comparison.bic() - assert len(bics) == 1 - assert bics[0] is None +def test_bic_fail_no_posterior(cc_noposterior: ChainConsumer) -> None: + bics = cc_noposterior.comparison.bic() + assert len(bics) == 0 -def test_bic_fail_no_data_points(): - d = norm.rvs(size=1000) - p = norm.logpdf(d) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_free_params=1) - bics = c.comparison.bic() - assert len(bics) == 1 - assert bics[0] is None +def test_bic_fail_no_data_points(cc_no_effective_data_points: ChainConsumer) -> None: + bics = cc_no_effective_data_points.comparison.bic() + assert len(bics) == 0 -def test_bic_fail_no_num_params(): - d = norm.rvs(size=1000) - p = norm.logpdf(d) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_eff_data_points=1000) - bics = c.comparison.bic() - assert len(bics) == 1 - assert bics[0] is None +def test_bic_fail_no_num_params(cc_no_free_params: ChainConsumer) -> None: + bics = cc_no_free_params.comparison.bic() + assert len(bics) == 0 -def test_bic_0(): - d = norm.rvs(size=1000) - p = norm.logpdf(d) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_free_params=1, num_eff_data_points=1000) - bics = c.comparison.bic() +def test_bic_0(cc: ChainConsumer) -> None: + bics = cc.comparison.bic() assert len(bics) == 1 - assert bics[0] == 0 + assert bics["A"] == 0 -def test_bic_posterior_dependence(): - d = norm.rvs(size=1000) - p = norm.logpdf(d) - p2 = norm.logpdf(d, scale=2) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_free_params=1, num_eff_data_points=1000) - c.add_chain(d, posterior=p2, num_free_params=1, num_eff_data_points=1000) - bics = c.comparison.bic() +def test_bic_posterior_dependence(cc: ChainConsumer) -> None: + chain1 = cc.get_chain("A") + df = chain1.samples.assign(log_posterior=lambda x: norm.logpdf(x["a"], scale=2)) + cc.add_chain(Chain(samples=df, name="B", num_eff_data_points=1000, num_free_params=1)) + bics = cc.comparison.bic() assert len(bics) == 2 - assert bics[0] == 0 + assert bics["A"] == 0 expected = 2 * np.log(2) - assert np.isclose(bics[1], expected, atol=1e-3) + assert np.isclose(bics["B"], expected, atol=1e-3) -def test_bic_parameter_dependence(): - d = norm.rvs(size=1000) - p = norm.logpdf(d) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_free_params=1, num_eff_data_points=1000) - c.add_chain(d, posterior=p, num_free_params=2, num_eff_data_points=1000) - bics = c.comparison.bic() +def test_bic_parameter_dependence(cc: ChainConsumer) -> None: + chain1 = cc.get_chain("A") + chain2 = chain1.model_copy() + chain2.num_free_params = 2 + chain2.name = "B" + cc.add_chain(chain2) + bics = cc.comparison.bic() assert len(bics) == 2 - assert bics[0] == 0 + assert bics["A"] == 0 expected = np.log(1000) - assert np.isclose(bics[1], expected, atol=1e-3) + assert np.isclose(bics["B"], expected, atol=1e-3) -def test_bic_data_dependence(): - d = norm.rvs(size=1000) - p = norm.logpdf(d) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_free_params=1, num_eff_data_points=1000) - c.add_chain(d, posterior=p, num_free_params=1, num_eff_data_points=500) - bics = c.comparison.bic() +def test_bic_data_dependence(cc: ChainConsumer) -> None: + chain1 = cc.get_chain("A") + chain2 = chain1.model_copy() + chain2.num_eff_data_points = 500 + chain2.name = "B" + cc.add_chain(chain2) + bics = cc.comparison.bic() assert len(bics) == 2 - assert bics[1] == 0 + assert bics["B"] == 0 expected = np.log(1000) - np.log(500) - assert np.isclose(bics[0], expected, atol=1e-3) + assert np.isclose(bics["A"], expected, atol=1e-3) -def test_bic_data_dependence2(): - d = norm.rvs(size=1000) - p = norm.logpdf(d) - c = ChainConsumer() - c.add_chain(d, posterior=p, num_free_params=2, num_eff_data_points=1000) - c.add_chain(d, posterior=p, num_free_params=3, num_eff_data_points=500) - bics = c.comparison.bic() +def test_bic_data_dependence2(cc: ChainConsumer) -> None: + chain1 = cc.get_chain("A") + chain1.num_free_params = 2 + chain2 = chain1.model_copy() + chain2.num_eff_data_points = 500 + chain2.num_free_params = 3 + chain2.name = "B" + cc.add_chain(chain2) + + bics = cc.comparison.bic() assert len(bics) == 2 - assert bics[0] == 0 + assert bics["A"] == 0 expected = 3 * np.log(500) - 2 * np.log(1000) - assert np.isclose(bics[1], expected, atol=1e-3) + assert np.isclose(bics["B"], expected, atol=1e-3) -def test_dic_fail_no_posterior(): - d = norm.rvs(size=1000) - c = ChainConsumer() - c.add_chain(d, num_eff_data_points=1000, num_free_params=1) - dics = c.comparison.dic() - assert len(dics) == 1 - assert dics[0] is None +def test_dic_fail_no_posterior(cc_noposterior: ChainConsumer) -> None: + dics = cc_noposterior.comparison.dic() + assert len(dics) == 0 -def test_dic_0(): - d = norm.rvs(size=1000) - p = norm.logpdf(d) - c = ChainConsumer() - c.add_chain(d, posterior=p) - dics = c.comparison.dic() +def test_dic_0(cc: ChainConsumer) -> None: + dics = cc.comparison.dic() assert len(dics) == 1 - assert dics[0] == 0 + assert dics["A"] == 0 -def test_dic_posterior_dependence(): - d = norm.rvs(size=1000000) - p = norm.logpdf(d) - p2 = norm.logpdf(d, scale=2) - c = ChainConsumer() - c.add_chain(d, posterior=p) - c.add_chain(d, posterior=p2) - bics = c.comparison.dic() +def test_dic_posterior_dependence(cc: ChainConsumer) -> None: + chain1 = cc.get_chain("A") + df = chain1.samples.assign(log_posterior=lambda x: norm.logpdf(x["a"], scale=2)) + cc.add_chain(Chain(samples=df, name="B", num_eff_data_points=1000, num_free_params=1)) + bics = cc.comparison.bic() assert len(bics) == 2 - assert bics[1] == 0 - dic1 = 2 * np.mean(-2 * p) + 2 * norm.logpdf(0) - dic2 = 2 * np.mean(-2 * p2) + 2 * norm.logpdf(0, scale=2) - assert np.isclose(bics[0], dic1 - dic2, atol=1e-3) + assert bics["A"] == 0 + dic1 = 2 * np.mean(-2 * chain1.log_posterior) + 2 * norm.logpdf(0) # type: ignore + dic2 = 2 * np.mean(-2 * chain1.log_posterior) + 2 * norm.logpdf(0, scale=2) # type: ignore + assert np.isclose(bics["B"], dic1 - dic2, atol=1e-3) diff --git a/tests/test_diagnostic.py b/tests/test_diagnostic.py index a113d062..17927385 100644 --- a/tests/test_diagnostic.py +++ b/tests/test_diagnostic.py @@ -1,120 +1,102 @@ import numpy as np +import pandas as pd import pytest -from chainconsumer import ChainConsumer +from chainconsumer import Chain, ChainConsumer -@pytest.fixture(scope="session") +@pytest.fixture def rng(): return np.random.default_rng() -def test_gelman_rubin_index(rng): +@pytest.fixture +def good_chain(rng) -> Chain: data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T - consumer = ChainConsumer() - consumer.add_chain(data, walkers=4) - assert consumer.diagnostic.gelman_rubin(chain=0) + chain = Chain(samples=pd.DataFrame(data, columns=["a", "b"]), name="A", walkers=4) + return chain -def test_gelman_rubin_index_not_converged(rng): +@pytest.fixture +def bad_chain(rng) -> Chain: data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T data[80000:, :] *= 2 data[80000:, :] += 1 - consumer = ChainConsumer() + chain = Chain(samples=pd.DataFrame(data, columns=["a", "b"]), name="A", walkers=4) + return chain - consumer.add_chain(data, walkers=4) - assert not consumer.diagnostic.gelman_rubin(chain=0) +@pytest.fixture +def good_cc(good_chain: Chain) -> ChainConsumer: + return ChainConsumer().add_chain(good_chain) -def test_gelman_rubin_index_not_converged2(rng): - data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T - data[:, 0] += np.linspace(0, 10, 100000) - consumer = ChainConsumer() - consumer.add_chain(data, walkers=8) - assert not consumer.diagnostic.gelman_rubin(chain=0) +@pytest.fixture +def good_cc2(good_chain: Chain) -> ChainConsumer: + c2 = good_chain.model_copy() + c2.name = "B" + return ChainConsumer().add_chain(good_chain).add_chain(c2) -def test_gelman_rubin_index_fails(rng): - data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T - consumer = ChainConsumer() - consumer.add_chain(data, walkers=4) - with pytest.raises(AssertionError): - consumer.diagnostic.gelman_rubin(chain=10) +@pytest.fixture +def bad_cc(bad_chain: Chain) -> ChainConsumer: + return ChainConsumer().add_chain(bad_chain) -def test_gelman_rubin_name(rng): - data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T - consumer = ChainConsumer() - consumer.add_chain(data, walkers=4, name="testchain") - assert consumer.diagnostic.gelman_rubin(chain="testchain") +def test_gelman_rubin_index(good_cc: ChainConsumer) -> None: + assert good_cc.diagnostic.gelman_rubin() -def test_gelman_rubin_name_fails(rng): - data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T - consumer = ChainConsumer() - consumer.add_chain(data, walkers=4, name="testchain") - with pytest.raises(AssertionError): - consumer.diagnostic.gelman_rubin(chain="testchain2") +def test_gelman_rubin_index2(good_cc2: ChainConsumer) -> None: + res = good_cc2.diagnostic.gelman_rubin() + assert res + assert res.passed + assert "A" in res.results + assert res.results["A"] + assert "B" in res.results + assert res.results["B"] -def test_gelman_rubin_unknown_fails(rng): - data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T - consumer = ChainConsumer() - consumer.add_chain(data, walkers=4, name="testchain") - with pytest.raises(ValueError): - consumer.diagnostic.gelman_rubin(chain=np.pi) +def test_gelman_rubin_index_not_converged(bad_cc: ChainConsumer) -> None: + assert not bad_cc.diagnostic.gelman_rubin() -def test_gelman_rubin_default(rng): +def test_gelman_rubin_index_not_converged2(rng) -> None: data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T + data[:, 0] += np.linspace(0, 10, 100000) consumer = ChainConsumer() - consumer.add_chain(data, walkers=4, name="c1") - consumer.add_chain(data, walkers=4, name="c2") - consumer.add_chain(data, walkers=4, name="c3") - assert consumer.diagnostic.gelman_rubin() + consumer.add_chain(Chain(samples=pd.DataFrame(data, columns=["A", "B"]), name="B", walkers=8)) + res = consumer.diagnostic.gelman_rubin() + assert not res + assert not res.passed + assert "B" in res.results + assert not res.results["B"] -def test_gelman_rubin_default_not_converge(rng): - data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T - consumer = ChainConsumer() - consumer.add_chain(data, walkers=4, name="c1") - consumer.add_chain(data, walkers=4, name="c2") - data2 = data.copy() - data2[:, 0] += np.linspace(-5, 5, 100000) - consumer.add_chain(data2, walkers=4, name="c3") - assert not consumer.diagnostic.gelman_rubin() +def test_geweke_index(good_cc: ChainConsumer) -> None: + assert good_cc.diagnostic.geweke() -def test_geweke_index(rng): - data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T - consumer = ChainConsumer() - consumer.add_chain(data, walkers=20, name="c1") - assert consumer.diagnostic.geweke(chain=0) +def test_geweke_index_failed(bad_cc: ChainConsumer) -> None: + assert not bad_cc.diagnostic.geweke() -def test_geweke_index_failed(rng): - data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T - consumer = ChainConsumer() - data[98000:, :] += 0.5 - consumer.add_chain(data, walkers=20, name="c1") - assert not consumer.diagnostic.geweke(chain=0) - -def test_geweke_default(rng): - generator = np.random.default_rng(0) - data = np.vstack((generator.normal(loc=0.0, size=100000), generator.normal(loc=1.0, size=100000))).T - consumer = ChainConsumer() - consumer.add_chain(data, walkers=20, name="c1") - consumer.add_chain(data, walkers=20, name="c2") - assert consumer.diagnostic.geweke(chain=0) +def test_geweke_default(good_cc2: ChainConsumer) -> None: + res = good_cc2.diagnostic.geweke() + assert res + assert res.passed + assert "A" in res.results + assert res.results["A"] + assert "B" in res.results + assert res.results["B"] -def test_geweke_default_failed(): +def test_geweke_default_failed(rng: np.random.Generator) -> None: data = np.vstack((rng.normal(loc=0.0, size=100000), rng.normal(loc=1.0, size=100000))).T consumer = ChainConsumer() - consumer.add_chain(data, walkers=20, name="c1") + consumer.add_chain(Chain(samples=pd.DataFrame(data, columns=["a", "b"]), walkers=20, name="c1")) data2 = data.copy() data2[98000:, :] += 0.3 - consumer.add_chain(data2, walkers=20, name="c2") + consumer.add_chain(Chain(samples=pd.DataFrame(data2, columns=["a", "b"]), walkers=20, name="c2")) assert not consumer.diagnostic.geweke() diff --git a/tests/test_plotter.py b/tests/test_plotter.py index 9f45cb74..787b8144 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -1,7 +1,8 @@ import numpy as np +import pandas as pd from scipy.stats import norm -from chainconsumer import ChainConsumer +from chainconsumer import Chain, ChainConsumer class TestChain: @@ -10,51 +11,49 @@ class TestChain: data = rng.normal(loc=5.0, scale=1.5, size=n) data2 = rng.normal(loc=3, scale=1.0, size=n) + chain1 = Chain(samples=pd.DataFrame(data, columns=["x"]), name="A") + chain2 = Chain(samples=pd.DataFrame(data2, columns=["x"]), name="B") + def test_plotter_extents1(self): c = ChainConsumer() - c.add_chain(self.data, parameters=["x"]) - c.configure_overrides() - minv, maxv = c.plotter._get_parameter_extents("x", c._chains) + c.add_chain(self.chain1) + minv, maxv = c.plotter._get_parameter_extents("x", list(c._chains.values())) assert np.isclose(minv, (5.0 - 1.5 * 3.7), atol=0.2) assert np.isclose(maxv, (5.0 + 1.5 * 3.7), atol=0.2) def test_plotter_extents2(self): c = ChainConsumer() - c.add_chain(self.data, parameters=["x"]) - c.add_chain(self.data + 5, parameters=["y"]) - c.configure_overrides() - minv, maxv = c.plotter._get_parameter_extents("x", c._chains) + c.add_chain(self.chain1) + chain2 = self.chain2.model_copy() + chain2.samples["x"] += 5 + chain2.samples = chain2.samples.rename(columns={"x": "y"}) + c.add_chain(chain2) + minv, maxv = c.plotter._get_parameter_extents("x", list(c._chains.values())) assert np.isclose(minv, (5.0 - 1.5 * 3.7), atol=0.2) assert np.isclose(maxv, (5.0 + 1.5 * 3.7), atol=0.2) def test_plotter_extents3(self): c = ChainConsumer() - c.add_chain(self.data, parameters=["x"]) - c.add_chain(self.data + 5, parameters=["x"]) - c.configure_overrides() - minv, maxv = c.plotter._get_parameter_extents("x", c._chains) + c.add_chain(self.chain1) + chain2 = self.chain1.model_copy(deep=True) + chain2.samples["x"] += 5 + chain2.name = "C" + c.add_chain(chain2) + minv, maxv = c.plotter._get_parameter_extents("x", list(c._chains.values())) assert np.isclose(minv, (5.0 - 1.5 * 3.7), atol=0.2) assert np.isclose(maxv, (10.0 + 1.5 * 3.7), atol=0.2) - def test_plotter_extents4(self): - c = ChainConsumer() - c.add_chain(self.data, parameters=["x"]) - c.add_chain(self.data + 5, parameters=["y"]) - c.configure_overrides() - minv, maxv = c.plotter._get_parameter_extents("x", c._chains[:1]) - assert np.isclose(minv, (5.0 - 1.5 * 3.7), atol=0.2) - assert np.isclose(maxv, (5.0 + 1.5 * 3.7), atol=0.2) - def test_plotter_extents5(self): x, y = np.linspace(-3, 3, 200), np.linspace(-5, 5, 200) xx, yy = np.meshgrid(x, y, indexing="ij") xs, ys = xx.flatten(), yy.flatten() chain = np.vstack((xs, ys)).T pdf = (1 / (2 * np.pi)) * np.exp(-0.5 * (xs * xs + ys * ys / 4)) + df = pd.DataFrame(chain, columns=["x", "y"]) + df["weight"] = pdf c = ChainConsumer() - c.add_chain(chain, parameters=["x", "y"], weights=pdf, grid=True) - c.configure_overrides() - minv, maxv = c.plotter._get_parameter_extents("x", c._chains) + c.add_chain(Chain(samples=df, grid=True, name="grid")) + minv, maxv = c.plotter._get_parameter_extents("x", list(c._chains.values())) assert np.isclose(minv, -3, atol=0.001) assert np.isclose(maxv, 3, atol=0.001) @@ -64,9 +63,9 @@ def test_plotter_extents6(self): data = self.rng.normal(loc=0, size=1000) posterior = norm.logpdf(data) data += mid - c.add_chain(data, parameters=["x"], posterior=posterior, plot_point=True, plot_contour=False) + df = pd.DataFrame(data, columns=["x"]).assign(log_posterior=posterior) + c.add_chain(Chain(samples=df, plot_point=True, plot_contour=False, name=f"point only {mid}")) - c.configure_overrides() - minv, maxv = c.plotter._get_parameter_extents("x", c._chains) + minv, maxv = c.plotter._get_parameter_extents("x", list(c._chains.values())) assert np.isclose(minv, -1, atol=0.01) assert np.isclose(maxv, 1, atol=0.01) From cbb10c72a0933e61d7d6598778860ebbe496e236 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Sun, 8 Oct 2023 20:12:26 +1000 Subject: [PATCH 09/22] Fixing tests and adding more examples --- docs/examples/distributions.py | 35 ++ docs/examples/plot_textual_output.py | 59 ++ docs/examples/{plot_summary.py => summary.py} | 0 docs/examples/walks.py | 23 + docs/resources/comparison_table.png | Bin 0 -> 67415 bytes docs/resources/summaries.png | Bin 0 -> 83144 bytes src/chainconsumer/analysis.py | 53 +- src/chainconsumer/chainconsumer.py | 2 +- src/chainconsumer/examples.py | 13 +- src/chainconsumer/helpers.py | 3 +- src/chainconsumer/plotter.py | 546 ++++++++---------- tests/test_analysis.py | 4 +- tests/test_diagnostic.py | 2 +- 13 files changed, 415 insertions(+), 325 deletions(-) create mode 100644 docs/examples/distributions.py create mode 100644 docs/examples/plot_textual_output.py rename docs/examples/{plot_summary.py => summary.py} (100%) create mode 100644 docs/examples/walks.py create mode 100644 docs/resources/comparison_table.png create mode 100644 docs/resources/summaries.png diff --git a/docs/examples/distributions.py b/docs/examples/distributions.py new file mode 100644 index 00000000..36ac4875 --- /dev/null +++ b/docs/examples/distributions.py @@ -0,0 +1,35 @@ +""" +# Introduction to Distributions + +When you have a few chains and want to contrast them all with +each other, you probably want a summary plot. + +To show you how they work, let's make some sample data that all +has the same average. +""" +from chainconsumer import Chain, ChainConsumer, Truth, make_sample + +# Here's what you might start with +df_1 = make_sample(num_dimensions=4, seed=1, randomise_mean=True) +df_2 = make_sample(num_dimensions=5, seed=2, randomise_mean=True) +print(df_1.head()) + +# %% New cell +## Using distributions + + +# And now we give this to chainconsumer +c = ChainConsumer() +c.add_chain(Chain(samples=df_1, name="An Example Contour")) +fig = c.plotter.plot_distributions() + +# %% Second cell +# If you want the summary stats you'll need to keep it just one +# chain. And if you don't want them, you can pass `summarise=False` +# to the `PlotConfig`. +# +# When you add a second chain, you'll see the summaries disappear. + +c.add_chain(Chain(samples=df_2, name="Another contour!")) +c.add_truth(Truth(location={"A": 0, "B": 0})) +fig = c.plotter.plot_distributions(col_wrap=3, columns=["A", "B"]) diff --git a/docs/examples/plot_textual_output.py b/docs/examples/plot_textual_output.py new file mode 100644 index 00000000..8606c70e --- /dev/null +++ b/docs/examples/plot_textual_output.py @@ -0,0 +1,59 @@ +""" +# Introduction to LaTeX Tables + +Because typing those things out is a **massive pain in the ass.** + +""" +from chainconsumer import Chain, ChainConsumer, Truth, make_sample + +# Here's a sample dataset +n_1, n_2 = 100000, 200000 +df_1 = make_sample(num_dimensions=2, seed=0, num_points=n_1) +df_2 = make_sample(num_dimensions=2, seed=1, num_points=n_2) + + +# Here's what the plot looks like: +c = ChainConsumer() +c.add_chain(Chain(samples=df_1, name="Model A", num_free_params=1, num_eff_data_points=n_1)) +c.add_chain(Chain(samples=df_2, name="Model B", num_free_params=2, num_eff_data_points=n_2)) +c.add_truth(Truth(location={"A": 0, "B": 1})) +fig = c.plotter.plot() + +# %% Second cell +# # Comparing Models +# +# Provided you have the log posteriors, comparing models is easy. + +latex_table = c.comparison.comparison_table() +print(latex_table) + +# %% +# Granted, it's hard to read a LaTeX table. It'll come out something +# like this, though I took a screenshot a while ago and the data has changed. +# You get the idea though... +# +# ![](../../resources/comparison_table.png) +# +# # Summarising Parameters +# +# Alright, so what if you've compared models and you're happy and want +# to publish that paper! +# +# You can get a LaTeX table of the summary statistics as well. + +print(c.analysis.get_latex_table()) +# %% +# Which would look like this (though I saved this screenshot out a while ago too) +# +# ![](../../resources/summaries.png) +# +# And sometimes you might want this table transposed if you have a lot of +# parameters and not many models to compare. +# +print(c.analysis.get_latex_table(transpose=True, caption="The best table")) + +# %% +# +# There are other things you can do if you dig around in the API, like +# correlations and covariance. +print(c.analysis.get_covariance_table("Model A")) diff --git a/docs/examples/plot_summary.py b/docs/examples/summary.py similarity index 100% rename from docs/examples/plot_summary.py rename to docs/examples/summary.py diff --git a/docs/examples/walks.py b/docs/examples/walks.py new file mode 100644 index 00000000..d4d4bf75 --- /dev/null +++ b/docs/examples/walks.py @@ -0,0 +1,23 @@ +""" +# Introduction to Walks + +Want to see if your chain is behaving nicely? Use a walk! +""" +from chainconsumer import Chain, ChainConsumer, Truth, make_sample + +# Here's a sample dataset +df_1 = make_sample(num_dimensions=4, seed=1, randomise_mean=True, num_points=10000) + +# And now we give this to chainconsumer +c = ChainConsumer() +c.add_chain(Chain(samples=df_1, name="An Example Contour")) +fig = c.plotter.plot_walks() + +# %% Second cell +# You can add other chains in if you want, though it can get messy to see things. +# +# To reduce the mess, try turning on convolve, which will +# get you a smoothed out version of the walk. And truth lines are always nice. + +c.add_truth(Truth(location={"A": 0, "B": 0})) +fig = c.plotter.plot_walks(convolve=100) diff --git a/docs/resources/comparison_table.png b/docs/resources/comparison_table.png new file mode 100644 index 0000000000000000000000000000000000000000..7e78b0283ca443b1e112b0216739647abf312510 GIT binary patch literal 67415 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


+&YHt(mmiw%7O%b|gN3Xigy7*fky* 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%0DEkRVtm=p_ev*<5n~SM+BdS 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 BCaeGe literal 0 HcmV?d00001 diff --git a/docs/resources/summaries.png b/docs/resources/summaries.png new file mode 100644 index 0000000000000000000000000000000000000000..8fa6527c71d0e9b92cce0df3f306b1586618bc7c GIT binary patch literal 83144 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 Cd+?0_ literal 0 HcmV?d00001 diff --git a/src/chainconsumer/analysis.py b/src/chainconsumer/analysis.py index 618dda87..77b2595f 100644 --- a/src/chainconsumer/analysis.py +++ b/src/chainconsumer/analysis.py @@ -57,7 +57,8 @@ def __init__(self, parent: ChainConsumer): def get_latex_table( self, - columns: list[str] | int | None = None, + chains: list[ChainName | Chain] | None = None, + columns: list[ColumnName] | None = None, transpose: bool = False, caption: str | None = None, label: str = "tab:model_params", @@ -68,9 +69,13 @@ def get_latex_table( """Generates a LaTeX table from parameter summaries. Args: - columns : list[str], int optional - A list of what parameters to include in the table. By default, includes all columns. - If an integer is passed, will include the first N columns. + chains: + Used to specify which chain to show if more than one chain is loaded in. + Can be an integer, specifying the + chain index, or a str, specifying the chain name. + columns: + If set, only creates a plot for those specific parameters (if list). If an + integer is given, only plots the fist so many parameters. transpose : bool, optional Defaults to False, which gives each column as a parameter, each chain (framework) as a row. You can swap it so that you have a parameter each row and a framework @@ -92,16 +97,12 @@ def get_latex_table( Returns: str: the LaTeX table. """ - if columns is None: - columns = self.parent._all_columns - elif isinstance(columns, int): - columns = self.parent._all_columns[:columns] - # TODO: ensure labels are a thin we can add - num_parameters = len(columns) - - chains = self.parent._chains - num_chains = len(chains) - fit_values = self.get_summary(chains=chains) + final_chains = self.parent.plotter._sanitise_chains(chains) + final_columns = self.parent.plotter._sanitise_columns(columns, final_chains) + + num_chains = len(final_chains) + num_parameters = len(final_columns) + fit_values = self.get_summary(chains=final_chains) if label is None: label = "" if caption is None: @@ -115,10 +116,10 @@ def get_latex_table( if hlines: center_text += hline_text + "\t\t" if transpose: - center_text += " & ".join(["Parameter"] + [c.name for c in chains.values()]) + end_text + center_text += " & ".join(["Parameter"] + [c.name for c in final_chains]) + end_text if hlines: center_text += "\t\t" + hline_text - for p in columns: + for p in final_columns: arr = ["\t\t" + self.parent.plotter.config.get_label(p)] for _, column_results in fit_values.items(): if p in column_results: @@ -127,12 +128,14 @@ def get_latex_table( arr.append(blank_fill) center_text += " & ".join(arr) + end_text else: - center_text += " & ".join(["Model", *[self.parent.plotter.config.get_label(c) for c in columns]]) + end_text + center_text += ( + " & ".join(["Model", *[self.parent.plotter.config.get_label(c) for c in final_columns]]) + end_text + ) if hlines: center_text += "\t\t" + hline_text for name, chain_res in fit_values.items(): arr = ["\t\t" + name] - for p in columns: + for p in final_columns: if p in chain_res: arr.append(self.get_parameter_text(chain_res[p], wrap=True)) else: @@ -152,8 +155,8 @@ def get_latex_table( def get_summary( self, - columns: list[str] | None = None, - chains: dict[str, Chain] | list[str] | None = None, + chains: list[Chain] | None = None, + columns: list[ColumnName] | None = None, ) -> dict[ChainName, dict[ColumnName, Bound]]: """Gets a summary of the marginalised parameter distributions. @@ -166,11 +169,11 @@ def get_summary( """ results = {} if chains is None: - chains = self.parent._chains - if isinstance(chains, list): - chains = {c: self.parent._chains[c] for c in chains} + chains = self.parent.plotter._sanitise_chains(None, include_skip=True) + if columns is None: + columns = self.parent.plotter._sanitise_columns(None, chains) - for name, chain in chains.items(): + for chain in chains: res = {} params_to_find = columns if columns is not None else chain.data_columns for p in params_to_find: @@ -178,7 +181,7 @@ def get_summary( continue summary = self.get_parameter_summary(chain, p) res[p] = summary - results[name] = res + results[chain.name] = res return results diff --git a/src/chainconsumer/chainconsumer.py b/src/chainconsumer/chainconsumer.py index 0c9f0c86..50bb440b 100644 --- a/src/chainconsumer/chainconsumer.py +++ b/src/chainconsumer/chainconsumer.py @@ -36,7 +36,7 @@ def __init__(self) -> None: @property def _all_columns(self) -> list[str]: """All the columns across all chains""" - return list(set([c for chain in self._chains.values() for c in chain.samples.columns])) + return list(set([c for chain in self._chains.values() for c in chain.data_columns])) def add_truth(self, truth: Truth) -> "ChainConsumer": """Add a truth to ChainConsumer. diff --git a/src/chainconsumer/examples.py b/src/chainconsumer/examples.py index c2914908..8ef2247a 100644 --- a/src/chainconsumer/examples.py +++ b/src/chainconsumer/examples.py @@ -3,16 +3,23 @@ from scipy.stats import multivariate_normal as mv -def make_sample(num_dimensions: int = 2, seed: int | None = None) -> pd.DataFrame: +def make_sample( + num_dimensions: int = 2, + seed: int | None = None, + randomise_mean: bool = False, + num_points: int = 1000000, +) -> pd.DataFrame: gen = np.random.default_rng(seed) vals = gen.random((num_dimensions, num_dimensions)) - 0.5 cov = np.dot(vals, vals.T) diag = np.sqrt(np.diag(cov)) outer = np.outer(diag, diag) cor = cov / outer - means = np.arange(num_dimensions) + means = np.arange(num_dimensions) * 1.0 + if randomise_mean: + means += gen.uniform(-1, 1, num_dimensions) norm = mv(mean=means, cov=cor) - samples = norm.rvs(size=1000000) + samples = norm.rvs(size=num_points) # Use the letters of the alphabet as the column names columns = [chr(65 + i) for i in range(num_dimensions)] return pd.DataFrame(samples, columns=columns).assign(log_posterior=norm.logpdf(samples)) diff --git a/src/chainconsumer/helpers.py b/src/chainconsumer/helpers.py index 8bf804e7..5cfd0f2f 100644 --- a/src/chainconsumer/helpers.py +++ b/src/chainconsumer/helpers.py @@ -38,7 +38,8 @@ def get_extents( def get_bins(chain: Chain) -> int: - return max((35, np.floor(1.0 * np.power(chain.samples.shape[0] / chain.samples.shape[1], 0.25)))) + max_v = 35 if chain.smooth > 0 else 100 + return max((max_v, np.floor(1.0 * np.power(chain.samples.shape[0] / chain.samples.shape[1], 0.25)))) def get_smoothed_bins( diff --git a/src/chainconsumer/plotter.py b/src/chainconsumer/plotter.py index 42e28f2a..1c13bb9a 100644 --- a/src/chainconsumer/plotter.py +++ b/src/chainconsumer/plotter.py @@ -381,288 +381,240 @@ def _add_watermark( else: axes.text(px, py, text, transform=axes.transAxes, fontdict=property_dict, rotation=rotation, fontsize=size) - # def plot_walks( - # self, - # parameters=None, - # truth=None, - # extents=None, - # display=False, - # filename=None, - # chains=None, - # convolve=None, - # figsize=None, - # plot_weights=True, - # plot_posterior=True, - # log_weight=None, - # log_scales=None, - # ): # pragma: no cover - # """Plots the chain walk; the parameter values as a function of step index. - - # This plot is more for a sanity or consistency check than for use with final results. - # Plotting this before plotting with :func:`plot` allows you to quickly see if the - # chains are well behaved, or if certain parameters are suspect - # or require a greater burn in period. - - # The desired outcome is to see an unchanging distribution along the x-axis of the plot. - # If there are obvious tails or features in the parameters, you probably want - # to investigate. - - # Parameters - # ---------- - # parameters : list[str]|int, optional - # Specify a subset of parameters to plot. If not set, all parameters are plotted. - # If an integer is given, only the first so many parameters are plotted. - # truth : list[float]|dict[str], optional - # A list of truth values corresponding to parameters, or a dictionary of - # truth values keyed by the parameter. - # extents : list[tuple]|dict[str], optional - # A list of two-tuples for plot extents per parameter, or a dictionary of - # extents keyed by the parameter. - # display : bool, optional - # If set, shows the plot using ``plt.show()`` - # filename : str, optional - # If set, saves the figure to the filename - # chains : int|str, list[str|int], optional - # Used to specify which chain to show if more than one chain is loaded in. - # Can be an integer, specifying the - # chain index, or a str, specifying the chain name. - # convolve : int, optional - # If set, overplots a smoothed version of the steps using ``convolve`` as - # the width of the smoothing filter. - # figsize : tuple, optional - # If set, sets the created figure size. - # plot_weights : bool, optional - # If true, plots the weight if they are available - # plot_posterior : bool, optional - # If true, plots the log posterior if they are available - # log_weight : bool, optional - # Whether to display weights in log space or not. If None, the value is - # inferred by the mean weights of the plotted chains. - # log_scales : bool, list[bool] or dict[bool], optional - # Whether or not to use a log scale on any given axis. Can be a list of True/False, a list of param - # names to set to true, a dictionary of param names with true/false - # or just a bool (just `True` would set everything to log scales). - - # Returns - # ------- - # figure - # the matplotlib figure created - - # """ - - # chains, parameters, truth, extents, _, log_scales = self._sanitise( - # chains, parameters, truth, extents, log_scales=log_scales - # ) - - # chains = [c for c in chains if c.mcmc_chain] - # n = len(parameters) - # extra = 0 - # if plot_weights: - # plot_weights = plot_weights and np.any([np.any(c.weights != 1.0) for c in chains]) - - # plot_posterior = plot_posterior and np.any([c.posterior is not None for c in chains]) - - # if plot_weights: - # extra += 1 - # if plot_posterior: - # extra += 1 - - # if figsize is None: - # figsize = (8, 0.75 + (n + extra)) - - # fig, axes = plt.subplots(figsize=figsize, nrows=n + extra, squeeze=False, sharex=True) - - # for i, axes_row in enumerate(axes): - # ax = axes_row[0] - # if i >= extra: - # p = parameters[i - n] - # for chain in chains: - # if p in chain.parameters: - # chain_row = chain.get_data(p) - # log = log_scales.get(p, False) - # self._plot_walk( - # ax, - # p, - # chain_row, - # extents=extents.get(p), - # convolve=convolve, - # color=chain.config["color"], - # log_scale=log, - # ) - # if truth.get(p) is not None: - # self._plot_walk_truth(ax, truth.get(p)) - # else: - # if i == 0 and plot_posterior: - # for chain in chains: - # if chain.log_posterior is not None: - # self._plot_walk( - # ax, - # r"$\log(P)$", - # chain.log_posterior - chain.log_posterior.max(), - # convolve=convolve, - # color=chain.config["color"], - # ) - # else: - # if log_weight is None: - # log_weight = np.any([chain.weights.mean() < 0.1 for chain in chains]) - # if log_weight: - # for chain in chains: - # self._plot_walk( - # ax, - # r"$\log_{10}(w)$", - # np.log10(chain.weights), - # convolve=convolve, - # color=chain.config["color"], - # ) - # else: - # for chain in chains: - # self._plot_walk(ax, "$w$", chain.weights, convolve=convolve, color=chain.config["color"]) - - # if filename is not None: - # if isinstance(filename, str): - # filename = [filename] - # for f in filename: - # self._save_fig(fig, f, 300) - # if display: - # plt.show() - - # return fig - - # def plot_distributions( - # self, - # parameters=None, - # truth=None, - # extents=None, - # display=False, - # filename=None, - # chains=None, - # col_wrap=4, - # figsize=None, - # blind=None, - # log_scales=None, - # ): # pragma: no cover - # """Plots the 1D parameter distributions for verification purposes. - - # This plot is more for a sanity or consistency check than for use with final results. - # Plotting this before plotting with :func:`plot` allows you to quickly see if the - # chains give well behaved distributions, or if certain parameters are suspect - # or require a greater burn in period. - - # Parameters - # ---------- - # parameters : list[str]|int, optional - # Specify a subset of parameters to plot. If not set, all parameters are plotted. - # If an integer is given, only the first so many parameters are plotted. - # truth : list[float]|dict[str], optional - # A list of truth values corresponding to parameters, or a dictionary of - # truth values keyed by the parameter. - # extents : list[tuple]|dict[str], optional - # A list of two-tuples for plot extents per parameter, or a dictionary of - # extents keyed by the parameter. - # display : bool, optional - # If set, shows the plot using ``plt.show()`` - # filename : str, optional - # If set, saves the figure to the filename - # chains : int|str, list[str|int], optional - # Used to specify which chain to show if more than one chain is loaded in. - # Can be an integer, specifying the - # chain index, or a str, specifying the chain name. - # col_wrap : int, optional - # How many columns to plot before wrapping. - # figsize : tuple(float)|float, optional - # Either a tuple specifying the figure size or a float scaling factor. - # blind : bool|string|list[string], optional - # Whether to blind axes values. Can be set to `True` to blind all parameters, - # or can pass in a string (or list of strings) which specify the parameters to blind. - # log_scales : bool, list[bool] or dict[bool], optional - # Whether or not to use a log scale on any given axis. Can be a list of True/False, a list of param - # names to set to true, a dictionary of param names with true/false - # or just a bool (just `True` would set everything to log scales). - - # Returns - # ------- - # figure - # the matplotlib figure created - - # """ - # chains, parameters, truth, extents, blind, log_scales = self._sanitise( - # chains, parameters, truth, extents, blind=blind, log_scales=log_scales - # ) - - # n = len(parameters) - # num_cols = min(n, col_wrap) - # num_rows = int(np.ceil(1.0 * n / col_wrap)) - - # if figsize is None: - # figsize = 1.0 - # if isinstance(figsize, float): - # figsize_float = figsize - # figsize = (num_cols * 2 * figsize, num_rows * 2 * figsize) - # else: - # figsize_float = 1.0 - - # summary = self.parent.config["summary"] - # label_font_size = self.parent.config["label_font_size"] - # tick_font_size = self.parent.config["tick_font_size"] - # max_ticks = self.parent.config["max_ticks"] - # diagonal_tick_labels = self.parent.config["diagonal_tick_labels"] - - # if summary is None: - # summary = len(self.parent.chains) == 1 - - # hspace = (0.8 if summary else 0.5) / figsize_float - # fig, axes = plt.subplots(nrows=num_rows, ncols=num_cols, figsize=figsize, squeeze=False) - # fig.subplots_adjust(left=0.1, right=0.95, top=0.95, bottom=0.1, wspace=0.05, hspace=hspace) - - # formatter = ScalarFormatter(useOffset=False) - # formatter.set_powerlimits((-3, 4)) - - # for i, ax in enumerate(axes.flatten()): - # if i >= len(parameters): - # ax.set_axis_off() - # continue - # p = parameters[i] - - # ax.set_yticks([]) - # if log_scales.get(p, False): - # ax.set_xscale("log") - # if p in blind: - # ax.set_xticks([]) - # else: - # if diagonal_tick_labels: - # _ = [l.set_rotation(45) for l in ax.get_xticklabels()] - # _ = [l.set_fontsize(tick_font_size) for l in ax.get_xticklabels()] - - # if log_scales.get(p, False): - # ax.xaxis.set_major_locator(LogLocator(numticks=max_ticks)) - # else: - # ax.xaxis.set_major_locator(MaxNLocator(max_ticks, prune="lower")) - # ax.xaxis.set_major_formatter(formatter) - # ax.set_xlim(extents.get(p) or self._get_parameter_extents(p, chains)) - - # max_val = -np.inf - # for chain in chains: - # if not chain.config["plot_contour"]: - # continue - # if p in chain.parameters: - # param_summary = summary and p not in blind - # m = self._plot_bars(ax, p, chain, summary=param_summary) - # if max_val is None or m > max_val: - # max_val = m - - # self._add_truth(ax, truth, None, py=p) - # ax.set_ylim(0, 1.1 * max_val) - # ax.set_xlabel(p, fontsize=label_font_size) - - # if filename is not None: - # if isinstance(filename, str): - # filename = [filename] - # for f in filename: - # self._save_fig(fig, f, 300) - # if display: - # plt.show() - - # return fig + def plot_walks( + self, + chains: list[ChainName | Chain] | None = None, + columns: list[ColumnName] | None = None, + filename: list[str | Path] | str | Path | None = None, + figsize: float | tuple[float, float] | None = None, + convolve: int | None = None, + plot_weights: bool = True, + plot_posterior: bool = True, + log_weight: bool = False, + ) -> Figure: # pragma: no cover + """Plots the chain walk; the parameter values as a function of step index. + + This plot is more for a sanity or consistency check than for use with final results. + Plotting this before plotting with :func:`plot` allows you to quickly see if the + chains are well behaved, or if certain parameters are suspect + or require a greater burn in period. + + The desired outcome is to see an unchanging distribution along the x-axis of the plot. + If there are obvious tails or features in the parameters, you probably want + to investigate. + + Args: + chains: + Used to specify which chain to show if more than one chain is loaded in. + Can be an integer, specifying the + chain index, or a str, specifying the chain name. + columns: + If set, only creates a plot for those specific parameters (if list). If an + integer is given, only plots the fist so many parameters. + filename: + If set, saves the figure to this location + figsize: + Scale horizontal and vertical figure size. + col_wrap: + How many columns to plot before wrapping. + convolve: + If set, overplots a smoothed version of the steps using ``convolve`` as + the width of the smoothing filter. + plot_weights: + If true, plots the weight if they are available + plot_posterior: + If true, plots the log posterior if they are available + log_weight: + Whether to display weights in log space or not. If None, the value is + inferred by the mean weights of the plotted chains. + + Returns: + the matplotlib figure created + + """ + + base = self._sanitise( + chains, + columns, + self.config.extents, + blind=self.config.blind, + log_scales=self.config.log_scales, + ) + + n = len(base.columns) + extra = 0 + + plot_posterior = plot_posterior and np.any([c.log_posterior is not None for c in base.chains]) + + if plot_weights: + extra += 1 + if plot_posterior: + extra += 1 + + if figsize is None: + figsize = (8, 0.75 + (n + extra)) + + fig, axes = plt.subplots(figsize=figsize, nrows=n + extra, squeeze=False, sharex=True) + + for i, axes_row in enumerate(axes): + ax = axes_row[0] + if i >= extra: + p = base.columns[i - extra] + for chain in base.chains: + if p in chain.data_columns: + chain_row = chain.get_data(p) + log = p in base.log_scales + self._plot_walk( + ax, + p, + chain_row, + extents=base.extents.get(p), + convolve=convolve, + color=colors.format(chain.color), + log_scale=log, + ) + for truth in self.parent._truths: + if p in truth.location: + self._plot_walk_truth(ax, truth, p) + + else: # noqa: PLR5501 + if i == 0 and plot_posterior: + for chain in base.chains: + if chain.log_posterior is not None: + self._plot_walk( + ax, + r"$\log(P)$", + chain.log_posterior - chain.log_posterior.max(), + convolve=convolve, + color=colors.format(chain.color), + ) + else: + label = r"$\log_{10}$Weight" if log_weight else "Weight" + + for chain in base.chains: + if chain.weights is not None: + self._plot_walk( + ax, + label, + np.log10(chain.weights) if log_weight else chain.weights, + convolve=convolve, + color=colors.format(chain.color), + ) + + if filename is not None: + if not isinstance(filename, list): + filename = [filename] + for f in filename: + self._save_fig(fig, f, self.config.dpi) + + return fig + + def plot_distributions( + self, + chains: list[ChainName | Chain] | None = None, + columns: list[ColumnName] | None = None, + filename: list[str | Path] | str | Path | None = None, + col_wrap: int = 4, + figsize: float | tuple[float, float] | None = None, + ) -> Figure: # pragma: no cover + """Plots the 1D parameter distributions for verification purposes. + + This plot is more for a sanity or consistency check than for use with final results. + Plotting this before plotting with :func:`plot` allows you to quickly see if the + chains give well behaved distributions, or if certain parameters are suspect + or require a greater burn in period. + + Args: + chains: + Used to specify which chain to show if more than one chain is loaded in. + Can be an integer, specifying the + chain index, or a str, specifying the chain name. + columns: + If set, only creates a plot for those specific parameters (if list). If an + integer is given, only plots the fist so many parameters. + filename: + If set, saves the figure to this location + figsize: + Scale horizontal and vertical figure size. + col_wrap: + How many columns to plot before wrapping. + + Returns: + the matplotlib figure created + + """ + base = self._sanitise( + chains, + columns, + self.config.extents, + blind=self.config.blind, + log_scales=self.config.log_scales, + ) + + n = len(base.columns) + num_cols = min(n, col_wrap) + num_rows = int(np.ceil(1.0 * n / col_wrap)) + + if figsize is None: + figsize = 1.0 + if isinstance(figsize, float): + figsize_float = figsize + figsize = (num_cols * 2.5 * figsize, num_rows * 2.5 * figsize) + else: + figsize_float = 1.0 + + summary = self.config.summarise and len(base.chains) == 1 + hspace = (0.8 if summary else 0.5) / figsize_float + fig, axes = plt.subplots(nrows=num_rows, ncols=num_cols, figsize=figsize, squeeze=False) + fig.subplots_adjust(left=0.1, right=0.95, top=0.95, bottom=0.1, wspace=0.05, hspace=hspace) + + formatter = ScalarFormatter(useOffset=False) + formatter.set_powerlimits((-3, 4)) + + for i, ax in enumerate(axes.flatten()): + if i >= n: + ax.set_axis_off() + continue + p = base.columns[i] + + ax.set_yticks([]) + if p in base.log_scales: + ax.set_xscale("log") + if p in base.blind: + ax.set_xticks([]) + else: + if self.config.diagonal_tick_labels: + _ = [label.set_rotation(45) for label in ax.get_xticklabels()] + _ = [label.set_fontsize(self.config.tick_font_size) for label in ax.get_xticklabels()] + + if p in base.log_scales: + ax.xaxis.set_major_locator(LogLocator(numticks=self.config.max_ticks)) + else: + ax.xaxis.set_major_locator(MaxNLocator(self.config.max_ticks, prune="lower")) + ax.xaxis.set_major_formatter(formatter) + ax.set_xlim(base.extents.get(p) or self._get_parameter_extents(p, base.chains)) + + max_val = -np.inf + for chain in base.chains: + if not chain.plot_contour: + continue + if p in chain.plotting_columns: + param_summary = summary and p not in base.blind + m = self._plot_bars(ax, p, chain, summary=param_summary) + if max_val is None or m > max_val: + max_val = m + for truth in self.parent._truths: + self._add_truth(ax, truth, py=p) + ax.set_ylim(0, 1.1 * max_val) + ax.set_xlabel(p, fontsize=self.config.label_font_size) + + if filename is not None: + if not isinstance(filename, list): + filename = [filename] + for f in filename: + self._save_fig(fig, f, self.config.dpi) + fig.tight_layout() + return fig def plot_summary( self, @@ -781,7 +733,7 @@ def plot_summary( # Put title in if i == 0: - ax.set_title(r"$%s$" % p, fontsize=label_font_size) + ax.set_title(self.config.get_label(p), fontsize=label_font_size) # Add truth values for truth in self.parent._truths: @@ -875,7 +827,7 @@ def _sanitise( blind=self._sanitise_blinds(blind, final_columns), ) - def set_rc_params(self): + def set_rc_params(self) -> None: if self.config.usetex: plt.rc("text", usetex=True) else: @@ -1078,7 +1030,9 @@ def _plot_point(self, ax: Axes, chain: Chain, px: str, py: str) -> PathCollectio ) return h - def _sanitise_chains(self, chains: list[Chain | ChainName] | dict[ChainName, Chain] | None) -> list[Chain]: + def _sanitise_chains( + self, chains: list[Chain | ChainName] | dict[ChainName, Chain] | None, include_skip: bool = False + ) -> list[Chain]: overriden_chains = self.parent._get_final_chains() final_chains = [] if isinstance(chains, list): @@ -1087,7 +1041,7 @@ def _sanitise_chains(self, chains: list[Chain | ChainName] | dict[ChainName, Cha final_chains = [overriden_chains[c.name] for c in chains.values()] else: final_chains = list(overriden_chains.values()) - return [c for c in final_chains if not c.skip] + return [c for c in final_chains if include_skip or not c.skip] def plot_contour( self, @@ -1232,7 +1186,7 @@ def _plot_bars( lower = xs.min() if upper > xs.max(): upper = xs.max() - x = np.linspace(lower, upper, 1000) + x = np.linspace(lower, upper, 1000) # type: ignore if flip: ax.fill_betweenx( x, @@ -1259,17 +1213,24 @@ def _plot_bars( ) else: ax.set_title(r"$%s$" % t, fontsize=self.config.summary_font_size) - return ys.max() + return float(ys.max()) def _plot_walk( - self, ax, parameter, data, truth=None, extents=None, convolve=None, color=None, log_scale=False - ): # pragma: no cover + self, + ax: Axes, + column: ColumnName, + data: pd.Series, + extents: tuple[float, float] | None = None, + convolve: int | None = None, + color: str | None = None, + log_scale: bool = False, + ) -> None: # pragma: no cover if extents is not None: ax.set_ylim(extents) assert convolve is None or isinstance(convolve, int), "Convolve must be an integer pixel window width" x = np.arange(data.size) ax.set_xlim(0, x[-1]) - ax.set_ylabel(parameter) + ax.set_ylabel(self.config.get_label(column)) if color is None: color = "#0345A1" ax.scatter(x, data, c=color, s=2, marker=".", edgecolors="none", alpha=0.5) @@ -1281,10 +1242,11 @@ def _plot_walk( ax.yaxis.set_major_locator(MaxNLocator(max_ticks, prune="lower")) if convolve is not None: + trim = int(0.5 * convolve) color2 = colors.scale_colour(color, 0.5) filt = np.ones(convolve) / convolve filtered = np.convolve(data, filt, mode="same") - ax.plot(x[:-1], filtered[:-1], ls=":", color=color2, alpha=1) + ax.plot(x[trim:-trim], filtered[trim:-trim], color=color2, alpha=1) def _plot_walk_truth(self, ax: Axes, truth: Truth, col: str) -> None: ax.axhline(truth.location[col], **truth._kwargs) diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 706a78a3..ce19dbd9 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -7,7 +7,7 @@ class TestChain: rng = np.random.default_rng(1) - n = 2000000 + n = 3000000 data = rng.normal(loc=5.0, scale=1.5, size=n) data2 = rng.normal(loc=3, scale=1.0, size=n) data_combined = np.vstack((data, data2)).T @@ -32,7 +32,7 @@ def test_summary_no_smooth(self): tolerance = 5e-2 consumer = ChainConsumer() consumer.add_chain(self.chain) - consumer.set_override(ChainConfig(smooth=0, bins=2.4)) + consumer.set_override(ChainConfig(smooth=0, bins=2.0)) summary = consumer.analysis.get_summary() actual = summary["a"]["x"].array expected = np.array([3.5, 5.0, 6.5]) diff --git a/tests/test_diagnostic.py b/tests/test_diagnostic.py index 17927385..3e36d912 100644 --- a/tests/test_diagnostic.py +++ b/tests/test_diagnostic.py @@ -7,7 +7,7 @@ @pytest.fixture def rng(): - return np.random.default_rng() + return np.random.default_rng(seed=0) @pytest.fixture From 8f90640ae369ed52c8727c67e2a419098b113279 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Sun, 8 Oct 2023 21:46:18 +1000 Subject: [PATCH 10/22] Adding more doco --- .pre-commit-config.yaml | 8 ++ Makefile | 2 +- docs/examples/README.md | 1 + docs/examples/advanced_examples/README.md | 1 + .../examples/advanced_examples/plot_0_grid.py | 30 +++++ .../advanced_examples/plot_1_blinding.py | 31 +++++ docs/examples/advanced_examples/plot_2_kde.py | 26 ++++ .../advanced_examples/plot_3_divide_chains.py | 24 ++++ .../plot_4_misc_chain_visuals.py | 96 ++++++++++++++ .../{contours.py => plot_0_contours.py} | 0 .../{summary.py => plot_1_summary.py} | 0 ...ual_output.py => plot_2_textual_output.py} | 0 ...stributions.py => plot_3_distributions.py} | 0 docs/examples/{walks.py => plot_4_walks.py} | 0 examples/Basics/README.txt | 7 - examples/Basics/plot_convergence.py | 56 -------- examples/Basics/plot_covariance_and_marker.py | 27 ---- examples/Basics/plot_grid.py | 45 ------- examples/Basics/plot_hundreds_of_chains.py | 122 ------------------ examples/Basics/plot_loading_data.py | 79 ------------ examples/Basics/plot_statistics.py | 79 ------------ examples/Basics/plot_truth_values.py | 36 ------ examples/Basics/plot_two_disjoint_chains.py | 26 ---- examples/README.txt | 14 -- examples/customisations/README.txt | 8 -- examples/customisations/plot_as_prior.py | 28 ---- examples/customisations/plot_axis.py | 30 ----- examples/customisations/plot_blinding.py | 24 ---- .../customisations/plot_chain_override.py | 31 ----- examples/customisations/plot_cloud_sigma.py | 28 ---- examples/customisations/plot_colorpoints.py | 50 ------- examples/customisations/plot_colours_shade.py | 34 ----- .../customisations/plot_confidence_levels.py | 39 ------ .../customisations/plot_contour_labels.py | 32 ----- examples/customisations/plot_dont_flip.py | 27 ---- .../customisations/plot_fewer_parameters.py | 27 ---- examples/customisations/plot_font_changes.py | 25 ---- examples/customisations/plot_kde_extents.py | 31 ----- .../customisations/plot_legend_options.py | 52 -------- examples/customisations/plot_linestyles.py | 26 ---- examples/customisations/plot_lists.py | 60 --------- examples/customisations/plot_logscale.py | 35 ----- examples/customisations/plot_no_histograms.py | 23 ---- examples/customisations/plot_no_smooth.py | 33 ----- examples/customisations/plot_one_chain.py | 25 ---- examples/customisations/plot_preliminary.py | 42 ------ .../customisations/plot_rainbow_serif_bins.py | 43 ------ .../customisations/plot_selected_chains.py | 29 ----- .../customisations/plot_shade_gradient.py | 26 ---- examples/customisations/plot_shift.py | 33 ----- examples/customisations/plot_spacing.py | 23 ---- examples/customisations/plot_three_chains.py | 34 ----- examples/customisations/plot_zorder.py | 35 ----- examples/more/README.txt | 7 - examples/more/plot_colorpoints2.py | 32 ----- examples/more/plot_divide_chain.py | 32 ----- examples/more/plot_many.py | 36 ------ examples/plot_correlations.py | 32 ----- examples/plot_covariance.py | 31 ----- examples/plot_distributions.py | 26 ---- examples/plot_introduction.py | 37 ------ examples/plot_model_selection.py | 35 ----- examples/plot_summary.py | 55 -------- examples/plot_table.py | 42 ------ examples/plot_walk.py | 37 ------ examples/resources/correlations.png | Bin 50767 -> 0 bytes examples/resources/covariance.png | Bin 52243 -> 0 bytes examples/resources/exampleWalk.png | Bin 461274 -> 0 bytes examples/resources/summary.png | Bin 6555 -> 0 bytes examples/resources/table.png | Bin 46600 -> 0 bytes examples/resources/table_comparison.png | Bin 49709 -> 0 bytes mkdocs.yml | 1 + src/chainconsumer/__init__.py | 3 +- src/chainconsumer/analysis.py | 4 +- src/chainconsumer/chain.py | 8 +- src/chainconsumer/chainconsumer.py | 6 +- .../{colors.py => color_finder.py} | 0 src/chainconsumer/helpers.py | 2 + src/chainconsumer/plotter.py | 30 +++-- src/chainconsumer/truth.py | 2 +- tests/test_analysis.py | 5 +- tests/test_colours.py | 2 +- 82 files changed, 254 insertions(+), 1854 deletions(-) create mode 100644 docs/examples/advanced_examples/README.md create mode 100644 docs/examples/advanced_examples/plot_0_grid.py create mode 100644 docs/examples/advanced_examples/plot_1_blinding.py create mode 100644 docs/examples/advanced_examples/plot_2_kde.py create mode 100644 docs/examples/advanced_examples/plot_3_divide_chains.py create mode 100644 docs/examples/advanced_examples/plot_4_misc_chain_visuals.py rename docs/examples/{contours.py => plot_0_contours.py} (100%) rename docs/examples/{summary.py => plot_1_summary.py} (100%) rename docs/examples/{plot_textual_output.py => plot_2_textual_output.py} (100%) rename docs/examples/{distributions.py => plot_3_distributions.py} (100%) rename docs/examples/{walks.py => plot_4_walks.py} (100%) delete mode 100644 examples/Basics/README.txt delete mode 100644 examples/Basics/plot_convergence.py delete mode 100644 examples/Basics/plot_covariance_and_marker.py delete mode 100644 examples/Basics/plot_grid.py delete mode 100644 examples/Basics/plot_hundreds_of_chains.py delete mode 100644 examples/Basics/plot_loading_data.py delete mode 100644 examples/Basics/plot_statistics.py delete mode 100644 examples/Basics/plot_truth_values.py delete mode 100644 examples/Basics/plot_two_disjoint_chains.py delete mode 100644 examples/README.txt delete mode 100644 examples/customisations/README.txt delete mode 100644 examples/customisations/plot_as_prior.py delete mode 100644 examples/customisations/plot_axis.py delete mode 100644 examples/customisations/plot_blinding.py delete mode 100644 examples/customisations/plot_chain_override.py delete mode 100644 examples/customisations/plot_cloud_sigma.py delete mode 100644 examples/customisations/plot_colorpoints.py delete mode 100644 examples/customisations/plot_colours_shade.py delete mode 100644 examples/customisations/plot_confidence_levels.py delete mode 100644 examples/customisations/plot_contour_labels.py delete mode 100644 examples/customisations/plot_dont_flip.py delete mode 100644 examples/customisations/plot_fewer_parameters.py delete mode 100644 examples/customisations/plot_font_changes.py delete mode 100644 examples/customisations/plot_kde_extents.py delete mode 100644 examples/customisations/plot_legend_options.py delete mode 100644 examples/customisations/plot_linestyles.py delete mode 100644 examples/customisations/plot_lists.py delete mode 100644 examples/customisations/plot_logscale.py delete mode 100644 examples/customisations/plot_no_histograms.py delete mode 100644 examples/customisations/plot_no_smooth.py delete mode 100644 examples/customisations/plot_one_chain.py delete mode 100644 examples/customisations/plot_preliminary.py delete mode 100644 examples/customisations/plot_rainbow_serif_bins.py delete mode 100644 examples/customisations/plot_selected_chains.py delete mode 100644 examples/customisations/plot_shade_gradient.py delete mode 100644 examples/customisations/plot_shift.py delete mode 100644 examples/customisations/plot_spacing.py delete mode 100644 examples/customisations/plot_three_chains.py delete mode 100644 examples/customisations/plot_zorder.py delete mode 100644 examples/more/README.txt delete mode 100644 examples/more/plot_colorpoints2.py delete mode 100644 examples/more/plot_divide_chain.py delete mode 100644 examples/more/plot_many.py delete mode 100644 examples/plot_correlations.py delete mode 100644 examples/plot_covariance.py delete mode 100644 examples/plot_distributions.py delete mode 100644 examples/plot_introduction.py delete mode 100644 examples/plot_model_selection.py delete mode 100644 examples/plot_summary.py delete mode 100644 examples/plot_table.py delete mode 100644 examples/plot_walk.py delete mode 100644 examples/resources/correlations.png delete mode 100644 examples/resources/covariance.png delete mode 100644 examples/resources/exampleWalk.png delete mode 100644 examples/resources/summary.png delete mode 100644 examples/resources/table.png delete mode 100644 examples/resources/table_comparison.png rename src/chainconsumer/{colors.py => color_finder.py} (100%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 521ea173..fe8aa0b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,3 +22,11 @@ repos: hooks: - id: ruff args: ["--fix"] + - repo: local + hooks: + - id: tests + name: tests + language: system + types: [python] + pass_filenames: false + entry: poetry run pytest diff --git a/Makefile b/Makefile index c5110b08..33398552 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ test: poetry run pytest serve: - rm -rf docs/generated/gallery; + # rm -rf docs/generated/gallery; poetry run mkdocs serve --clean tests: test diff --git a/docs/examples/README.md b/docs/examples/README.md index 65afe604..cf457a20 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -1,2 +1,3 @@ # Examples +All the basics laid out in a few short examples. \ No newline at end of file diff --git a/docs/examples/advanced_examples/README.md b/docs/examples/advanced_examples/README.md new file mode 100644 index 00000000..ec68baf6 --- /dev/null +++ b/docs/examples/advanced_examples/README.md @@ -0,0 +1 @@ +# Specific Examples \ No newline at end of file diff --git a/docs/examples/advanced_examples/plot_0_grid.py b/docs/examples/advanced_examples/plot_0_grid.py new file mode 100644 index 00000000..a58697b8 --- /dev/null +++ b/docs/examples/advanced_examples/plot_0_grid.py @@ -0,0 +1,30 @@ +""" +# Plotting Grid Data + + +If you don't have Monte Carlo chains, and have grid evaluations instead, that's fine too! + +Just flatten your grid, set the weights to the grid evaluation, and set the grid flag. Here is +a nice diamond that you get from modifying a simple multivariate normal distribution. + +Note that by default, grid data is not smoothed, though you can explicitly set the smooth +parameter in ``configure_general`` if you do want smoothing. + +Note that you *cannot* use dictionary input with the grid method and not specify the full +flattened array. This is because we cannot construct the meshgrid from a dictionary, as +the order of the parameters is not preserved in the dictionary. + +""" +import numpy as np +import pandas as pd + +from chainconsumer import Chain, ChainConsumer + +x, y = np.linspace(-3, 3, 50), np.linspace(-7, 7, 100) +xx, yy = np.meshgrid(x, y, indexing="ij") +likelihood = np.exp(-0.5 * (xx * xx + yy * yy / 4 + np.abs(xx * yy))) +df = pd.DataFrame({"x": xx.flatten(), "y": yy.flatten(), "weight": likelihood.flatten()}) + +c = ChainConsumer() +c.add_chain(Chain(samples=df, grid=True, name="Grid Data")) +fig = c.plotter.plot() diff --git a/docs/examples/advanced_examples/plot_1_blinding.py b/docs/examples/advanced_examples/plot_1_blinding.py new file mode 100644 index 00000000..b03cbdef --- /dev/null +++ b/docs/examples/advanced_examples/plot_1_blinding.py @@ -0,0 +1,31 @@ +""" +# Blinding Plots + + +You can blind parameters and not show axis labels very easily! + +Just give ChainConsumer the `blind` parameter when plotting. You can specify `True` to blind all parameters, +or give it a string (or list of strings) detailing the specific parameters you want blinded! + +""" +from chainconsumer import Chain, ChainConsumer, PlotConfig, make_sample + +df = make_sample(num_dimensions=4, seed=1) +c = ChainConsumer() +c.add_chain(Chain(samples=df, name="Blind Me!")) +c.set_plot_config(PlotConfig(blind=["A", "B"])) +fig = c.plotter.plot() + +# %% +# Notice the blinding applies to all plots +fig = c.plotter.plot_summary() + +# %% +fig = c.plotter.plot_walks() + +# %% + +fig = c.plotter.plot_distributions() +# %% +# And the LaTeX output +print(c.analysis.get_latex_table()) diff --git a/docs/examples/advanced_examples/plot_2_kde.py b/docs/examples/advanced_examples/plot_2_kde.py new file mode 100644 index 00000000..6e427122 --- /dev/null +++ b/docs/examples/advanced_examples/plot_2_kde.py @@ -0,0 +1,26 @@ +""" +# KDE + + +I don't recommend using KDEs in general, as its very easy to have +them inflate your contours if the bandpass isn't tuned well, and +its hard to see when it's too large. + +Always run more samples if you can, instead of covering up +a lack of data with some extra smoothing. + +But, if there's no other way, here's how you can do it. + +Notice how the KDE, unless its perfectly matched to your distribution, +increses the width of the marginal distributions. + +""" +from chainconsumer import Chain, ChainConsumer, PlotConfig, make_sample + +df = make_sample(num_dimensions=2, seed=3, num_points=1000) +c = ChainConsumer() +c.add_chain(Chain(samples=df, name="No KDE")) +c.add_chain(Chain(samples=df + 1, name="KDE", kde=1.0)) +c.add_chain(Chain(samples=df + 2, name="KDE that's too large", kde=2.0)) +c.set_plot_config(PlotConfig(flip=True)) +fig = c.plotter.plot() diff --git a/docs/examples/advanced_examples/plot_3_divide_chains.py b/docs/examples/advanced_examples/plot_3_divide_chains.py new file mode 100644 index 00000000..c4d9b4d8 --- /dev/null +++ b/docs/examples/advanced_examples/plot_3_divide_chains.py @@ -0,0 +1,24 @@ +""" +# Dividing Chains + +It's common with algorithms like MH and other random walkers to have +multiple walkers each providing their own chains. Typically, you +want to verify each walker is burnt in, and then you put all of their +samples into one chain. + +But, if your final samples are made up for four walkers each contributing +10k samples, you may want to inspect each walker's surface individually. + +In this toy example, all the chains are from the same random generator, +so they're on top of each other. Except MCMC chains to not be as perfect. +""" +from chainconsumer import Chain, ChainConsumer, PlotConfig, make_sample + +df = make_sample(num_dimensions=2, seed=3, num_points=40000) + +c = ChainConsumer() +combined = Chain(samples=df, name="Model", walkers=4) +for chain in combined.divide(): + c.add_chain(chain) +c.set_plot_config(PlotConfig(flip=True)) +fig = c.plotter.plot() diff --git a/docs/examples/advanced_examples/plot_4_misc_chain_visuals.py b/docs/examples/advanced_examples/plot_4_misc_chain_visuals.py new file mode 100644 index 00000000..bdf859bf --- /dev/null +++ b/docs/examples/advanced_examples/plot_4_misc_chain_visuals.py @@ -0,0 +1,96 @@ +""" +# Miscellanous Visual Options + +Rather than having one example for each option, let's condense things. +""" +# %% +# # Shade Gradient +# +# Pretty simple - it controls how much visual difference there is in your contours. +import numpy as np + +from chainconsumer import Chain, ChainConfig, ChainConsumer, PlotConfig, make_sample + +df1 = make_sample(num_dimensions=2, seed=3) - 1 +df2 = make_sample(num_dimensions=2, seed=1) + 2 + +c = ChainConsumer() +c.add_chain(Chain(samples=df1, name="High Contrast", color="emerald", shade_gradient=2.0)) +c.add_chain(Chain(samples=df2, name="Low Contrast", color="sky", shade_gradient=0.2)) + +c.set_plot_config(PlotConfig(flip=True)) +fig = c.plotter.plot() + +# %% +# # Shade Alpha +# +# Controls how opaque the contours are. Like everything else, you +# can specify this when making the chain, or apply a single override +# to everything like so. +c.set_override(ChainConfig(shade_alpha=0.1)) +fig = c.plotter.plot() + +# %% +# # Contour Labels +# +# Add labels to contours. I used to have this configurable to be either +# sigma levels or percentages, but there was confusion over the 1D vs 2D sigma levels, +# in that $1\sigma$ in a 2D gaussian is *not* 68% of the volume. So now we just +# do percentages. +c.set_override(ChainConfig(show_contour_labels=True)) +fig = c.plotter.plot() + +# %% +# # Linestyles and widths +# +# Fairly simple to do. To show different ones, I'll remake the chains, +# rather than having a single override. Note you *could* try something +# like `chain.line_width = 5`, but this is a sneaky override, and it +# won't be registered in the internal "You set this attribute and didn't +# use the default when you made the chain, so don't screw with it." +# +# Nothing *does* screw with line width, or similar, but it's a good habit. +c2 = ChainConsumer() +c2.add_chain(Chain(samples=df1, name="Thin dots", color="emerald", linestyle=":", linewidth=0.5)) +c2.add_chain(Chain(samples=df2, name="Thick dashes", color="sky", linestyle="--", linewidth=2.0)) +fig = c2.plotter.plot() + +# %% +# # Marker styles and sizes +# +# Provided you have a posterior column, you can plot the maximum probability point. + +c.set_override(ChainConfig(plot_point=True, marker_style="P", marker_size=100)) +fig = c.plotter.plot() + +# %% +# # Cloud and Sigma Levels +# +# Choose custom sigma levels and display point cloud. +c.set_override( + ChainConfig( + shade_alpha=1.0, + sigmas=np.linspace(0, 1, 10).tolist(), + shade_gradient=1.0, + plot_cloud=True, + ) +) +fig = c.plotter.plot() + +# %% +# # Smoothing (or not) +# +# The histograms behind the scene in ChainConsumer are smoothed. But you can turn this off. +# The higher the smoothing vaule, the more subidivisions of your bins there will be. +c.set_override(ChainConfig(smooth=0)) +fig = c.plotter.plot() + +# %% +# But changing the smoothing doesn't change the number of bins. That's separate. +c.set_override(ChainConfig(smooth=0, bins=10)) +fig = c.plotter.plot() + +# %% +# It's beautiful. And it's hard to find a nice balance. +c.set_override(ChainConfig(smooth=0, bins=100)) +fig = c.plotter.plot() diff --git a/docs/examples/contours.py b/docs/examples/plot_0_contours.py similarity index 100% rename from docs/examples/contours.py rename to docs/examples/plot_0_contours.py diff --git a/docs/examples/summary.py b/docs/examples/plot_1_summary.py similarity index 100% rename from docs/examples/summary.py rename to docs/examples/plot_1_summary.py diff --git a/docs/examples/plot_textual_output.py b/docs/examples/plot_2_textual_output.py similarity index 100% rename from docs/examples/plot_textual_output.py rename to docs/examples/plot_2_textual_output.py diff --git a/docs/examples/distributions.py b/docs/examples/plot_3_distributions.py similarity index 100% rename from docs/examples/distributions.py rename to docs/examples/plot_3_distributions.py diff --git a/docs/examples/walks.py b/docs/examples/plot_4_walks.py similarity index 100% rename from docs/examples/walks.py rename to docs/examples/plot_4_walks.py diff --git a/examples/Basics/README.txt b/examples/Basics/README.txt deleted file mode 100644 index 5f3f78b0..00000000 --- a/examples/Basics/README.txt +++ /dev/null @@ -1,7 +0,0 @@ -.. _basic_examples: - -Basic Usages ------------- - -Things like how to load data and how to use the primary functions of the plotting library. - diff --git a/examples/Basics/plot_convergence.py b/examples/Basics/plot_convergence.py deleted file mode 100644 index 601bc7ba..00000000 --- a/examples/Basics/plot_convergence.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -======================= -Convergence Diagnostics -======================= - -How to use the built in convergence diagnostic tests! - -""" - - -############################################################################### -# Here we create some good and bad data, and then run convergence tests on it! - - -import numpy as np -from numpy.random import normal - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -# Here we have some nice data, and then some bad data, -# where the last part of the chain has walked off, and the first part -# of the chain isn't agreeing with anything else! -data_good = normal(size=100000) -data_bad = data_good.copy() -data_bad += np.linspace(-0.5, 0.5, 100000) -data_bad[98000:] += 2 - -# Lets load it into ChainConsumer, and pretend 10 walks went into making the chain -c = ChainConsumer() -c.add_chain(data_good, walkers=10, name="good") -c.add_chain(data_bad, walkers=10, name="bad") - -# Now, lets check our convergence using the Gelman-Rubin statistic -gelman_rubin_converged = c.diagnostic.gelman_rubin() -# And also using the Geweke metric -geweke_converged = c.diagnostic.geweke() - -# Lets just output the results too -print(gelman_rubin_converged, geweke_converged) - -############################################################################### -# We can see that both the Gelman-Rubin and Geweke statistics failed. -# Note that by not specifying a chain when calling the diagnostics, -# they are invoked on *all* chains. For example, to invoke the statistic -# on only the second chain we can pass in either the chain index, or the chain -# name: - -print(c.diagnostic.geweke(chain="bad")) - -############################################################################### -# Finally, note that the statistics are set to fail easily. For example, -# if you have 10 chains and run `diagnostic_gelman_rubin` with the defaults, -# you will get false if *any* parameter of *any* chain has not converged. -# The printed output will let you know which chains and parameters are -# culpable. diff --git a/examples/Basics/plot_covariance_and_marker.py b/examples/Basics/plot_covariance_and_marker.py deleted file mode 100644 index b8682fe6..00000000 --- a/examples/Basics/plot_covariance_and_marker.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -=============================== -Covariance, Fisher and Markers! -=============================== - -Sometimes you want to compare your data to a Fisher matrix projection, -or you just have some Gaussian you want also drawn. - -Or maybe its just a random point you want to put on the plot. - -It's all easy to do. - -""" -# -*- coding: utf-8 -*- -from chainconsumer import ChainConsumer - -mean = [1, 5] -cov = [[1, 1], [1, 3]] -parameters = ["a", "b"] - -c = ChainConsumer() -c.add_covariance(mean, cov, parameters=parameters, name="Cov") -c.add_marker(mean, parameters=parameters, name="Marker!", marker_style="*", marker_size=100, color="r") -c.configure_overrides(usetex=False, serif=False) -fig = c.plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/Basics/plot_grid.py b/examples/Basics/plot_grid.py deleted file mode 100644 index de1c4a2c..00000000 --- a/examples/Basics/plot_grid.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -========== -Grid Data! -========== - -If you don't have Monte Carlo chains, and have grid evaluations instead, that's fine too! - -Just flatten your grid, set the weights to the grid evaluation, and set the grid flag. Here is -a nice diamond that you get from modifying a simple multivariate normal distribution. - -Note that by default, grid data is not smoothed, though you can explicitly set the smooth -parameter in ``configure_general`` if you do want smoothing. - -Note that you *cannot* use dictionary input with the grid method and not specify the full -flattened array. This is because we cannot construct the meshgrid from a dictionary, as -the order of the parameters is not preserved in the dictionary. - -""" -import numpy as np -from scipy.stats import multivariate_normal - -from chainconsumer import ChainConsumer - -x, y = np.linspace(-3, 3, 50), np.linspace(-7, 7, 100) -xx, yy = np.meshgrid(x, y, indexing="ij") -pdf = np.exp(-0.5 * (xx * xx + yy * yy / 4 + np.abs(xx * yy))) - -c = ChainConsumer() -c.add_chain([x, y], parameters=["$x$", "$y$"], weights=pdf, grid=True) -fig = c.plotter.plot() -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# If you have the flattened array already, you can also pass this - -# Turning 2D data to flat data. -xs, ys = xx.flatten(), yy.flatten() -coords = np.vstack((xs, ys)).T -pdf_flat = multivariate_normal.pdf(coords, mean=[0.0, 0.0], cov=[[1.0, 0.7], [0.7, 3.5]]) -c = ChainConsumer() -c.add_chain([xs, ys], parameters=["$x$", "$y$"], weights=pdf_flat, grid=True) -c.configure_overrides(smooth=1) # Notice how smoothing changes the results! -fig = c.plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/Basics/plot_hundreds_of_chains.py b/examples/Basics/plot_hundreds_of_chains.py deleted file mode 100644 index 4f201115..00000000 --- a/examples/Basics/plot_hundreds_of_chains.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -================== -Hundreds of Chains -================== - -Sometimes you have a lot of results and want to see the distribution of your results. - -When you have hundreds of chains, for example you've fit a model on 100 realsiations -of different data to validate your model, it's impractical to show hundreds of contours. - -In this case, it is often desired to show the distribution of maximum likelihood points, which -helps quantify whether your model is biased and the statistical uncertainty of your model. - -To do this, you will need to pass in the posterior values. By default, if you pass in enough chains (more than 20), -ChainConsumer will automatically only plot maximum posterior points for chains which have posteriors. You can -explicitly control this by setting `plot_point` and/or `plot_contour` when adding a chain. - -Importantly, if you have a set of chains that represent the same thing, you can group them together -by giving the chains the same name. It is also good practise to set the same colour for these chains. - -""" -# sphinx_gallery_thumbnail_number = 2 - -import numpy as np -from scipy.stats import multivariate_normal - -from chainconsumer import ChainConsumer - -c = ChainConsumer() -for i in range(1000): - # Generate some data centered at a random location with uncertainty - # equal to the scatter - mean = [3, 8] - cov = [[1.0, 0.5], [0.5, 2.0]] - mean_scattered = multivariate_normal.rvs(mean=mean, cov=cov) - data = multivariate_normal.rvs(mean=mean_scattered, cov=cov, size=1000) - posterior = multivariate_normal.logpdf(data, mean=mean_scattered, cov=cov) - c.add_chain(data, posterior=posterior, parameters=["$x$", "$y$"], color="r", name="Simulation validation") -fig = c.plotter.plot() -fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# We can add multiple datasets, and even mix in plotting contours and points -# together. In this example, we generate two sets of data to plot two clusters -# of maximum posterior points. Additionally we show the contours of a -# 'representative' surface in amber. - -c = ChainConsumer() -p = ["$x$", "$y$", "$z$"] -for i in range(200): - # Generate some data centered at a random location with uncertainty - # equal to the scatter - mean = [3, 8, 4] - cov = [[1.0, 0.5, 0.5], [0.5, 2.0, 0.5], [0.5, 0.5, 1.4]] - mean_scattered = multivariate_normal.rvs(mean=mean, cov=cov) - data = multivariate_normal.rvs(mean=mean_scattered, cov=cov, size=5000) - data2 = data + multivariate_normal.rvs(mean=[8, -8, 7], cov=cov) - posterior = multivariate_normal.logpdf(data, mean=mean_scattered, cov=cov) - plot_contour = i == 0 - - c.add_chain(data, posterior=posterior, parameters=p, color="p", name="Sim1") - - c.add_chain( - data2, - posterior=posterior, - parameters=p, - color="k", - marker_style="+", - marker_size=20, - name="Sim2", - marker_alpha=0.5, - ) - -c.add_chain( - data + np.array([4, -4, 3]), - parameters=p, - posterior=posterior, - name="Contour Too", - plot_contour=True, - plot_point=True, - marker_style="*", - marker_size=40, - color="a", - shade=True, - shade_alpha=0.3, - kde=True, - linestyle="--", - bar_shade=True, -) - -c.configure_overrides(legend_artists=True) - -fig = c.plotter.plot() -fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# If you've loaded a whole host of chains in, but only want to focus on one -# set, you can also pick out all chains with the same name when plotting. - -fig = c.plotter.plot(chains="Sim1") -fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# Finally, we should clarify what exactly the points mean! If you don't specify -# anything, by defaults the points represent the coordinates of the -# maximum posterior value. However, in high dimensional surfaces, this maximum -# value across all dimensions can be different to the maximum posterior value -# of a 2D slice. If we want to plot, instead of the global maximum as defined -# by the posterior values, the maximum point of each 2D slice, we can specify -# to `configure` that `global_point=False`. - -c.configure_overrides(legend_artists=True, global_point=False) -fig = c.plotter.plot(chains="Sim1") -fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# Note here that the histograms have disappeared. This is because the maximal -# point changes for each pair of parameters, and so none of the points can -# be used in a histogram. Whilst one could use the maximum point, marginalising -# across all parameters, this can be misleading if only two parameters -# are requested to be plotted. As such, we do not report histograms for -# the maximal 2D posterior points. diff --git a/examples/Basics/plot_loading_data.py b/examples/Basics/plot_loading_data.py deleted file mode 100644 index e5abe3d1..00000000 --- a/examples/Basics/plot_loading_data.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -============ -Loading Data -============ - -Demonstrates the different ways of loading data! - -If you want examples of loading grid data, see the grid data example! - -""" - -############################################################################### -# You can specify truth values using a list (in the same order as the -# declared parameters). - -import os -import tempfile - -import numpy as np -import pandas as pd -from numpy.random import multivariate_normal - -from chainconsumer import ChainConsumer - -# Lets create some data here to set things up -np.random.seed(4) -truth = [0, 5] -data = multivariate_normal(truth, np.eye(2), size=100000) -parameters = ["$x$", "$y$"] -df = pd.DataFrame(data, columns=parameters) - -directory = tempfile._get_default_tempdir() -filename = next(tempfile._get_candidate_names()) -filename1 = directory + os.sep + filename + ".csv" -filename2 = directory + os.sep + filename + ".npy" -df.to_csv(filename1, index=False) -np.save(filename2, data) - -# Now the normal way of giving data is passing a # -*- coding: utf-8 -*-numpy array and parameter separately -c = ChainConsumer().add_chain(data, parameters=parameters) -fig = c.plotter.plot(truth=truth) -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# You don't actually need to have them as a 2D array, if you have each parameter independently, just list em up! - -x, y = data[:, 0], data[:, 1] -c = ChainConsumer().add_chain([x, y], parameters=parameters) -fig = c.plotter.plot(truth=truth) -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# Of course, a true master uses pandas everywhere -c = ChainConsumer().add_chain(df) -fig = c.plotter.plot(truth=truth) -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - - -############################################################################### -# And yet we can do the same thing using a dictionary: - -dictionary = {"$x$": data[:, 0], "$y$": data[:, 1]} -c = ChainConsumer().add_chain(dictionary) -fig = c.plotter.plot(truth=truth) -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# Or we can pass a filename in containing a text dump of the chain - -c = ChainConsumer().add_chain(filename1) -fig = c.plotter.plot(truth=truth) -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# Or we can pass a filename for a file containing a binary numpy array - -c = ChainConsumer().add_chain(filename2, parameters=parameters) -fig = c.plotter.plot(truth=truth) -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/Basics/plot_statistics.py b/examples/Basics/plot_statistics.py deleted file mode 100644 index 80bc24aa..00000000 --- a/examples/Basics/plot_statistics.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -========== -Statistics -========== - -Demonstrates the different statistics you can use with ChainConsumer. - - -""" - -############################################################################### -# By default, ChainConsumer uses maximum likelihood statistics. Thus you do not -# need to explicitly enable maximum likelihood statistics. If you want to -# anyway, the keyword is `"max"`. - -import numpy as np -from scipy.stats import skewnorm - -from chainconsumer import ChainConsumer - -# Lets create some data here to set things up -rng = np.random.default_rng(0) -data = skewnorm.rvs(5, size=(1000000, 2)) -parameters = ["$x$", "$y$"] - - -# Now the normal way of giving data is passing a numpy array and parameter separately -c = ChainConsumer().add_chain(data, parameters=parameters).configure(statistics="max") -fig = c.plotter.plot() -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# Or we can enable cumulative statistics - -c = ChainConsumer().add_chain(data, parameters=parameters).configure(statistics="cumulative") -fig = c.plotter.plot() -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# Or we can enable mean statistics - -c = ChainConsumer().add_chain(data, parameters=parameters).configure(statistics="mean") -fig = c.plotter.plot() -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# Or we can enable maximum symmetric statistics - -c = ChainConsumer().add_chain(data, parameters=parameters).configure(statistics="max_symmetric") -fig = c.plotter.plot() -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# Or we can enable maximum closest statistics - -c = ChainConsumer().add_chain(data, parameters=parameters).configure(statistics="max_shortest") -fig = c.plotter.plot() -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# Or we can enable maximum central statistics - -c = ChainConsumer().add_chain(data, parameters=parameters).configure(statistics="max_central") -fig = c.plotter.plot() -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# We can also take advantage of the ability to pass lists to ChainConsumer's -# configuration to have report different statistics for different chains. -# Please note, I don't recommend you do this in practise, it is just begging -# for confusion. - -c = ChainConsumer() -stats = list(c.analysis._summaries.keys()) -for stat in stats: - c.add_chain(data, parameters=parameters, name=stat.replace("_", " ").title()) -c.configure_overrides(statistics=stats, bar_shade=True) -fig = c.plotter.plot() -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/Basics/plot_truth_values.py b/examples/Basics/plot_truth_values.py deleted file mode 100644 index f9aa88e8..00000000 --- a/examples/Basics/plot_truth_values.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -============ -Truth Values -============ - -Plot truth values on top of your contours. - -""" - -############################################################################### -# You can specify truth values using a list (in the same order as the -# declared parameters). - -import numpy as np -from numpy.random import multivariate_normal, normal - -from chainconsumer import ChainConsumer - -np.random.seed(2) -cov = 0.2 * normal(size=(3, 3)) + np.identity(3) -truth = normal(size=3) -data = multivariate_normal(truth, np.dot(cov, cov.T), size=100000) - -c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", r"$\beta$"]) -fig = c.plotter.plot(truth=truth) -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# Or you can specify truth values using a dictionary. This allows you to specify -# truth values for only some parameters. You can also customise the look -# of your truth lines. - - -c.configure_truth(color="w", ls=":", alpha=0.8) -fig2 = c.plotter.plot(truth={"$x$": truth[0], "$y$": truth[1]}) -fig2.set_size_inches(0 + fig2.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/Basics/plot_two_disjoint_chains.py b/examples/Basics/plot_two_disjoint_chains.py deleted file mode 100644 index 971c6baa..00000000 --- a/examples/Basics/plot_two_disjoint_chains.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -=================== -Two Disjoint Chains -=================== - -You can plot multiple chains. They can even have different parameters! - - -""" -import numpy as np -from numpy.random import multivariate_normal, normal - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -cov = normal(size=(3, 3)) -cov2 = normal(size=(4, 4)) -data = multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) -data2 = multivariate_normal(normal(size=4), np.dot(cov2, cov2.T), size=100000) - -c = ChainConsumer() -c.add_chain(data, parameters=["$x$", "$y$", r"$\alpha$"]) -c.add_chain(data2, parameters=["$x$", "$y$", r"$\alpha$", r"$\gamma$"]) -fig = c.plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/README.txt b/examples/README.txt deleted file mode 100644 index 106114f3..00000000 --- a/examples/README.txt +++ /dev/null @@ -1,14 +0,0 @@ -.. _examples-index: - -======== -Examples -======== - -ChainConsumer can, at this point in time, do several distinct, useful tasks. -The primary one is to create the likelihood surfaces and marginalised -distributions that should be familiar. ChainConsumer can also plot -the chains as walks to check convergence and mixing. Additionally, ChainConsumer -can output LaTeX ready tables of parameter summaries that can be -copied and pasted directly into a TeX document, or similar LaTeX tables -with parameter correlation, covariance or inter-model comparisons using AIC, BIC and DIC. - diff --git a/examples/customisations/README.txt b/examples/customisations/README.txt deleted file mode 100644 index 9565248a..00000000 --- a/examples/customisations/README.txt +++ /dev/null @@ -1,8 +0,0 @@ -.. _customisations_examples: - -Customisations --------------- - -Here I have included some basic examples showing the different output you -can get with ChainConsumer. - diff --git a/examples/customisations/plot_as_prior.py b/examples/customisations/plot_as_prior.py deleted file mode 100644 index be54c537..00000000 --- a/examples/customisations/plot_as_prior.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -================ -Plotting a prior -================ - -If you have 1D priors that you don't want to appear in the contour, thats possible too. - -""" -import numpy as np -from numpy.random import normal, random - -from chainconsumer import ChainConsumer - -if __name__ == "__main__": - rng = np.random.default_rng(0) - cov = random(size=(2, 2)) + np.identity(2) - data = rng.multivariate_normal(normal(size=2), np.dot(cov, cov.T), size=100000) - - prior = normal(0, 1, size=100000) - - fig = ( - ChainConsumer() - .add_chain(data, parameters=["x", "y"], name="Normal") - .add_chain(prior, parameters=["y"], name="Prior", show_as_1d_prior=True) - .plotter.plot() - ) - - fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_axis.py b/examples/customisations/plot_axis.py deleted file mode 100644 index e658af82..00000000 --- a/examples/customisations/plot_axis.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -============= -External axes -============= - -To put contours in external figures. - -To help in inserting contours into other plots and figures, you can call the simplified -`plot_contour` method and pass in an axis. Note that this is a minimal routine, and will not -do auto-extents, labels, ticks, truth values, etc. But it will make contours for you. - -""" - -import matplotlib.pyplot as plt -from scipy.stats import multivariate_normal as mv - -from chainconsumer import ChainConsumer - -data = mv.rvs(mean=[5, 6], cov=[[1, 0.9], [0.9, 1]], size=10000) - -fig, axes = plt.subplots(nrows=2, figsize=(4, 6), sharex=True) -axes[0].scatter(data[:, 0], data[:, 1], s=1, alpha=0.1) - -c = ChainConsumer() -c.add_chain(data, parameters=["a", "b"]) -c.plotter.plot_contour(axes[1], "a", "b") - -for ax in axes: - ax.axvline(5) - ax.axhline(6) diff --git a/examples/customisations/plot_blinding.py b/examples/customisations/plot_blinding.py deleted file mode 100644 index 2ddcbe73..00000000 --- a/examples/customisations/plot_blinding.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -=================== -Blinding Parameters -=================== - -You can blind parameters and not show axis labels very easily! - -Just give ChainConsumer the `blind` parameter when plotting. You can specify `True` to blind all parameters, -or give it a string (or list of strings) detailing the specific parameters you want blinded! - -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) - -c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$"]) -c.configure(colors=["g"]) -fig = c.plotter.plot(blind="$y$") - -fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_chain_override.py b/examples/customisations/plot_chain_override.py deleted file mode 100644 index 83fb0e14..00000000 --- a/examples/customisations/plot_chain_override.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -==================== -Overriding Configure -==================== - -You can specify display options when adding chains. - -This is useful for when you are playing around with code, adding and removing chains -as you tweak the plot. Normally, this would involve modifying the lists passed into `configure` -if you wanted to keep a specific chain with a specific style. To make it easier, -you can specify chain properties when addng them via `add_chain`. If set, these values override -anything specified in configure (and # -*- coding: utf-8 -*- -thus override the default configure behaviour). - -""" -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data1 = rng.multivariate_normal([-2, 0], [[1, 0], [0, 1]], size=100000) -data2 = rng.multivariate_normal([4, -4], [[1, 0], [0, 1]], size=100000) -data3 = rng.multivariate_normal([-2, -4], [[1, 0.7], [0.7, 1]], size=100000) - -c = ChainConsumer() -c.add_chain(data1, parameters=["x", "y"], color="red", linestyle=":", name="Red dots") -c.add_chain(data2, parameters=["x", "y"], color="#4286f4", shade_alpha=1.0, name="Blue solid") -c.add_chain(data3, parameters=["x", "y"], color="lg", kde=1.5, linewidth=2.0, name="Green smoothed") - -fig = c.plotter.plot() -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_cloud_sigma.py b/examples/customisations/plot_cloud_sigma.py deleted file mode 100644 index 791c25aa..00000000 --- a/examples/customisations/plot_cloud_sigma.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -====================== -Cloud and Sigma Levels -====================== - -Choose custom sigma levels and display point cloud. - -In this example we display more sigma levels, turn on the point cloud, and -disable the parameter summaries on the top of the marginalised distributions. - -Note that because of the very highly correlated distribution we have, it is -useful to increase the number of bins the plots are generated with, to capture the -thinness of the correlation. -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(1) -cov = rng.normal(size=(3, 3)) -data = rng.multivariate_normal(rng.normal(size=3), np.dot(cov, cov.T), size=100000) - -c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", "$z$"]) -c.configure(summary=False, bins=1.4, cloud=True, sigmas=np.linspace(0, 2, 10)) -fig = c.plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_colorpoints.py b/examples/customisations/plot_colorpoints.py deleted file mode 100644 index 848b0178..00000000 --- a/examples/customisations/plot_colorpoints.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -============= -Colour Points -============= - -Add colour scatter to show an extra dimension. - -If we have a secondary parameter that might not be best displayed -as a posterior surface and would be useful to instead give -context to other surfaces, we can select that point to give a -colour mapped scatter plot. - -We can *also* display this as a posterior surface by setting -`plot_colour_params=True`, if we wanted. -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(1) -cov = rng.normal(size=(3, 3)) -data = rng.multivariate_normal(rng.normal(size=3), np.dot(cov, cov.T), size=100000) - -c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", "$z$"]) -c.configure(color_params="$z$") -fig = c.plotter.plot(figsize=1.0) - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# You can also plot the weights or posterior if they are specified. Showing weights here. - -weights = 1 / (1 + data[:, 0] ** 2 + data[:, 1] ** 2) -c = ChainConsumer().add_chain(data[:, :2], parameters=["$x$", "$y$"], weights=weights) -c.configure(color_params="weights") -fig = c.plotter.plot(figsize=3.0) - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# And showing the posterior color parameter here - -weights = 1 / (1 + data[:, 0] ** 2 + data[:, 1] ** 2) -posterior = np.log(weights) -c = ChainConsumer().add_chain(data[:, :2], parameters=["$x$", "$y$"], weights=weights, posterior=posterior) -c.configure(color_params="posterior") -fig = c.plotter.plot(figsize=3.0) - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_colours_shade.py b/examples/customisations/plot_colours_shade.py deleted file mode 100644 index 76fa51aa..00000000 --- a/examples/customisations/plot_colours_shade.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -=================== -Colours and Shading -=================== - -Choose custom colours and plot multiple chains with shading. - -Normally when plotting more than two chains, shading is removed so -you can clearly see the outlines. However, you can turn shading back -on and modify the shade opacity if you prefer colourful plots. - -Note that the contour shading and marginalised shading are separate -and are configured independently. - -Colours should be given as hex colours. -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(2) -cov = rng.normal(size=(2, 2)) + np.identity(2) -d1 = rng.multivariate_normal(rng.normal(size=2), np.dot(cov, cov.T), size=100000) -cov = rng.normal(size=(2, 2)) + np.identity(2) -d2 = rng.multivariate_normal(rng.normal(size=2), np.dot(cov, cov.T), size=100000) -cov = rng.normal(size=(2, 2)) + np.identity(2) -d3 = rng.multivariate_normal(rng.normal(size=2), np.dot(cov, cov.T), size=100000) - -c = ChainConsumer().add_chain(d1, parameters=["$x$", "$y$"]).add_chain(d2).add_chain(d3) -c.configure(colors=["#B32222", "#D1D10D", "#455A64"], shade=True, shade_alpha=0.2, bar_shade=True) -fig = c.plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_confidence_levels.py b/examples/customisations/plot_confidence_levels.py deleted file mode 100644 index 49a9fcb9..00000000 --- a/examples/customisations/plot_confidence_levels.py +++ /dev/null @@ -1,39 +0,0 @@ -r""" -================= -Confidence Levels -================= - -When setting the sigma levels for ChainConsumer, we need to be careful -if we are talking about 1D or 2D Gaussians. For 1D Gaussians, 1 and 2 :math:`\sigma` correspond -to 68% and 95% confidence levels. However, for a a 2D Gaussian, integrating over 1 and 2 :math:`\sigma` -levels gives 39% and 86% confidence levels. - -By default ChainConsumer uses the 2D levels, such that the contours will line up and agree with the -marginalised distributions shown above them, however you can also choose to switch to using the 1D -Gaussian method, such that the contour encloses 68% and 95% confidence regions, by switching `sigma2d` to `False` - -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) - -c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$"]) -c.configure(flip=False, sigma2d=False, sigmas=[1, 2]) # The default case, so you don't need to specify sigma2d -fig = c.plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# Demonstrating the 1D Gaussian confidence levels. Notice the change in contour size -# The contours shown below now show the 68% and 95% confidence regions. - -c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$"]) -c.configure(flip=False, sigma2d=True, sigmas=[1, 2]) -fig = c.plotter.plot() # -*- coding: utf-8 -*- - - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_contour_labels.py b/examples/customisations/plot_contour_labels.py deleted file mode 100644 index 4c46eb67..00000000 --- a/examples/customisations/plot_contour_labels.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -============== -Contour Labels -============== - -Plot contours using labels. - -You can set the contour_labels to display confidence levels, as shown below. - -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data = rng.multivariate_normal([0, 0], [[1, 0.5], [0.5, 1.0]], size=1000000) - - -c = ChainConsumer().add_chain(data).configure_overrides(contour_labels="confidence") -fig = c.plotter.plot() -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - - -############################################################################### -# Or you can plot in terms of sigma. Note that most people prefer stating -# the confidence levels, because of the ambiguity over sigma levels introduced -# by the `sigma2d` keyword. - -c = ChainConsumer().add_chain(data).configure_overrides(contour_labels="sigma") -fig = c.plotter.plot() -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_dont_flip.py b/examples/customisations/plot_dont_flip.py deleted file mode 100644 index 87a3c4f6..00000000 --- a/examples/customisations/plot_dont_flip.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -===================== -Flips, Ticks and Size -===================== - -You can stop the second parameter rotating in the plot if you prefer squares! - -Unlike the Introduction example, which shows the rotated plots, this example shows them -without the rotation. - -Also, you can pass in a tuple for the figure size. We also demonstrate adding more -ticks to the axis in this example. Also, I change the colour to red, just for fun. -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data = np.random.multivariate_normal([1.5, 4.0], [[1.0, 0.7], [0.7, 1.5]], size=1000000) -data[:, 0] = np.abs(data[:, 0]) - -c = ChainConsumer().add_chain(data, parameters=["$x_1$", "$x_2$"]) -c.configure(flip=False, max_ticks=10, colors="#D32F2F") -fig = c.plotter.plot(figsize=(6, 6)) - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_fewer_parameters.py b/examples/customisations/plot_fewer_parameters.py deleted file mode 100644 index eff3215d..00000000 --- a/examples/customisations/plot_fewer_parameters.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -==================== -Truncated Parameters -==================== - -If you have a large model, you don't need to plot all parameters at once. - -Here we only plot the first four parameters. You could also simply pass the number four, -which means the *first* four parameters. - -For fun, we also plot everything in green. Note you don't need to give multiple colours, -the shading is all computed from the colour hex code. -""" - -import numpy as np -from numpy.random import normal, random - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -cov = random(size=(6, 6)) -data = rng.multivariate_normal(normal(size=6), np.dot(cov, cov.T), size=200000) -parameters = ["$x$", "$y$", "$z$", "$a$", "$b$", "$c$"] -c = ChainConsumer().add_chain(data, parameters=parameters).configure(colors="#388E3C") -fig = c.plotter.plot(parameters=parameters[:4], figsize="page") - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_font_changes.py b/examples/customisations/plot_font_changes.py deleted file mode 100644 index 43fa3a67..00000000 --- a/examples/customisations/plot_font_changes.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -=================== -Change Font Options -=================== - -Control tick rotation and font sizes. - -Here the tick rotation has been turned off, ticks made smaller, -more ticks added, and label size increased! -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data = rng.multivariate_normal([0, 1, 2], np.eye(3) + 0.2, size=100000) - -# If you pass in parameter labels and only one chain, you can also get parameter bounds -c = ChainConsumer() -c.add_chain(data, parameters=["$x$", "$y^2$", r"$\Omega_\beta$"], name="Example") -c.configure_overrides(diagonal_tick_labels=False, tick_font_size=8, label_font_size=25, max_ticks=8) -fig = c.plotter.plot(figsize="column", show_legend=True) - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_kde_extents.py b/examples/customisations/plot_kde_extents.py deleted file mode 100644 index d7fa8bb9..00000000 --- a/examples/customisations/plot_kde_extents.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -======================== -Gaussian KDE and Extents -======================== - -Smooth marginalised distributions with a Gaussian KDE, and pick custom extents. - - -Note that invoking the KDE on large data sets will significantly increase rendering time when -you have a large number of points. You can also pass a float to your KDE to modify the width -of the bandpass by that factor! - -You can see the increase in contour smoothness (without broadening) for when you have a -low number of samples in your chains! -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data = np.random.multivariate_normal([0.0, 4.0], [[1.0, -0.7], [-0.7, 1.5]], size=3000) - -c = ChainConsumer() -c.add_chain(data, name="KDE on") -c.add_chain(data + 1, name="KDE off") -c.add_chain(data + 2, name="KDE x2!") -c.configure_overrides(kde=[True, False, 2.0], shade_alpha=0.1, flip=False) -fig = c.plotter.plot(extents=[(-2, 4), (0, 9)]) - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_legend_options.py b/examples/customisations/plot_legend_options.py deleted file mode 100644 index 216b7262..00000000 --- a/examples/customisations/plot_legend_options.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -============== -Legend Options -============== - -Legends are hard. - -Because of that, you can pass any keywords to the legend call you want via `legend_kwargs`. -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data1 = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) -data2 = data1 + 2 - -c = ChainConsumer() -c.add_chain(data1, parameters=["$x$", "$y$"], name="Chain 1") -c.add_chain(data2, parameters=["$x$", "$y$"], name="Chain 2") -c.configure_overrides(colors=["lb", "g"]) -fig = c.plotter.plot() -fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# If the linestyles are different and the colours are the same, the artists -# will reappear. - -c = ChainConsumer() -c.add_chain(data1, parameters=["$x$", "$y$"], name="Chain 1") -c.add_chain(data2, parameters=["$x$", "$y$"], name="Chain 2") -c.configure_overrides(colors=["lb", "lb"], linestyles=["-", "--"]) -fig = c.plotter.plot() -fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# You might also want to relocate the legend to another subplot if your -# contours don't have enough space for the legend! - -c = ChainConsumer() -c.add_chain(data1, parameters=["$x$", "$y$"], name="Chain 1") -c.add_chain(data2, parameters=["$x$", "$y$"], name="Chain 2") -c.configure_overrides( - linestyles=["-", "--"], - sigmas=[0, 1, 2, 3], - legend_kwargs={"loc": "upper left", "fontsize": 10}, - legend_color_text=False, - legend_location=(0, 0), -) -fig = c.plotter.plot(figsize=1.5) -fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_linestyles.py b/examples/customisations/plot_linestyles.py deleted file mode 100644 index 2909058a..00000000 --- a/examples/customisations/plot_linestyles.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -=================== -Changing Linestyles -=================== - -Customise the plot line styles. - -In this example we customise the line styles used, and make use of -the ability to pass lists of parameters to the configuration methods. - -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(1) -cov = rng.normal(size=(3, 3)) -data = rng.multivariate_normal(rng.normal(size=3), np.dot(cov, cov.T), size=100000) -data2 = data * 1.1 + 0.5 - -c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", "$z$"]).add_chain(data2) -c.configure(linestyles=["-", "--"], linewidths=[1.0, 2.0], shade=[True, False], shade_alpha=[0.2, 0.0]) -fig = c.plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_lists.py b/examples/customisations/plot_lists.py deleted file mode 100644 index d4347740..00000000 --- a/examples/customisations/plot_lists.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -================== -Using List Options -================== - -Utilise all the list options in the configuration! - -This is a general example to illustrate that most parameters -that you can pass to the configuration methods accept lists. - -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(2) -cov = rng.normal(size=(2, 2)) + np.identity(2) -d1 = rng.multivariate_normal(rng.normal(size=2), np.dot(cov, cov.T), size=100000) -cov = rng.normal(size=(2, 2)) + np.identity(2) -d2 = rng.multivariate_normal(rng.normal(size=2), np.dot(cov, cov.T), size=100000) -cov = rng.normal(size=(2, 2)) + np.identity(2) -d3 = rng.multivariate_normal(rng.normal(size=2), np.dot(cov, cov.T), size=1000000) - -c = ChainConsumer() -c.add_chain(d1, parameters=["$x$", "$y$"]) -c.add_chain(d2) -c.add_chain(d3) - -c.configure_overrides( - linestyles=["-", "--", "-"], - linewidths=[1.0, 3.0, 1.0], - bins=[3.0, 1.0, 1.0], - colors=["#1E88E5", "#D32F2F", "#111111"], - smooth=[0, 1, 2], - shade=[True, True, False], - shade_alpha=[0.2, 0.1, 0.0], - bar_shade=[True, False, False], -) -fig = c.plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - -############################################################################### -# List options are useful for when the properties of the chains are -# interconnected. But if you know the properties at the start, -# you can define them when adding the chains. List options will not -# override explicit chain configurations, so you can use the global -# configure to set options for all chains you haven't explicitly specified. -# -# Note here how even though we set 'all' chains to dotted lines of width 2, our third -# chain, with its explicit options, ignores that. - -c = ChainConsumer() -c.add_chain(d1, parameters=["$x$", "$y$"]).add_chain(d2).add_chain(d3, linestyle="-", linewidth=5) - -c.configure_overrides(linestyles=":", linewidths=2) -fig = c.plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_logscale.py b/examples/customisations/plot_logscale.py deleted file mode 100644 index 9bcebdf2..00000000 --- a/examples/customisations/plot_logscale.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -======== -Logscale -======== - -For when linear is just not good enough. - -You can set the `log_scale` property with a boolean, or specify a list of bools, or a list of parameter indexes, -or a list of parameter names, or a dictionary from parameter names to boolean values. Most things work, just give -it a crack. - -""" - -from scipy.stats import lognorm - -from chainconsumer import ChainConsumer - -data = lognorm.rvs(0.95, loc=0, size=(100000, 2)) - -c = ChainConsumer() -c.add_chain(data, parameters=["$x_1$", "$x_2$"]) - -fig = c.plotter.plot(figsize="column", log_scales=True) -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - - -############################################################################### -# It's not just for the main corner plot, you can do it anywhere. - -fig = c.plotter.plot_walks(log_scales={"$x_1$": False}) # Dict example -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. - - -fig = c.plotter.plot_distributions(log_scales=[True, False]) # list[bool] example -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_no_histograms.py b/examples/customisations/plot_no_histograms.py deleted file mode 100644 index 14ea2601..00000000 --- a/examples/customisations/plot_no_histograms.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -============= -No Histograms -============= - -Sometimes marginalised histograms are not needed. - -""" - - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -cov = rng.normal(size=(3, 3)) -data = rng.multivariate_normal(rng.normal(size=3), np.dot(cov, cov.T), size=100000) - -c = ChainConsumer().add_chain(data) -c.configure_overrides(plot_hists=False) -fig = c.plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_no_smooth.py b/examples/customisations/plot_no_smooth.py deleted file mode 100644 index e31565c2..00000000 --- a/examples/customisations/plot_no_smooth.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -============ -No Smoothing -============ - -We can turn off the default gaussian filter on marginalised distributions. - -This can be done by setting ``smooth`` to either ``0``, ``None`` or ``False``. -Note that the parameter summaries also have smoothing turned off, and -thus summaries may change. - -Fun colour change! And thicker lines! - -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -data = np.random.multivariate_normal([0.0, 4.0], [[1.0, 0.7], [0.7, 1.5]], size=100000) - -c = ChainConsumer() -c.add_chain(data, parameters=["$x_1$", "$x_2$"]) -c.configure_overrides(smooth=0, linewidths=2, colors="#673AB7") -fig = c.plotter.plot(figsize="column", truth=[0.0, 4.0]) - -# If we wanted to save to file, we would instead have written -# fig = c.plotter.plot(filename="location", figsize="column", truth=[0.0, 4.0]) - -# If we wanted to display the plot interactively... -# fig = c.plotter.plot(display=True, figsize="column", truth=[0.0, 4.0]) - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_one_chain.py b/examples/customisations/plot_one_chain.py deleted file mode 100644 index 8d1c64d9..00000000 --- a/examples/customisations/plot_one_chain.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -========= -One Chain -========= - -Plot one chain with parameter names. - -Because we are only plotting one chain, we will get -parameter bounds on the marginalised surfaces by -default. -""" - -import numpy as np -from numpy.random import normal - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -cov = 1e2 * normal(size=(3, 3)) -data = rng.multivariate_normal(1e3 * normal(size=3), np.dot(cov, cov.T), size=100000) - -# If you pass in parameter labels and only one chain, you can also get parameter bounds -fig = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", r"$\epsilon$"]).plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_preliminary.py b/examples/customisations/plot_preliminary.py deleted file mode 100644 index bf7355cb..00000000 --- a/examples/customisations/plot_preliminary.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -================== -Watermarking Plots -================== - -Make it obvious that those results are preliminary! - - -It's easy to do, just supply a string to the `watermark` option when plotting your contours, -and remember that when using TeX `matplotlib` settings like `weight` don't do anything - -if you want bold text make it TeX bold. - -The code for this is based off the preliminize github repo at -https://github.com/cpadavis/preliminize, which will add watermark to arbitrary -figures! - -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data1 = rng.multivariate_normal([3, 5], [[1, 0], [0, 1]], size=1000000) -data2 = rng.multivariate_normal([5, 3], [[1, 0], [0, 1]], size=10000) - - -c = ChainConsumer() -c.add_chain(data1, parameters=["$x$", "$y$"], name="Good results") -c.add_chain(data2, name="Unfinished results") -fig = c.plotter.plot(watermark=r"\textbf{Preliminary}", figsize=2.0) - - -############################################################################### -# You can also control the text options sent to the matplotlib text call. - -c = ChainConsumer() -c.add_chain(data1, parameters=["$x$", "$y$"], name="Good results") -c.add_chain(data2, name="Unfinished results") -kwargs = {"color": "purple", "alpha": 1.0, "family": "sanserif", "usetex": False, "weight": "bold"} -c.configure_overrides(watermark_text_kwargs=kwargs, flip=True) -fig = c.plotter.plot(watermark="SECRET RESULTS", figsize=2.0) diff --git a/examples/customisations/plot_rainbow_serif_bins.py b/examples/customisations/plot_rainbow_serif_bins.py deleted file mode 100644 index cb0e7f79..00000000 --- a/examples/customisations/plot_rainbow_serif_bins.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -======================= -Cmap and Custom Bins -======================= - -Invoke the cmap colour scheme and choose how many bins to use with your data. - -By default, the cmap colour scheme is used if you have many, many chains. You can -enable it before that point if you wish and pass in the cmap you want to use. - -You can also pick how many bins you want to display your data with. - -You can see that in this example, we pick too many bins and would not get good -summaries. If you simply want more (or less) bins than the default estimate, -if you input a float instead of an integer, the number of bins will simply scale -by that amount. For example, if the estimated picks 20 bins, and you set ``bins=1.5`` -your plots and summaries would be calculated with 30 bins. - -""" -import numpy as np -from numpy.random import normal, random - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -cov = 0.3 * random(size=(3, 3)) + np.identity(3) -data = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) -cov = 0.3 * random(size=(3, 3)) + np.identity(3) -data2 = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) -cov = 0.3 * random(size=(3, 3)) + np.identity(3) -data3 = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) -cov = 0.3 * random(size=(3, 3)) + np.identity(3) -data4 = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) - -c = ChainConsumer() -c.add_chain(data, name="A") -c.add_chain(data2, name="B") -c.add_chain(data3, name="C") -c.add_chain(data4, name="D") -c.configure_overrides(bins=50, cmap="plasma") -fig = c.plotter.plot(figsize=0.75) # Also making the figure 75% of its original size, for fun - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_selected_chains.py b/examples/customisations/plot_selected_chains.py deleted file mode 100644 index 55b4cda1..00000000 --- a/examples/customisations/plot_selected_chains.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -================ -Excluding Chains -================ - -You don't have to plot everything at once! - - -For the main plotting methods you can specify which chains you want to plot. You can -do this using either the chain index or using the chain names. Like so: - -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data1 = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) -data2 = rng.multivariate_normal([2, 0], [[1, 0], [0, 1]], size=1000000) -data3 = rng.multivariate_normal([4, 0], [[1, 0], [0, 1]], size=1000000) - -c = ChainConsumer() -c.add_chain(data1, parameters=["$x$", "$y$"], name="Chain A") -c.add_chain(data2, name="Chain B") -c.add_chain(data3, name="Chain C") -fig = c.plotter.plot(chains=["Chain A", "Chain C"]) - -fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_shade_gradient.py b/examples/customisations/plot_shade_gradient.py deleted file mode 100644 index 96dbeb5b..00000000 --- a/examples/customisations/plot_shade_gradient.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -============== -Shade Gradient -============== - -Control contour contrast! - -To help make your confidence levels more obvious, you can play with the gradient steepness and -resulting contrast in your contours. -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data1 = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=1000000) -data2 = rng.multivariate_normal([4, -4], [[1, 0], [0, 1]], size=1000000) - -c = ChainConsumer() -c.add_chain(data1, parameters=["$x$", "$y$"]) -c.add_chain(data2, parameters=["$x$", "$y$"]) -c.configure_overrides(shade_gradient=[0.1, 3.0], colors=["o", "k"], sigmas=[0, 1, 2, 3], shade_alpha=1.0) -fig = c.plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_shift.py b/examples/customisations/plot_shift.py deleted file mode 100644 index d340f809..00000000 --- a/examples/customisations/plot_shift.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -============== -Shifting Plots -============== - -Shift all your plots to the same location for blind uncertainty comparison. - - -Plots will shift to the location you tell them to, in the same format as a truth dictionary. -So you can use truth dict for both! Takes a list or a dict as input for convenience. - -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data1 = rng.multivariate_normal([1, 0], [[3, 2], [2, 3]], size=300000) -data2 = rng.multivariate_normal([0, 0.5], [[1, -0.7], [-0.7, 1]], size=300000) -data3 = rng.multivariate_normal([2, -1], [[0.5, 0], [0, 0.5]], size=300000) - -############################################################################### -# And this is how easy it is to shift them. Note the different means for each dataset! - -truth = {"$x$": 1, "$y$": 0} -c = ChainConsumer() -c.add_chain(data1, parameters=["$x$", "$y$"], name="Chain A", shift_params=truth) -c.add_chain(data2, name="Chain B", shift_params=truth) -c.add_chain(data3, name="Chain C", shift_params=truth) -fig = c.plotter.plot(truth=truth) - -fig.set_size_inches(2.5 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_spacing.py b/examples/customisations/plot_spacing.py deleted file mode 100644 index 189ec812..00000000 --- a/examples/customisations/plot_spacing.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -=============== -Subplot Spacing -=============== - -By default ChainConsumer will reduce subplot whitespace when you hit -a certain dimensionality, but you can also customise this yourself. -""" - -import numpy as np -from numpy.random import normal, random - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -cov = random(size=(3, 3)) -data = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=200000) - -c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", "$z$"]) -c.configure(spacing=0.0) -fig = c.plotter.plot(figsize="column") - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_three_chains.py b/examples/customisations/plot_three_chains.py deleted file mode 100644 index af0a170e..00000000 --- a/examples/customisations/plot_three_chains.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -============ -Three Chains -============ - -Plot three chains together. Name the chains to get a legend. - - -""" -import numpy as np -from numpy.random import normal, random - -from chainconsumer import ChainConsumer - -if __name__ == "__main__": - rng = np.random.default_rng(0) - cov = random(size=(3, 3)) + np.identity(3) - data = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) - cov = random(size=(3, 3)) + np.identity(3) - data2 = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) - cov = random(size=(3, 3)) + np.identity(3) - data3 = rng.multivariate_normal(normal(size=3), np.dot(cov, cov.T), size=100000) - - # If the parameters are the same between chains, you can just pass it the - # first time, and they will become the default parameters. - fig = ( - ChainConsumer() - .add_chain(data, parameters=["$x$", "$y$", r"$\epsilon$"], name="Test chain") - .add_chain(data2, name="Chain2") - .add_chain(data3, name="Chain3") - .plotter.plot() - ) - - fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/customisations/plot_zorder.py b/examples/customisations/plot_zorder.py deleted file mode 100644 index 491d8630..00000000 --- a/examples/customisations/plot_zorder.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -================ -Changing Z-Order -================ - -Force matplotlib to show the plots we want. - -Here is a bad plot because it's hiding what we want. - - -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data1 = rng.multivariate_normal([3, 5], [[1, 0], [0, 1]], size=100000) -data2 = rng.multivariate_normal([3, 5], [[0.2, 0.1], [0.1, 0.3]], size=100000) - - -c = ChainConsumer() -c.add_chain(data1, parameters=["$x$", "$y$"], color="k", shade_alpha=0.7, zorder=1) -c.add_chain(data2, color="o", shade_alpha=0.7, zorder=2) -c.configure_overrides(spacing=0) -c.plotter.plot(display=True, figsize=2.0) - -############################################################################### -# Reversing the zorder - -c = ChainConsumer() -c.add_chain(data1, parameters=["$x$", "$y$"], color="k", shade_alpha=0.7, zorder=2) -c.add_chain(data2, color="o", shade_alpha=0.7, zorder=1) -c.configure_overrides(spacing=0) -c.plotter.plot(display=True, figsize=2.0) diff --git a/examples/more/README.txt b/examples/more/README.txt deleted file mode 100644 index 4fd19851..00000000 --- a/examples/more/README.txt +++ /dev/null @@ -1,7 +0,0 @@ -.. _more_examples: - -More Complicated Examples -------------------------- - -To illustrate some of the interesting ways you can use the options in ChainConsumer, see below. - diff --git a/examples/more/plot_colorpoints2.py b/examples/more/plot_colorpoints2.py deleted file mode 100644 index dc8cd1df..00000000 --- a/examples/more/plot_colorpoints2.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -======================== -Multiple Colour Scatter! -======================== - -Why show only one colour, when you can display more! - -In the basic colour example, we showed one parameter being used -to give colour information. However, you can pick a different colour, or no colour (`None`), -for each chain. - -You can also pick the same parameter in multiple chains, and all the scatter points will be put -on the same colour scale. The underlying contours will still be distinguishable automatically -by adding alternative linestyles, as shown below. -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(1) -cov = rng.normal(size=(4, 4)) -data = rng.multivariate_normal(rng.normal(size=4), np.dot(cov, cov.T), size=100000) -cov = 1 + 0.5 * rng.normal(size=(4, 4)) -data2 = rng.multivariate_normal(4 + rng.normal(size=4), np.dot(cov, cov.T), size=100000) - -c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$", "$z$", "$g$"], name="a") -c.add_chain(data2, parameters=["$x$", "$y$", "$z$", "$t$"], name="b") -c.configure(color_params=["$g$", "$t$"]) -fig = c.plotter.plot(figsize=1.75) - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/more/plot_divide_chain.py b/examples/more/plot_divide_chain.py deleted file mode 100644 index 5c45a6e8..00000000 --- a/examples/more/plot_divide_chain.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -================ -Dividing a chain -================ - -ChainConsumer can split one chain into many! - -If you use a sampling algorithm with multiple walkers (which -is fairly common), it can be useful to plot each walker as a separate chain -so that you can verify that your walkers have all converged to the same place. - -You can use the `plot_walks` method for this, or the convergence diagnostics, -but the more options the better! - -In the below example, I assume the generated data was created using ten walkers. -I introduce some fake shift in the data to badly emulate walker drift. - -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data = rng.multivariate_normal([0.0, 4.0], [[1.0, 0.7], [0.7, 1.5]], size=1000000) -data[:, 0] += np.linspace(0, 1, data.shape[0]) - -c = ChainConsumer().add_chain(data, parameters=["$x$", "$y$"], walkers=5) -c2 = c.divide_chain() -fig = c2.plotter.plot() - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/more/plot_many.py b/examples/more/plot_many.py deleted file mode 100644 index 1edf2c68..00000000 --- a/examples/more/plot_many.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -========================= -Plot Many Things For Fun! -========================= - -Lets try a few things to give what might be a usable plot to through into our latest paper. - -Or something. - -First lets mock some highly correlated data with colour scatter. And then throw a few more -data sets in to get some overlap. -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(1) -n = 1000000 -data = rng.multivariate_normal([0.4, 1], [[0.01, -0.003], [-0.003, 0.001]], size=n) -data = np.hstack((data, (67 + 10 * data[:, 0] - data[:, 1] ** 2)[:, None])) -data2 = np.vstack((rng.uniform(-0.1, 1.1, n), rng.normal(1.2, 0.1, n))).T -data2[:, 1] -= data2[:, 0] ** 2 -data3 = rng.multivariate_normal([0.3, 0.7], [[0.02, 0.05], [0.05, 0.1]], size=n) - -c = ChainConsumer() -c.add_chain(data2, parameters=[r"$\Omega_m$", "$-w$"], name="B") -c.add_chain(data3, name="S") -c.add_chain(data, parameters=[r"$\Omega_m$", "$-w$", "$H_0$"], name="P") - -c.configure_overrides( - color_params="$H_0$", shade=[True, True, False], shade_alpha=0.2, bar_shade=True, linestyles=["-", "--", "-"] -) -fig = c.plotter.plot(figsize=2.0, extents=[[0, 1], [0, 1.5]]) - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/plot_correlations.py b/examples/plot_correlations.py deleted file mode 100644 index d80a47b2..00000000 --- a/examples/plot_correlations.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -=========================== -Plot Parameter Correlations -=========================== - -You can also get LaTeX tables for parameter correlations. - -Turned into glorious LaTeX, we would get something like the following: - -.. figure:: ../../examples/resources/correlations.png - :align: center - :width: 60% - -""" - -############################################################################### -# The code to produce this, and the raw LaTeX, is given below: - - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -cov = [[1, 0.5, 0.2], [0.5, 1, 0.3], [0.2, 0.3, 1.0]] -data = rng.multivariate_normal([0, 0, 1], cov, size=100000) -parameters = ["x", "y", "z"] -c = ChainConsumer() -c.add_chain(data, parameters=parameters) -latex_table = c.analysis.get_correlation_table() - -print(latex_table) diff --git a/examples/plot_covariance.py b/examples/plot_covariance.py deleted file mode 100644 index 8c05e179..00000000 --- a/examples/plot_covariance.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -========================= -Plot Parameter Covariance -========================= - -You can also get LaTeX tables for parameter covariance. - -Turned into glorious LaTeX, we would get something like the following: - -.. figure:: ../../examples/resources/covariance.png - :align: center - :width: 60% - -""" - -############################################################################### -# The code to produce this, and the raw LaTeX, is given below: - - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -cov = [[1.0, 0.5, 0.2], [0.5, 2.0, 0.3], [0.2, 0.3, 3.0]] -data = rng.multivariate_normal([0, 0, 1], cov, size=1000000) -parameters = ["x", "y", "z"] -c = ChainConsumer() -c.add_chain(data, parameters=parameters) -latex_table = c.analysis.get_covariance_table() -print(latex_table) diff --git a/examples/plot_distributions.py b/examples/plot_distributions.py deleted file mode 100644 index f26fd456..00000000 --- a/examples/plot_distributions.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -================== -Plot Distributions -================== - -If you want a fast check of your distributions for high dimensional spaces (such -that you can only generate a surfaces for a subset of parameters), you can -simply plot all of the marginalised distributions using this method. - - -""" - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -means, cov = np.arange(8), rng.random(size=(8, 8)) -data = rng.multivariate_normal(means, np.dot(cov, cov.T), size=1000000) - -params = ["$x$", "$y$", "$z$", "a", "b", "c", "d", "e"] -c = ChainConsumer().add_chain(data, parameters=params) - -fig = c.plotter.plot_distributions(truth=means) - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/plot_introduction.py b/examples/plot_introduction.py deleted file mode 100644 index 2eaf801d..00000000 --- a/examples/plot_introduction.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -============= -Plot Contours -============= - -A trivial example using data from a multivariate normal. - -We give truth values, parameter labels and set the figure size to -fit one column of a two column document. - -It is important to note that (in future examples) we always call the -configuration methods *after* loading in all the data. - -Note that the parameter summaries are all calculated from the chosen bin size and take -into account if the data is being smoothed or not. It is thus important to consider -whether you want smoothing enabled or (depending on your surfaces) more or less -bins than automatically estimated. See the extended customisation examples for -more information. - -""" - -import numpy as np -import pandas as pd - -from chainconsumer import Chain, ChainConsumer - -# Here's what you might start with -rng = np.random.default_rng(0) -data = rng.multivariate_normal([0.0, 4.0], [[1.0, 0.7], [0.7, 1.5]], size=1000000) -df = pd.DataFrame(data, columns=["x_1", "x_2"]) - -# And how we give this to chainconsumer -c = ChainConsumer() -c.add_chain(Chain(samples=df, name="An Example Contour")) -fig = c.plotter.plot() - -print("whoa") diff --git a/examples/plot_model_selection.py b/examples/plot_model_selection.py deleted file mode 100644 index 4793fa31..00000000 --- a/examples/plot_model_selection.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -===================== -Plot Model Comparison -===================== - -You can also get LaTeX tables for model comparison. - -Turned into glorious LaTeX, we would get something like the following: - -.. figure:: ../../examples/resources/table_comparison.png - :align: center - :width: 60% - -""" - -############################################################################### -# The code to produce this, and the raw LaTeX, is given below: - - -from scipy.stats import norm - -from chainconsumer import ChainConsumer - -n = 10000 -d1 = norm.rvs(size=n) -p1 = norm.logpdf(d1) -p2 = norm.logpdf(d1, scale=1.1) - -c = ChainConsumer() -c.add_chain(d1, posterior=p1, name="Model A", num_eff_data_points=n, num_free_params=4) -c.add_chain(d1, posterior=p2, name="Model B", num_eff_data_points=n, num_free_params=5) -c.add_chain(d1, posterior=p2, name="Model C", num_eff_data_points=n, num_free_params=4) -c.add_chain(d1, posterior=p1, name="Model D", num_eff_data_points=n, num_free_params=14) -table = c.comparison.comparison_table(caption="Model comparisons!") -print(table) diff --git a/examples/plot_summary.py b/examples/plot_summary.py deleted file mode 100644 index 43f92e79..00000000 --- a/examples/plot_summary.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -============ -Plot Summary -============ - -Have a bunch of models and want to compare summaries, but in a plot instead of LaTeX? Can do! - - -""" - -############################################################################### -# Lets add a bunch of chains represnting all these different models of ours. - -import numpy as np - -from chainconsumer import ChainConsumer - - -def get_instance(): - rng = np.random.default_rng(0) - c = ChainConsumer() - parameters = ["$x$", r"$\Omega_\epsilon$", "$r^2(x_0)$"] - for name in ["Ref. model", "Test A", "Test B", "Test C"]: - # Add some random data - mean = rng.normal(loc=0, scale=3, size=3) - sigma = rng.uniform(low=1, high=3, size=3) - data = rng.multivariate_normal(mean=mean, cov=np.diag(sigma**2), size=100000) - c.add_chain(data, parameters=parameters, name=name) - return c - - -############################################################################### -# If we want the full shape of the distributions, well, thats the default -# behaviour! -c = get_instance() -c.configure_overrides(bar_shade=True) -c.plotter.plot_summary() - -############################################################################### -# But lets make some changes. Say we don't like the colourful text. And we -# want errorbars, not distributions. And some fun truth values. - -c = get_instance() -c.configure_overrides(legend_color_text=False) -c.configure_truth(ls=":", color="#FB8C00") -c.plotter.plot_summary(errorbar=True, truth=[[0], [-1, 1], [-2, 0, 2]]) - -############################################################################### -# Even better, lets use our reference model as the truth value and not plot -# it with the others - -c = get_instance() -c.configure_overrides(legend_color_text=False) -c.configure_truth(ls="-", color="#555555") -c.plotter.plot_summary(errorbar=True, truth="Ref. model", include_truth_chain=False, extra_parameter_spacing=1.5) diff --git a/examples/plot_table.py b/examples/plot_table.py deleted file mode 100644 index 16ab668a..00000000 --- a/examples/plot_table.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -=========== -Plot Tables -=========== - -You can also get LaTeX tables for parameter summaries. - -Turned into glorious LaTeX, we would get something like the following: - -.. figure:: ../../examples/resources/table.png - :align: center - -""" - -############################################################################### -# The code to produce this, and the raw LaTeX, is given below: - - -import numpy as np - -from chainconsumer import ChainConsumer - -ndim, nsamples = 4, 200000 -rng = np.random.default_rng(0) - -data = rng.random((nsamples, ndim)) -data[:, 2] += data[:, 1] * data[:, 2] -data[:, 1] = data[:, 1] * 3 + 5 -data[:, 3] /= np.abs(data[:, 1]) + 1 - -data2 = np.random.randn(nsamples, ndim) -data2[:, 0] -= 1 -data2[:, 2] += data2[:, 1] ** 2 -data2[:, 1] = data2[:, 1] * 2 - 5 -data2[:, 3] = data2[:, 3] * 1.5 + 2 - -# If you pass in parameter labels and only one chain, you can also get parameter bounds -c = ChainConsumer() -c.add_chain(data, parameters=["$x$", "$y$", r"$\alpha$", r"$\beta$"], name="Model A") -c.add_chain(data2, parameters=["$x$", "$y$", r"$\alpha$", r"$\gamma$"], name="Model B") -table = c.analysis.get_latex_table(caption="Results for the tested models", label="tab:example") -print(table) diff --git a/examples/plot_walk.py b/examples/plot_walk.py deleted file mode 100644 index df9603c9..00000000 --- a/examples/plot_walk.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -========== -Plot Walks -========== - -You can also plot the walks that your chains have undertaken. - -This is a very helpful plot to create when determining if your chains have -converged and mixed. Below is an example walk from a Metropolis-Hastings run, -where we have set the optional parameters for the weights and posteriors, -giving the top two subplots. - -.. figure:: ../../examples/resources/exampleWalk.png - :align: center - -""" - - -############################################################################### -# To generate your own walk, with a 100 point smoothed walk overplotting, -# you can use the following code: - -import numpy as np - -from chainconsumer import ChainConsumer - -rng = np.random.default_rng(0) -data1 = rng.random((100000, 2)) -data2 = rng.random((100000, 2)) - 2 -data1[:, 1] += 1 - -c = ChainConsumer() -c.add_chain(data1, parameters=["$x$", "$y$"]) -c.add_chain(data2, parameters=["$x$", "$z$"]) -fig = c.plotter.plot_walks(truth={"$x$": -1, "$y$": 1, "$z$": -2}, convolve=100) - -fig.set_size_inches(3 + fig.get_size_inches()) # Resize fig for doco. You don't need this. diff --git a/examples/resources/correlations.png b/examples/resources/correlations.png deleted file mode 100644 index 5bf1ea837da4263f3c0ba398731ccba18e47eb17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50767 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 diff --git a/examples/resources/covariance.png b/examples/resources/covariance.png deleted file mode 100644 index b015dac5122d4e6f3181c3554e6713211542cff2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52243 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 diff --git a/examples/resources/exampleWalk.png b/examples/resources/exampleWalk.png deleted file mode 100644 index 8722fbf4937b31aeaee7b85dfa0ee7af7ca3bd95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 461274 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%0Acio>R@m;{%(j1Yk@^Z# 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 Q{JR3u)ilzmfZu)kU!M)OivR!s diff --git a/examples/resources/table.png b/examples/resources/table.png deleted file mode 100644 index d62124d503e114ebc184c250f49b1f496ae6069b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46600 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 diff --git a/examples/resources/table_comparison.png b/examples/resources/table_comparison.png deleted file mode 100644 index 0098a18ede881b49d98d13be39620127cb731e64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49709 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

Q)et#YT*Y$RNx#YzbZ$S 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 MsEkOJkY2$51B|4!@Bjb+ diff --git a/mkdocs.yml b/mkdocs.yml index 835e60de..af7a889a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,6 +44,7 @@ plugins: examples_dirs: docs/examples # path to your example scripts gallery_dirs: docs/generated/gallery # where to save generated gallery image_srcset: ['2x'] + within_subsection_order: FileNameSortKey search: {} markdown_extensions: diff --git a/src/chainconsumer/__init__.py b/src/chainconsumer/__init__.py index 63d6859a..a7f7b113 100644 --- a/src/chainconsumer/__init__.py +++ b/src/chainconsumer/__init__.py @@ -1,8 +1,9 @@ from .analysis import Bound from .chain import Chain, ChainConfig from .chainconsumer import ChainConsumer +from .color_finder import colors from .examples import make_sample from .plotter import PlotConfig from .truth import Truth -__all__ = ["ChainConsumer", "Chain", "ChainConfig", "Truth", "PlotConfig", "make_sample", "Bound"] +__all__ = ["ChainConsumer", "Chain", "ChainConfig", "Truth", "PlotConfig", "make_sample", "Bound", "colors"] diff --git a/src/chainconsumer/analysis.py b/src/chainconsumer/analysis.py index 77b2595f..da67b11e 100644 --- a/src/chainconsumer/analysis.py +++ b/src/chainconsumer/analysis.py @@ -99,7 +99,9 @@ def get_latex_table( """ final_chains = self.parent.plotter._sanitise_chains(chains) final_columns = self.parent.plotter._sanitise_columns(columns, final_chains) + blind = self.parent.plotter._sanitise_blinds(self.parent.plotter.config.blind, final_columns) + final_columns = [c for c in final_columns if c not in blind] num_chains = len(final_chains) num_parameters = len(final_columns) fit_values = self.get_summary(chains=final_chains) @@ -303,7 +305,7 @@ def _get_smoothed_histogram( cs /= cs.max() return xs, ys, cs - def _get_2d_latex_table(self, named_matrix: Named2DMatrix, caption: str, label: str): + def _get_2d_latex_table(self, named_matrix: Named2DMatrix, caption: str, label: str) -> str: parameters = [self.parent.plotter.config.get_label(c) for c in named_matrix.columns] matrix = named_matrix.matrix latex_table = get_latex_table_frame(caption=caption, label=label) diff --git a/src/chainconsumer/chain.py b/src/chainconsumer/chain.py index 5ac74874..49e317ac 100644 --- a/src/chainconsumer/chain.py +++ b/src/chainconsumer/chain.py @@ -17,7 +17,7 @@ from pydantic import Field, field_validator, model_validator from .base import BetterBase -from .colors import ColorInput, colors +from .color_finder import ColorInput, colors from .statistics import SummaryStatistic ChainName: TypeAlias = str @@ -52,9 +52,7 @@ class ChainConfig(BetterBase): shade_alpha: float = Field(default=0.5, description="The alpha of the shading") shade_gradient: float = Field(default=1.0, description="The contrast between contour levels") bar_shade: bool = Field(default=True, description="Whether to shade marginalised distributions") - bins: int | float = Field( - default=1.0, description="The number of bins to use for histograms. If a float, used to scale the default bins" - ) + bins: int | None = Field(default=None, description="The number of bins to use for histograms.") kde: int | float | bool = Field(default=False, description="The bandwidth for KDEs") smooth: int = Field(default=3, description="The smoothing for histograms. Set to 0 for no smoothing") color_param: str | None = Field(default=None, description="The parameter (column) to use for coloring") @@ -309,6 +307,8 @@ def divide(self) -> list[Chain]: for i, split in enumerate(splits): df = pd.DataFrame(split, columns=self.samples.columns) options = self.model_dump(exclude={"samples", "name"}) + if "color" in options: + options.pop("color") chain = Chain(samples=df, name=f"{self.name} Walker {i}", **options) chains.append(chain) diff --git a/src/chainconsumer/chainconsumer.py b/src/chainconsumer/chainconsumer.py index 50bb440b..64d13495 100644 --- a/src/chainconsumer/chainconsumer.py +++ b/src/chainconsumer/chainconsumer.py @@ -5,10 +5,9 @@ from .analysis import Analysis from .chain import Chain, ChainConfig, ChainName, ColumnName -from .colors import ColorInput, colors +from .color_finder import ColorInput, colors from .comparisons import Comparison from .diagnostic import Diagnostic -from .helpers import get_bins from .plotter import PlotConfig, Plotter from .truth import Truth @@ -168,9 +167,6 @@ def _get_final_chains(self) -> dict[ChainName, Chain]: # copy global config into local config local_config = global_config.copy() - if isinstance(chain.bins, float): - chain.bins = int(chain.bins * get_bins(chain)) - # Reduce shade alpha if we're showing contour labels if chain.show_contour_labels: local_config["shade_alpha"] *= 0.5 diff --git a/src/chainconsumer/colors.py b/src/chainconsumer/color_finder.py similarity index 100% rename from src/chainconsumer/colors.py rename to src/chainconsumer/color_finder.py diff --git a/src/chainconsumer/helpers.py b/src/chainconsumer/helpers.py index 5cfd0f2f..3e26e56e 100644 --- a/src/chainconsumer/helpers.py +++ b/src/chainconsumer/helpers.py @@ -38,6 +38,8 @@ def get_extents( def get_bins(chain: Chain) -> int: + if chain.bins is not None: + return chain.bins max_v = 35 if chain.smooth > 0 else 100 return max((max_v, np.floor(1.0 * np.power(chain.samples.shape[0] / chain.samples.shape[1], 0.25)))) diff --git a/src/chainconsumer/plotter.py b/src/chainconsumer/plotter.py index 1c13bb9a..0228eab2 100644 --- a/src/chainconsumer/plotter.py +++ b/src/chainconsumer/plotter.py @@ -23,8 +23,8 @@ from .base import BetterBase from .chain import Chain, ChainName, ColumnName -from .colors import ColorInput, colors -from .helpers import get_extents, get_grid_bins, get_smoothed_bins +from .color_finder import ColorInput, colors +from .helpers import get_bins, get_extents, get_grid_bins, get_smoothed_bins from .kde import MegKDE @@ -445,7 +445,6 @@ def plot_walks( extra = 0 plot_posterior = plot_posterior and np.any([c.log_posterior is not None for c in base.chains]) - if plot_weights: extra += 1 if plot_posterior: @@ -455,7 +454,7 @@ def plot_walks( figsize = (8, 0.75 + (n + extra)) fig, axes = plt.subplots(figsize=figsize, nrows=n + extra, squeeze=False, sharex=True) - + max_points = 100000 for i, axes_row in enumerate(axes): ax = axes_row[0] if i >= extra: @@ -463,6 +462,8 @@ def plot_walks( for chain in base.chains: if p in chain.data_columns: chain_row = chain.get_data(p) + if len(chain_row) > max_points: + chain_row = chain_row[:: int(len(chain_row) / max_points)] log = p in base.log_scales self._plot_walk( ax, @@ -477,14 +478,20 @@ def plot_walks( if p in truth.location: self._plot_walk_truth(ax, truth, p) + if p in base.blind: + ax.set_yticks([]) else: # noqa: PLR5501 if i == 0 and plot_posterior: for chain in base.chains: if chain.log_posterior is not None: + posterior = chain.log_posterior - chain.log_posterior.max() + if len(posterior) > max_points: + posterior = posterior[:: int(len(posterior) / max_points)] + self._plot_walk( ax, r"$\log(P)$", - chain.log_posterior - chain.log_posterior.max(), + posterior, convolve=convolve, color=colors.format(chain.color), ) @@ -493,10 +500,13 @@ def plot_walks( for chain in base.chains: if chain.weights is not None: + weights = chain.weights + if len(weights) > max_points: + weights = weights[:: int(len(weights) / max_points)] self._plot_walk( ax, label, - np.log10(chain.weights) if log_weight else chain.weights, + np.log10(weights) if log_weight else weights, # type: ignore convolve=convolve, color=colors.format(chain.color), ) @@ -1070,7 +1080,7 @@ def _plot_scatter(self, ax: Axes, chain: Chain, color: str, x: pd.Series, y: pd. else: kwargs = {"c": color, "alpha": 0.3} - h = ax.scatter(x[::skip], y[::skip], s=10, marker=".", edgecolors="none", zorder=chain.zorder - 1, **kwargs) + h = ax.scatter(x[::skip], y[::skip], s=10, marker=".", edgecolors="none", zorder=chain.zorder - 5, **kwargs) if chain.color_data is not None: return h else: @@ -1156,7 +1166,7 @@ def _plot_bars( if chain.grid: bins = get_grid_bins(data) else: - bins, _ = get_smoothed_bins(chain.smooth, int(chain.bins), data, chain.weights) + bins, _ = get_smoothed_bins(chain.smooth, get_bins(chain), data, chain.weights) hist, edges = np.histogram(data, bins=bins, density=True, weights=chain.weights) if chain.power is not None: hist = hist**chain.power @@ -1295,8 +1305,8 @@ def _get_smoothed_histogram2d( binsy = get_grid_bins(y) hist, x_bins, y_bins = np.histogram2d(x, y, bins=[binsx, binsy], weights=w) else: - binsx, smooth = get_smoothed_bins(chain.smooth, int(chain.bins), x, w) - binsy, _ = get_smoothed_bins(smooth, int(chain.bins), y, w) + binsx, smooth = get_smoothed_bins(chain.smooth, get_bins(chain), x, w) + binsy, _ = get_smoothed_bins(smooth, get_bins(chain), y, w) hist, x_bins, y_bins = np.histogram2d(x, y, bins=[binsx, binsy], weights=w) if chain.power is not None: diff --git a/src/chainconsumer/truth.py b/src/chainconsumer/truth.py index 0dc6c670..4d95151b 100644 --- a/src/chainconsumer/truth.py +++ b/src/chainconsumer/truth.py @@ -4,7 +4,7 @@ from pydantic import Field, ValidationError, field_validator from .base import BetterBase -from .colors import ColorInput +from .color_finder import ColorInput class Truth(BetterBase): diff --git a/tests/test_analysis.py b/tests/test_analysis.py index ce19dbd9..86051e4b 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -32,7 +32,7 @@ def test_summary_no_smooth(self): tolerance = 5e-2 consumer = ChainConsumer() consumer.add_chain(self.chain) - consumer.set_override(ChainConfig(smooth=0, bins=2.0)) + consumer.set_override(ChainConfig(smooth=0, bins=100)) summary = consumer.analysis.get_summary() actual = summary["a"]["x"].array expected = np.array([3.5, 5.0, 6.5]) @@ -71,7 +71,6 @@ def test_summary_disjoint(self): c2.name = "chain2" c2.samples = c2.samples.rename(columns={"x": "y"}) consumer.add_chain(c2) - consumer.set_override(ChainConfig(bins=0.8)) summary = consumer.analysis.get_summary(columns=["x"]) assert len(summary) == 2 # Two chains assert not summary["chain2"] # but this one has no cols @@ -94,7 +93,6 @@ def test_summary_power(self): def test_output_text(self): consumer = ChainConsumer() consumer.add_chain(self.chain) - consumer.set_override(ChainConfig(bins=0.8)) vals = consumer.analysis.get_summary()["a"] text = consumer.analysis.get_parameter_text(vals["x"]) assert text == r"5.0\pm 1.5" @@ -242,7 +240,6 @@ def test_divide_chains_name(self): chain = Chain(samples=pd.DataFrame(data, columns=["x"]), walkers=num_walkers, name="test") for c in chain.divide(): consumer.add_chain(c) - consumer.set_override(ChainConfig(bins=0.7)) means = [0, 1.0] stats = consumer.analysis.get_summary() diff --git a/tests/test_colours.py b/tests/test_colours.py index b61f42c0..203f2d22 100644 --- a/tests/test_colours.py +++ b/tests/test_colours.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from chainconsumer.colors import ALL_COLOURS, colors +from chainconsumer.color_finder import ALL_COLOURS, colors def test_colors_rgb2hex_1(): From 97b1032d0ac8fc29d103740cb9f7ae4ba98a69c9 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Sun, 8 Oct 2023 22:04:37 +1000 Subject: [PATCH 11/22] Adding a contributing --- CONTRIBUTING.md | 9 ++++++ docs/statistics.md | 7 ----- docs/usage.md | 50 +++++++++++++++++++++++++++++++++ mkdocs.yml | 4 +-- src/chainconsumer/statistics.py | 2 +- 5 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 CONTRIBUTING.md delete mode 100644 docs/statistics.md create mode 100644 docs/usage.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..78a13c91 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# Guide for contributing! + +1. Please read over the online documentation, or use GitHub discussions to chat if you're unsure about anything. +2. Fork the repo + clone it out +3. I highly recommend either a nix system or using the Windows Subsystem for Linux +4. Install a developer version by running (in the directory you just cloned into): `make install` + 1. This should install poetry, dependencies, and a pre-commit hook + 2. Verify it all looks good by running `make precommit` +5. Code! \ No newline at end of file diff --git a/docs/statistics.md b/docs/statistics.md deleted file mode 100644 index 391212c6..00000000 --- a/docs/statistics.md +++ /dev/null @@ -1,7 +0,0 @@ -# Statistics - -When summarising chains, ChainConsumer offers several different methods. The below image shows the upper and lower bounds and central points for the "MEAN", "CUMULATIVE", and "MAX" methods respectively. The "MAX_CENTRAL" method is the blue central value and the red bounds. - -![](resources/stats.png) - -::: chainconsumer.statistics.SummaryStatistic \ No newline at end of file diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 00000000..a8e9d27e --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,50 @@ +# Usage + +In general, this is the flow: + +1. Get your samples into a pandas dataframe. If you have another format and want to contribute a translation function, that would be amazing. +2. Turn those samples into a `Chain`. At this point, you can specify a ton of visual and statistical choices. +3. Give those chains to `ChainConsumer` +4. If you know the truth value of the chains, you can call `c.add_truth(Truth(...))` to add this to your plots. Note you can have as many truth lines as you want, not just one. +5. If you want to change plotting specific options (but not chain specific options), you can `set_plot_config(PlotConfig(...))` to control things like the number of ticks, font size, etc. +6. Optionally, use the diagnostics available to see if your chains are stable, or do this via using `plot_walks` +7. Make your contours via `plot`, or your summaries, or your LaTeX tables. + + + +## Statistics + +When summarising chains, ChainConsumer offers several different methods. The below image shows the upper and lower bounds and central points for the "MEAN", "CUMULATIVE", and "MAX" methods respectively. The "MAX_CENTRAL" method is the blue central value and the red bounds. + +![](resources/stats.png) + +::: chainconsumer.statistics.SummaryStatistic + +## Why all these classes and not just kwargs? + +Python type hinting for kwargs isn't quite there yet. `TypedDict` with Python 3.12 is a big step forward, +but I know it'll be a while before the scientific community is all on 3.12. The initial version of ChainConsumer, +which was Python 2.7 compatible, didn't have type hints at all. It just took tons of kwargs and passed them around, +which also caused a huge ton of duplicated docstring and functions. By encapsulating the options into a dataclass, +it becomes much easier for anyone, me or someone who wants to contribute to the repo, to simply add to this class. + +You don't need to remember to update five other functions, and their docstring. I like that. I *don't* like the extra +verbosity, but it's a price I'm willing to pay for more explicit code and better type hinting. + +## How to do overrides + +When you make a Chain, you specify its initial properties. + +```python +c = ChainConsumer() +chain = Chain(samples=df, name="Something", shade=True, plot_point=True, color="red") +c.add_chain(c) +``` + +If you then tell `ChainConsumer` to add an override, this will then replace your original value, for all chains. + +```python +c.add_override(ChainConfig(shade=False)) +``` + +I note that this override does not modify your original chain. When the plotting code requests final chains from `ChainConsumer`, the initial chains are copied and their attributes updated by the override. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index af7a889a..3c996c19 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,6 +64,6 @@ watch: - src nav: - Home: index.md - - Statistics: statistics.md - - Examples: generated/gallery + - Usage: usage.md - Python API: api.md + - Examples: generated/gallery diff --git a/src/chainconsumer/statistics.py b/src/chainconsumer/statistics.py index 5dbcedab..e3c83a71 100644 --- a/src/chainconsumer/statistics.py +++ b/src/chainconsumer/statistics.py @@ -8,7 +8,7 @@ class SummaryStatistic(Enum): finding an iso-likelihood surface which encapsulates the required volume.""" MAX_CENTRAL = "max_central" - """"As per the MAX method, this has the centre point at the maximum likelihood. + """As per the MAX method, this has the centre point at the maximum likelihood. However the lower and upper values come from the CDF, like the cumulative method.""" CUMULATIVE = "cumulative" From 4f26626c1156f97601f9c797add77ea5a855a0ea Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Mon, 9 Oct 2023 13:24:30 +1000 Subject: [PATCH 12/22] Adding testing to actions --- .github/workflows/deploy.yml | 26 +++++++++++ pyproject.toml | 8 ++-- src/chainconsumer/helpers.py | 52 +++++++++++++++++++++ src/chainconsumer/plotter.py | 90 +++++++++++------------------------- 4 files changed, 108 insertions(+), 68 deletions(-) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..a6f96798 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,26 @@ +name: Python package + +on: [push] + +jobs: + test: + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + # Optional - x64 or x86 architecture, defaults to x64 + architecture: 'x64' + + - name: Display Python version + run: python -c "import sys; print(sys.version)" + + - name: Install + run: make install + + - name: Verify formatting and tests + run: make precommit \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index eb094678..f2b18a12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,13 +13,13 @@ version = "0.35.0" [tool.poetry.dependencies] python = ">=3.10,<3.13" -numpy = "^1.26.0" -scipy = "^1.11.3" -matplotlib = "^3.8.0" +numpy = "^1.23.0" +scipy = "^1.8.0" +matplotlib = "^3.6.0" statsmodels = "^0.14.0" pandas = "^2.1.1" pillow = "^10.0.1" -pydantic = "^2.4.2" +pydantic = "^2.2.0" [tool.poetry.group.test.dependencies] diff --git a/src/chainconsumer/helpers.py b/src/chainconsumer/helpers.py index 3e26e56e..aea44c96 100644 --- a/src/chainconsumer/helpers.py +++ b/src/chainconsumer/helpers.py @@ -2,8 +2,10 @@ import numpy as np import pandas as pd +from scipy.ndimage import gaussian_filter from .chain import Chain +from .kde import MegKDE def get_extents( @@ -71,6 +73,56 @@ def get_smoothed_bins( return np.linspace(minv, maxv, 2 * smooth * bins), smooth +def get_smoothed_histogram2d( + chain: Chain, + col1: str, + col2: str, +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: # pragma: no cover + """Returns a smoothed 2D histogram of two parameters. + + Args: + chain (Chain): The chain to plot + col1 (str): The first parameter + col2 (str): The second parameter + + Returns: + tuple[np.ndarray, np.ndarray, np.ndarray]: The histogram, x bin enters, y bin centers + """ + x = chain.get_data(col1) + y = chain.get_data(col2) + w = chain.weights + + if chain.grid: + binsx = get_grid_bins(x) + binsy = get_grid_bins(y) + hist, x_bins, y_bins = np.histogram2d(x, y, bins=[binsx, binsy], weights=w) + else: + binsx, smooth = get_smoothed_bins(chain.smooth, get_bins(chain), x, w) + binsy, _ = get_smoothed_bins(smooth, get_bins(chain), y, w) + hist, x_bins, y_bins = np.histogram2d(x, y, bins=[binsx, binsy], weights=w) + + if chain.power is not None: + hist = hist**chain.power + + x_centers = 0.5 * (x_bins[:-1] + x_bins[1:]) + y_centers = 0.5 * (y_bins[:-1] + y_bins[1:]) + + if chain.kde: + nn = x_centers.size * 2 # Double samples for KDE because smooth + x_centers = np.linspace(x_bins.min(), x_bins.max(), nn) + y_centers = np.linspace(y_bins.min(), y_bins.max(), nn) + xx, yy = np.meshgrid(x_centers, y_centers, indexing="ij") + coords = np.vstack((xx.flatten(), yy.flatten())).T + data = np.vstack((x, y)).T + hist = MegKDE(data, w, chain.kde).evaluate(coords).reshape((nn, nn)) + if chain.power is not None: + hist = hist**chain.power + elif chain.smooth: + hist = gaussian_filter(hist, chain.smooth, mode="reflect") + + return hist, x_centers, y_centers + + def get_grid_bins(data: pd.Series[float]) -> np.ndarray: bin_c = np.sort(np.unique(data)) delta = 0.5 * (bin_c[1] - bin_c[0]) diff --git a/src/chainconsumer/plotter.py b/src/chainconsumer/plotter.py index 0228eab2..2e83d0ff 100644 --- a/src/chainconsumer/plotter.py +++ b/src/chainconsumer/plotter.py @@ -6,6 +6,7 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd +from matplotlib.artist import Artist from matplotlib.axes import Axes from matplotlib.collections import PathCollection from matplotlib.figure import Figure @@ -13,10 +14,8 @@ from matplotlib.lines import Line2D from matplotlib.textpath import TextPath from matplotlib.ticker import LogLocator, MaxNLocator, ScalarFormatter -from numpy import meshgrid from pydantic import Field from scipy.interpolate import interp1d # type: ignore -from scipy.ndimage import gaussian_filter # type: ignore from scipy.stats import norm # type: ignore from chainconsumer.truth import Truth @@ -24,8 +23,7 @@ from .base import BetterBase from .chain import Chain, ChainName, ColumnName from .color_finder import ColorInput, colors -from .helpers import get_bins, get_extents, get_grid_bins, get_smoothed_bins -from .kde import MegKDE +from .helpers import get_bins, get_extents, get_grid_bins, get_smoothed_bins, get_smoothed_histogram2d class PlottingBase(BetterBase): @@ -46,7 +44,7 @@ class PlotConfig(BetterBase): diagonal_tick_labels: bool = Field(default=True, description="Whether to show tick labels on the diagonal") label_font_size: int = Field(default=12, ge=0, description="Font size for axis labels") tick_font_size: int = Field(default=10, ge=0, description="Font size for axis ticks") - spacing: float = Field(default=None, ge=0, description="Spacing between subplots") + spacing: float | None = Field(default=None, ge=0, description="Spacing between subplots") contour_label_font_size: int = Field(default=10, ge=0, description="Font size for contour labels") show_legend: bool | None = Field( default=None, @@ -129,8 +127,8 @@ def get_size( return 3 + grow_factor * 2 * num_columns + (1 if has_cax else 0), 3 + grow_factor * 2 * num_columns -def get_artists_from_chains(chains: list[Chain]): - artists = [] +def get_artists_from_chains(chains: list[Chain]) -> list[Artist]: + artists: list[Artist] = [] for chain in chains: if chain.plot_contour and not chain.plot_point: artists.append( @@ -234,7 +232,7 @@ def plot( fig_size = FigSize.get_size(figsize, len(base.columns), num_cax > 0) plot_hists = self.config.plot_hists flip = len(base.columns) == 2 and plot_hists and self.config.flip - fig, axes, params_x, params_y = self._get_figure(base, figsize=fig_size) + fig, axes, params_x, params_y = self._get_triangle_figure(base, figsize=fig_size) axl = axes.ravel().tolist() summarise = self.config.summarise and len(base.chains) == 1 @@ -348,7 +346,13 @@ def _save_fig(self, fig: Figure, filename: str | Path, dpi: int) -> None: # pra fig.savefig(filename, bbox_inches="tight", dpi=dpi, transparent=True, pad_inches=0.05) def _add_watermark( - self, fig: Figure, axes: Axes | None, fig_size: tuple[float, float], text: str, dpi=300, size_scale: float = 1.0 + self, + fig: Figure, + axes: Axes | None, + fig_size: tuple[float, float], + text: str, + dpi: int = 300, + size_scale: float = 1.0, ) -> None: # pragma: no cover # Code based off github repository https://github.com/cpadavis/preliminize dx, dy = fig_size @@ -444,7 +448,7 @@ def plot_walks( n = len(base.columns) extra = 0 - plot_posterior = plot_posterior and np.any([c.log_posterior is not None for c in base.chains]) + plot_posterior = plot_posterior and any([c.log_posterior is not None for c in base.chains]) if plot_weights: extra += 1 if plot_posterior: @@ -635,7 +639,7 @@ def plot_summary( errorbar: bool = False, extra_parameter_spacing: float = 1.0, vertical_spacing_ratio: float = 1.0, - ): # pragma: no cover + ) -> Figure: # pragma: no cover """Plots parameter summaries This plot is more for a sanity or consistency check than for use with final results. @@ -872,7 +876,7 @@ def _get_custom_extents( extents[p] = self._get_parameter_extents(p, chains, wide_extents=wide_extents) return extents - def _get_figure( + def _get_triangle_figure( self, base: PlottingBase, figsize: tuple[float, float] ) -> tuple[Figure, np.ndarray, list[ColumnName], list[ColumnName]]: n = len(base.columns) @@ -1080,7 +1084,15 @@ def _plot_scatter(self, ax: Axes, chain: Chain, color: str, x: pd.Series, y: pd. else: kwargs = {"c": color, "alpha": 0.3} - h = ax.scatter(x[::skip], y[::skip], s=10, marker=".", edgecolors="none", zorder=chain.zorder - 5, **kwargs) + h = ax.scatter( + x[::skip], + y[::skip], + s=10, + marker=".", + edgecolors="none", + zorder=chain.zorder - 5, + **kwargs, # type: ignore + ) if chain.color_data is not None: return h else: @@ -1105,7 +1117,7 @@ def _plot_contour(self, ax: Axes, chain: Chain, px: str, py: str) -> PathCollect colors.scale_colour(c, sub) for c in contour_colours[:-1] ] - hist, x_centers, y_centers = self._get_smoothed_histogram2d(chain, py, px) + hist, x_centers, y_centers = get_smoothed_histogram2d(chain, py, px) hist[hist == 0] = 1e-16 vals = self._convert_to_stdev(hist.T) @@ -1280,56 +1292,6 @@ def _scale_colours(self, colour: ColorInput, num: int, shade_gradient: float) -> colours = [colors.scale_colour(colour, scale) for scale in scales] return colours - def _get_smoothed_histogram2d( - self, - chain: Chain, - col1: str, - col2: str, - ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: # pragma: no cover - """Returns a smoothed 2D histogram of two parameters. - - Args: - chain (Chain): The chain to plot - col1 (str): The first parameter - col2 (str): The second parameter - - Returns: - tuple[np.ndarray, np.ndarray, np.ndarray]: The histogram, x bin enters, y bin centers - """ - x = chain.get_data(col1) - y = chain.get_data(col2) - w = chain.weights - - if chain.grid: - binsx = get_grid_bins(x) - binsy = get_grid_bins(y) - hist, x_bins, y_bins = np.histogram2d(x, y, bins=[binsx, binsy], weights=w) - else: - binsx, smooth = get_smoothed_bins(chain.smooth, get_bins(chain), x, w) - binsy, _ = get_smoothed_bins(smooth, get_bins(chain), y, w) - hist, x_bins, y_bins = np.histogram2d(x, y, bins=[binsx, binsy], weights=w) - - if chain.power is not None: - hist = hist**chain.power - - x_centers = 0.5 * (x_bins[:-1] + x_bins[1:]) - y_centers = 0.5 * (y_bins[:-1] + y_bins[1:]) - - if chain.kde: - nn = x_centers.size * 2 # Double samples for KDE because smooth - x_centers = np.linspace(x_bins.min(), x_bins.max(), nn) - y_centers = np.linspace(y_bins.min(), y_bins.max(), nn) - xx, yy = meshgrid(x_centers, y_centers, indexing="ij") - coords = np.vstack((xx.flatten(), yy.flatten())).T - data = np.vstack((x, y)).T - hist = MegKDE(data, w, chain.kde).evaluate(coords).reshape((nn, nn)) - if chain.power is not None: - hist = hist**chain.power - elif chain.smooth: - hist = gaussian_filter(hist, chain.smooth, mode="reflect") - - return hist, x_centers, y_centers - if __name__ == "__main__": from .chainconsumer import ChainConsumer From 6a4eab537f4a3c112fbbf2ac602512e965089e3c Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Mon, 9 Oct 2023 13:45:15 +1000 Subject: [PATCH 13/22] Workflow fun --- .github/workflows/deploy.yml | 39 ++++++++++++++++++++++-------------- Makefile | 8 +++++++- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a6f96798..e3f14cb4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,26 +1,35 @@ -name: Python package +name: CICD -on: [push] +on: + push: + pull_request: + workflow_dispatch: + +defaults: + run: + shell: bash jobs: test: - runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Set up Python 3.10 - uses: actions/setup-python@v4 + - uses: actions/setup-python@v4 with: python-version: '3.10' - # Optional - x64 or x86 architecture, defaults to x64 architecture: 'x64' + cache: 'poetry' + - run: make install + - run: make precommit - - name: Display Python version - run: python -c "import sys; print(sys.version)" - - - name: Install - run: make install - - - name: Verify formatting and tests - run: make precommit \ No newline at end of file + build_docs: + runs-on: ubuntu-latest + needs: [test] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + architecture: 'x64' + cache: 'poetry' + - run: make install \ No newline at end of file diff --git a/Makefile b/Makefile index 33398552..f621cb0e 100644 --- a/Makefile +++ b/Makefile @@ -13,9 +13,15 @@ test: poetry run pytest serve: - # rm -rf docs/generated/gallery; + rm -rf docs/generated/gallery; poetry run mkdocs serve --clean +docs: + poetry run mkdocs build + +deploy: + poetry run mkdocs gh-deploy --force + tests: test all: precommit tests \ No newline at end of file From 209defd064ab877c3d3e5c6ef215114c62fab9ac Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Mon, 9 Oct 2023 13:49:16 +1000 Subject: [PATCH 14/22] Adding poetry install --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e3f14cb4..84ade59b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,6 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - run: pip install poetry - uses: actions/setup-python@v4 with: python-version: '3.10' From 231d98d0dde104b51754f3a25c5b3e8bb6760161 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Mon, 9 Oct 2023 13:51:58 +1000 Subject: [PATCH 15/22] Updating workflow again --- .github/workflows/deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 84ade59b..1573f51b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,6 +3,7 @@ name: CICD on: push: pull_request: + types: [opened] workflow_dispatch: defaults: @@ -28,6 +29,7 @@ jobs: needs: [test] steps: - uses: actions/checkout@v4 + - run: pip install poetry - uses: actions/setup-python@v4 with: python-version: '3.10' From 25d9892e85f25ef1a2123ea5e5592a9f7c3f1a15 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Mon, 9 Oct 2023 13:58:20 +1000 Subject: [PATCH 16/22] Adding more steps --- .github/workflows/deploy.yml | 23 ++++++++++++++++++++++- Makefile | 2 +- poetry.lock | 24 ++++-------------------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1573f51b..771fff8b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,6 +5,8 @@ on: pull_request: types: [opened] workflow_dispatch: + release: + types: [created] defaults: run: @@ -35,4 +37,23 @@ jobs: python-version: '3.10' architecture: 'x64' cache: 'poetry' - - run: make install \ No newline at end of file + - run: make install + - run: make docs + + push_docs: + runs-on: ubuntu-latest + needs: [build_docs] + if: | + github.event_name == 'release' || github.ref == 'refs/heads/master' || false + + steps: + - uses: actions/checkout@v4 + - run: pip install poetry + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + architecture: 'x64' + cache: 'poetry' + - run: make install + - run: make docs + - run: make pushdocs \ No newline at end of file diff --git a/Makefile b/Makefile index f621cb0e..6b67b442 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ serve: docs: poetry run mkdocs build -deploy: +pushdocs: poetry run mkdocs gh-deploy --force tests: test diff --git a/poetry.lock b/poetry.lock index 1a14c002..2b6a8b66 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1734,22 +1734,6 @@ files = [ {file = "mkdocs_material_extensions-1.2.tar.gz", hash = "sha256:27e2d1ed2d031426a6e10d5ea06989d67e90bb02acd588bc5673106b5ee5eedf"}, ] -[[package]] -name = "mkdocs-render-swagger-plugin" -version = "0.1.1" -description = "MKDocs plugin for rendering swagger & openapi files." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs_render_swagger_plugin-0.1.1-py3-none-any.whl", hash = "sha256:1b0f4f92bf69d7e0d56b13f520fdad072ddd1f4ccf031f11706d45d831d4df7b"}, -] - -[package.dependencies] -mkdocs = ">=1.4" - -[package.extras] -dev = ["coverage", "flake8", "mypy", "pyyaml"] - [[package]] name = "mkdocstrings" version = "0.23.0" @@ -3695,13 +3679,13 @@ files = [ [[package]] name = "websocket-client" -version = "1.6.3" +version = "1.6.4" description = "WebSocket client for Python with low level API options" optional = false python-versions = ">=3.8" files = [ - {file = "websocket-client-1.6.3.tar.gz", hash = "sha256:3aad25d31284266bcfcfd1fd8a743f63282305a364b8d0948a43bd606acc652f"}, - {file = "websocket_client-1.6.3-py3-none-any.whl", hash = "sha256:6cfc30d051ebabb73a5fa246efdcc14c8fbebbd0330f8984ac3bb6d9edd2ad03"}, + {file = "websocket-client-1.6.4.tar.gz", hash = "sha256:b3324019b3c28572086c4a319f91d1dcd44e6e11cd340232978c684a7650d0df"}, + {file = "websocket_client-1.6.4-py3-none-any.whl", hash = "sha256:084072e0a7f5f347ef2ac3d8698a5e0b4ffbfcab607628cadabc650fc9a83a24"}, ] [package.extras] @@ -3723,4 +3707,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "8006b96eff435b820dd9ae4893db8bafcf39657d9167fb3bc21987162ed70c40" +content-hash = "948e746594bb624b9047c335105a87e6f5bcd432fa8e1be0b387f14c0182cb0d" From 92f3947db56ad32ed63d5e13b18ae96fa0fbff13 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Mon, 9 Oct 2023 14:00:55 +1000 Subject: [PATCH 17/22] Adding phony --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6b67b442..2dffc94f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: tests +.PHONY: tests docs install: pip install -U pip poetry -q From fd54aeecafdee64effaff358e70c3db68b3e32d1 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Mon, 9 Oct 2023 14:14:45 +1000 Subject: [PATCH 18/22] Adding publication --- .github/workflows/deploy.yml | 23 +++++++++++++++-------- Makefile | 8 +++++++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 771fff8b..bb1b372b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -26,7 +26,7 @@ jobs: - run: make install - run: make precommit - build_docs: + docs: runs-on: ubuntu-latest needs: [test] steps: @@ -39,13 +39,13 @@ jobs: cache: 'poetry' - run: make install - run: make docs + - run: make pushdocs + if: | + github.event_name == 'release' || github.ref == 'refs/heads/master' - push_docs: + build_and_publish: runs-on: ubuntu-latest - needs: [build_docs] - if: | - github.event_name == 'release' || github.ref == 'refs/heads/master' || false - + needs: [test] steps: - uses: actions/checkout@v4 - run: pip install poetry @@ -55,5 +55,12 @@ jobs: architecture: 'x64' cache: 'poetry' - run: make install - - run: make docs - - run: make pushdocs \ No newline at end of file + - run: make build + - name: Publish + run: | + poetry config pypi-token.pypi $PYPI_TOKEN + poetry publish --build + if: github.event_name == 'release' + env: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + diff --git a/Makefile b/Makefile index 2dffc94f..2213cc05 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: tests docs +.PHONY: tests docs build install: pip install -U pip poetry -q @@ -22,6 +22,12 @@ docs: pushdocs: poetry run mkdocs gh-deploy --force +build: + poetry publish --build --dry-run + +publish: + poetry publish --build + tests: test all: precommit tests \ No newline at end of file From e447bb99062f0cd75ee8ace8a47ab731aa569682 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Mon, 9 Oct 2023 14:53:05 +1000 Subject: [PATCH 19/22] Updating tags --- .github/workflows/deploy.yml | 4 +--- Makefile | 4 ++-- pyproject.toml | 4 +++- src/chainconsumer/__init__.py | 4 ++++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bb1b372b..655eda72 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -57,9 +57,7 @@ jobs: - run: make install - run: make build - name: Publish - run: | - poetry config pypi-token.pypi $PYPI_TOKEN - poetry publish --build + run: make publish if: github.event_name == 'release' env: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} diff --git a/Makefile b/Makefile index 2213cc05..f13f6f93 100644 --- a/Makefile +++ b/Makefile @@ -23,10 +23,10 @@ pushdocs: poetry run mkdocs gh-deploy --force build: - poetry publish --build --dry-run + poetry version $(git describe --tags --abbrev=0) && poetry publish --build --dry-run publish: - poetry publish --build + poetry config pypi-token.pypi $PYPI_TOKEN && poetry version $(git describe --tags --abbrev=0) && poetry publish --build tests: test diff --git a/pyproject.toml b/pyproject.toml index f2b18a12..f23893e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,8 +8,10 @@ description = "ChainConsumer: Consumer your MCMC chains" name = "ChainConsumer" packages = [{include = "chainconsumer", from = "src"}] readme = "README.md" -version = "0.35.0" +version = "0.0.0" +[tool.poetry-version-plugin] +source = "git-tag" [tool.poetry.dependencies] python = ">=3.10,<3.13" diff --git a/src/chainconsumer/__init__.py b/src/chainconsumer/__init__.py index a7f7b113..c65bfd99 100644 --- a/src/chainconsumer/__init__.py +++ b/src/chainconsumer/__init__.py @@ -1,3 +1,5 @@ +import importlib.metadata as importlib_metadata + from .analysis import Bound from .chain import Chain, ChainConfig from .chainconsumer import ChainConsumer @@ -7,3 +9,5 @@ from .truth import Truth __all__ = ["ChainConsumer", "Chain", "ChainConfig", "Truth", "PlotConfig", "make_sample", "Bound", "colors"] + +__version__ = importlib_metadata.version(__name__) From cdd5b851aa6676cd25dbb62b97306770551cb461 Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Mon, 9 Oct 2023 14:56:18 +1000 Subject: [PATCH 20/22] Makefile double dollar sign --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f13f6f93..050f7767 100644 --- a/Makefile +++ b/Makefile @@ -23,10 +23,10 @@ pushdocs: poetry run mkdocs gh-deploy --force build: - poetry version $(git describe --tags --abbrev=0) && poetry publish --build --dry-run + poetry version $$(git describe --tags --abbrev=0) && poetry publish --build --dry-run publish: - poetry config pypi-token.pypi $PYPI_TOKEN && poetry version $(git describe --tags --abbrev=0) && poetry publish --build + poetry config pypi-token.pypi $PYPI_TOKEN && poetry version $$(git describe --tags --abbrev=0) && poetry publish --build tests: test From a58f851c265eeccfb88c1f9879539015fce014cb Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Mon, 9 Oct 2023 15:23:13 +1000 Subject: [PATCH 21/22] Versioning is a pain --- Makefile | 9 +++++---- mkdocs.yml | 12 +++++++++--- pyproject.toml | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 050f7767..20fb30a3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ .PHONY: tests docs build +VERSION := $(shell git for-each-ref refs/tags --format='%(refname:short)' | grep -E "^v?[0-9]+\..*" | tail -n1) install: pip install -U pip poetry -q @@ -17,16 +18,16 @@ serve: poetry run mkdocs serve --clean docs: - poetry run mkdocs build + poetry run poetry version $(VERSION) && mkdocs build pushdocs: - poetry run mkdocs gh-deploy --force + poetry run poetry version $(VERSION) && mkdocs gh-deploy --force build: - poetry version $$(git describe --tags --abbrev=0) && poetry publish --build --dry-run + rm -rf dist; poetry version $(VERSION) && poetry publish --build --dry-run publish: - poetry config pypi-token.pypi $PYPI_TOKEN && poetry version $$(git describe --tags --abbrev=0) && poetry publish --build + rm -rf dist; poetry config pypi-token.pypi $PYPI_TOKEN && poetry version $(VERSION) && poetry publish --build -y tests: test diff --git a/mkdocs.yml b/mkdocs.yml index 3c996c19..19cadd8a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,11 @@ site_name: ChainConsumer +site_url: https://samreay.github.io/chainconsumer/ +site_author: Samuel Hinton + +edit_uri: "" +repo_name: samreay/chainconsumer +repo_url: https://github.com/samreay/chainconsumer + theme: name: material icon: @@ -25,10 +32,9 @@ theme: toggle: icon: material/weather-night name: Switch to dark mode -repo_name: chainconsumer -repo_url: https://github.com/samreay/chainconsumer + plugins: - # autorefs: {} + autorefs: {} mkdocstrings: handlers: python: diff --git a/pyproject.toml b/pyproject.toml index f23893e6..7f2554e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "ChainConsumer: Consumer your MCMC chains" name = "ChainConsumer" packages = [{include = "chainconsumer", from = "src"}] readme = "README.md" -version = "0.0.0" +version = "v0.34.0" [tool.poetry-version-plugin] source = "git-tag" From ee4a3561272894174b7c6659c7c8f65973b1f09f Mon Sep 17 00:00:00 2001 From: Samuel Hinton Date: Mon, 9 Oct 2023 15:26:43 +1000 Subject: [PATCH 22/22] Running inside poetry --- Makefile | 4 ++-- docs/index.md | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 20fb30a3..d4a87c22 100644 --- a/Makefile +++ b/Makefile @@ -18,10 +18,10 @@ serve: poetry run mkdocs serve --clean docs: - poetry run poetry version $(VERSION) && mkdocs build + poetry run poetry version $(VERSION) && poetry run mkdocs build pushdocs: - poetry run poetry version $(VERSION) && mkdocs gh-deploy --force + poetry run poetry version $(VERSION) && poetry run mkdocs gh-deploy --force build: rm -rf dist; poetry version $(VERSION) && poetry publish --build --dry-run diff --git a/docs/index.md b/docs/index.md index 9505a8dc..3c66916e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,8 @@ ChainConsumer is a python package designed to do one thing - consume the chains ## Installation -The latest version of ChainConsumer requires at least Python 3.10. +The latest version of ChainConsumer requires at least Python 3.10. If you have a version of `ChainConsumer` that is older (v0.34.0 or below) +you will find this documentation not very useful. `pip install chainconsumer`

UTJL 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