diff --git a/.all-contributorsrc b/.all-contributorsrc index b5a1e9d0..c888d999 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -85,6 +85,24 @@ "contributions": [ "doc" ] + }, + { + "login": "dantrim", + "name": "Daniel Antrim", + "avatar_url": "https://avatars.githubusercontent.com/u/7841565?v=4", + "profile": "http://dantrim.github.io", + "contributions": [ + "code" + ] + }, + { + "login": "nsmith-", + "name": "Nicholas Smith", + "avatar_url": "https://avatars.githubusercontent.com/u/6587412?v=4", + "profile": "https://github.com/nsmith-", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 332b9464..83d87e73 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -4,11 +4,36 @@ detailed description of best practices for developing Scikit-HEP packages. [skhep-dev-intro]: https://scikit-hep.org/developer/intro -# Setting up a development environment +# Contributing + +## Setting up a development environment + +### Nox + +The fastest way to start with development is to use nox. If you don't have nox, +you can use `pipx run nox` to run it without installing, or `pipx install nox`. +If you don't have pipx (pip for applications), then you can install with with +`pip install pipx` (the only case were installing an application with regular +pip is reasonable). If you use macOS, then pipx and nox are both in brew, use +`brew install pipx nox`. + +To use, run `nox`. This will lint and test using every installed version of +Python on your system, skipping ones that are not installed. You can also run +specific jobs: + +```console +$ nox -s lint # Lint only +$ nox -s tests-3.9 # Python 3.9 tests only +$ nox -s docs -- serve # Build and serve the docs +$ nox -s build # Make an SDist and wheel +``` + +Nox handles everything for you, including setting up an temporary virtual +environment for each run. ### PyPI -You can set up a development environment using PyPI. +For extended development, you can set up a development environment using PyPI. ```bash $ python3 -m venv venv @@ -29,7 +54,7 @@ $ conda activate hist (hist)$ python -m ipykernel install --name hist ``` -# Post setup +## Post setup You should prepare pre-commit, which will help you by checking that commits pass required checks: @@ -42,7 +67,7 @@ pre-commit install # Will install a pre-commit hook into the git repo You can also/alternatively run `pre-commit run` (changes only) or `pre-commit run --all-files` to check even without installing the hook. -# Testing +## Testing Use PyTest to run the unit checks: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7e6183d..a0279dfa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: python-version: - - 3.6 + - 3.7 - 3.8 - 3.9 name: Check Python ${{ matrix.python-version }} @@ -40,8 +40,11 @@ jobs: - name: Test package run: python -m pytest + - name: Temporarily pin mplhep + run: echo "mplhep<=0.3.7" >> constraints.txt + - name: Install plotting requirements too - run: python -m pip install -e ".[test,plot]" + run: python -m pip install -e ".[test,plot]" -c constraints.txt - name: Test plotting too run: python -m pytest --mpl diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a66a33d..907e5f0e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.5b2 + rev: 21.7b0 hooks: - id: black @@ -19,15 +19,15 @@ repos: - id: trailing-whitespace - repo: https://github.com/PyCQA/isort - rev: 5.8.0 + rev: 5.9.3 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.19.0 + rev: v2.23.3 hooks: - id: pyupgrade - args: [--py36-plus] + args: [--py37-plus] - repo: https://github.com/asottile/setup-cfg-fmt rev: v1.17.0 @@ -36,13 +36,13 @@ repos: # Notebook formatting - repo: https://github.com/nbQA-dev/nbQA - rev: 0.10.0 + rev: 1.1.0 hooks: - id: nbqa-black additional_dependencies: [black==20.8b1] - id: nbqa-pyupgrade - additional_dependencies: [pyupgrade==2.7.4] - args: ["--py36-plus"] + additional_dependencies: [pyupgrade==2.12.0] + args: ["--py37-plus"] - repo: https://github.com/pycqa/flake8 rev: 3.9.2 @@ -52,11 +52,12 @@ repos: additional_dependencies: [flake8-bugbear] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.812 + rev: v0.910 hooks: - id: mypy files: ^src - additional_dependencies: ["numpy>=1.20", "matplotlib>=3.3", "boost-histogram~=1.0.1"] + args: [] + additional_dependencies: ["numpy==1.21.*", "matplotlib>=3.3", "boost-histogram~=1.0.1", "uhi~=0.3.0"] - repo: https://github.com/mgedmin/check-manifest rev: "0.46" diff --git a/README.md b/README.md index f74b8188..ada56787 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ [![pre-commit.ci status][pre-commit-badge]][pre-commit-link] [![Code style: black][black-badge]][black-link] - [![PyPI version][pypi-version]][pypi-link] [![Conda-Forge][conda-badge]][conda-link] [![PyPI platforms][pypi-platforms]][pypi-link] @@ -19,7 +18,7 @@ Hist is an analyst-friendly front-end for [boost-histogram](https://github.com/scikit-hep/boost-histogram), designed for -Python 3.7+ (3.6 users get version 2.3). See [what's new](https://hist.readthedocs.io/en/latest/changelog.html). +Python 3.7+ (3.6 users get version 2.4). See [what's new](https://hist.readthedocs.io/en/latest/changelog.html). ## Installation @@ -110,7 +109,7 @@ From a git checkout, run: python -m pip install -e .[dev] ``` -See [CONTRIBUTING.md](./.github/CONTRIBUTING.md) for information on setting up a development environment. +See [Contributing](https://hist.readthedocs.io/en/latest/contributing.html) guidelines for information on setting up a development environment. ## Contributors @@ -130,6 +129,8 @@ We would like to acknowledge the contributors that made this project possible ([
Kyle Cranmer

📖 +
Daniel Antrim

💻 +
Nicholas Smith

💻 @@ -138,14 +139,12 @@ We would like to acknowledge the contributors that made this project possible ([ - - This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. ## Talks -* [2020-07-07 SciPy Proceedings](https://www.youtube.com/watch?v=ERraTfHkPd0&list=PLYx7XA2nY5GfY4WWJjG5cQZDc7DIUmn6Z&index=4) -* [2020-07-17 PyHEP2020](https://indico.cern.ch/event/882824/contributions/3931299/) +- [2020-07-07 SciPy Proceedings](https://www.youtube.com/watch?v=ERraTfHkPd0&list=PLYx7XA2nY5GfY4WWJjG5cQZDc7DIUmn6Z&index=4) +- [2020-07-17 PyHEP2020](https://indico.cern.ch/event/882824/contributions/3931299/) --- @@ -155,7 +154,6 @@ This library was primarily developed by Henry Schreiner and Nino Lau. Support for this work was provided by the National Science Foundation cooperative agreement OAC-1836650 (IRIS-HEP) and OAC-1450377 (DIANA/HEP). Any opinions, findings, conclusions or recommendations expressed in this material are those of the authors and do not necessarily reflect the views of the National Science Foundation. - [actions-badge]: https://github.com/scikit-hep/hist/workflows/CI/badge.svg [actions-link]: https://github.com/scikit-hep/hist/actions [black-badge]: https://img.shields.io/badge/code%20style-black-000000.svg diff --git a/docs/_images/axis_category.png b/docs/_images/axis_category.png new file mode 100644 index 00000000..10c937a0 Binary files /dev/null and b/docs/_images/axis_category.png differ diff --git a/docs/_images/axis_circular.png b/docs/_images/axis_circular.png new file mode 100644 index 00000000..bb158448 Binary files /dev/null and b/docs/_images/axis_circular.png differ diff --git a/docs/_images/axis_integer.png b/docs/_images/axis_integer.png new file mode 100644 index 00000000..cb3e3f3e Binary files /dev/null and b/docs/_images/axis_integer.png differ diff --git a/docs/_images/axis_regular.png b/docs/_images/axis_regular.png new file mode 100644 index 00000000..a556c49a Binary files /dev/null and b/docs/_images/axis_regular.png differ diff --git a/docs/_images/axis_variable.png b/docs/_images/axis_variable.png new file mode 100644 index 00000000..60fc1b79 Binary files /dev/null and b/docs/_images/axis_variable.png differ diff --git a/docs/_images/ex_hist_density.png b/docs/_images/ex_hist_density.png new file mode 100644 index 00000000..37afdbe4 Binary files /dev/null and b/docs/_images/ex_hist_density.png differ diff --git a/docs/_images/histogram_design.png b/docs/_images/histogram_design.png new file mode 100644 index 00000000..a08a079f Binary files /dev/null and b/docs/_images/histogram_design.png differ diff --git a/docs/_src/images/.gitignore b/docs/_src/images/.gitignore new file mode 100644 index 00000000..ecf5e870 --- /dev/null +++ b/docs/_src/images/.gitignore @@ -0,0 +1,2 @@ +*.pdf +*.png diff --git a/docs/_src/images/Makefile b/docs/_src/images/Makefile new file mode 100644 index 00000000..893bed72 --- /dev/null +++ b/docs/_src/images/Makefile @@ -0,0 +1,14 @@ +SRC = $(wildcard *.tex) +PDFs = $(patsubst %.tex, %.pdf, $(SRC)) +PNGs = $(patsubst %.tex, %.png, $(SRC)) + +.PHONY: all + +all: $(PNGs) + +%.pdf: %.tex Makefile + pdflatex $< + @rm $*.aux $*.log + +%.png: %.pdf + convert -density 150 $< -quality 95 -colorspace Gray $@ diff --git a/docs/_src/images/axis_category.tex b/docs/_src/images/axis_category.tex new file mode 100644 index 00000000..507a1366 --- /dev/null +++ b/docs/_src/images/axis_category.tex @@ -0,0 +1,39 @@ +\documentclass{article} +\usepackage{tikz} +\usetikzlibrary{arrows, calc, decorations.pathreplacing} +\usepackage{units} +\usepackage[graphics, active, tightpage]{preview} +\usepackage{pgfplots} +\usepackage{amsmath} +\usepackage[outline]{contour} +\contourlength{1.2pt} +\usepackage{ocr} +\PreviewEnvironment{tikzpicture} + +\pgfplotsset{compat=1.16} + +\definecolor{presDark}{RGB}{39,1,136} +\definecolor{presDark2}{RGB}{87,80,149} +\definecolor{presLight}{RGB}{139,131,215} +\definecolor{presLight2}{RGB}{0,129,203} + +\definecolor{presHighlight}{RGB}{215,50,50} + +\begin{document} + +\begin{tikzpicture}[ + xscale=4, + every path/.style={thick}] +\draw (0,0) -- (1,0); +\foreach \i in {0, .2, ..., 1} { + \draw (\i,0) -- (\i,.2); +} +\node at (.1, 0) [above] {2}; +\node at (.3, 0) [above] {5}; +\node at (.5, 0) [above] {8}; +\node at (.7, 0) [above] {3}; +\node at (.9, 0) [above] {7}; +\node at (.5,0) [below] {\verb|hist.axis.IntCategory([2,5,8,3,7])|}; +\end{tikzpicture} + +\end{document} diff --git a/docs/_src/images/axis_circular.tex b/docs/_src/images/axis_circular.tex new file mode 100644 index 00000000..a5ff07e4 --- /dev/null +++ b/docs/_src/images/axis_circular.tex @@ -0,0 +1,37 @@ +\documentclass{article} +\usepackage{tikz} +\usetikzlibrary{arrows, calc, decorations.pathreplacing} +\usepackage{units} +\usepackage[graphics, active, tightpage]{preview} +\usepackage{pgfplots} +\usepackage{amsmath} +\usepackage[outline]{contour} +\contourlength{1.2pt} +\usepackage{ocr} +\PreviewEnvironment{tikzpicture} + +\pgfplotsset{compat=1.16} + +\definecolor{presDark}{RGB}{39,1,136} +\definecolor{presDark2}{RGB}{87,80,149} +\definecolor{presLight}{RGB}{139,131,215} +\definecolor{presLight2}{RGB}{0,129,203} + +\definecolor{presHighlight}{RGB}{215,50,50} + +\begin{document} + +\begin{tikzpicture}[ + every path/.style={thick}] +\draw (0,0) circle (.75); +\foreach \i in {0, 45, ..., 360} { + \draw (\i:.75) -- (\i:.95); +} +\node at (0:.95) [right] {$\pi/2$}; +\node at (90:.95) [above] {0, $2\pi$}; +\node at (180:.95) [left] {$\pi$}; +\node at (270:.95) [below] {$3\pi/3$}; +\node at (0,-1.3) [below] {\verb|hist.axis.Regular(8,0,2*np.pi, circular=True)|}; +\end{tikzpicture} + +\end{document} diff --git a/docs/_src/images/axis_integer.tex b/docs/_src/images/axis_integer.tex new file mode 100644 index 00000000..01803492 --- /dev/null +++ b/docs/_src/images/axis_integer.tex @@ -0,0 +1,37 @@ +\documentclass{article} +\usepackage{tikz} +\usetikzlibrary{arrows, calc, decorations.pathreplacing} +\usepackage{units} +\usepackage[graphics, active, tightpage]{preview} +\usepackage{pgfplots} +\usepackage{amsmath} +\usepackage[outline]{contour} +\contourlength{1.2pt} +\usepackage{ocr} +\PreviewEnvironment{tikzpicture} + +\pgfplotsset{compat=1.16} + +\definecolor{presDark}{RGB}{39,1,136} +\definecolor{presDark2}{RGB}{87,80,149} +\definecolor{presLight}{RGB}{139,131,215} +\definecolor{presLight2}{RGB}{0,129,203} + +\definecolor{presHighlight}{RGB}{215,50,50} + +\begin{document} + +\begin{tikzpicture}[ + xscale=4, + every path/.style={thick}] +\draw (0,0) -- (1,0); +\foreach \i in {0, .2, ..., 1} { + \draw (\i,0) -- (\i,.2); +} +\foreach \i in {0,...,4} { + \node at (\i/5 + .1, 0) [above] {\i}; +} +\node at (.5,0) [below] {\verb|hist.axis.Integer(0,5)|}; +\end{tikzpicture} + +\end{document} diff --git a/docs/_src/images/axis_regular.tex b/docs/_src/images/axis_regular.tex new file mode 100644 index 00000000..ce621c75 --- /dev/null +++ b/docs/_src/images/axis_regular.tex @@ -0,0 +1,37 @@ +\documentclass{article} +\usepackage{tikz} +\usetikzlibrary{arrows, calc, decorations.pathreplacing} +\usepackage{units} +\usepackage[graphics, active, tightpage]{preview} +\usepackage{pgfplots} +\usepackage{amsmath} +\usepackage[outline]{contour} +\contourlength{1.2pt} +\usepackage{ocr} +\PreviewEnvironment{tikzpicture} + +\pgfplotsset{compat=1.16} + +\definecolor{presDark}{RGB}{39,1,136} +\definecolor{presDark2}{RGB}{87,80,149} +\definecolor{presLight}{RGB}{139,131,215} +\definecolor{presLight2}{RGB}{0,129,203} + +\definecolor{presHighlight}{RGB}{215,50,50} + +\begin{document} + +\begin{tikzpicture}[ + xscale=4, + every path/.style={thick}] +\draw (0,0) -- (1,0); +\foreach \i in {0, .1, ..., 1.1} { + \draw (\i,0) -- (\i,.2); +} +\node at (0,.2) [above] {0}; +\node at (.5,.2) [above] {0.5}; +\node at (1,.2) [above] {1}; +\node at (.5,0) [below] {\verb|hist.axis.Regular(10,0,1)|}; +\end{tikzpicture} + +\end{document} diff --git a/docs/_src/images/axis_variable.tex b/docs/_src/images/axis_variable.tex new file mode 100644 index 00000000..27dcb346 --- /dev/null +++ b/docs/_src/images/axis_variable.tex @@ -0,0 +1,38 @@ +\documentclass{article} +\usepackage{tikz} +\usetikzlibrary{arrows, calc, decorations.pathreplacing} +\usepackage{units} +\usepackage[graphics, active, tightpage]{preview} +\usepackage{pgfplots} +\usepackage{amsmath} +\usepackage[outline]{contour} +\contourlength{1.2pt} +\usepackage{ocr} +\PreviewEnvironment{tikzpicture} + +\pgfplotsset{compat=1.16} + +\definecolor{presDark}{RGB}{39,1,136} +\definecolor{presDark2}{RGB}{87,80,149} +\definecolor{presLight}{RGB}{139,131,215} +\definecolor{presLight2}{RGB}{0,129,203} + +\definecolor{presHighlight}{RGB}{215,50,50} + +\begin{document} + +\begin{tikzpicture}[ + xscale=4, + every path/.style={thick}] +\draw (0,0) -- (1,0); +\foreach \i in {0, .3, .5, 1} { + \draw (\i,0) -- (\i,.2); +} +\node at (0,.2) [above] {0}; +\node at (.3,.2) [above] {0.3}; +\node at (.5,.2) [above] {0.5}; +\node at (1,.2) [above] {1}; +\node at (.5,0) [below] {\verb|hist.axis.Variable([0,.3,.5,1])|}; +\end{tikzpicture} + +\end{document} diff --git a/docs/changelog.rst b/docs/changelog.rst index c7226173..e55bf980 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,65 @@ Changelog ==================== +Version 2.5.0 +-------------------- + +* Dropped Python 3.6 support. + `#194 `_ + +* Add ``"efficiency"`` ``uncertainty_type`` option for ``ratio_plot`` API. + `#266 `_ + `#278 `_ + +* Improve and clarify treatment of confidence intervals in ``intervals`` submodule. + `#281 `_ + + +Version 2.4.0 +-------------------- + +* Support ``.stack(axis)`` and stacked histograms. + `#244 `_ + `#257 `_ + `#258 `_ + +* Support selection lists (experimental with boost-histogram 1.1.0). + `#255 `_ + +* Support full names for QuickConstruct, and support mistaken usage in constructor. + `#256 `_ + +* Add ``.sort(axis)`` for quickly sorting a categorical axis. + `#243 `_ + + +Smaller features or fixes: + +* Support nox for easier contributor setup. + `#228 `_ + +* Better name axis error. + `#232 `_ + +* Fix for issue plotting size 0 axes. + `#238 `_ + +* Fix issues with repr information missing. + `#241 `_ + +* Fix issues with wrong plot shortcut being triggered by Integer axes. + `#247 `_ + +* Warn and better error if overlapping keyword used as axis name. + `#250 `_ + +Along with lots of smaller docs updates. + + + + + + Version 2.3.0 -------------------- diff --git a/docs/conf.py b/docs/conf.py index 2ea652b5..35518219 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,13 +4,22 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +from __future__ import annotations + # Warning: do not change the path here. To use autodoc, you need to install the # package first. - -from typing import List +import os +import shutil +import sys +from pathlib import Path from pkg_resources import get_distribution +DIR = Path(__file__).parent.resolve() +BASEDIR = DIR.parent + +sys.path.append(str(BASEDIR / "src/hist")) + # -- Project information ----------------------------------------------------- project = "Hist" @@ -30,6 +39,7 @@ "sphinx.ext.mathjax", "sphinx.ext.napoleon", "sphinx_copybutton", + "myst_parser", ] # Add any paths that contain templates here, relative to this directory. @@ -63,7 +73,7 @@ # 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: List[str] = [] +html_static_path: list[str] = [] # -- Options for Notebook input ---------------------------------------------- @@ -81,3 +91,24 @@ ] nbsphinx_kernel_name = "python3" + + +def prepare(app): + outer = BASEDIR / ".github" + inner = DIR + contributing = "CONTRIBUTING.md" + shutil.copy(outer / contributing, inner / "contributing.md") + + +def clean_up(app, exception): + inner = DIR + os.unlink(inner / "contributing.md") + + +def setup(app): + + # Copy the file in + app.connect("builder-inited", prepare) + + # Clean up the generated file + app.connect("build-finished", clean_up) diff --git a/docs/development.rst b/docs/development.rst deleted file mode 100644 index f0e59235..00000000 --- a/docs/development.rst +++ /dev/null @@ -1,30 +0,0 @@ -Development -=========================== - -We welcome you to contribute to this project. If you want to develop this package, you can -use the following methods. - -Pip ------------------------- - -You can set up a development environment using pip. - -.. code-block:: bash - - python3 -m venv .env # Make a new environment in ./.env/ - source .env/bin/activate # Use the new environment - (.env)$ pip install -e .[dev] - (.env)$ python -m ipykernel install --user --name hist - -*You should have pip 10 or later*. - -Conda -------------------------- - -You can also set up a development environment using Conda. With Conda, you can search some channels for development. - -.. code-block:: bash - - $ conda env create -f dev-environment.yml -n hist - $ conda activate hist - (hist)$ python -m ipykernel install --name hist diff --git a/docs/examples/index.rst b/docs/examples/index.rst deleted file mode 100644 index 4d5ff29c..00000000 --- a/docs/examples/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. _examples: - -Examples -======== - -.. toctree:: - :maxdepth: 2 - :titlesonly: - :glob: - - HistDemo diff --git a/docs/index.rst b/docs/index.rst index bc0c8876..25d50c27 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,29 +1,58 @@ +.. image:: _images/histlogo.png + :width: 60% + :alt: Hist logo + :align: center Welcome to Hist's documentation! ================================ +|Actions Status| |Documentation Status| |pre-commit.ci Status| |Code style: black| |PyPI version| +|Conda-Forge| |PyPI platforms| |DOI| |GitHub Discussion| |Gitter| |Scikit-HEP| Introduction ------------ `Hist `_ is a powerful Histogramming tool for analysis based on `boost-histogram `_ (the Python binding of the Histogram library in Boost). It is a friendly analysis-focused project that uses `boost-histogram `_ as a backend to do the work, but provides plotting tools, shortcuts, and new ideas. -To get an idea of creating histograms in Hist looks like, you can take a look at the :doc:`Examples `. Once you have a feel for what is involved in using Hist, we recommend you start by following the instructions in :doc:`Installation `. Then, go through the :doc:`User Guide `, and read the :doc:`Reference ` documentation. We value your contributions and you can follow the instructions in :doc:`Development `. Finally, if you’re having problems, please do let us know at our :doc:`Support ` page. +To get an idea of creating histograms in Hist looks like, you can take a look at the :doc:`Examples `. Once you have a feel for what is involved in using Hist, we recommend you start by following the instructions in :doc:`Installation `. Then, go through the :doc:`User Guide `, and read the :doc:`Reference ` documentation. We value your contributions and you can follow the instructions in :doc:`Contributing `. Finally, if you’re having problems, please do let us know at our :doc:`Support ` page. .. toctree:: :maxdepth: 2 :titlesonly: - :caption: Contents + :caption: User Guide :glob: installation - user-guide/index - examples/index - development + user-guide/quickstart + user-guide/axes + user-guide/storages + user-guide/accumulators + user-guide/notebooks/Transform + user-guide/notebooks/Reprs + user-guide/notebooks/Plots + user-guide/analyses + user-guide/notebooks/Histogram + user-guide/notebooks/Stack + +.. toctree:: + :maxdepth: 2 + :titlesonly: + :caption: Developers + :glob: + + contributing support changelog +.. toctree:: + :maxdepth: 2 + :titlesonly: + :caption: Examples + :glob: + + examples/HistDemo + .. toctree:: :maxdepth: 2 :titlesonly: @@ -40,3 +69,26 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` + +.. |Actions Status| image:: https://github.com/scikit-hep/hist/workflows/CI/badge.svg + :target: https://github.com/scikit-hep/hist/actions +.. |Documentation Status| image:: https://readthedocs.org/projects/hist/badge/?version=latest + :target: https://hist.readthedocs.io/en/latest/?badge=latest +.. |pre-commit.ci Status| image:: https://results.pre-commit.ci/badge/github/scikit-hep/hist/main.svg + :target: https://results.pre-commit.ci/repo/github/scikit-hep/hist +.. |Code style: black| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black +.. |PyPI version| image:: https://badge.fury.io/py/hist.svg + :target: https://pypi.org/project/hist/ +.. |Conda-Forge| image:: https://img.shields.io/conda/vn/conda-forge/hist + :target: https://github.com/conda-forge/hist-feedstock +.. |PyPI platforms| image:: https://img.shields.io/pypi/pyversions/hist + :target: https://pypi.org/project/hist/ +.. |DOI| image:: https://zenodo.org/badge/239605861.svg + :target: https://zenodo.org/badge/latestdoi/239605861 +.. |GitHub Discussion| image:: https://img.shields.io/static/v1?label=Discussions&message=Ask&color=blue&logo=github + :target: https://github.com/scikit-hep/hist/discussions +.. |Gitter| image:: https://badges.gitter.im/HSF/PyHEP-histogramming.svg + :target: https://gitter.im/HSF/PyHEP-histogramming?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge +.. |Scikit-HEP| image:: https://scikit-hep.org/assets/images/Scikit--HEP-Project-blue.svg + :target: https://scikit-hep.org/ diff --git a/docs/installation.rst b/docs/installation.rst index 098f277a..fd120bd5 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,10 +1,21 @@ Installation =========================== -Hist is now available on PyPI. You can install this library from PyPI with pip: +Hist is available on PyPI. You can install this library from PyPI with pip: .. code-block:: bash python3 -m pip install "hist[plot]" If you do not need the plotting features, you can skip the ``[plot]`` extra. + +You can also install it with Conda from conda-forge. + +.. code-block:: bash + + conda install -c conda-forge hist + + +Supported platforms are identical to +`boost-histogram `_, +though for the latest version of Hist you need Python 3.7+. diff --git a/docs/reference/hist.rst b/docs/reference/hist.rst index 4ae78a58..26d6054e 100644 --- a/docs/reference/hist.rst +++ b/docs/reference/hist.rst @@ -76,6 +76,14 @@ hist.numpy module :undoc-members: :show-inheritance: +hist.stack module +------------------- + +.. automodule:: hist.stack + :members: + :undoc-members: + :show-inheritance: + hist.storage module ------------------- diff --git a/docs/user-guide/accumulators.rst b/docs/user-guide/accumulators.rst new file mode 100644 index 00000000..d2924c52 --- /dev/null +++ b/docs/user-guide/accumulators.rst @@ -0,0 +1,162 @@ +.. _usage-accumulators: + +Accumulators +============ + +Common properties +----------------- + +All accumulators can be filled like a histogram. You just call `.fill` with +values, and this looks and behaves like filling a single-bin or "scalar" +histogram. Like histograms, the fill is inplace. + +All accumulators have a `.value` property as well, which gives the primary +value being accumulated. + +Types +----- + +There are several accumulators. + +Sum +^^^ + +This is the simplest accumulator, and is never returned from a histogram. This +is internally used by the Double and Unlimited storages to perform sums when +needed. It uses a highly accurate Neumaier sum to compute the floating point +sum with a correction term. Since this accumulator is never returned by a +histogram, it is not available in a view form, but only as a single accumulator +for comparison and access to the algorithm. Usage example in Python 3.8, +showing how non-accurate sums fail to produce the obvious answer, 2.0:: + + import math + import numpy as np + import hist + + values = [1.0, 1e100, 1.0, -1e100] + print(f"{sum(values) = } (simple)") + print(f"{math.fsum(values) = }") + print(f"{np.sum(values) = } (pairwise)") + print(f"{hist.accumulators.Sum().fill(values) = }") + +.. code:: text + + sum(values) = 0.0 (simple) + math.fsum(values) = 2.0 + np.sum(values) = 0.0 (pairwise) + hist.accumulators.Sum().fill(values) = Sum(0 + 2) + + +Note that this is still intended for performance and does not guarantee +correctness as ``math.fsum`` does. In general, you must not have more than two +orders of values:: + + values = [1., 1e100, 1e50, 1., -1e50, -1e100] + print(f"{math.fsum(values) = }") + print(f"{hist.accumulators.Sum().fill(values) = }") + +.. code:: text + + math.fsum(values) = 2.0 + hist.accumulators.Sum().fill(values) = Sum(0 + 0) + +You should note that this is a highly contrived example and the Sum accumulator +should still outperform simple and pairwise summation methods for a minimal +performance cost. Most notably, you have to have large cancellations with +negative values, which histograms generally do not have. + +You can use ``+=`` with a float value or a Sum to fill as well. + +WeightedSum +^^^^^^^^^^^ + +This accumulator is contained in the Weight storage, and supports Views. It +provides two values; ``.value``, and ``.variance``. The value is the sum of the +weights, and the variance is the sum of the squared weights. + +For example, you could sum the following values:: + + import hist + + values = [10]*10 + smooth = hist.accumulators.WeightedSum().fill(values) + print(f"{smooth = }") + + values = [1]*9 + [91] + rough = hist.accumulators.WeightedSum().fill(values) + print(f"{rough = }") + +.. code:: text + + smooth = WeightedSum(value=100, variance=1000) + rough = WeightedSum(value=100, variance=8290) + +When filling, you can optionally provide a ``variance=`` keyword, with either a +single value or a matching length array of values. + +You can also fill with ``+=`` on a value or another WeighedSum. + +Mean +^^^^ + +This accumulator is contained in the Mean storage, and supports Views. It +provides three values; ``.count``, ``.value``, and ``.variance``. Internally, +the variance is stored as ``_sum_of_deltas_squared``, which is used to compute +``variance``. + +For example, you could compute the mean of the following values:: + + import hist + + values = [10]*10 + smooth = hist.accumulators.Mean().fill(values) + print(f"{smooth = }") + + values = [1]*9 + [91] + rough = hist.accumulators.Mean().fill(values) + print(f"{rough = }") + +.. code:: text + + smooth = Mean(count=10, value=10, variance=0) + rough = Mean(count=10, value=10, variance=810) + +You can add a `weight=` keyword when filling, with either a single value +or a matching length array of values. + +You can call a Mean with a value or with another Mean to fill inplace, as well. + +WeightedMean +^^^^^^^^^^^^ + +This accumulator is contained in the WeightedMean storage, and supports Views. +It provides four values; ``.sum_of_weights``, ``sum_of_weights_squared``, +``.value``, and ``.variance``. Internally, the variance is stored as +``_sum_of_weighted_deltas_squared``, which is used to compute ``variance``. + +For example, you could compute the mean of the following values:: + + import hist + + values = [1]*9 + [91] + wm = hist.accumulators.WeightedMean().fill(values, weight=2) + print(f"{wm = }") + +.. code:: text + + wm = WeightedMean(sum_of_weights=20, sum_of_weights_squared=40, value=10, variance=810) + +You can add a `weight=` keyword when filling, with either a single value or a +matching length array of values. + +You can call a WeightedMean with a value or with another WeightedMean to fill +inplace, as well. + +Views +----- + +Most of the accumulators (except Sum) support a View. This is what is returned from +a histogram when ``.view()`` is requested. This is a structured Numpy ndarray, with a few small +additions to make them easier to work with. Like a Numpy recarray, you can access the fields with +attributes; you can even access (but not set) computed attributes like ``.variance``. A view will +also return an accumulator instance if you select a single item. diff --git a/docs/user-guide/analyses.rst b/docs/user-guide/analyses.rst new file mode 100644 index 00000000..49bc618e --- /dev/null +++ b/docs/user-guide/analyses.rst @@ -0,0 +1,42 @@ +.. _usage-analyses: + +Analyses examples +================= + +Bool and category axes +---------------------- + +Taken together, the flexibility in axes and the tools to easily sum over +axes can be applied to transform the way you approach analysis with +histograms. For example, let’s say you are presented with the following +data in a 3xN table: + +============== ======================== +Data Details +============== ======================== +``value`` +``is_valid`` True or False +``run_number`` A collection of integers +============== ======================== + +In a traditional analysis, you might bin over ``value`` where +``is_valid`` is True, and then make a collection of histograms, one for +each run number. With hist, you can make a single histogram, +and use an axis for each: + +.. code:: python3 + + value_ax = hist.axis.Regular(100, -5, 5) + bool_ax = hist.axis.Integer(0, 2, underflow=False, overflow=False) + run_number_ax = hist.axis.IntCategory([], growth=True) + +Now, you can use these axes to create a single histogram that you can +fill. If you want to get a histogram of all run numbers and just the +True ``is_valid`` selection, you can use a ``sum``: + +.. code:: python3 + + h1 = hist[:, True, sum] + +You can expand this example to any number of dimensions, boolean flags, +and categories. diff --git a/docs/user-guide/axes.rst b/docs/user-guide/axes.rst new file mode 100644 index 00000000..ea288a19 --- /dev/null +++ b/docs/user-guide/axes.rst @@ -0,0 +1,214 @@ +.. _usage-axes: + +Axes +==== + +In hist, a histogram is collection of Axis objects and a +storage. Based on `boost-histogram `_’s +Axis, hist support six types of axis, ``Regular``, ``Boolean``, ``Variable``, ``Integer``, ``IntCategory`` +and ``StrCategory`` with additional names and labels. + +Axis names +---------- + +Names are pretty useful for some histogramming shortcuts, thus +greatly facilitate HEP’s studies. Note that the name is the identifier +for an axis in a histogram and must be unique. + +.. code:: python3 + + import hist + from hist import Hist + +.. code:: python3 + + axis0 = hist.axis.Regular(10, -5, 5, overflow=False, underflow=False, name="A") + axis1 = hist.axis.Boolean(name="B") + axis2 = hist.axis.Variable(range(10), name="C") + axis3 = hist.axis.Integer(-5, 5, overflow=False, underflow=False, name="D") + axis4 = hist.axis.IntCategory(range(10), name="E") + axis5 = hist.axis.StrCategory(["T", "F"], name="F") + +Histogram’s Axis +---------------- + +Histogram is consisted with various axes, there are two ways to create a histogram, +currently. You can either fill a histogram object with axes or add axes to a +histogram object. You cannot add axes to an existing histogram. *Note that to distinguish +these two method, the second way has different axis type names (abbr.).* + +.. code:: python3 + + # fill the axes + h = Hist(axis0, axis1, axis2, axis3, axis4, axis5) + +.. code:: python3 + + # add the axes using the shortcut method + h = ( + Hist.new.Reg(10, -5, 5, overflow=False, underflow=False, name="A") + .Bool(name="B") + .Var(range(10), name="C") + .Int(-5, 5, overflow=False, underflow=False, name="D") + .IntCat(range(10), name="E") + .StrCat(["T", "F"], name="F") + .Double() + ) + +Hist adds a new ``flow=False`` shortcut to axes that take ``underflow`` and ``overflow``. + +AxesTuple is a new feature since boost-histogram 0.8.0, which provides you free access to axis properties in a histogram. + +.. code:: python3 + + assert h.axes[0].name == axis0.name + assert h.axes[1].label == axis1.name # label will be returned as name if not provided + assert all(h.axes[2].widths == axis2.widths) + assert all(h.axes[3].edges == axis3.edges) + assert h.axes[4].metadata == axis4.metadata + assert all(h.axes[5].centers == axis5.centers) + + +Axis types +---------- + +There are several axis types to choose from. + +Regular axis +^^^^^^^^^^^^ + +.. image:: ../_images/axis_regular.png + :alt: Regular axis illustration + :align: center + +.. py:function:: hist.axis.Regular(bins, start, stop, name, label, *, metadata="", underflow=True, overflow=True, circular=False, growth=False, transform=None) + :noindex: + +The regular axis can have overflow and/or underflow bins (enabled by +default). It can also grow if ``growth=True`` is given. In general, you +should not mix options, as growing axis will already have the correct +flow bin settings. The exception is ``underflow=False, overflow=False``, which +is quite useful together to make an axis with no flow bins at all. + +There are some other useful axis types based on regular axis: + +.. image:: ../_images/axis_circular.png + :alt: Regular axis illustration + :align: center + +.. py:function:: hist.axis.Regular(..., circular=True) + :noindex: + + This wraps around, so that out-of-range values map back into the valid range circularly. + +Regular axis: Transforms +"""""""""""""""""""""""" + +Regular axes support transforms, as well; these are functions that convert from an external, +non-regular bin spacing to an internal, regularly spaced one. A transform is made of two functions, +a ``forward`` function, which converts external to internal (and for which the transform is usually named), +and a ``inverse`` function, which converts from the internal space back to the external space. If you +know the functional form of your spacing, you can get the benefits of a constant performance scaling +just like you would with a normal regular axis, rather than falling back to a variable axis and a poorer +scaling from the bin edge lookup required there. + +You can define your own functions for transforms, see :ref:`usage-transforms`. If you use compiled/numba +functions, you can keep the high performance you would expect from a Regular axis. There are also several +precompiled transforms: + +.. py:function:: hist.axis.Regular(..., transform=hist.axis.transform.sqrt) + :noindex: + + This is an axis with bins transformed by a sqrt. + +.. py:function:: hist.axis.Regular(..., transform=hist.axis.transform.log) + :noindex: + + Transformed by log. + +.. py:function:: hist.axis.Regular(..., transform=hist.axis.transform.Power(v)) + :noindex: + + Transformed by a power (the argument is the power). + + +Variable axis +^^^^^^^^^^^^^ + +.. image:: ../_images/axis_variable.png + :alt: Regular axis illustration + :align: center + +.. py:function:: hist.axis.Variable([edge1, ...], name, label, *, metadata="", underflow=True, overflow=True, circular=False, growth=False) + :noindex: + + You can set the bin edges explicitly with a variable axis. The options are mostly the same as the Regular axis. + +Integer axis +^^^^^^^^^^^^ + +.. image:: ../_images/axis_integer.png + :alt: Regular axis illustration + :align: center + +.. py:function:: hist.axis.Integer(start, stop, name, label, *, metadata="", underflow=True, overflow=True, circular=False, growth=False) + :noindex: + + This could be mimicked with a regular axis, but is simpler and slightly faster. Bins are whole integers only, + so there is no need to specify the number of bins. + +One common use for an integer axis could be a true/false axis: + +.. code:: python3 + + bool_axis = hist.axis.Integer(0, 2, underflow=False, overflow=False) + + +Another could be for an IntEnum (Python 3 or backport) if the values are contiguous. + +Category axis +^^^^^^^^^^^^^ + +.. image:: ../_images/axis_category.png + :alt: Regular axis illustration + :align: center + +.. py:function:: hist.axis.IntCategory([value1, ...], name, label, metadata="", grow=False) + :noindex: + + You should put integers in a category axis; but unlike an integer axis, the integers do not need to be adjacent. + +One use for an IntCategory axis is for an IntEnum: + +.. code:: python3 + + import enum + + class MyEnum(enum.IntEnum): + a = 1 + b = 5 + + my_enum_axis = hist.axis.IntEnum(list(MyEnum), underflow=False, overflow=False) + + +You can sort the Categorty axes via ``.sort()`` method: + +.. code:: python3 + + h = Hist(axis.IntCategory([3, 1, 2], label="Number"), axis.StrCategory(["Teacher", "Police", "Artist"], label="Profession")) + h.sort(0).axes[0] # IntCategory([1, 2, 3], label='Number') + h.sort(1, reverse=True).axes[1] # StrCategory(['Teacher', 'Police', 'Artist'], label='Profession') + + +.. py:function:: hist.axis.StrCategory([str1, ...], name, label, metadata="", grow=False) + :noindex: + + You can put strings in a category axis as well. The fill method supports lists or arrays of strings + to allow this to be filled. + +Manipulating Axes +----------------- + +Axes have a variety of methods and properties that are useful. When inside a histogram, you can also access +these directly on the ``hist.axes`` object, and they return a tuple of valid results. If the property or method +normally returns an array, the ``axes`` version returns a broadcasting-ready version in the output tuple. diff --git a/docs/user-guide/index.rst b/docs/user-guide/index.rst deleted file mode 100644 index 74ce499b..00000000 --- a/docs/user-guide/index.rst +++ /dev/null @@ -1,16 +0,0 @@ -.. _userguide: - -User Guide -========== - -.. toctree:: - :maxdepth: 2 - :titlesonly: - :glob: - - notebooks/Axis - notebooks/Storage - notebooks/Transform - notebooks/Reprs - notebooks/Plots - notebooks/Histogram diff --git a/docs/user-guide/notebooks/Axis.ipynb b/docs/user-guide/notebooks/Axis.ipynb deleted file mode 100644 index 01be2631..00000000 --- a/docs/user-guide/notebooks/Axis.ipynb +++ /dev/null @@ -1,132 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Axis\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Axis Types\n", - "\n", - "Based on [boost-histogram](https://github.com/scikit-hep/boost-histogram)'s Axis, hist support six types of axis, `Regular`, `Boolean`, `Variable`, `Integer`, `IntCategory` and `StrCategory` with additional names and labels. \n", - "\n", - "Names are pretty useful for some histogramming shortcuts, thus greatly facilitate HEP's studies. Note that the name is the identifier for an axis in a histogram and must be unique." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import hist\n", - "from hist import Hist" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "axis0 = hist.axis.Regular(10, -5, 5, overflow=False, underflow=False, name=\"A\")\n", - "axis1 = hist.axis.Boolean(name=\"B\")\n", - "axis2 = hist.axis.Variable(range(10), name=\"C\")\n", - "axis3 = hist.axis.Integer(-5, 5, overflow=False, underflow=False, name=\"D\")\n", - "axis4 = hist.axis.IntCategory(range(10), name=\"E\")\n", - "axis5 = hist.axis.StrCategory([\"T\", \"F\"], name=\"F\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Histogram's Axis\n", - "\n", - "Histogram is consisted with various axes, there are two ways to create a histogram, currently. You can either fill a histogram object with axes or add axes to a histogram object. You cannot add axes to an existing histogram. *Note that to distinguish these two method, the second way has different axis type names (abbr.).*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# fill the axes\n", - "h = Hist(axis0, axis1, axis2, axis3, axis4, axis5)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# add the axes using the shortcut method\n", - "h = (\n", - " Hist.new.Reg(10, -5, 5, overflow=False, underflow=False, name=\"A\")\n", - " .Bool(name=\"B\")\n", - " .Var(range(10), name=\"C\")\n", - " .Int(-5, 5, overflow=False, underflow=False, name=\"D\")\n", - " .IntCat(range(10), name=\"E\")\n", - " .StrCat([\"T\", \"F\"], name=\"F\")\n", - " .Double()\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Hist adds a new `flow=False` shortcut to axes that take `underflow` and `overflow`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "AxesTuple is a new feature since boost-histogram 0.8.0, which provides you free access to axis properties in a histogram." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "assert h.axes[0].name == axis0.name\n", - "assert h.axes[1].label == axis1.name # label will be returned as name if not provided\n", - "assert all(h.axes[2].widths == axis2.widths)\n", - "assert all(h.axes[3].edges == axis3.edges)\n", - "assert h.axes[4].metadata == axis4.metadata\n", - "assert all(h.axes[5].centers == axis5.centers)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "hist", - "language": "python", - "name": "hist" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.4" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/user-guide/notebooks/Histogram.ipynb b/docs/user-guide/notebooks/Histogram.ipynb index 582f6f4f..52e8719e 100644 --- a/docs/user-guide/notebooks/Histogram.ipynb +++ b/docs/user-guide/notebooks/Histogram.ipynb @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -55,7 +55,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -81,7 +81,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -111,9 +111,2558 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "\n", + "\n", + "-5\n", + "\n", + "\n", + "5\n", + "\n", + "\n", + "-5\n", + "\n", + "\n", + "5\n", + "\n", + "\n", + "s [units]\n", + "\n", + "\n", + "w [units]\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + "
\n", + "Regular(50, -5, 5, name='S', label='s [units]')
\n", + "Regular(50, -5, 5, name='W', label='w [units]')
\n", + "
\n", + "Double() Σ=50000.0\n", + "\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + "Hist(\n", + " Regular(50, -5, 5, name='S', label='s [units]'),\n", + " Regular(50, -5, 5, name='W', label='w [units]'),\n", + " storage=Double()) # Sum: 50000.0" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "import numpy as np\n", "\n", @@ -141,9 +2690,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "291.0" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Access by bin number\n", "h[25, 25]" @@ -151,9 +2711,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "291.0" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Access by data coordinate\n", "# Identical to: h[hist.loc(0), hist.loc(0)]\n", @@ -162,9 +2733,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "291.0" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Identical to: h[hist.loc(-1) + 5, hist.loc(-4) + 20]\n", "h[-1j + 5, -4j + 20]" @@ -179,9 +2761,47 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "\n", + "\n", + "\n", + "-1\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "s [units]\n", + "\n", + "\n", + "\n", + "
\n", + "
\n", + "Regular(5, -1, 1, name='S', label='s [units]')
\n", + "
\n", + "Double() Σ=34136.0 (50000.0 with flow)\n", + "\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + "Hist(Regular(5, -1, 1, name='S', label='s [units]'), storage=Double()) # Sum: 34136.0 (50000.0 with flow)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Identical to: h.project(\"S\")[20 : 30 : hist.rebin(2)]\n", "h.project(\"S\")[20:30:2j]" @@ -196,13 +2816,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "s = Hist(\n", " hist.axis.Regular(50, -5, 5, name=\"Norm\", label=\"normal distribution\"),\n", - " hist.axis.Regular(50, -5, 5, name=\"Unif\", label=\"uniform distribution\"),\n", + " hist.axis.Regular(50, 0, 1, name=\"Unif\", label=\"uniform distribution\"),\n", " hist.axis.StrCategory([\"hi\", \"hello\"], name=\"Greet\"),\n", " hist.axis.Boolean(name=\"Yes\"),\n", " hist.axis.Integer(0, 1000, name=\"Int\"),\n", @@ -211,9 +2831,52 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "5D\n", + "\n", + "\n", + "
\n", + "
\n", + "Regular(50, -5, 5, name='Norm', label='normal distribution')
\n", + "Regular(50, 0, 1, name='Unif', label='uniform distribution')
\n", + "StrCategory(['hi', 'hello'], name='Greet', label='Greet')
\n", + "Boolean(name='Yes', label='Yes')
\n", + "Integer(0, 1000, name='Int', label='Int')
\n", + "
\n", + "Double() Σ=1000.0\n", + "\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + "Hist(\n", + " Regular(50, -5, 5, name='Norm', label='normal distribution'),\n", + " Regular(50, 0, 1, name='Unif', label='uniform distribution'),\n", + " StrCategory(['hi', 'hello'], name='Greet', label='Greet'),\n", + " Boolean(name='Yes', label='Yes'),\n", + " Integer(0, 1000, name='Int', label='Int'),\n", + " storage=Double()) # Sum: 1000.0" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "s.fill(\n", " Norm=np.random.normal(size=1000),\n", @@ -226,20 +2889,42 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "3.0" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "s[0j, -0j + 2, \"hi\", True, 1]" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "13.0" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "s[{0: 0j, 3: True, 4: 1, 1: -0j + 2, 2: \"hi\"}] = 10\n", + "s[{0: 0j, 3: True, 4: 1, 1: -0j + 2, 2: \"hi\"}] += 10\n", "\n", "s[{\"Greet\": \"hi\", \"Unif\": -0j + 2, \"Yes\": True, \"Int\": 1, \"Norm\": 0j}]" ] @@ -250,18 +2935,2650 @@ "source": [ "#### Get Density\n", "\n", - "Sometimes we want to get the density of an existing histogram. For this issue, `.density()` is capable to do it and will return you the density array without overflow and underflow bins. (*This may return a \"smart\" object in the future; for now it's a simple NumPy array.*)" + "If you want to get the density of an existing histogram, `.density()` is capable to do it and will return you the density array without overflow and underflow bins. (*This may return a \"smart\" object in the future; for now it's a simple NumPy array.*)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "array([[1.27341152, 1.37405916, 1.25153159, 1.00647646, 0.84894101],\n", + " [1.31717136, 1.22965167, 1.07211623, 0.97146858, 0.77454927],\n", + " [1.15525993, 1.17276387, 1.06774024, 0.95834063, 0.76142132],\n", + " [1.05898827, 1.09399615, 0.91895677, 0.91895677, 0.71328549],\n", + " [0.95834063, 0.77454927, 0.93208472, 0.75266935, 0.64326974]])" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "h[25:30, 25:30].density()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Get Project\n", + "\n", + "Hist allows you to get the projection of an N-D Histogram:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "\n", + "\n", + "-5\n", + "\n", + "\n", + "5\n", + "\n", + "\n", + "0\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "normal distribution\n", + "\n", + "\n", + "uniform distribution\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + "
\n", + "Regular(50, -5, 5, name='Norm', label='normal distribution')
\n", + "Regular(50, 0, 1, name='Unif', label='uniform distribution')
\n", + "
\n", + "Double() Σ=1010.0\n", + "\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + "Hist(\n", + " Regular(50, -5, 5, name='Norm', label='normal distribution'),\n", + " Regular(50, 0, 1, name='Unif', label='uniform distribution'),\n", + " storage=Double()) # Sum: 1010.0" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "s_2d = s.project(\"Norm\", \"Unif\")\n", + "s_2d" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Get Profile\n", + "\n", + "To compute the (N-1)-D profile from an existing histogram, you can:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([-1.48029737e-16, 4.00000000e-01, 2.00000000e+00])" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "xy = np.array(\n", + " [\n", + " [-2, 1.5],\n", + " [-2, -3.5],\n", + " [-2, 1.5], # x = -2\n", + " [0.0, -2.0],\n", + " [0.0, -2.0],\n", + " [0.0, 0.0],\n", + " [0.0, 2.0],\n", + " [0.0, 4.0], # x = 0\n", + " [2, 1.5], # x = +2\n", + " ]\n", + ")\n", + "h_xy = hist.Hist(\n", + " hist.axis.Regular(5, -5, 5, name=\"x\"), hist.axis.Regular(5, -5, 5, name=\"y\")\n", + ").fill(*xy.T)\n", + "\n", + "# Profile out the y-axis\n", + "hp = h_xy.profile(\"y\")\n", + "hp.values()[1:-1]\n", + "# hp.variances()[1:-1]" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -273,9 +5590,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "import matplotlib.pyplot as plt\n", "\n", @@ -296,7 +5626,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -317,9 +5647,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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geote8FiKSkp4eabb+bZZ5+t3C2hf//+bNni/70rKio44ogj+Pbbb5v0PDQ3taiJSPQq+orM1ddC54ncsOMcSiqM5EQfWRkDyExNbrEwau1+3QVs/cI/4UAkCpSVlbFkyRJeffVV/vjHP/LGG28wa9Ys1q9fT35+PnFxcWzdupXS0lKuvvpqXnrpJbp3786zzz7Lb3/7W2bPng1AUVERb7/tH8v52WefMXnyZM4++2wAEhMT+fnPfw7A7373Ox577DGuvvrqBsXypz/9iby8vMr9PT/55BOefvpprr32Wt544w2GDx/OQQcd1BJPVYMpUROR6FRR4R+4X1FG5tiBZA4bBAcd7kkoSYk+CoIka0l8C2/eCqfPbNwCuyIesVqGC+wrnzJlCgAjR45kw4YNALzxxhtcfvnlxMX5U46uXbuycuVKVq5cycSJEwF/q1vVPTj3bboezMqVK/nd735HUVERO3fuJCMjI2i9YLHs77LLLuOMM87g2muvZfbs2Vx66aW1Pq5XlKiJSHR6byZs+hCGngW9x0A371qtsjIGkD1nRbXuT198DFmdFsGal/yTGtIu05IdEva6devG999/X61s69at9OvnH2uZkJAA+DdQLysrA8A5VyPBc84xZMgQFi9eHPRx2revfXHoSy65hNzcXIYPH87jjz/OwoULg9YLFsv+evfuTY8ePViwYAEffPABTz/9dK2P6xV9KohI9PlmDSy4FQ4e5F/I9tBjWmziQDCZqcncPmUYyYk+DEhO9HH7lBQyz78C4hLg3bv8C+KKhLkOHTrQs2dP3nzzTcCfpL322msce+yxtR5z8skn89BDD1UmS1u3bmXAgAFs2bKlMlErLS1l1argfwMdO3Zkx44dlbd37NhBz549KS0tbXRitf+5AH72s59xwQUXcM455xAbG35bvSlRE5HoUlYC/7oEYtvA0B/D4eMg3ud1VGSmJrNo+njWzziVRdPH+8fIJR/l315qRyG8cTMUf1//iUQ89uSTT3LrrbcyYsQIxo8fzx/+8AcOP7z2YQU/+9nP6NOnDykpKQwfPpx//OMftGnThueff54bb7yR4cOHM2LECN57772gx0+dOpWcnBxSU1P5/PPP+b//+z+OPvpoJk6cyMCBAxsV+7hx41i9enXlZAKA008/nZ07d4ZltyeAOee8jiEk0tLSXF5entdhiEhL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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "plt.figure(figsize=(10, 6))\n", "\n", @@ -337,9 +5680,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "import mplhep\n", "\n", @@ -363,7 +5719,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ @@ -379,7 +5735,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": {}, "outputs": [], "source": [ @@ -395,9 +5751,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "# plot2d full\n", "h.plot2d_full(\n", @@ -414,9 +5783,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "# plot pull\n", "h.project(\"W\").plot_pull(\n", diff --git a/docs/user-guide/notebooks/Plots.ipynb b/docs/user-guide/notebooks/Plots.ipynb index 3e6d2ec1..cbc5bba0 100644 --- a/docs/user-guide/notebooks/Plots.ipynb +++ b/docs/user-guide/notebooks/Plots.ipynb @@ -289,6 +289,33 @@ "fig = plt.figure(figsize=(10, 8))\n", "main_ax_artists, sublot_ax_arists = hist_1.plot_ratio(pdf)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using the `.plot_ratio` API you can also make efficiency plots (where the numerator is a strict subset of the denominator)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hist_3 = hist_2.copy() * 0.7\n", + "hist_2.fill(np.random.uniform(-5, 5, 600))\n", + "hist_3.fill(np.random.uniform(-5, 5, 200))\n", + "\n", + "fig = plt.figure(figsize=(10, 8))\n", + "main_ax_artists, sublot_ax_arists = hist_3.plot_ratio(\n", + " hist_2,\n", + " rp_num_label=\"hist3\",\n", + " rp_denom_label=\"hist2\",\n", + " rp_uncert_draw_type=\"line\",\n", + " rp_uncertainty_type=\"efficiency\",\n", + ")" + ] } ], "metadata": { diff --git a/docs/user-guide/notebooks/Stack.ipynb b/docs/user-guide/notebooks/Stack.ipynb new file mode 100644 index 00000000..84e18c31 --- /dev/null +++ b/docs/user-guide/notebooks/Stack.ipynb @@ -0,0 +1,261 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0ada0219-170a-418a-a6a5-b241b9b9fe42", + "metadata": {}, + "source": [ + "# Stack\n", + "\n", + "## Build via Axes\n", + "\n", + "A histogram stack holds multiple 1-D histograms into a stack, whose axes are required to match." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6ed3ec2c-9d11-4c69-b7ce-0365079b22ff", + "metadata": {}, + "outputs": [], + "source": [ + "from hist import Hist, Stack, axis, NamedHist, BaseHist\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "ax = axis.Regular(50, -5, 5, underflow=False, overflow=False, name=\"X\")\n", + "\n", + "h1 = Hist(ax).fill(2 * np.random.normal(size=500) + 2 * np.ones((500,)))\n", + "\n", + "h2 = Hist(ax).fill(2 * np.random.normal(size=500) - 2 * np.ones((500,)))\n", + "\n", + "h3 = Hist(ax).fill(np.random.normal(size=600))\n", + "\n", + "s = Stack(h1, h2, h3)" + ] + }, + { + "cell_type": "markdown", + "id": "d5da14f4-c02c-493c-a3a6-d4eaa88ffbec", + "metadata": {}, + "source": [ + "HistStack has `.plot()` method which calls mplhep and plots the histograms in the stack:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "de684262-0c07-43c8-ba0b-a7a8d5e9f0c5", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "s.plot()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "be7a7b2a-4c88-4bb6-bc3f-4c0add90c981", + "metadata": {}, + "source": [ + "## Build via A Category Axis" + ] + }, + { + "cell_type": "markdown", + "id": "22661219-2599-4bcc-9dce-7082d331ea4f", + "metadata": {}, + "source": [ + "You can also build a histogram stack from a 2-D histogram's Category axis (`IntCat`, `StrCat`), for example," + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4004cc28-b712-46d9-baa4-7168b08527d3", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "h = (\n", + " Hist.new.Reg(50, -5, 5, name=\"x\")\n", + " .StrCat([\"good\", \"bad\"], name=\"quality\")\n", + " .Double()\n", + " .fill(x=np.random.randn(100), quality=[\"good\", \"good\", \"good\", \"good\", \"bad\"] * 20)\n", + ")\n", + "\n", + "# Turn an existin axis into a stack\n", + "s = h.stack(\"quality\")\n", + "s[::-1].plot(stack=True, histtype=\"fill\", color=[\"indianred\", \"steelblue\"], alpha=0.8)\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "5e392951-033f-44eb-bf18-5928afcae370", + "metadata": {}, + "source": [ + "The histograms in this kind of stack can have names. The names of histograms are the categories, which are corresponding profiled histograms:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "d5a0e342-7d73-4c04-b5ce-5faf87166e73", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "good\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "\n", + "\n", + "\n", + "-5\n", + "\n", + "\n", + "5\n", + "\n", + "\n", + "x\n", + "\n", + "\n", + "\n", + "
\n", + "
\n", + "Regular(50, -5, 5, name='x', label='x')
\n", + "
\n", + "Double() Σ=80.0\n", + "\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + "Hist(Regular(50, -5, 5, name='x', label='x'), storage=Double()) # Sum: 80.0" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(s[0].name)\n", + "s[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c3f81b2c-9bb9-4e98-acb2-5238960e9812", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bad\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "\n", + "\n", + "\n", + "-5\n", + "\n", + "\n", + "5\n", + "\n", + "\n", + "x\n", + "\n", + "\n", + "\n", + "
\n", + "
\n", + "Regular(50, -5, 5, name='x', label='x')
\n", + "
\n", + "Double() Σ=20.0\n", + "\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + "Hist(Regular(50, -5, 5, name='x', label='x'), storage=Double()) # Sum: 20.0" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(s[1].name)\n", + "s[1]" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "hist", + "language": "python", + "name": "hist" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/user-guide/notebooks/Storage.ipynb b/docs/user-guide/notebooks/Storage.ipynb deleted file mode 100644 index e0792270..00000000 --- a/docs/user-guide/notebooks/Storage.ipynb +++ /dev/null @@ -1,127 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Storage\n", - "\n", - "```warning\n", - "The hist package is still under active development, the usage and contents are in flux.\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Based on [boost-histogram](https://github.com/scikit-hep/boost-histogram)'s Storage, hist supports seven storage types, `Double`, `Int64`, `AutomicInt64`, `Weight`, `Mean`, `WeightedMean` and `Unlimited`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Histogram's Storage\n", - "\n", - "You can use boost-histogram's Storage in hist. For example," - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from hist import Hist\n", - "import hist" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Int64 Storage\n", - "h = Hist(hist.axis.Regular(10, -5, 5, name=\"x\"), storage=hist.storage.Int64())\n", - "h.fill([1.5, 2.5, 2.5, 2.5])\n", - "\n", - "print(h[1.5j])\n", - "print(h[2.5j])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Double Storage\n", - "h = Hist(hist.axis.Regular(10, -5, 5, name=\"x\"), storage=hist.storage.Double())\n", - "h.fill([1.5, 2.5], weight=[0.5, 1.5])\n", - "\n", - "print(h[1.5j])\n", - "print(h[2.5j])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Keeping the original features of boost-histogram's Storage, hist gives dynamic shortcuts of Storage Proxy. You can add Storage types after adding the axes." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Int64 Storage\n", - "h = Hist.new.Reg(10, 0, 1, name=\"x\").Int64().fill([0.5, 0.5])\n", - "\n", - "print(h[0.5j])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Double Storage\n", - "h = (\n", - " Hist.new.Reg(10, 0, 1, name=\"x\")\n", - " .Reg(10, 0, 1, name=\"y\")\n", - " .Double()\n", - " .fill(x=[0.5, 0.5], y=[0.2, 0.6])\n", - ")\n", - "\n", - "print(h[0.5j, 0.2j])\n", - "print(h[0.5j, 0.6j])" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "hist", - "language": "python", - "name": "hist" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.4" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/user-guide/quickstart.rst b/docs/user-guide/quickstart.rst new file mode 100644 index 00000000..bf448d08 --- /dev/null +++ b/docs/user-guide/quickstart.rst @@ -0,0 +1,208 @@ +.. _usage-quickstart: + +Quickstart +========== + +All of the examples will assume the following import: + +.. code:: python3 + + import hist + from hist import Hist + +In boost-histogram, a histogram is collection of Axis objects and a +storage. + + +.. image:: ../_images/histogram_design.png + :alt: Regular axis illustration + :align: center + +Making a histogram +------------------ + +You can make a histogram like this: + +.. code:: python3 + + hist = Hist(hist.axis.Regular(bins=10, start=0, stop=1, name="x")) + +If you’d like to type less, you can leave out the keywords for the numbers. + +.. code:: python3 + + hist = Hist(hist.axis.Regular(10, 0, 1, name="x")) + +Hist also supports a "quick-construct" system, which does not require using anything beyond the ``Hist`` class: + +.. code:: python3 + + hist = Hist.new.Regular(10, 0, 1, name="x").Double() + +Note that you have to specify the storage at the end (but this does make it easier to use ``Weight`` or other useful storages). + +The exact same syntax is used any number of dimensions: + +.. code:: python3 + + hist3D = ( + Hist.new + .Regular(10, 0, 100, circular=True, name="x") + .Regular(10, 0.0, 10.0, name="y") + .Variable([1, 2, 3, 4, 5, 5.5, 6], name="z") + .Weight() + ) + +See :ref:`usage-axes` and :ref:`usage-transforms`. + +You can also select a different storage with the ``storage=`` keyword argument; +see :ref:`usage-storage` for details about the other storages. + +Filling a histogram +------------------- + +Once you have a histogram, you can fill it using ``.fill``. Ideally, you +should give arrays, but single values work as well: + +.. code:: python3 + + hist = Hist(hist.axis.Regular(10, 0.0, 1.0, name="x")) + hist.fill(0.9) + hist.fill([0.9, 0.3, 0.4]) + + +Slicing and rebinning +--------------------- + +You can slice into a histogram using bin coordinates or data coordinates by +appending a ``j`` to an index. You can also rebin with a number ending in ``j`` +in the third slice entry, or remove an entire axis using ``sum``: + +.. code:: python3 + + hist = Hist( + hist.axis.Regular(10, 0, 1, name="x"), + hist.axis.Regular(10, 0, 1, name="y"), + hist.axis.Regular(10, 0, 1, name="z"), + ) + mini = hist[1:5, .2j:.9j, sum] + # Will be 4 bins x 7 bins + +See :ref:`usage-indexing`. + +Accessing the contents +---------------------- + +You can use ``hist.values()`` to get a NumPy array from any histogram. You can +get the variances with ``hist.variances()``, though if you fill an unweighted +storage with weights, this will return None, as you no longer can compute the +variances correctly (please use a weighted storage if you need to). You can +also get the number of entries in a bin with ``.counts()``; this will return +counts even if your storage is a mean storage. See :ref:`usage-plotting`. + +If you want access to the full underlying storage, ``.view()`` will return a +NumPy array for simple storages or a RecArray-like wrapper for non-simple +storages. Most methods offer an optional keyword argument that you can pass, +``flow=True``, to enable the under and overflow bins (disabled by default). + +.. code:: python3 + + np_array = hist.view() + + +Setting the contents +-------------------- + +You can set the contents directly as you would a NumPy array; +you can set either values or arrays at a time: + +.. code:: python3 + + hist[2] = 3.5 + hist[bh.underflow] = 0 # set the underflow bin + hist2d[3:5, 2:4] = np.eye(2) # set with array + +For non-simple storages, you can add an extra dimension that matches the +constructor arguments of that accumulator. For example, if you want to fill +a Weight histogram with three values, you can dimension: + +.. code:: python3 + + hist[0:3] = [[1, 0.1], [2, 0.2], [3, 0.3]] + +See :ref:`usage-indexing`. + +Accessing Axes +-------------- + +The axes are directly available in the histogram, and you can access +a variety of properties, such as the ``edges`` or the ``centers``. All +properties and methods are also available directly on the ``axes`` tuple: + +.. code:: python3 + + ax0 = hist.axes[0] + X, Y = hist.axes.centers + +See :ref:`usage-axes`. + + +Saving Histograms +----------------- + +You can save histograms using pickle: + +.. code:: python3 + + import pickle + + with open("file.pkl", "wb") as f: + pickle.dump(h, f) + + with open("file.pkl", "rb") as f: + h2 = pickle.load(f) + + assert h == h2 + +Special care was taken to ensure that this is fast and efficient. Please use +the latest version of the Pickle protocol you feel comfortable using; you +cannot use version 0, the version that was default on Python 2. The most recent +versions provide performance benefits. + +Computing with Histograms +------------------------- + +As an complete example, let's say you wanted to compute and plot the density, without using ``.density()``: + +.. code:: python3 + + import functools + import operator + + import matplotlib.pyplot as plt + import numpy as np + + import hist + + # Make a 2D histogram + hist = hist.Hist(hist.axis.Regular(50, -3, 3), hist.axis.Regular(50, -3, 3)) + + # Fill with Gaussian random values + hist.fill(np.random.normal(size=1_000_000), np.random.normal(size=1_000_000)) + + # Compute the areas of each bin + areas = functools.reduce(operator.mul, hist.axes.widths) + + # Compute the density + density = hist.values() / hist.sum() / areas + + # Make the plot + fig, ax = plt.subplots() + mesh = ax.pcolormesh(*hist.axes.edges.T, density.T) + fig.colorbar(mesh) + plt.savefig("simple_density.png") + + +.. image:: ../_images/ex_hist_density.png + :alt: Density histogram output + :align: center diff --git a/docs/user-guide/storages.rst b/docs/user-guide/storages.rst new file mode 100644 index 00000000..c4e4c46c --- /dev/null +++ b/docs/user-guide/storages.rst @@ -0,0 +1,141 @@ +.. _usage-storage: + +Storages +======== + +There are several storages to choose from. Based on +`boost-histogram `_’s Storage, +hist supports seven storage types, ``Double``, ``Unlimited``, ``Int64``, +``AutomicInt64``, ``Weight``, ``Mean``, and ``WeightedMean``. + +There are two methods to select a Storage in hist: + +* **Method 1:** You can use `boost-histogram `_’s Storage in hist by passing the ``storage=hist.storage.`` argument when making a histogram. + +* **Method 2:** Keeping the original features of `boost-histogram `_’s Storage, hist gives dynamic shortcuts of Storage Proxy. You can also add Storage types after adding the axes. + +Simple Storages +--------------- + +These storages hold a single value that keeps track of a count, possibly a +weighed count. + +Double +^^^^^^ + +By default, hist selects the ``Double()`` storage. For most uses, +this should be ideal. It is just as fast as the ``Int64()`` storage, it can fill +up to 53 bits of information (9 quadrillion) counts per cell, and supports +weighted fills. It can also be scaled by a floating point values without making +a copy. + +.. code:: python3 + + # Method 1 + h = Hist(hist.axis.Regular(10, -5, 5, name="x"), storage=hist.storage.Double()) + h.fill([1.5, 2.5], weight=[0.5, 1.5]) + + print(h[1.5j]) + print(h[2.5j]) + +.. code:: text + + 0.5 + 1.5 + +.. code:: python3 + + # Method 2 + h = ( + Hist.new.Reg(10, 0, 1, name="x") + .Reg(10, 0, 1, name="y") + .Double() + .fill(x=[0.5, 0.5], y=[0.2, 0.6]) + ) + + print(h[0.5j, 0.2j]) + print(h[0.5j, 0.6j]) + +.. code:: text + + 1.0 + 1.0 + + +Unlimited +^^^^^^^^^ + +The Unlimited storage starts as an 8-bit integer and grows, and converts to a +double if weights are used (or, currently, if a view is requested). This allows +you to keep the memory usage minimal, at the expense of occasionally making an +internal copy. + +Int64 +^^^^^ + +A true integer storage is provided, as well; this storage has the ``np.uint64`` +datatype. This eventually should provide type safety by not accepting +non-integer fills for data that should represent raw, unweighed counts. + +.. code:: python3 + + # Method 1 + h = Hist(hist.axis.Regular(10, -5, 5, name="x"), storage=hist.storage.Int64()) + h.fill([1.5, 2.5, 2.5, 2.5]) + + print(h[1.5j]) + print(h[2.5j]) + +.. code:: text + + 1 + 3 + +.. code:: python3 + + # Method 2 + h = ( + Hist.new.Reg(10, 0, 1, name="x") + .Int64() + .fill([0.5, 0.5]) + ) + + print(h[0.5j]) + +.. code:: text + + 2 + + +AtomicInt64 +^^^^^^^^^^^ + +This storage is like ``Int64()``, but also provides a thread safety guarantee. +You can fill a single histogram from multiple threads. + + +Accumulator storages +-------------------- + +These storages hold more than one number internally. They return a smart view when queried +with ``.view()``; see :ref:`usage-accumulators` for information on each accumulator and view. + +Weight +^^^^^^ + +This storage keeps a sum of weights as well (in CERN ROOT, this is like calling +``.Sumw2()`` before filling a histogram). It uses the ``WeightedSum`` accumulator. + + +Mean +^^^^ + +This storage tracks a "Profile", that is, the mean value of the accumulation instead of the sum. +It stores the count (as a double), the mean, and a term that is used to compute the variance. When +filling, you can add a ``sample=`` term. + + +WeightedMean +^^^^^^^^^^^^ + +This is similar to Mean, but also keeps track a sum of weights like term as well. diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..118ea7ee --- /dev/null +++ b/noxfile.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import shutil +from pathlib import Path + +import nox + +ALL_PYTHONS = ["3.7", "3.8", "3.9"] + +nox.options.sessions = ["lint", "tests"] + + +DIR = Path(__file__).parent.resolve() + + +@nox.session +def lint(session): + """ + Run the linter. + """ + session.install("pre-commit") + session.run("pre-commit", "run", "--all-files", *session.posargs) + + +@nox.session(python=ALL_PYTHONS, reuse_venv=True) +def tests(session): + """ + Run the unit and regular tests. + """ + session.install(".[test]") + session.run("pytest", *session.posargs) + + +@nox.session +def docs(session): + """ + Build the docs. Pass "serve" to serve. + """ + + session.install(".[docs]") + session.chdir("docs") + session.run("sphinx-build", "-M", "html", ".", "_build") + + if session.posargs: + if "serve" in session.posargs: + print("Launching docs at http://localhost:8000/ - use Ctrl-C to quit") + session.run("python", "-m", "http.server", "8000", "-d", "_build/html") + else: + print("Unsupported argument to docs") + + +@nox.session +def build(session): + """ + Build an SDist and wheel. + """ + + build_p = DIR.joinpath("build") + if build_p.exists(): + shutil.rmtree(build_p) + + session.install("build") + session.run("python", "-m", "build") diff --git a/pyproject.toml b/pyproject.toml index 71012d90..a2637fb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,69 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "src/hist/version.py" +[tool.pytest.ini_options] +minverison = "6.0" +addopts = "-ra -Wd --strict-markers" +xfail_strict = true +testpaths = ["tests"] +required_plugins = ["pytest-mpl"] +log_cli_level = "DEBUG" + + [tool.nbqa.mutate] black = 1 pyupgrade = 1 + +[tool.isort] +profile = "black" +multi_line_output = 3 + +[tool.check-manifest] +ignore = [ + ".pre-commit-config.yaml", + ".readthedocs.yml", + "examples/**", + "notebooks/**", + "docs/**", + "CONTRIBUTING.md", + "*.html", + "*.in", + "*.json", + "*.yml", + "src/hist/version.py", + "tests/.pytest_cache/**", + ".all-contributorsrc", + "noxfile.py", +] + + +[tool.mypy] +warn_unused_configs = true +files = "src" +python_version = "3.7" +disallow_any_generics = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_return_any = true +no_implicit_reexport = true +strict_equality = true + +[[tool.mypy.overrides]] +module = [ + "histoprint.*", + "scipy.optimize.*", + "uncertainties.*", + "matplotlib.*", + "scipy.*", + "iminuit.*", + "mplhep.*", + "hist.version", +] +ignore_missing_imports = true diff --git a/setup.cfg b/setup.cfg index 372ad870..7e465e9e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,6 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -42,11 +41,11 @@ project_urls = [options] packages = find: install_requires = - boost-histogram~=1.0.2 + boost-histogram~=1.1.0 histoprint>=1.6 - numpy>=1.13.3 + numpy>=1.14.5 typing_extensions;python_version<"3.8" -python_requires = >=3.6 +python_requires = >=3.7 include_package_data = True package_dir = =src @@ -59,60 +58,7 @@ where = src console_scripts = hist=hist.classichist:main -[mypy] -warn_unused_configs = True -files = src -python_version = 3.6 -disallow_any_generics = True -disallow_subclassing_any = True -disallow_untyped_calls = True -disallow_untyped_defs = True -disallow_incomplete_defs = True -check_untyped_defs = True -disallow_untyped_decorators = True -no_implicit_optional = True -warn_redundant_casts = True -warn_unused_ignores = True -warn_return_any = True -no_implicit_reexport = True -strict_equality = True - -[mypy-histoprint.*] -ignore_missing_imports = True - -[mypy-scipy.optimize.*] -ignore_missing_imports = True - -[mypy-uncertainties.*] -ignore_missing_imports = True - -[mypy-matplotlib.*] -ignore_missing_imports = True - -[mypy-mplhep.plot.*] -ignore_missing_imports = True - [flake8] max-line-length = 80 select = C, E, F, W, B, B9 ignore = E203, E231, E501, E722, W503, B950 - -[check-manifest] -ignore = - .pre-commit-config.yaml - .readthedocs.yml - examples/** - notebooks/** - docs/** - CONTRIBUTING.md - *.html - *.in - *.json - *.yml - src/hist/version.py - tests/.pytest_cache/** - .all-contributorsrc - -[tool:isort] -profile = black -multi_line_output = 3 diff --git a/setup.py b/setup.py index ad2ee2a2..f49638d0 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,8 @@ # Distributed under the 3-clause BSD license, see accompanying file LICENSE # or https://github.com/scikit-hep/hist for details. +from __future__ import annotations + from setuptools import setup extras_require = { @@ -18,7 +20,7 @@ extras_require["test"] = [ *extras_require["plot"], - "pytest >=4.6", + "pytest >=6", "pytest-mpl >=0.12", ] @@ -35,6 +37,7 @@ "ipykernel", "pillow", "uncertainties>=3", + "myst_parser>=0.14", ] extras_require["all"] = sorted(set(sum(extras_require.values(), []))) diff --git a/src/hist/__init__.py b/src/hist/__init__.py index 21683fd5..1855f542 100644 --- a/src/hist/__init__.py +++ b/src/hist/__init__.py @@ -3,14 +3,16 @@ # Distributed under the 3-clause BSD license, see accompanying file LICENSE # or https://github.com/scikit-hep/hist for details. +from __future__ import annotations + import warnings from types import ModuleType -from typing import Tuple from . import accumulators, axis, numpy, storage, tag from .basehist import BaseHist from .hist import Hist from .namedhist import NamedHist +from .stack import Stack from .tag import loc, overflow, rebin, sum, underflow # Convenient access to the version number @@ -21,6 +23,7 @@ "Hist", "BaseHist", "NamedHist", + "Stack", "accumulators", "axis", "loc", @@ -34,12 +37,10 @@ ) -# Python 3.7 only -def __dir__() -> Tuple[str, ...]: +def __dir__() -> tuple[str, ...]: return __all__ -# Python 3.7 only def __getattr__(name: str) -> ModuleType: if name == "axes": diff --git a/src/hist/accumulators.py b/src/hist/accumulators.py index d7c4306a..d3b99201 100644 --- a/src/hist/accumulators.py +++ b/src/hist/accumulators.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from boost_histogram.accumulators import Mean, Sum, WeightedMean, WeightedSum __all__ = ("Sum", "Mean", "WeightedSum", "WeightedMean") diff --git a/src/hist/axestuple.py b/src/hist/axestuple.py index 401e040c..88bf92fd 100644 --- a/src/hist/axestuple.py +++ b/src/hist/axestuple.py @@ -1,18 +1,20 @@ -from typing import Any, Tuple, Union +from __future__ import annotations + +from typing import Any from boost_histogram.axis import ArrayTuple, AxesTuple __all__ = ("NamedAxesTuple", "AxesTuple", "ArrayTuple") -def __dir__() -> Tuple[str, ...]: +def __dir__() -> tuple[str, ...]: return __all__ class NamedAxesTuple(AxesTuple): __slots__ = () - def _get_index_by_name(self, name: Union[int, str, None]) -> Union[int, None]: + def _get_index_by_name(self, name: int | str | None) -> int | None: if not isinstance(name, str): return name @@ -34,14 +36,14 @@ def __getitem__(self, item: Any) -> Any: return super().__getitem__(item) @property - def name(self) -> Tuple[str]: + def name(self) -> tuple[str]: """ The names of the axes. May be empty strings. """ return tuple(ax.name for ax in self) # type: ignore @property - def label(self) -> Tuple[str]: + def label(self) -> tuple[str]: """ The labels of the axes. Defaults to name if label not given, or Axis N if neither was given. diff --git a/src/hist/axis/__init__.py b/src/hist/axis/__init__.py index acbd2d52..7895aa3b 100644 --- a/src/hist/axis/__init__.py +++ b/src/hist/axis/__init__.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import sys -from typing import Any, Dict, Iterable, List, Optional, Tuple +from typing import Any, Iterable import boost_histogram.axis as bha @@ -28,12 +30,12 @@ ) -def __dir__() -> Tuple[str, ...]: +def __dir__() -> tuple[str, ...]: return __all__ class CoreAxisProtocol(Protocol): - metadata: Dict[str, Any] + metadata: dict[str, Any] class AxisProtocol(Protocol): @@ -57,7 +59,7 @@ def __init_subclass__(cls, **kwargs: Any) -> None: @property def name(self: AxisProtocol) -> str: """ - Get or set the name for the Regular axis + Get the name for the Regular axis """ return self._ax.metadata.get("name", "") @@ -72,11 +74,11 @@ def label(self: AxisProtocol) -> str: def label(self: AxisProtocol, value: str) -> None: self._ax.metadata["label"] = value - def _repr_args_(self: AxisProtocol) -> List[str]: + def _repr_args_(self: AxisProtocol) -> list[str]: """ Return options for use in repr. """ - ret: List[str] = super()._repr_args_() # type: ignore + ret: list[str] = super()._repr_args_() # type: ignore if self.name: ret.append(f"name={self.name!r}") @@ -99,12 +101,12 @@ def __init__( label: str = "", metadata: Any = None, flow: bool = True, - underflow: Optional[bool] = None, - overflow: Optional[bool] = None, + underflow: bool | None = None, + overflow: bool | None = None, growth: bool = False, circular: bool = False, - transform: Optional[bha.transform.AxisTransform] = None, - __dict__: Optional[Dict[str, Any]] = None, + transform: bha.transform.AxisTransform | None = None, + __dict__: dict[str, Any] | None = None, ) -> None: super().__init__( bins, @@ -131,7 +133,7 @@ def __init__( name: str = "", label: str = "", metadata: Any = None, - __dict__: Optional[Dict[str, Any]] = None, + __dict__: dict[str, Any] | None = None, ) -> None: super().__init__( metadata=metadata, @@ -141,7 +143,7 @@ def __init__( self.label: str = label -class Variable(bha.Variable, AxesMixin, family=hist): +class Variable(AxesMixin, bha.Variable, family=hist): __slots__ = () def __init__( @@ -150,13 +152,13 @@ def __init__( *, name: str = "", label: str = "", + metadata: Any = None, flow: bool = True, - underflow: Optional[bool] = None, - overflow: Optional[bool] = None, + underflow: bool | None = None, + overflow: bool | None = None, growth: bool = False, circular: bool = False, - metadata: Any = None, - __dict__: Optional[Dict[str, Any]] = None, + __dict__: dict[str, Any] | None = None, ) -> None: super().__init__( edges, @@ -171,7 +173,7 @@ def __init__( self.label: str = label -class Integer(bha.Integer, AxesMixin, family=hist): +class Integer(AxesMixin, bha.Integer, family=hist): __slots__ = () def __init__( @@ -181,13 +183,13 @@ def __init__( *, name: str = "", label: str = "", + metadata: Any = None, flow: bool = True, - underflow: Optional[bool] = None, - overflow: Optional[bool] = None, + underflow: bool | None = None, + overflow: bool | None = None, growth: bool = False, circular: bool = False, - metadata: Any = None, - __dict__: Optional[Dict[str, Any]] = None, + __dict__: dict[str, Any] | None = None, ) -> None: super().__init__( start, @@ -203,7 +205,7 @@ def __init__( self.label: str = label -class IntCategory(bha.IntCategory, AxesMixin, family=hist): +class IntCategory(AxesMixin, bha.IntCategory, family=hist): __slots__ = () def __init__( @@ -212,9 +214,9 @@ def __init__( *, name: str = "", label: str = "", - growth: bool = False, metadata: Any = None, - __dict__: Optional[Dict[str, Any]] = None, + growth: bool = False, + __dict__: dict[str, Any] | None = None, ) -> None: super().__init__( categories, @@ -226,7 +228,7 @@ def __init__( self.label: str = label -class StrCategory(bha.StrCategory, AxesMixin, family=hist): +class StrCategory(AxesMixin, bha.StrCategory, family=hist): __slots__ = () def __init__( @@ -235,9 +237,9 @@ def __init__( *, name: str = "", label: str = "", - growth: bool = False, metadata: Any = None, - __dict__: Optional[Dict[str, Any]] = None, + growth: bool = False, + __dict__: dict[str, Any] | None = None, ) -> None: super().__init__( categories, diff --git a/src/hist/axis/transform.py b/src/hist/axis/transform.py index 7de2c366..fa911c59 100644 --- a/src/hist/axis/transform.py +++ b/src/hist/axis/transform.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from boost_histogram.axis.transform import AxisTransform, Function, Pow, log, sqrt __all__ = ("AxisTransform", "Pow", "Function", "sqrt", "log") diff --git a/src/hist/basehist.py b/src/hist/basehist.py index cf8d0511..9d6e8e84 100644 --- a/src/hist/basehist.py +++ b/src/hist/basehist.py @@ -1,20 +1,10 @@ +from __future__ import annotations + import functools import operator +import typing import warnings -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - List, - Mapping, - Optional, - Sequence, - Tuple, - Type, - TypeVar, - Union, -) +from typing import Any, Callable, Iterator, Mapping, Sequence, Tuple, TypeVar, Union import boost_histogram as bh import histoprint @@ -27,9 +17,9 @@ from .quick_construct import MetaConstructor from .storage import Storage from .svgplots import html_hist, svg_hist_1d, svg_hist_1d_c, svg_hist_2d, svg_hist_nd -from .typing import ArrayLike, SupportsIndex +from .typing import ArrayLike, Protocol, SupportsIndex -if TYPE_CHECKING: +if typing.TYPE_CHECKING: from builtins import ellipsis import matplotlib.axes @@ -37,6 +27,12 @@ from .plot import FitResultArtists, MainAxisArtists, RatiolikeArtists + +class SupportsLessThan(Protocol): + def __lt__(self, __other: Any) -> bool: + ... + + InnerIndexing = Union[ SupportsIndex, str, Callable[[bh.axis.Axis], int], slice, "ellipsis" ] @@ -45,7 +41,7 @@ # Workaround for bug in mplhep -def _proc_kw_for_lw(kwargs: Mapping[str, Any]) -> Dict[str, Any]: +def _proc_kw_for_lw(kwargs: Mapping[str, Any]) -> dict[str, Any]: return { f"{k[:-3]}_linestyle" if k.endswith("_ls") @@ -64,10 +60,10 @@ class BaseHist(bh.Histogram, metaclass=MetaConstructor, family=hist): def __init__( self, - *args: Union[AxisProtocol, Storage, str, Tuple[int, float, float]], - storage: Optional[Union[Storage, str]] = None, + *args: AxisProtocol | Storage | str | tuple[int, float, float], + storage: Storage | str | None = None, metadata: Any = None, - data: Optional[np.ndarray] = None, + data: np.typing.NDArray[Any] | None = None, ) -> None: """ Initialize BaseHist object. Axis params can contain the names. @@ -79,6 +75,14 @@ def __init__( storage = args[-1] args = args[:-1] + # Support raw Quick Construct being accidentally passed in + args = [ + a.axes[0] # type: ignore + if isinstance(a, hist.quick_construct.ConstructProxy) and len(a.axes) == 1 + else a + for a in args + ] + if args: if isinstance(storage, str): storage_str = storage.title() @@ -94,6 +98,13 @@ def __init__( warnings.warn(msg) storage = storage() super().__init__(*args, storage=storage, metadata=metadata) # type: ignore + + disallowed_names = {"weight", "sample", "threads"} + for ax in self.axes: + if ax.name in disallowed_names: + disallowed_warning = f"{ax.name} is a protected keyword and cannot be used as axis name" + warnings.warn(disallowed_warning) + valid_names = [ax.name for ax in self.axes if ax.name] if len(valid_names) != len(set(valid_names)): raise KeyError( @@ -116,7 +127,9 @@ def _generate_axes_(self) -> NamedAxesTuple: return NamedAxesTuple(self._axis(i) for i in range(self.ndim)) def _repr_html_(self) -> str: - if self.ndim == 1: + if self.size == 0: + return str(self) + elif self.ndim == 1: if self.axes[0].traits.circular: return str(html_hist(self, svg_hist_1d_c)) else: @@ -125,6 +138,7 @@ def _repr_html_(self) -> str: return str(html_hist(self, svg_hist_2d)) elif self.ndim > 2: return str(html_hist(self, svg_hist_nd)) + return str(self) def _name_to_index(self, name: str) -> int: @@ -136,18 +150,18 @@ def _name_to_index(self, name: str) -> int: if name == axis.name: return index - raise ValueError("The axis names could not be found") + raise ValueError(f"The axis name {name} could not be found") @classmethod def from_columns( - cls: Type[T], + cls: type[T], data: Mapping[str, ArrayLike], - axes: Sequence[Union[str, AxisProtocol]], + axes: Sequence[str | AxisProtocol], *, - weight: Optional[str] = None, + weight: str | None = None, storage: hist.storage.Storage = hist.storage.Double(), # noqa: B008 ) -> T: - axes_list: List[Any] = list() + axes_list: list[Any] = list() for ax in axes: if isinstance(ax, str): assert ax in data, f"{ax} must be present in data={list(data)}" @@ -160,9 +174,9 @@ def from_columns( raise TypeError( f"{ax} must be all int or strings if axis not given" ) + elif not ax.name or ax.name not in data: + raise TypeError("All axes must have names present in the data") else: - if not ax.name or ax.name not in data: - raise TypeError("All axes must have names present in the data") axes_list.append(ax) weight_arr = data[weight] if weight else None @@ -172,9 +186,7 @@ def from_columns( self.fill(**data_list, weight=weight_arr) # type: ignore return self - def project( - self: T, *args: Union[int, str] - ) -> Union[T, float, bh.accumulators.Accumulator]: + def project(self: T, *args: int | str) -> T | float | bh.accumulators.Accumulator: """ Projection of axis idx. """ @@ -184,9 +196,9 @@ def project( def fill( self: T, *args: ArrayLike, - weight: Optional[ArrayLike] = None, - sample: Optional[ArrayLike] = None, - threads: Optional[int] = None, + weight: ArrayLike | None = None, + sample: ArrayLike | None = None, + threads: int | None = None, **kwargs: ArrayLike, ) -> T: """ @@ -200,19 +212,41 @@ def fill( } if set(data_dict) != set(range(len(args), self.ndim)): - raise TypeError("All axes must be accounted for in fill") + raise TypeError( + "All axes must be accounted for in fill, you may have used a disallowed name in the axes" + ) data = (data_dict[i] for i in range(len(args), self.ndim)) total_data = tuple(args) + tuple(data) # Python 2 can't unpack twice return super().fill(*total_data, weight=weight, sample=sample, threads=threads) + def sort( + self: T, + axis: int | str, + key: ( + Callable[[int], SupportsLessThan] | Callable[[str], SupportsLessThan] | None + ) = None, + reverse: bool = False, + ) -> T: + """ + Sort a categorical axis. + """ + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + sorted_cats = sorted(self.axes[axis], key=key, reverse=reverse) + # This can only return T, not float, etc., so we ignore the extra types here + return self[{axis: [bh.loc(x) for x in sorted_cats]}] # type: ignore + def _loc_shortcut(self, x: Any) -> Any: """ Convert some specific indices to location. """ - if isinstance(x, slice): + if isinstance(x, list): + return [self._loc_shortcut(each) for each in x] + elif isinstance(x, slice): return slice( self._loc_shortcut(x.start), self._loc_shortcut(x.stop), @@ -233,17 +267,19 @@ def _step_shortcut(self, x: Any) -> Any: Convert some specific indices to step. """ - if isinstance(x, complex): - if x.real != 0: - raise ValueError("The step should not have real part") - elif x.imag % 1 != 0: - raise ValueError("The imaginary part should be an integer") - else: - return bh.rebin(int(x.imag)) - else: + if not isinstance(x, complex): return x - def _index_transform(self, index: IndexingExpr) -> bh.IndexingExpr: + if x.real != 0: + raise ValueError("The step should not have real part") + elif x.imag % 1 != 0: + raise ValueError("The imaginary part should be an integer") + else: + return bh.rebin(int(x.imag)) + + def _index_transform( + self, index: list[IndexingExpr] | IndexingExpr + ) -> bh.IndexingExpr: """ Auxiliary function for __getitem__ and __setitem__. """ @@ -261,14 +297,14 @@ def _index_transform(self, index: IndexingExpr) -> bh.IndexingExpr: ) return new_indices - elif not hasattr(index, "__iter__"): + elif not isinstance(index, tuple): index = (index,) # type: ignore return tuple(self._loc_shortcut(v) for v in index) # type: ignore def __getitem__( # type: ignore self: T, index: IndexingExpr - ) -> Union[T, float, bh.accumulators.Accumulator]: + ) -> T | float | bh.accumulators.Accumulator: """ Get histogram item. """ @@ -276,7 +312,7 @@ def __getitem__( # type: ignore return super().__getitem__(self._index_transform(index)) def __setitem__( # type: ignore - self, index: IndexingExpr, value: Union[ArrayLike, bh.accumulators.Accumulator] + self, index: IndexingExpr, value: ArrayLike | bh.accumulators.Accumulator ) -> None: """ Set histogram item. @@ -284,7 +320,7 @@ def __setitem__( # type: ignore return super().__setitem__(self._index_transform(index), value) - def profile(self: T, axis: Union[int, str]) -> T: + def profile(self: T, axis: int | str) -> T: """ Returns a profile (Mean/WeightedMean) histogram from a normal histogram with N-1 axes. The axis given is profiled over and removed from the @@ -319,12 +355,12 @@ def profile(self: T, axis: Union[int, str]) -> T: retval[...] = np.stack([count, new_values, count * new_variances], axis=-1) return retval - def density(self) -> np.ndarray: + def density(self) -> np.typing.NDArray[Any]: """ Density NumPy array. """ total = np.sum(self.values()) * functools.reduce(operator.mul, self.axes.widths) - dens: np.ndarray = self.values() / np.where(total > 0, total, 1) + dens: np.typing.NDArray[Any] = self.values() / np.where(total > 0, total, 1) return dens def show(self, **kwargs: Any) -> Any: @@ -335,12 +371,17 @@ def show(self, **kwargs: Any) -> Any: return histoprint.print_hist(self, **kwargs) def plot( - self, *args: Any, overlay: "Optional[str]" = None, **kwargs: Any - ) -> "Union[Hist1DArtists, Hist2DArtists]": + self, *args: Any, overlay: str | None = None, **kwargs: Any + ) -> Hist1DArtists | Hist2DArtists: """ Plot method for BaseHist object. """ - _has_categorical = np.sum([ax.traits.discrete for ax in self.axes]) == 1 + _has_categorical = 0 + if ( + np.sum(self.axes.traits.ordered) == 1 + and np.sum(self.axes.traits.discrete) == 1 + ): + _has_categorical = 1 _project = _has_categorical or overlay is not None if self.ndim == 1 or (self.ndim == 2 and _project): return self.plot1d(*args, overlay=overlay, **kwargs) @@ -352,10 +393,10 @@ def plot( def plot1d( self, *, - ax: "Optional[matplotlib.axes.Axes]" = None, - overlay: "Optional[Union[str, int]]" = None, + ax: matplotlib.axes.Axes | None = None, + overlay: str | int | None = None, **kwargs: Any, - ) -> "Hist1DArtists": + ) -> Hist1DArtists: """ Plot1d method for BaseHist object. """ @@ -375,9 +416,9 @@ def plot1d( def plot2d( self, *, - ax: "Optional[matplotlib.axes.Axes]" = None, + ax: matplotlib.axes.Axes | None = None, **kwargs: Any, - ) -> "Hist2DArtists": + ) -> Hist2DArtists: """ Plot2d method for BaseHist object. """ @@ -389,9 +430,9 @@ def plot2d( def plot2d_full( self, *, - ax_dict: "Optional[Dict[str, matplotlib.axes.Axes]]" = None, + ax_dict: dict[str, matplotlib.axes.Axes] | None = None, **kwargs: Any, - ) -> "Tuple[Hist2DArtists, Hist1DArtists, Hist1DArtists]": + ) -> tuple[Hist2DArtists, Hist1DArtists, Hist1DArtists]: """ Plot2d_full method for BaseHist object. @@ -405,11 +446,13 @@ def plot2d_full( def plot_ratio( self, - other: Union["hist.BaseHist", Callable[[np.ndarray], np.ndarray], str], + other: hist.BaseHist + | Callable[[np.typing.NDArray[Any]], np.typing.NDArray[Any]] + | str, *, - ax_dict: "Optional[Dict[str, matplotlib.axes.Axes]]" = None, + ax_dict: dict[str, matplotlib.axes.Axes] | None = None, **kwargs: Any, - ) -> "Tuple[MainAxisArtists, RatiolikeArtists]": + ) -> tuple[MainAxisArtists, RatiolikeArtists]: """ ``plot_ratio`` method for ``BaseHist`` object. @@ -425,11 +468,11 @@ def plot_ratio( def plot_pull( self, - func: Union[Callable[[np.ndarray], np.ndarray], str], + func: Callable[[np.typing.NDArray[Any]], np.typing.NDArray[Any]] | str, *, - ax_dict: "Optional[Dict[str, matplotlib.axes.Axes]]" = None, + ax_dict: dict[str, matplotlib.axes.Axes] | None = None, **kwargs: Any, - ) -> "Tuple[FitResultArtists, RatiolikeArtists]": + ) -> tuple[FitResultArtists, RatiolikeArtists]: """ ``plot_pull`` method for ``BaseHist`` object. @@ -446,10 +489,24 @@ def plot_pull( def plot_pie( self, *, - ax: "Optional[matplotlib.axes.Axes]" = None, + ax: matplotlib.axes.Axes | None = None, **kwargs: Any, ) -> Any: import hist.plot return hist.plot.plot_pie(self, ax=ax, **kwargs) + + def stack(self, axis: int | str) -> hist.stack.Stack: + """ + Returns a stack from a normal histogram axes. + """ + if self.ndim < 2: + raise RuntimeError("Cannot stack with less than two axis") + stack_histograms: Iterator[BaseHist] = [ + self[{axis: i}] for i in range(len(self.axes[axis])) # type: ignore + ] + for name, h in zip(self.axes[axis], stack_histograms): + h.name = name # type: ignore + + return hist.stack.Stack(*stack_histograms) diff --git a/src/hist/classichist.py b/src/hist/classichist.py index 39a3a32b..50c1fa3a 100644 --- a/src/hist/classichist.py +++ b/src/hist/classichist.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import shutil import sys diff --git a/src/hist/hist.py b/src/hist/hist.py index aa054f55..315155a9 100644 --- a/src/hist/hist.py +++ b/src/hist/hist.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import hist from .basehist import BaseHist diff --git a/src/hist/intervals.py b/src/hist/intervals.py index 678e6b79..59431117 100644 --- a/src/hist/intervals.py +++ b/src/hist/intervals.py @@ -1,4 +1,6 @@ -from typing import Any, Optional, Tuple +from __future__ import annotations + +from typing import Any import numpy as np @@ -18,27 +20,31 @@ __all__ = ("poisson_interval", "clopper_pearson_interval", "ratio_uncertainty") -def __dir__() -> Tuple[str, ...]: +def __dir__() -> tuple[str, ...]: return __all__ def poisson_interval( - values: np.ndarray, variances: np.ndarray, coverage: "Optional[float]" = None -) -> np.ndarray: + values: np.typing.NDArray[Any], + variances: np.typing.NDArray[Any] | None = None, + coverage: float | None = None, +) -> np.typing.NDArray[Any]: r""" The Frequentist coverage interval for Poisson-distributed observations. - What is calculated is the "Garwood" interval, - c.f. https://www.ine.pt/revstat/pdf/rs120203.pdf or - http://ms.mcmaster.ca/peter/s743/poissonalpha.html. - For weighted data, this approximates the observed count by - ``values**2/variances``, which effectively scales the unweighted - Poisson interval by the average weight. - This may not be the optimal solution: see https://arxiv.org/abs/1309.1287 - for a proper treatment. + What is calculated is the "Garwood" interval, c.f. + `V. Patil, H. Kulkarni (Revstat, 2012) `_ + or http://ms.mcmaster.ca/peter/s743/poissonalpha.html. + If ``variances`` is supplied, the data is assumed to be weighted, and the + unweighted count is approximated by ``values**2/variances``, which effectively + scales the unweighted Poisson interval by the average weight. + This may not be the optimal solution: see + `10.1016/j.nima.2014.02.021 `_ + (`arXiv:1309.1287 `_) for a proper treatment. - When a bin is zero, the scale of the nearest nonzero bin is substituted to - scale the nominal upper bound. + In cases where the value is zero, an upper limit is well-defined only in the case of + unweighted data, so if ``variances`` is supplied, the upper limit for a zero value + will be set to ``NaN``. Args: values: Sum of weights. @@ -53,31 +59,30 @@ def poisson_interval( # https://github.com/CoffeaTeam/coffea/blob/8c58807e199a7694bf15e3803dbaf706d34bbfa0/LICENSE if coverage is None: coverage = stats.norm.cdf(1) - stats.norm.cdf(-1) - scale = np.empty_like(values) - scale[values != 0] = variances[values != 0] / values[values != 0] - if np.sum(values == 0) > 0: - missing = np.where(values == 0) - available = np.nonzero(values) - if len(available[0]) == 0: - raise RuntimeError( - "All values are zero! Cannot compute meaningful uncertainties.", - ) - nearest = np.sum( - [np.square(np.subtract.outer(d, d0)) for d, d0 in zip(available, missing)] - ).argmin(axis=0) - argnearest = tuple(dim[nearest] for dim in available) - scale[missing] = scale[argnearest] - counts = values / scale - interval_min = scale * stats.chi2.ppf((1 - coverage) / 2, 2 * counts) / 2.0 - interval_max = scale * stats.chi2.ppf((1 + coverage) / 2, 2 * (counts + 1)) / 2.0 + if variances is None: + interval_min = stats.chi2.ppf((1 - coverage) / 2, 2 * values) / 2.0 + interval_min[values == 0.0] = 0.0 # chi2.ppf produces NaN for values=0 + interval_max = stats.chi2.ppf((1 + coverage) / 2, 2 * (values + 1)) / 2.0 + else: + scale = np.ones_like(values) + mask = np.isfinite(values) & (values != 0) + np.divide(variances, values, out=scale, where=mask) + counts: np.typing.NDArray[Any] = values / scale + interval_min = scale * stats.chi2.ppf((1 - coverage) / 2, 2 * counts) / 2.0 + interval_min[values == 0.0] = 0.0 # chi2.ppf produces NaN for values=0 + interval_max = ( + scale * stats.chi2.ppf((1 + coverage) / 2, 2 * (counts + 1)) / 2.0 + ) + interval_max[values == 0.0] = np.nan interval = np.stack((interval_min, interval_max)) - interval[interval == np.nan] = 0.0 # chi2.ppf produces nan for counts=0 return interval def clopper_pearson_interval( - num: np.ndarray, denom: np.ndarray, coverage: "Optional[float]" = None -) -> np.ndarray: + num: np.typing.NDArray[Any], + denom: np.typing.NDArray[Any], + coverage: float | None = None, +) -> np.typing.NDArray[Any]: r""" Compute the Clopper-Pearson coverage interval for a binomial distribution. c.f. http://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval @@ -103,15 +108,15 @@ def clopper_pearson_interval( interval_min = stats.beta.ppf((1 - coverage) / 2, num, denom - num + 1) interval_max = stats.beta.ppf((1 + coverage) / 2, num + 1, denom - num) interval = np.stack((interval_min, interval_max)) - interval[:, num == 0.0] = 0.0 + interval[0, num == 0.0] = 0.0 interval[1, num == denom] = 1.0 return interval def ratio_uncertainty( - num: np.ndarray, - denom: np.ndarray, - uncertainty_type: Literal["poisson", "poisson-ratio"] = "poisson", + num: np.typing.NDArray[Any], + denom: np.typing.NDArray[Any], + uncertainty_type: Literal["poisson", "poisson-ratio", "efficiency"] = "poisson", ) -> Any: r""" Calculate the uncertainties for the values of the ratio ``num/denom`` using @@ -122,22 +127,40 @@ def ratio_uncertainty( denom: Denominator or number of trials. uncertainty_type: Coverage interval type to use in the calculation of the uncertainties. - ``"poisson"`` (default) implements the Poisson interval for the - numerator scaled by the denominator. - ``"poisson-ratio"`` implements the Clopper-Pearson interval for Poisson - distributed ``num`` and ``denom``. + + * ``"poisson"`` (default) implements the Garwood confidence interval for + a Poisson-distributed numerator scaled by the denominator. + See :func:`hist.intervals.poisson_interval` for further details. + * ``"poisson-ratio"`` implements a confidence interval for the ratio ``num / denom`` + assuming it is an estimator of the ratio of the expected rates from + two independent Poisson distributions. + It over-covers to a similar degree as the Clopper-Pearson interval + does for the Binomial efficiency parameter estimate. + * ``"efficiency"`` implements the Clopper-Pearson confidence interval + for the ratio ``num / denom`` assuming it is an estimator of a Binomial + efficiency parameter. + This is only valid if the entries contributing to ``num`` are a strict + subset of those contributing to ``denom``. Returns: The uncertainties for the ratio. """ # Note: As return is a numpy ufuncs the type is "Any" - with np.errstate(divide="ignore"): + with np.errstate(divide="ignore", invalid="ignore"): + # Nota bene: x/0 = inf, 0/0 = nan ratio = num / denom if uncertainty_type == "poisson": - ratio_uncert = np.abs(poisson_interval(ratio, num / np.square(denom)) - ratio) + with np.errstate(divide="ignore", invalid="ignore"): + ratio_variance = num * np.power(denom, -2.0) + ratio_uncert = np.abs(poisson_interval(ratio, ratio_variance) - ratio) elif uncertainty_type == "poisson-ratio": - # poisson ratio n/m is equivalent to binomial n/(n+m) - ratio_uncert = np.abs(clopper_pearson_interval(num, num + denom) - ratio) + # Details: see https://github.com/scikit-hep/hist/issues/279 + p_lim = clopper_pearson_interval(num, num + denom) + with np.errstate(divide="ignore", invalid="ignore"): + r_lim: np.typing.NDArray[Any] = p_lim / (1 - p_lim) + ratio_uncert = np.abs(r_lim - ratio) + elif uncertainty_type == "efficiency": + ratio_uncert = np.abs(clopper_pearson_interval(num, denom) - ratio) else: raise TypeError( f"'{uncertainty_type}' is an invalid option for uncertainty_type." diff --git a/src/hist/namedhist.py b/src/hist/namedhist.py index a9979f23..9f39205a 100644 --- a/src/hist/namedhist.py +++ b/src/hist/namedhist.py @@ -1,4 +1,6 @@ -from typing import Any, Optional, TypeVar, Union +from __future__ import annotations + +from typing import Any, TypeVar import boost_histogram as bh @@ -23,9 +25,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: ) # TODO: This can return a single value - def project( - self: T, *args: Union[int, str] - ) -> Union[T, float, bh.accumulators.Accumulator]: + def project(self: T, *args: int | str) -> T | float | bh.accumulators.Accumulator: """ Projection of axis idx. """ @@ -40,9 +40,9 @@ def project( def fill( # type: ignore self: T, - weight: Optional[ArrayLike] = None, - sample: Optional[ArrayLike] = None, - threads: Optional[int] = None, + weight: ArrayLike | None = None, + sample: ArrayLike | None = None, + threads: int | None = None, **kwargs: ArrayLike, ) -> T: """ @@ -61,7 +61,7 @@ def fill( # type: ignore def __getitem__( # type: ignore self: T, index: IndexingExpr, - ) -> Union[T, float, bh.accumulators.Accumulator]: + ) -> T | float | bh.accumulators.Accumulator: """ Get histogram item. """ @@ -76,7 +76,7 @@ def __getitem__( # type: ignore def __setitem__( # type: ignore self, index: IndexingExpr, - value: Union[ArrayLike, bh.accumulators.Accumulator], + value: ArrayLike | bh.accumulators.Accumulator, ) -> None: """ Set histogram item. diff --git a/src/hist/numpy.py b/src/hist/numpy.py index f724d87f..d0226394 100644 --- a/src/hist/numpy.py +++ b/src/hist/numpy.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from boost_histogram.numpy import histogram, histogram2d, histogramdd __all__ = ("histogram", "histogram2d", "histogramdd") diff --git a/src/hist/plot.py b/src/hist/plot.py index 80200386..c907fc82 100644 --- a/src/hist/plot.py +++ b/src/hist/plot.py @@ -1,17 +1,8 @@ +from __future__ import annotations + import inspect import sys -from typing import ( - Any, - Callable, - Dict, - Iterable, - List, - NamedTuple, - Optional, - Set, - Tuple, - Union, -) +from typing import Any, Callable, Iterable, NamedTuple, Union import numpy as np @@ -62,7 +53,7 @@ class RatioBarArtists(NamedTuple): class PullArtists(NamedTuple): bar: matplotlib.container.BarContainer - patch_artist: List[matplotlib.patches.Rectangle] + patch_artist: list[matplotlib.patches.Rectangle] MainAxisArtists = Union[FitResultArtists, Hist1DArtists] @@ -71,7 +62,7 @@ class PullArtists(NamedTuple): RatiolikeArtists = Union[RatioArtists, PullArtists] -def __dir__() -> Tuple[str, ...]: +def __dir__() -> tuple[str, ...]: return __all__ @@ -82,8 +73,8 @@ def _expand_shortcuts(key: str) -> str: def _filter_dict( - __dict: Dict[str, Any], prefix: str, *, ignore: Optional[Set[str]] = None -) -> Dict[str, Any]: + __dict: dict[str, Any], prefix: str, *, ignore: set[str] | None = None +) -> dict[str, Any]: """ Keyword argument conversion: convert the kwargs to several independent args, pulling them out of the dict given. Prioritize prefix_kw dict. @@ -91,10 +82,10 @@ def _filter_dict( # If passed explicitly, use that if f"{prefix}kw" in __dict: - res: Dict[str, Any] = __dict.pop(f"{prefix}kw") + res: dict[str, Any] = __dict.pop(f"{prefix}kw") return {_expand_shortcuts(k): v for k, v in res.items()} - ignore_set: Set[str] = ignore or set() + ignore_set: set[str] = ignore or set() return { _expand_shortcuts(key[len(prefix) :]): __dict.pop(key) for key in list(__dict) @@ -134,11 +125,11 @@ def _expr_to_lambda(expr: str) -> Callable[..., Any]: def _curve_fit_wrapper( func: Callable[..., Any], - xdata: np.ndarray, - ydata: np.ndarray, - yerr: np.ndarray, + xdata: np.typing.NDArray[Any], + ydata: np.typing.NDArray[Any], + yerr: np.typing.NDArray[Any], likelihood: bool = False, -) -> Tuple[Tuple[float, ...], np.ndarray]: +) -> tuple[tuple[float, ...], np.typing.NDArray[Any]]: """ Wrapper around `scipy.optimize.curve_fit`. Initial parameters (`p0`) can be set in the function definition with defaults for kwargs @@ -165,7 +156,7 @@ def _curve_fit_wrapper( from iminuit import Minuit from scipy.special import gammaln - def fnll(v: Iterable[np.ndarray]) -> float: + def fnll(v: Iterable[np.typing.NDArray[Any]]) -> float: ypred = func(xdata, *v) if (ypred <= 0.0).any(): return 1e6 @@ -188,9 +179,9 @@ def fnll(v: Iterable[np.ndarray]) -> float: def plot2d_full( self: hist.BaseHist, *, - ax_dict: Optional[Dict[str, matplotlib.axes.Axes]] = None, + ax_dict: dict[str, matplotlib.axes.Axes] | None = None, **kwargs: Any, -) -> Tuple[Hist2DArtists, Hist1DArtists, Hist1DArtists]: +) -> tuple[Hist2DArtists, Hist1DArtists, Hist1DArtists]: """ Plot2d_full method for BaseHist object. @@ -270,7 +261,7 @@ def plot2d_full( def _construct_gaussian_callable( __hist: hist.BaseHist, -) -> Callable[[np.ndarray], np.ndarray]: +) -> Callable[[np.typing.NDArray[Any]], np.typing.NDArray[Any]]: x_values = __hist.axes[0].centers hist_values = __hist.values() @@ -281,13 +272,13 @@ def _construct_gaussian_callable( # gauss is a closure that will get evaluated in _fit_callable_to_hist def gauss( - x: np.ndarray, + x: np.typing.NDArray[Any], constant: float = constant, mean: float = mean, sigma: float = sigma, - ) -> np.ndarray: - # Note: Force np.ndarray type as numpy ufuncs have type "Any" - ret: np.ndarray = constant * np.exp( + ) -> np.typing.NDArray[Any]: + # Note: Force np.typing.NDArray[Any] type as numpy ufuncs have type "Any" + ret: np.typing.NDArray[Any] = constant * np.exp( -np.square(x - mean) / (2 * np.square(sigma)) ) return ret @@ -296,10 +287,15 @@ def gauss( def _fit_callable_to_hist( - model: Callable[[np.ndarray], np.ndarray], + model: Callable[[np.typing.NDArray[Any]], np.typing.NDArray[Any]], histogram: hist.BaseHist, likelihood: bool = False, -) -> "Tuple[np.ndarray, np.ndarray, np.ndarray, Tuple[Tuple[float, ...], np.ndarray]]": +) -> tuple[ + np.typing.NDArray[Any], + np.typing.NDArray[Any], + np.typing.NDArray[Any], + tuple[tuple[float, ...], np.typing.NDArray[Any]], +]: """ Fit a model, a callable function, to the histogram values. """ @@ -321,7 +317,7 @@ def _fit_callable_to_hist( n_samples = 100 vopts = np.random.multivariate_normal(popt, pcov, n_samples) sampled_ydata = np.vstack([model(xdata, *vopt).T for vopt in vopts]) - model_uncert = np.nanstd(sampled_ydata, axis=0) + model_uncert = np.nanstd(sampled_ydata, axis=0) # type: ignore else: model_uncert = np.zeros_like(hist_uncert) @@ -330,12 +326,12 @@ def _fit_callable_to_hist( def _plot_fit_result( __hist: hist.BaseHist, - model_values: np.ndarray, - model_uncert: np.ndarray, + model_values: np.typing.NDArray[Any], + model_uncert: np.typing.NDArray[Any], ax: matplotlib.axes.Axes, - eb_kwargs: Dict[str, Any], - fp_kwargs: Dict[str, Any], - ub_kwargs: Dict[str, Any], + eb_kwargs: dict[str, Any], + fp_kwargs: dict[str, Any], + ub_kwargs: dict[str, Any], ) -> FitResultArtists: """ Plot fit of model to histogram data @@ -370,8 +366,8 @@ def _plot_fit_result( def plot_ratio_array( __hist: hist.BaseHist, - ratio: np.ndarray, - ratio_uncert: np.ndarray, + ratio: np.typing.NDArray[Any], + ratio_uncert: np.typing.NDArray[Any], ax: matplotlib.axes.Axes, **kwargs: Any, ) -> RatioArtists: @@ -392,7 +388,7 @@ def plot_ratio_array( ) # Type now due to control flow - axis_artists: Union[RatioErrorbarArtists, RatioBarArtists] + axis_artists: RatioErrorbarArtists | RatioBarArtists uncert_draw_type = kwargs.pop("uncert_draw_type", "line") if uncert_draw_type == "line": @@ -444,7 +440,7 @@ def plot_ratio_array( valid_ratios + ratio_uncert[1][valid_ratios_idx], ] ) - max_delta = np.max(np.abs(extrema - central_value)) + max_delta = np.amax(np.abs(extrema - central_value)) ratio_extrema = np.abs(max_delta + central_value) _alpha = 2.0 @@ -462,10 +458,10 @@ def plot_ratio_array( def plot_pull_array( __hist: hist.BaseHist, - pulls: np.ndarray, + pulls: np.typing.NDArray[Any], ax: matplotlib.axes.Axes, - bar_kwargs: Dict[str, Any], - pp_kwargs: Dict[str, Any], + bar_kwargs: dict[str, Any], + pp_kwargs: dict[str, Any], ) -> PullArtists: """ Plot a pull plot on the given axes @@ -511,14 +507,16 @@ def plot_pull_array( def _plot_ratiolike( self: hist.BaseHist, - other: Union[hist.BaseHist, Callable[[np.ndarray], np.ndarray], str], + other: hist.BaseHist + | Callable[[np.typing.NDArray[Any]], np.typing.NDArray[Any]] + | str, likelihood: bool = False, *, - ax_dict: Optional[Dict[str, matplotlib.axes.Axes]] = None, + ax_dict: dict[str, matplotlib.axes.Axes] | None = None, view: Literal["ratio", "pull"], - fit_fmt: Optional[str] = None, + fit_fmt: str | None = None, **kwargs: Any, -) -> Tuple[MainAxisArtists, RatiolikeArtists]: +) -> tuple[MainAxisArtists, RatiolikeArtists]: r""" Plot ratio-like plots (ratio plots and pull plots) for BaseHist @@ -583,6 +581,9 @@ def _plot_ratiolike( rp_kwargs.setdefault("legend_loc", "best") rp_kwargs.setdefault("num_label", None) rp_kwargs.setdefault("denom_label", None) + if rp_kwargs["uncertainty_type"] == "efficiency": + rp_kwargs.setdefault("ylabel", "Efficiency") + rp_kwargs.setdefault("ylim", [0, 1.1]) # patch plot keyword arguments pp_kwargs = _filter_dict(kwargs, "pp_") @@ -614,7 +615,7 @@ def _plot_ratiolike( if fit_fmt is not None: parnames = list(inspect.signature(other).parameters)[1:] popt, pcov = bestfit_result - perr = np.sqrt(np.diag(pcov)) + perr = np.sqrt(np.diagonal(pcov)) fp_label = "Fit" for name, value, error in zip(parnames, popt, perr): @@ -657,7 +658,9 @@ def _plot_ratiolike( ) elif view == "pull": - pulls = (hist_values - compare_values) / hist_values_uncert + pulls: np.typing.NDArray[Any] = ( + hist_values - compare_values + ) / hist_values_uncert pulls[np.isnan(pulls) | np.isinf(pulls)] = 0 @@ -672,7 +675,7 @@ def _plot_ratiolike( return main_ax_artists, subplot_ax_artists -def get_center(x: Union[str, int, Tuple[float, float]]) -> Union[str, float]: +def get_center(x: str | int | tuple[float, float]) -> str | float: if isinstance(x, tuple): return (x[0] + x[1]) / 2 else: @@ -682,7 +685,7 @@ def get_center(x: Union[str, int, Tuple[float, float]]) -> Union[str, float]: def plot_pie( self: hist.BaseHist, *, - ax: Optional[matplotlib.axes.Axes] = None, + ax: matplotlib.axes.Axes | None = None, **kwargs: Any, ) -> Any: diff --git a/src/hist/quick_construct.py b/src/hist/quick_construct.py index c0568110..0b63edb8 100644 --- a/src/hist/quick_construct.py +++ b/src/hist/quick_construct.py @@ -1,4 +1,6 @@ -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Optional, Type +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable, Iterable from . import axis, storage from .axis import AxisProtocol @@ -23,11 +25,11 @@ def __repr__(self) -> str: inside = ", ".join(repr(ax) for ax in self.axes) return f"{self.__class__.__name__}({self.hist_class.__name__}, {inside})" - def __init__(self, hist_class: "Type[BaseHist]", *axes: AxisProtocol) -> None: + def __init__(self, hist_class: type[BaseHist], *axes: AxisProtocol) -> None: self.hist_class = hist_class self.axes = axes - def Reg( + def Regular( self, bins: int, start: float, @@ -37,13 +39,13 @@ def Reg( label: str = "", metadata: Any = None, flow: bool = True, - underflow: Optional[bool] = None, - overflow: Optional[bool] = None, + underflow: bool | None = None, + overflow: bool | None = None, growth: bool = False, circular: bool = False, - transform: Optional[AxisTransform] = None, - __dict__: Optional[Dict[str, Any]] = None, - ) -> "ConstructProxy": + transform: AxisTransform | None = None, + __dict__: dict[str, Any] | None = None, + ) -> ConstructProxy: return ConstructProxy( self.hist_class, *self.axes, @@ -63,6 +65,8 @@ def Reg( ), ) + Reg = Regular + def Sqrt( self, bins: int, @@ -72,8 +76,8 @@ def Sqrt( name: str = "", label: str = "", metadata: Any = None, - __dict__: Optional[Dict[str, Any]] = None, - ) -> "ConstructProxy": + __dict__: dict[str, Any] | None = None, + ) -> ConstructProxy: return ConstructProxy( self.hist_class, *self.axes, @@ -98,8 +102,8 @@ def Log( name: str = "", label: str = "", metadata: Any = None, - __dict__: Optional[Dict[str, Any]] = None, - ) -> "ConstructProxy": + __dict__: dict[str, Any] | None = None, + ) -> ConstructProxy: return ConstructProxy( self.hist_class, *self.axes, @@ -125,8 +129,8 @@ def Pow( label: str = "", power: float, metadata: Any = None, - __dict__: Optional[Dict[str, Any]] = None, - ) -> "ConstructProxy": + __dict__: dict[str, Any] | None = None, + ) -> ConstructProxy: return ConstructProxy( self.hist_class, *self.axes, @@ -153,8 +157,8 @@ def Func( forward: Callable[[float], float], inverse: Callable[[float], float], metadata: Any = None, - __dict__: Optional[Dict[str, Any]] = None, - ) -> "ConstructProxy": + __dict__: dict[str, Any] | None = None, + ) -> ConstructProxy: return ConstructProxy( self.hist_class, *self.axes, @@ -170,13 +174,13 @@ def Func( ), ) - def Bool( + def Boolean( self, name: str = "", label: str = "", metadata: Any = None, - __dict__: Optional[Dict[str, Any]] = None, - ) -> "ConstructProxy": + __dict__: dict[str, Any] | None = None, + ) -> ConstructProxy: return ConstructProxy( self.hist_class, *self.axes, @@ -188,7 +192,9 @@ def Bool( ), ) - def Var( + Bool = Boolean + + def Variable( self, edges: Iterable[float], *, @@ -196,12 +202,12 @@ def Var( label: str = "", metadata: Any = None, flow: bool = True, - underflow: Optional[bool] = None, - overflow: Optional[bool] = None, + underflow: bool | None = None, + overflow: bool | None = None, growth: bool = False, circular: bool = False, - __dict__: Optional[Dict[str, Any]] = None, - ) -> "ConstructProxy": + __dict__: dict[str, Any] | None = None, + ) -> ConstructProxy: return ConstructProxy( self.hist_class, *self.axes, @@ -219,7 +225,9 @@ def Var( ), ) - def Int( + Var = Variable + + def Integer( self, start: int, stop: int, @@ -228,12 +236,12 @@ def Int( label: str = "", metadata: Any = None, flow: bool = True, - underflow: Optional[bool] = None, - overflow: Optional[bool] = None, + underflow: bool | None = None, + overflow: bool | None = None, growth: bool = False, circular: bool = False, - __dict__: Optional[Dict[str, Any]] = None, - ) -> "ConstructProxy": + __dict__: dict[str, Any] | None = None, + ) -> ConstructProxy: return ConstructProxy( self.hist_class, *self.axes, @@ -252,7 +260,9 @@ def Int( ), ) - def IntCat( + Int = Integer + + def IntCategory( self, categories: Iterable[int], *, @@ -260,8 +270,8 @@ def IntCat( label: str = "", metadata: Any = None, growth: bool = False, - __dict__: Optional[Dict[str, Any]] = None, - ) -> "ConstructProxy": + __dict__: dict[str, Any] | None = None, + ) -> ConstructProxy: return ConstructProxy( self.hist_class, *self.axes, @@ -275,6 +285,8 @@ def IntCat( ), ) + IntCat = IntCategory + def StrCat( self, categories: Iterable[str], @@ -283,8 +295,8 @@ def StrCat( label: str = "", metadata: Any = None, growth: bool = False, - __dict__: Optional[Dict[str, Any]] = None, - ) -> "ConstructProxy": + __dict__: dict[str, Any] | None = None, + ) -> ConstructProxy: return ConstructProxy( self.hist_class, *self.axes, @@ -298,33 +310,35 @@ def StrCat( ), ) + StrCategory = StrCat + class ConstructProxy(QuickConstruct): __slots__ = () - def Double(self) -> "BaseHist": + def Double(self) -> BaseHist: return self.hist_class(*self.axes, storage=storage.Double()) - def Int64(self) -> "BaseHist": + def Int64(self) -> BaseHist: return self.hist_class(*self.axes, storage=storage.Int64()) - def AtomicInt64(self) -> "BaseHist": + def AtomicInt64(self) -> BaseHist: return self.hist_class(*self.axes, storage=storage.AtomicInt64()) - def Weight(self) -> "BaseHist": + def Weight(self) -> BaseHist: return self.hist_class(*self.axes, storage=storage.Weight()) - def Mean(self) -> "BaseHist": + def Mean(self) -> BaseHist: return self.hist_class(*self.axes, storage=storage.Mean()) - def WeightedMean(self) -> "BaseHist": + def WeightedMean(self) -> BaseHist: return self.hist_class(*self.axes, storage=storage.WeightedMean()) - def Unlimited(self) -> "BaseHist": + def Unlimited(self) -> BaseHist: return self.hist_class(*self.axes, storage=storage.Unlimited()) class MetaConstructor(type): @property - def new(cls: "Type[BaseHist]") -> QuickConstruct: # type: ignore + def new(cls: type[BaseHist]) -> QuickConstruct: # type: ignore return QuickConstruct(cls) diff --git a/src/hist/stack.py b/src/hist/stack.py new file mode 100644 index 00000000..00fbf3f8 --- /dev/null +++ b/src/hist/stack.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import typing +from typing import Any, Iterator, TypeVar + +from .basehist import BaseHist + +if typing.TYPE_CHECKING: + from mplhep.plot import Hist1DArtists + +__all__ = ("Stack",) + +T = TypeVar("T", bound="Stack") + + +class Stack: + def __init__( + self, + *args: BaseHist, + ) -> None: + """ + Initialize Stack of histograms. + """ + + self._stack = args + + if len(args) == 0: + raise ValueError("There should be histograms in the Stack") + + if not all(isinstance(a, BaseHist) for a in args): + raise ValueError("There should be only histograms in Stack") + + first_axes = args[0].axes + for a in args[1:]: + if first_axes != a.axes: + raise ValueError("The Histogram axes don't match") + + @typing.overload + def __getitem__(self, val: int) -> BaseHist: + ... + + @typing.overload + def __getitem__(self: T, val: slice) -> T: + ... + + def __getitem__(self: T, val: int | slice) -> BaseHist | T: + if isinstance(val, slice): + return self.__class__(*self._stack.__getitem__(val)) + + return self._stack.__getitem__(val) + + def __iter__(self) -> Iterator[BaseHist]: + return iter(self._stack) + + def __len__(self) -> int: + return len(self._stack) + + def __repr__(self) -> str: + str_stack = ", ".join(repr(h) for h in self) + return f"{self.__class__.__name__}({str_stack})" + + def plot(self, **kwargs: Any) -> list[Hist1DArtists]: + """ + Plot method for Stack object. + """ + + import hist.plot + + if self[0].ndim != 1: + raise NotImplementedError("Please project to 1D before calling plot") + + if "label" not in kwargs: + # TODO: add .name to static typing. And runtime, for that matter. + if all(getattr(h, "name", None) is not None for h in self): + kwargs["label"] = [h.name for h in self] # type: ignore + + return hist.plot.histplot(list(self), **kwargs) # type: ignore + + +def __dir__() -> tuple[str, ...]: + return __all__ diff --git a/src/hist/storage.py b/src/hist/storage.py index 6a143bea..045e2bd2 100644 --- a/src/hist/storage.py +++ b/src/hist/storage.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from boost_histogram.storage import ( AtomicInt64, Double, diff --git a/src/hist/svgplots.py b/src/hist/svgplots.py index 859170b4..8bfe8b9b 100644 --- a/src/hist/svgplots.py +++ b/src/hist/svgplots.py @@ -1,4 +1,6 @@ -from typing import Callable, Union +from __future__ import annotations + +from typing import Any, Callable import numpy as np from boost_histogram.axis import Axis @@ -19,7 +21,7 @@ ) -def _desc_hist(h: "hist.BaseHist") -> str: +def _desc_hist(h: hist.BaseHist) -> str: main_sum = h.sum() flow_too_sum = h.sum(flow=True) @@ -33,7 +35,7 @@ def _desc_hist(h: "hist.BaseHist") -> str: return output -def html_hist(h: "hist.BaseHist", function: Callable[["hist.BaseHist"], svg]) -> html: +def html_hist(h: hist.BaseHist, function: Callable[[hist.BaseHist], svg]) -> html: left_column = div(function(h), style="width:290px;") right_column = div(_desc_hist(h), style="flex=grow:1;") @@ -44,7 +46,7 @@ def html_hist(h: "hist.BaseHist", function: Callable[["hist.BaseHist"], svg]) -> return html(container) -def make_text(txt: Union[str, float], **kwargs: SupportsStr) -> text: +def make_text(txt: str | float, **kwargs: SupportsStr) -> text: style = "fill:currentColor;" kwargs["style"] = style + str(kwargs.get("style", "")) if isinstance(txt, float): @@ -58,7 +60,7 @@ def make_ax_text(ax: Axis, **kwargs: SupportsStr) -> text: return make_text(ax.label or ax.name, **kwargs) -def svg_hist_1d(h: "hist.BaseHist") -> svg: +def svg_hist_1d(h: hist.BaseHist) -> svg: width = 250 height = 100 @@ -68,8 +70,8 @@ def svg_hist_1d(h: "hist.BaseHist") -> svg: (edges,) = h.axes.edges norm_edges = (edges - edges[0]) / (edges[-1] - edges[0]) density = h.density() - max_dens = np.max(density) or 1 - norm_vals = density / max_dens + max_dens = np.amax(density) or 1 + norm_vals: np.typing.NDArray[Any] = density / max_dens arr = np.empty((2, len(norm_vals) * 2 + 2), dtype=float) arr[0, 0:-1:2] = arr[0, 1::2] = width * norm_edges @@ -105,7 +107,7 @@ def svg_hist_1d(h: "hist.BaseHist") -> svg: ) -def svg_hist_1d_c(h: "hist.BaseHist") -> svg: +def svg_hist_1d_c(h: hist.BaseHist) -> svg: width = 250 height = 250 radius = 100 @@ -117,8 +119,8 @@ def svg_hist_1d_c(h: "hist.BaseHist") -> svg: (edges,) = h.axes.edges norm_edges = (edges - edges[0]) / (edges[-1] - edges[0]) * np.pi * 2 density = h.density() - max_dens = np.max(density) or 1 - norm_vals = density / max_dens + max_dens = np.amax(density) or 1 + norm_vals: np.typing.NDArray[Any] = density / max_dens arr = np.empty((2, len(norm_vals) * 2), dtype=float) arr[0, :-1:2] = arr[0, 1::2] = norm_edges[:-1] @@ -141,7 +143,7 @@ def svg_hist_1d_c(h: "hist.BaseHist") -> svg: return svg(bins, center, viewBox=f"{-width/2} {-height/2} {width} {height}") -def svg_hist_2d(h: "hist.BaseHist") -> svg: +def svg_hist_2d(h: hist.BaseHist) -> svg: width = 250 height = 250 assert h.ndim == 2, "Must be 2D" @@ -151,8 +153,8 @@ def svg_hist_2d(h: "hist.BaseHist") -> svg: ey = -(e1 - e1[0]) / (e1[-1] - e1[0]) * height density = h.density() - max_dens = np.max(density) or 1 - norm_vals = density / max_dens + max_dens = np.amax(density) or 1 + norm_vals: np.typing.NDArray[Any] = density / max_dens boxes = [] for r, (up_edge, bottom_edge) in enumerate(zip(ey[:-1], ey[1:])): @@ -193,7 +195,7 @@ def svg_hist_2d(h: "hist.BaseHist") -> svg: return svg(*texts, *boxes, viewBox=f"{-20} {-height - 20} {width+40} {height+40}") -def svg_hist_nd(h: "hist.BaseHist") -> svg: +def svg_hist_nd(h: hist.BaseHist) -> svg: assert h.ndim > 2, "Must be more than 2D" width = 200 diff --git a/src/hist/svgutils.py b/src/hist/svgutils.py index 486cfc62..09ffb977 100644 --- a/src/hist/svgutils.py +++ b/src/hist/svgutils.py @@ -1,4 +1,6 @@ -from typing import Type, TypeVar, Union +from __future__ import annotations + +from typing import TypeVar from .typing import Protocol @@ -9,9 +11,7 @@ def __str__(self) -> str: class XML: - def __init__( - self, *contents: Union["XML", SupportsStr], **kargs: SupportsStr - ) -> None: + def __init__(self, *contents: XML | SupportsStr, **kargs: SupportsStr) -> None: self.properties = kargs self.contents = contents @@ -44,7 +44,7 @@ def _repr_xml_(self) -> str: class svg(XML): - def __init__(self, *args: Union[XML, str], **kwargs: str) -> None: + def __init__(self, *args: XML | str, **kwargs: str) -> None: super().__init__(*args, xmlns="http://www.w3.org/2000/svg", **kwargs) def _repr_svg_(self) -> str: @@ -73,7 +73,7 @@ class div(XML): class rect(XML): @classmethod def pad( - cls: Type[T], + cls: type[T], x: float, y: float, scale_x: float, diff --git a/src/hist/tag.py b/src/hist/tag.py index cdbffac6..a6c6bd81 100644 --- a/src/hist/tag.py +++ b/src/hist/tag.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from boost_histogram.tag import ( Locator, Slicer, diff --git a/src/hist/typing.py b/src/hist/typing.py index d3c3aa6d..1acb3481 100644 --- a/src/hist/typing.py +++ b/src/hist/typing.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import sys -from typing import TYPE_CHECKING, Any, Tuple +from typing import TYPE_CHECKING, Any if sys.version_info < (3, 8): from typing_extensions import Literal, Protocol, SupportsIndex @@ -17,5 +19,5 @@ __all__ = ("Literal", "Protocol", "SupportsIndex", "Ufunc", "ArrayLike") -def __dir__() -> Tuple[str, ...]: +def __dir__() -> tuple[str, ...]: return __all__ diff --git a/tests/conftest.py b/tests/conftest.py index 70023a87..fd83bad1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from hist import Hist, NamedHist diff --git a/tests/test_axis.py b/tests/test_axis.py index a9217b62..8c67ee2d 100644 --- a/tests/test_axis.py +++ b/tests/test_axis.py @@ -1,4 +1,8 @@ -from hist import axis +from __future__ import annotations + +import pytest + +from hist import axis, hist def test_axis_names(): @@ -51,3 +55,13 @@ def test_axis_flow(): assert axis.Integer(0, 8, flow=False, overflow=True) == axis.Integer( 0, 8, underflow=False ) + + +def test_axis_disallowed_names(): + + with pytest.warns(UserWarning): + hist.Hist(axis.Regular(10, 0, 10, name="weight")) + with pytest.warns(UserWarning): + hist.Hist(axis.Regular(10, 0, 10, name="sample")) + with pytest.warns(UserWarning): + hist.Hist(axis.Regular(10, 0, 10, name="threads")) diff --git a/tests/test_bh.py b/tests/test_bh.py index b2bb0627..06f42464 100644 --- a/tests/test_bh.py +++ b/tests/test_bh.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import boost_histogram as bh import hist diff --git a/tests/test_general.py b/tests/test_general.py index 78ebf48d..5b0ddab3 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import ctypes import math @@ -746,6 +748,13 @@ def test_hist_proxy(): assert h["T", "F"] == 1 +def test_hist_proxy_mistake(): + h = Hist(Hist.new.IntCat(range(10))) + h2 = Hist.new.IntCategory(range(10)).Double() + + assert h == h2 + + def test_general_density(): """ Test general density -- whether Hist density work properly. @@ -846,3 +855,55 @@ def test_from_array(named_hist): axis.Regular(7, 1, 3, name="B"), data=np.ones((11, 9)), ) + + +def test_sum_empty_axis(): + hist = bh.Histogram( + bh.axis.StrCategory("", growth=True), + bh.axis.Regular(10, 0, 1), + storage=bh.storage.Weight(), + ) + assert hist.sum().value == 0 + assert "Str" in repr(hist) + + +def test_sum_empty_axis_hist(): + h = Hist( + axis.StrCategory("", growth=True), + axis.Regular(10, 0, 1), + storage=storage.Weight(), + ) + assert h.sum().value == 0 + assert "Str" in repr(h) + h._repr_html_() + + +@pytest.mark.filterwarnings("ignore:List indexing selection is experimental") +def test_select_by_index(): + h = Hist( + axis.StrCategory(["a", "two", "3"]), + storage=storage.Weight(), + ) + + assert tuple(h[["a", "3"]].axes[0]) == ("a", "3") + assert tuple(h[["a"]].axes[0]) == ("a",) + + +@pytest.mark.filterwarnings("ignore:List indexing selection is experimental") +def test_select_by_index_imag(): + h = Hist( + axis.IntCategory([7, 8, 9]), + storage=storage.Int64(), + ) + + assert tuple(h[[2, 1]].axes[0]) == (9, 8) + assert tuple(h[[8j, 7j]].axes[0]) == (8, 7) + + +def test_sorted_simple(): + h = Hist.new.IntCat([4, 1, 2]).StrCat(["AB", "BCC", "BC"]).Double() + assert tuple(h.sort(0).axes[0]) == (1, 2, 4) + assert tuple(h.sort(0, reverse=True).axes[0]) == (4, 2, 1) + assert tuple(h.sort(0, key=lambda x: -x).axes[0]) == (4, 2, 1) + assert tuple(h.sort(1).axes[1]) == ("AB", "BC", "BCC") + assert tuple(h.sort(1, reverse=True).axes[1]) == ("BCC", "BC", "AB") diff --git a/tests/test_intervals.py b/tests/test_intervals.py index b09a6752..b2420e15 100644 --- a/tests/test_intervals.py +++ b/tests/test_intervals.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numpy as np import pytest from pytest import approx @@ -59,6 +61,12 @@ def test_poisson_interval(hist_fixture): ] ) + interval_min, interval_max = intervals.poisson_interval(np.arange(4)) + assert approx(interval_min) == np.array([0.0, 0.17275378, 0.70818544, 1.36729531]) + assert approx(interval_max) == np.array( + [1.84102165, 3.29952656, 4.63785962, 5.91818583] + ) + def test_clopper_pearson_interval(hist_fixture): hist_1, _ = hist_fixture @@ -98,44 +106,95 @@ def test_clopper_pearson_interval(hist_fixture): ) -def test_ratio_uncertainty(hist_fixture): - hist_1, hist_2 = hist_fixture +def test_ratio_uncertainty(): + num, denom = np.meshgrid(np.array([0, 1, 4, 512]), np.array([0, 1, 4, 512])) + + uncertainty_min, uncertainty_max = intervals.ratio_uncertainty( + num, denom, uncertainty_type="poisson" + ) + + assert approx(uncertainty_min, nan_ok=True) == np.array( + [ + [np.nan, np.nan, np.nan, np.nan], + [0.0, 8.27246221e-01, 1.91433919e00, 2.26200365e01], + [0.0, 2.06811555e-01, 4.78584797e-01, 5.65500911e00], + [0.0, 1.61571528e-03, 3.73894372e-03, 4.41797587e-02], + ] + ) + + assert approx(uncertainty_max, nan_ok=True) == np.array( + [ + [np.nan, np.nan, np.nan, np.nan], + [np.nan, 2.29952656e00, 3.16275317e00, 2.36421589e01], + [np.nan, 5.74881640e-01, 7.90688293e-01, 5.91053972e00], + [np.nan, 4.49126281e-03, 6.17725229e-03, 4.61760915e-02], + ] + ) uncertainty_min, uncertainty_max = intervals.ratio_uncertainty( - hist_1.values(), hist_2.values(), uncertainty_type="poisson" + num, denom, uncertainty_type="poisson-ratio" ) - assert approx(uncertainty_min) == np.array( + assert approx(uncertainty_min, nan_ok=True) == np.array( [ - 0.1439794096271186, - 0.12988019998066708, - 0.0711565635066328, - 0.045722288708959336, - 0.04049103990124614, - 0.038474711321686006, - 0.045227104349518155, - 0.06135954973309016, - 0.12378460125991042, - 0.19774186117590858, + [np.nan, np.inf, np.inf, np.inf], + [0.0, 9.09782858e-01, 3.09251539e00, 3.57174304e02], + [0.0, 2.14845433e-01, 6.11992834e-01, 5.67393184e01], + [0.0, 1.61631629e-03, 3.75049626e-03, 6.24104251e-02], ] ) - assert approx(uncertainty_max) == np.array( + assert approx(uncertainty_max, nan_ok=True) == np.array( [ - 0.22549817680979262, - 0.1615766277480729, - 0.07946632561746425, - 0.04954668134626106, - 0.04327624938437291, - 0.04106267733757407, - 0.04891233040201837, - 0.06909296140898324, - 0.1485919630151803, - 0.2817958228477908, + [np.nan, np.nan, np.nan, np.nan], + [5.30297438e00, 1.00843679e01, 2.44458061e01, 2.45704433e03], + [5.84478627e-01, 8.51947064e-01, 1.57727199e00, 1.18183919e02], + [3.60221785e-03, 4.50575120e-03, 6.22048393e-03, 6.65647601e-02], + ] + ) + + with pytest.raises(ValueError): + intervals.ratio_uncertainty(num, denom, uncertainty_type="efficiency") + + uncertainty_min, uncertainty_max = intervals.ratio_uncertainty( + np.minimum(num, denom), denom, uncertainty_type="efficiency" + ) + + assert approx(uncertainty_min, nan_ok=True) == np.array( + [ + [np.nan, np.nan, np.nan, np.nan], + [0.0, 0.8413447460685429, 0.8413447460685429, 0.8413447460685429], + [0.0, 0.207730893696323, 0.36887757085042716, 0.36887757085042716], + [0.0, 0.0016157721916044239, 0.003735294987003171, 0.0035892884494188593], + ] + ) + assert approx(uncertainty_max, nan_ok=True) == np.array( + [ + [np.nan, np.nan, np.nan, np.nan], + [0.8413447460685429, 0.0, 0.0, 0.0], + [0.3688775708504272, 0.36840242550395996, 0.0, 0.0], + [0.0035892884494188337, 0.004476807721636625, 0.006134065381665161, 0.0], ] ) with pytest.raises(TypeError): - intervals.ratio_uncertainty( - hist_1.values(), hist_2.values(), uncertainty_type="fail" - ) + intervals.ratio_uncertainty(num, denom, uncertainty_type="fail") + + +def test_valid_efficiency_ratio_uncertainty(hist_fixture): + """ + Test that the upper bound for the error interval does not exceed unity + for efficiency ratio plots. + """ + + hist_1, _ = hist_fixture + num = hist_1.values() + den = num + + efficiency_ratio = num / den + _, uncertainty_max = intervals.ratio_uncertainty( + num, den, uncertainty_type="efficiency" + ) + efficiency_err_up = efficiency_ratio + uncertainty_max + + assert len(efficiency_err_up[efficiency_err_up > 1.0]) == 0 diff --git a/tests/test_mock_plot.py b/tests/test_mock_plot.py new file mode 100644 index 00000000..b81eb6d9 --- /dev/null +++ b/tests/test_mock_plot.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import numpy as np +import pytest + +from hist import Hist + + +@pytest.fixture(autouse=True) +def mock_test(monkeypatch): + monkeypatch.setattr(Hist, "plot1d", plot1d_mock) + monkeypatch.setattr(Hist, "plot2d", plot2d_mock) + + +def plot1d_mock(*args, **kwargs): + return "called plot1d" + + +def plot2d_mock(*args, **kwargs): + return "called plot2d" + + +def test_categorical_plot(): + testCat = ( + Hist.new.StrCat("", name="dataset", growth=True) + .Reg(10, 0, 10, name="good", label="y-axis") + .Int64() + ) + + testCat.fill(dataset="A", good=np.random.normal(5, 9, 27)) + + assert testCat.plot() == "called plot1d" + + +def test_integer_plot(): + testInt = ( + Hist.new.Int(1, 10, name="nice", label="x-axis") + .Reg(10, 0, 10, name="good", label="y-axis") + .Int64() + ) + testInt.fill(nice=np.random.normal(5, 1, 10), good=np.random.normal(5, 1, 10)) + + assert testInt.plot() == "called plot2d" diff --git a/tests/test_named.py b/tests/test_named.py index b401dcf9..5e8c5c53 100644 --- a/tests/test_named.py +++ b/tests/test_named.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import ctypes import math diff --git a/tests/test_plot.py b/tests/test_plot.py index a05c61cf..5184e8e3 100644 --- a/tests/test_plot.py +++ b/tests/test_plot.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numpy as np import pytest diff --git a/tests/test_profile.py b/tests/test_profile.py index 0d109137..f99b64cc 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numpy as np from pytest import approx diff --git a/tests/test_reprs.py b/tests/test_reprs.py index 58317d38..4d441712 100644 --- a/tests/test_reprs.py +++ b/tests/test_reprs.py @@ -1,45 +1,102 @@ +from __future__ import annotations + +from hist import Hist, Stack, axis + + def test_1D_empty_repr(named_hist): - h = named_hist.new.Reg(10, -1, 1, name="x").Double() + h = named_hist.new.Reg(10, -1, 1, name="x", label="y").Double() html = h._repr_html_() assert html + assert "name='x'" in repr(h) + assert "label='y'" in repr(h) + + +def test_1D_var_empty_repr(named_hist): + + h = named_hist.new.Var(range(10), name="x", label="y").Double() + html = h._repr_html_() + assert html + assert "name='x'" in repr(h) + assert "label='y'" in repr(h) + + +def test_1D_int_empty_repr(named_hist): + + h = named_hist.new.Int(-9, 9, name="x", label="y").Double() + html = h._repr_html_() + assert html + assert "name='x'" in repr(h) + assert "label='y'" in repr(h) def test_1D_intcat_empty_repr(named_hist): - h = named_hist.new.IntCat([1, 3, 5], name="x").Double() + h = named_hist.new.IntCat([1, 3, 5], name="x", label="y").Double() html = h._repr_html_() assert html + assert "name='x'" in repr(h) + assert "label='y'" in repr(h) def test_1D_strcat_empty_repr(named_hist): - h = named_hist.new.StrCat(["1", "3", "5"], name="x").Double() + h = named_hist.new.StrCat(["1", "3", "5"], name="x", label="y").Double() html = h._repr_html_() assert html + assert "name='x'" in repr(h) + assert "label='y'" in repr(h) def test_2D_empty_repr(named_hist): - h = named_hist.new.Reg(10, -1, 1, name="x").Int(0, 15, name="y").Double() + h = ( + named_hist.new.Reg(10, -1, 1, name="x", label="y") + .Int(0, 15, name="p", label="q") + .Double() + ) html = h._repr_html_() assert html + assert "name='x'" in repr(h) + assert "name='p'" in repr(h) + assert "label='y'" in repr(h) + assert "label='q'" in repr(h) def test_1D_circ_empty_repr(named_hist): - h = named_hist.new.Reg(10, -1, 1, circular=True, name="r").Double() + h = named_hist.new.Reg(10, -1, 1, circular=True, name="R", label="r").Double() html = h._repr_html_() assert html + assert "name='R'" in repr(h) + assert "label='r'" in repr(h) def test_ND_empty_repr(named_hist): h = ( - named_hist.new.Reg(10, -1, 1, name="x") - .Reg(12, -3, 3, name="y") - .Reg(15, -2, 4, name="z") + named_hist.new.Reg(10, -1, 1, name="x", label="y") + .Reg(12, -3, 3, name="p", label="q") + .Reg(15, -2, 4, name="a", label="b") .Double() ) html = h._repr_html_() assert html + assert "name='x'" in repr(h) + assert "name='p'" in repr(h) + assert "name='a'" in repr(h) + assert "label='y'" in repr(h) + assert "label='q'" in repr(h) + assert "label='b'" in repr(h) + + +def test_stack_repr(named_hist): + + a1 = axis.Regular( + 50, -5, 5, name="A", label="a [unit]", underflow=False, overflow=False + ) + a2 = axis.Regular( + 50, -5, 5, name="A", label="a [unit]", underflow=False, overflow=False + ) + assert "name='A'" in repr(Stack(Hist(a1), Hist(a2))) + assert "label='a [unit]'" in repr(Stack(Hist(a1), Hist(a2))) diff --git a/tests/test_stacks.py b/tests/test_stacks.py new file mode 100644 index 00000000..d6575695 --- /dev/null +++ b/tests/test_stacks.py @@ -0,0 +1,239 @@ +from __future__ import annotations + +import numpy as np +import pytest + +from hist import Hist, NamedHist, Stack, axis + +# different histograms here! +reg_ax = axis.Regular(10, 0, 1) +boo_ax = axis.Boolean() +var_ax = axis.Variable(range(-3, 3)) +int_ax = axis.Integer(-3, 3) +int_cat_ax = axis.IntCategory(range(-3, 3)) +str_cat_ax = axis.StrCategory(["F", "T"]) + +reg_hist = Hist(reg_ax).fill(np.random.randn(10)) +boo_hist = Hist(boo_ax).fill([True, False, True]) +var_hist = Hist(var_ax).fill(np.random.randn(10)) +int_hist = Hist(int_ax).fill(np.random.randn(10)) +int_cat_hist = Hist(int_cat_ax).fill(np.random.randn(10)) +str_cat_hist = Hist(str_cat_ax).fill(["T", "F", "T"]) + +named_reg_ax = axis.Regular(10, 0, 1, name="A") +named_boo_ax = axis.Boolean(name="B") +named_var_ax = axis.Variable(range(-3, 3), name="C") +named_int_ax = axis.Integer(-3, 3, name="D") +named_int_cat_ax = axis.IntCategory(range(-3, 3), name="E") +named_str_cat_ax = axis.StrCategory(["F", "T"], name="F") + +named_reg_hist = NamedHist(named_reg_ax).fill(A=np.random.randn(10)) +named_boo_hist = NamedHist(named_boo_ax).fill(B=[True, False, True]) +named_var_hist = NamedHist(named_var_ax).fill(C=np.random.randn(10)) +named_int_hist = NamedHist(named_int_ax).fill(D=np.random.randn(10)) +named_int_cat_hist = NamedHist(named_int_cat_ax).fill(E=np.random.randn(10)) +named_str_cat_hist = NamedHist(named_str_cat_ax).fill(F=["T", "F", "T"]) + +reg_hist_2d = Hist(reg_ax, reg_ax).fill(np.random.randn(10), np.random.randn(10)) + +boo_hist_2d = Hist(boo_ax, boo_ax).fill([True, False, True], [True, False, True]) +var_hist_2d = Hist(var_ax, var_ax).fill(np.random.randn(10), np.random.randn(10)) +int_hist_2d = Hist(int_ax, int_ax).fill(np.random.randn(10), np.random.randn(10)) +int_cat_hist_2d = Hist(int_cat_ax, int_cat_ax).fill( + np.random.randn(10), np.random.randn(10) +) +str_cat_hist_2d = Hist(str_cat_ax, str_cat_ax).fill(["T", "F", "T"], ["T", "F", "T"]) + +axs = (reg_ax, boo_ax, var_ax, int_ax, int_cat_ax, str_cat_ax) +fills = (int, bool, int, int, int, str) +ids = ("reg", "boo", "var", "int", "icat", "scat") + + +@pytest.fixture(params=zip(axs, fills), ids=ids) +def hist_1d(request): + def make_hist(): + ax, fill = request.param + h = Hist(ax) + if fill is int: + h.fill(np.random.randn(10)) + elif fill is bool: + h.fill(np.random.randint(0, 1, size=10) == 1) + elif fill is str: + h.fill(np.random.choice(("T", "F"), size=10)) + return h + + return make_hist + + +def test_stack_init(hist_1d): + """ + Test stack init -- whether Stack can be properly initialized. + """ + h1 = hist_1d() + h2 = hist_1d() + h3 = hist_1d() + + # Allow to construct stack with same-type and same-type-axis histograms + stack = Stack(h1, h2, h3) + assert stack[0] == h1 + assert stack[1] == h2 + assert stack[2] == h3 + + assert tuple(stack) == (h1, h2, h3) + + +def test_stack_constructor_fails(): + # Don't allow construction directly from axes with no Histograms + with pytest.raises(Exception): + assert Stack(reg_ax) + + with pytest.raises(Exception): + assert Stack(reg_ax, reg_ax, reg_ax) + + # not allow to construct stack with different-type but same-type-axis histograms + with pytest.raises(Exception): + Stack(reg_hist, named_reg_hist) + with pytest.raises(Exception): + assert Stack(boo_hist, named_boo_hist) + with pytest.raises(Exception): + Stack(var_hist, named_var_hist) + with pytest.raises(Exception): + Stack(int_hist, named_int_hist) + with pytest.raises(Exception): + Stack(int_cat_hist, named_int_cat_hist) + with pytest.raises(Exception): + Stack(str_cat_hist, named_str_cat_hist) + + # not allow to construct stack with same-type but different-type-axis histograms + with pytest.raises(Exception): + Stack(reg_hist, boo_hist, var_hist) + with pytest.raises(Exception): + Stack(int_hist, int_cat_hist, str_cat_hist) + + # allow to construct stack with 2d histograms + Stack(reg_hist_2d, reg_hist_2d, reg_hist_2d) + Stack(boo_hist_2d, boo_hist_2d, boo_hist_2d) + Stack(var_hist_2d, var_hist_2d, var_hist_2d) + Stack(int_hist_2d, int_hist_2d, int_hist_2d) + Stack(int_cat_hist_2d, int_cat_hist_2d, int_cat_hist_2d) + Stack(str_cat_hist_2d, str_cat_hist_2d, str_cat_hist_2d) + + # not allow to constuct stack with different ndim + with pytest.raises(Exception): + Stack(reg_hist, reg_hist_2d) + with pytest.raises(Exception): + Stack(boo_hist, boo_hist_2d) + with pytest.raises(Exception): + Stack(var_hist, var_hist_2d) + with pytest.raises(Exception): + Stack(int_hist, int_hist_2d) + with pytest.raises(Exception): + Stack(int_cat_hist, int_cat_hist_2d) + with pytest.raises(Exception): + Stack(str_cat_hist, str_cat_hist_2d) + + # not allow to struct stack from histograms with different axes + with pytest.raises(Exception): + NamedHist(named_reg_ax, axis.Regular(10, 0, 1, name="X")).stack("A", "X") + with pytest.raises(Exception): + NamedHist(named_boo_ax, axis.Boolean(name="X")).stack("B", "X") + with pytest.raises(Exception): + NamedHist(named_var_ax, axis.Variable(range(-3, 3), name="X")).stack("C", "X") + with pytest.raises(Exception): + NamedHist(named_int_ax, axis.Integer(-3, 3, name="X")).stack("D", "X") + with pytest.raises(Exception): + NamedHist(named_int_cat_ax, axis.IntCategory(range(-3, 3), name="X")).stack( + "E", "X" + ) + with pytest.raises(Exception): + NamedHist(named_str_cat_ax, axis.StrCategory(["F", "T"], name="X")).stack( + "F", "X" + ) + + +def test_stack_plot_construct(): + """ + Test stack plot -- whether Stack can be properly plot. + """ + # not allow axes stack to plot + with pytest.raises(Exception): + Stack(reg_ax, reg_ax, reg_ax).plot() + with pytest.raises(Exception): + Stack(boo_ax, boo_ax, boo_ax).plot() + with pytest.raises(Exception): + Stack(var_ax, var_ax, var_ax).plot() + with pytest.raises(Exception): + Stack(int_ax, int_ax, int_ax).plot() + with pytest.raises(Exception): + Stack(int_cat_ax, int_cat_ax, int_cat_ax).plot() + with pytest.raises(Exception): + Stack(str_cat_ax, str_cat_ax, str_cat_ax).plot() + + # allow to plot stack with 1d histogram + assert Stack(reg_hist, reg_hist, reg_hist).plot() + assert Stack(boo_hist, boo_hist, boo_hist).plot() + assert Stack(var_hist, var_hist, var_hist).plot() + assert Stack(int_hist, int_hist, int_hist).plot() + assert Stack(int_cat_hist, int_cat_hist, int_cat_hist).plot() + assert Stack(str_cat_hist, str_cat_hist, str_cat_hist).plot() + + # allow to plot stack with projection of 2d histograms + assert Stack(reg_hist_2d.project(0)).plot() + assert Stack(boo_hist_2d.project(0)).plot() + assert Stack(var_hist_2d.project(0)).plot() + assert Stack(int_hist_2d.project(0)).plot() + assert Stack(int_cat_hist_2d.project(0)).plot() + assert Stack(str_cat_hist_2d.project(0)).plot() + + # not allow to plot stack with 2d histograms + with pytest.raises(Exception): + Stack(reg_hist_2d).plot() + + with pytest.raises(Exception): + Stack(boo_hist_2d).plot() + + with pytest.raises(Exception): + Stack(var_hist_2d).plot() + + with pytest.raises(Exception): + Stack(int_hist_2d).plot() + + with pytest.raises(Exception): + Stack(int_cat_hist_2d).plot() + + with pytest.raises(Exception): + Stack(str_cat_hist_2d).plot() + + +def test_stack_method(): + h = Hist.new.Regular(10, 0, 1).StrCategory(["one", "two"], name="str").Double() + s = h.stack(1) + assert s[0].axes[0] == h.axes[0] + assert s[0].name == "one" + assert s[1].name == "two" + + s2 = h.stack("str") + assert s2[0].axes[0] == h.axes[0] + assert s2[0].name == "one" + assert s2[1].name == "two" + + +def collect(*args, **kwargs): + return args, kwargs + + +def test_stack_plot(monkeypatch): + import hist.plot + + monkeypatch.setattr(hist.plot, "histplot", collect) + + h = Hist.new.Regular(10, 0, 1).StrCategory(["one", "two"], name="str").Double() + s = h.stack(1) + + args, kwargs = s.plot(silly=...) + + assert len(s) == 2 + assert len(list(s)) == 2 + + assert args == (list(s),) + assert kwargs == {"label": ["one", "two"], "silly": ...} diff --git a/tests/test_version.py b/tests/test_version.py index fe8e7392..66d5e07e 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import hist